aboutsummaryrefslogtreecommitdiff
path: root/Minecraft.Server/Console/commands
diff options
context:
space:
mode:
Diffstat (limited to 'Minecraft.Server/Console/commands')
-rw-r--r--Minecraft.Server/Console/commands/CommandParsing.h39
-rw-r--r--Minecraft.Server/Console/commands/IServerCliCommand.h50
-rw-r--r--Minecraft.Server/Console/commands/ban-ip/CliCommandBanIp.cpp171
-rw-r--r--Minecraft.Server/Console/commands/ban-ip/CliCommandBanIp.h19
-rw-r--r--Minecraft.Server/Console/commands/ban-list/CliCommandBanList.cpp136
-rw-r--r--Minecraft.Server/Console/commands/ban-list/CliCommandBanList.h23
-rw-r--r--Minecraft.Server/Console/commands/ban/CliCommandBan.cpp145
-rw-r--r--Minecraft.Server/Console/commands/ban/CliCommandBan.h20
-rw-r--r--Minecraft.Server/Console/commands/defaultgamemode/CliCommandDefaultGamemode.cpp117
-rw-r--r--Minecraft.Server/Console/commands/defaultgamemode/CliCommandDefaultGamemode.h16
-rw-r--r--Minecraft.Server/Console/commands/enchant/CliCommandEnchant.cpp87
-rw-r--r--Minecraft.Server/Console/commands/enchant/CliCommandEnchant.h16
-rw-r--r--Minecraft.Server/Console/commands/experience/CliCommandExperience.cpp184
-rw-r--r--Minecraft.Server/Console/commands/experience/CliCommandExperience.h17
-rw-r--r--Minecraft.Server/Console/commands/gamemode/CliCommandGamemode.cpp109
-rw-r--r--Minecraft.Server/Console/commands/gamemode/CliCommandGamemode.h18
-rw-r--r--Minecraft.Server/Console/commands/give/CliCommandGive.cpp103
-rw-r--r--Minecraft.Server/Console/commands/give/CliCommandGive.h16
-rw-r--r--Minecraft.Server/Console/commands/help/CliCommandHelp.cpp46
-rw-r--r--Minecraft.Server/Console/commands/help/CliCommandHelp.h17
-rw-r--r--Minecraft.Server/Console/commands/kill/CliCommandKill.cpp64
-rw-r--r--Minecraft.Server/Console/commands/kill/CliCommandKill.h16
-rw-r--r--Minecraft.Server/Console/commands/list/CliCommandList.cpp49
-rw-r--r--Minecraft.Server/Console/commands/list/CliCommandList.h16
-rw-r--r--Minecraft.Server/Console/commands/pardon-ip/CliCommandPardonIp.cpp98
-rw-r--r--Minecraft.Server/Console/commands/pardon-ip/CliCommandPardonIp.h19
-rw-r--r--Minecraft.Server/Console/commands/pardon/CliCommandPardon.cpp173
-rw-r--r--Minecraft.Server/Console/commands/pardon/CliCommandPardon.h19
-rw-r--r--Minecraft.Server/Console/commands/stop/CliCommandStop.cpp32
-rw-r--r--Minecraft.Server/Console/commands/stop/CliCommandStop.h16
-rw-r--r--Minecraft.Server/Console/commands/time/CliCommandTime.cpp118
-rw-r--r--Minecraft.Server/Console/commands/time/CliCommandTime.h16
-rw-r--r--Minecraft.Server/Console/commands/tp/CliCommandTp.cpp82
-rw-r--r--Minecraft.Server/Console/commands/tp/CliCommandTp.h18
-rw-r--r--Minecraft.Server/Console/commands/weather/CliCommandWeather.cpp49
-rw-r--r--Minecraft.Server/Console/commands/weather/CliCommandWeather.h15
-rw-r--r--Minecraft.Server/Console/commands/whitelist/CliCommandWhitelist.cpp285
-rw-r--r--Minecraft.Server/Console/commands/whitelist/CliCommandWhitelist.h17
38 files changed, 2451 insertions, 0 deletions
diff --git a/Minecraft.Server/Console/commands/CommandParsing.h b/Minecraft.Server/Console/commands/CommandParsing.h
new file mode 100644
index 00000000..edef68d0
--- /dev/null
+++ b/Minecraft.Server/Console/commands/CommandParsing.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include <cerrno>
+#include <cstdlib>
+#include <limits>
+#include <string>
+
+namespace ServerRuntime
+{
+ namespace CommandParsing
+ {
+ inline bool TryParseInt(const std::string &text, int *outValue)
+ {
+ if (outValue == nullptr || text.empty())
+ {
+ return false;
+ }
+
+ char *end = nullptr;
+ errno = 0;
+ const long parsedValue = std::strtol(text.c_str(), &end, 10);
+ if (end == text.c_str() || *end != '\0')
+ {
+ return false;
+ }
+ if (errno == ERANGE)
+ {
+ return false;
+ }
+ if (parsedValue < (std::numeric_limits<int>::min)() || parsedValue > (std::numeric_limits<int>::max)())
+ {
+ return false;
+ }
+
+ *outValue = static_cast<int>(parsedValue);
+ return true;
+ }
+ }
+}
diff --git a/Minecraft.Server/Console/commands/IServerCliCommand.h b/Minecraft.Server/Console/commands/IServerCliCommand.h
new file mode 100644
index 00000000..9cf5ef0e
--- /dev/null
+++ b/Minecraft.Server/Console/commands/IServerCliCommand.h
@@ -0,0 +1,50 @@
+#pragma once
+
+#include <string>
+#include <vector>
+
+namespace ServerRuntime
+{
+ class ServerCliEngine;
+ struct ServerCliParsedLine;
+ struct ServerCliCompletionContext;
+
+ /**
+ * **Command interface for server CLI**
+ *
+ * Implement this contract to add new commands without changing engine internals.
+ */
+ class IServerCliCommand
+ {
+ public:
+ virtual ~IServerCliCommand() = default;
+
+ /** Primary command name */
+ virtual const char *Name() const = 0;
+ /** Optional aliases */
+ virtual std::vector<std::string> Aliases() const { return {}; }
+ /** Usage text for help */
+ virtual const char *Usage() const = 0;
+ /** Short command description*/
+ virtual const char *Description() const = 0;
+
+ /**
+ * **Execute command logic**
+ *
+ * Called after tokenization and command lookup.
+ */
+ virtual bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) = 0;
+
+ /**
+ * **Provide argument completion candidates**
+ *
+ * Override when command-specific completion is needed.
+ */
+ virtual void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
+ {
+ (void)context;
+ (void)engine;
+ (void)out;
+ }
+ };
+}
diff --git a/Minecraft.Server/Console/commands/ban-ip/CliCommandBanIp.cpp b/Minecraft.Server/Console/commands/ban-ip/CliCommandBanIp.cpp
new file mode 100644
index 00000000..99c1455e
--- /dev/null
+++ b/Minecraft.Server/Console/commands/ban-ip/CliCommandBanIp.cpp
@@ -0,0 +1,171 @@
+#include "stdafx.h"
+
+#include "CliCommandBanIp.h"
+
+#include "..\..\ServerCliEngine.h"
+#include "..\..\ServerCliParser.h"
+#include "..\..\..\Access\Access.h"
+#include "..\..\..\Common\NetworkUtils.h"
+#include "..\..\..\Common\StringUtils.h"
+#include "..\..\..\ServerLogManager.h"
+#include "..\..\..\..\Minecraft.Client\MinecraftServer.h"
+#include "..\..\..\..\Minecraft.Client\PlayerConnection.h"
+#include "..\..\..\..\Minecraft.Client\PlayerList.h"
+#include "..\..\..\..\Minecraft.Client\ServerPlayer.h"
+#include "..\..\..\..\Minecraft.World\Connection.h"
+#include "..\..\..\..\Minecraft.World\DisconnectPacket.h"
+
+namespace ServerRuntime
+{
+ namespace
+ {
+ // The dedicated server keeps the accepted remote IP in ServerLogManager, keyed by connection smallId.
+ // It's a bit strange from a responsibility standpoint, so we'll need to implement it separately.
+ static bool TryGetPlayerRemoteIp(const std::shared_ptr<ServerPlayer> &player, std::string *outIp)
+ {
+ if (outIp == nullptr || player == nullptr || player->connection == nullptr || player->connection->connection == nullptr || player->connection->connection->getSocket() == nullptr)
+ {
+ return false;
+ }
+
+ const unsigned char smallId = player->connection->connection->getSocket()->getSmallId();
+ if (smallId == 0)
+ {
+ return false;
+ }
+
+ return ServerRuntime::ServerLogManager::TryGetConnectionRemoteIp(smallId, outIp);
+ }
+
+ // After persisting the ban, walk a snapshot of current players so every matching session is removed.
+ static int DisconnectPlayersByRemoteIp(const std::string &ip)
+ {
+ auto *server = MinecraftServer::getInstance();
+ if (server == nullptr || server->getPlayers() == nullptr)
+ {
+ return 0;
+ }
+
+ const std::string normalizedIp = NetworkUtils::NormalizeIpToken(ip);
+ const std::vector<std::shared_ptr<ServerPlayer>> playerSnapshot = server->getPlayers()->players;
+ int disconnectedCount = 0;
+ for (const auto &player : playerSnapshot)
+ {
+ std::string playerIp;
+ if (!TryGetPlayerRemoteIp(player, &playerIp))
+ {
+ continue;
+ }
+
+ if (NetworkUtils::NormalizeIpToken(playerIp) == normalizedIp)
+ {
+ if (player != nullptr && player->connection != nullptr)
+ {
+ player->connection->disconnect(DisconnectPacket::eDisconnect_Banned);
+ ++disconnectedCount;
+ }
+ }
+ }
+
+ return disconnectedCount;
+ }
+ }
+
+ const char *CliCommandBanIp::Name() const
+ {
+ return "ban-ip";
+ }
+
+ const char *CliCommandBanIp::Usage() const
+ {
+ return "ban-ip <address|player> [reason ...]";
+ }
+
+ const char *CliCommandBanIp::Description() const
+ {
+ return "Ban an IP address or a player's current IP.";
+ }
+
+ /**
+ * Resolves either a literal IP or an online player's current IP, persists the ban, and disconnects every matching connection
+ * IPまたは接続中プレイヤーの現在IPをBANし一致する接続を切断する
+ */
+ bool CliCommandBanIp::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
+ {
+ if (line.tokens.size() < 2)
+ {
+ engine->LogWarn("Usage: ban-ip <address|player> [reason ...]");
+ return false;
+ }
+ if (!ServerRuntime::Access::IsInitialized())
+ {
+ engine->LogWarn("Access manager is not initialized.");
+ return false;
+ }
+
+ const std::string targetToken = line.tokens[1];
+ std::string remoteIp;
+ // Match Java Edition behavior by accepting either a literal IP or an online player name.
+ const auto targetPlayer = engine->FindPlayerByNameUtf8(targetToken);
+ if (targetPlayer != nullptr)
+ {
+ if (!TryGetPlayerRemoteIp(targetPlayer, &remoteIp))
+ {
+ engine->LogWarn("Cannot ban that player's IP because no current remote IP is available.");
+ return false;
+ }
+ }
+ else if (NetworkUtils::IsIpLiteral(targetToken))
+ {
+ remoteIp = StringUtils::TrimAscii(targetToken);
+ }
+ else
+ {
+ engine->LogWarn("Unknown player or invalid IP address: " + targetToken);
+ return false;
+ }
+
+ // Refuse duplicate bans so operators get immediate feedback instead of rewriting the same entry.
+ if (ServerRuntime::Access::IsIpBanned(remoteIp))
+ {
+ engine->LogWarn("That IP address is already banned.");
+ return false;
+ }
+
+ ServerRuntime::Access::BanMetadata metadata = ServerRuntime::Access::BanManager::BuildDefaultMetadata("Console");
+ metadata.reason = StringUtils::JoinTokens(line.tokens, 2);
+ if (metadata.reason.empty())
+ {
+ metadata.reason = "Banned by an operator.";
+ }
+
+ // Publish the ban before disconnecting players so reconnect attempts are rejected immediately.
+ if (!ServerRuntime::Access::AddIpBan(remoteIp, metadata))
+ {
+ engine->LogError("Failed to write IP ban.");
+ return false;
+ }
+
+ const int disconnectedCount = DisconnectPlayersByRemoteIp(remoteIp);
+ // Report the resolved IP rather than the original token so player-name targets are explicit in the console.
+ engine->LogInfo("Banned IP address " + remoteIp + ".");
+ if (disconnectedCount > 0)
+ {
+ engine->LogInfo("Disconnected " + std::to_string(disconnectedCount) + " player(s) with that IP.");
+ }
+ return true;
+ }
+
+ /**
+ * Suggests online player names for the player-target form of the Java Edition command
+ * プレイヤー名指定時の補完候補を返す
+ */
+ void CliCommandBanIp::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
+ {
+ if (context.currentTokenIndex == 1)
+ {
+ engine->SuggestPlayers(context.prefix, context.linePrefix, out);
+ }
+ }
+}
+
diff --git a/Minecraft.Server/Console/commands/ban-ip/CliCommandBanIp.h b/Minecraft.Server/Console/commands/ban-ip/CliCommandBanIp.h
new file mode 100644
index 00000000..1c116fa6
--- /dev/null
+++ b/Minecraft.Server/Console/commands/ban-ip/CliCommandBanIp.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "..\IServerCliCommand.h"
+
+namespace ServerRuntime
+{
+ /**
+ * Applies a dedicated-server IP ban using Java Edition style syntax and Access-backed persistence
+ */
+ class CliCommandBanIp : public IServerCliCommand
+ {
+ public:
+ virtual const char *Name() const;
+ virtual const char *Usage() const;
+ virtual const char *Description() const;
+ virtual bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine);
+ virtual void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const;
+ };
+}
diff --git a/Minecraft.Server/Console/commands/ban-list/CliCommandBanList.cpp b/Minecraft.Server/Console/commands/ban-list/CliCommandBanList.cpp
new file mode 100644
index 00000000..14641617
--- /dev/null
+++ b/Minecraft.Server/Console/commands/ban-list/CliCommandBanList.cpp
@@ -0,0 +1,136 @@
+#include "stdafx.h"
+
+#include "CliCommandBanList.h"
+
+#include "..\..\ServerCliEngine.h"
+#include "..\..\ServerCliParser.h"
+#include "..\..\..\Access\Access.h"
+#include "..\..\..\Common\StringUtils.h"
+
+#include <algorithm>
+
+namespace ServerRuntime
+{
+ namespace
+ {
+ static void AppendUniqueText(const std::string &text, std::vector<std::string> *out)
+ {
+ if (out == nullptr || text.empty())
+ {
+ return;
+ }
+
+ if (std::find(out->begin(), out->end(), text) == out->end())
+ {
+ out->push_back(text);
+ }
+ }
+
+ static bool CompareLowerAscii(const std::string &left, const std::string &right)
+ {
+ return StringUtils::ToLowerAscii(left) < StringUtils::ToLowerAscii(right);
+ }
+
+ static bool LogBannedPlayers(ServerCliEngine *engine)
+ {
+ std::vector<ServerRuntime::Access::BannedPlayerEntry> entries;
+ if (!ServerRuntime::Access::SnapshotBannedPlayers(&entries))
+ {
+ engine->LogError("Failed to read banned players.");
+ return false;
+ }
+
+ std::vector<std::string> names;
+ for (const auto &entry : entries)
+ {
+ AppendUniqueText(entry.name, &names);
+ }
+ std::sort(names.begin(), names.end(), CompareLowerAscii);
+
+ engine->LogInfo("There are " + std::to_string(names.size()) + " banned player(s).");
+ for (const auto &name : names)
+ {
+ engine->LogInfo(" " + name);
+ }
+ return true;
+ }
+
+ static bool LogBannedIps(ServerCliEngine *engine)
+ {
+ std::vector<ServerRuntime::Access::BannedIpEntry> entries;
+ if (!ServerRuntime::Access::SnapshotBannedIps(&entries))
+ {
+ engine->LogError("Failed to read banned IPs.");
+ return false;
+ }
+
+ std::vector<std::string> ips;
+ for (const auto &entry : entries)
+ {
+ AppendUniqueText(entry.ip, &ips);
+ }
+ std::sort(ips.begin(), ips.end(), CompareLowerAscii);
+
+ engine->LogInfo("There are " + std::to_string(ips.size()) + " banned IP(s).");
+ for (const auto &ip : ips)
+ {
+ engine->LogInfo(" " + ip);
+ }
+ return true;
+ }
+
+ static bool LogAllBans(ServerCliEngine *engine)
+ {
+ if (!LogBannedPlayers(engine))
+ {
+ return false;
+ }
+
+ // Always print the IP snapshot as well so ban-ip entries are visible from the same command output.
+ return LogBannedIps(engine);
+ }
+ }
+
+ const char *CliCommandBanList::Name() const
+ {
+ return "banlist";
+ }
+
+ const char *CliCommandBanList::Usage() const
+ {
+ return "banlist";
+ }
+
+ const char *CliCommandBanList::Description() const
+ {
+ return "List all banned players and IPs.";
+ }
+
+ /**
+ * Reads the current Access snapshots and always prints both banned players and banned IPs
+ * Access の一覧を読みプレイヤーBANとIP BANをまとめて表示する
+ */
+ bool CliCommandBanList::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
+ {
+ if (line.tokens.size() > 1)
+ {
+ engine->LogWarn("Usage: banlist");
+ return false;
+ }
+ if (!ServerRuntime::Access::IsInitialized())
+ {
+ engine->LogWarn("Access manager is not initialized.");
+ return false;
+ }
+
+ return LogAllBans(engine);
+ }
+
+ void CliCommandBanList::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
+ {
+ (void)context;
+ (void)engine;
+ (void)out;
+ }
+}
+
diff --git a/Minecraft.Server/Console/commands/ban-list/CliCommandBanList.h b/Minecraft.Server/Console/commands/ban-list/CliCommandBanList.h
new file mode 100644
index 00000000..1db32bc1
--- /dev/null
+++ b/Minecraft.Server/Console/commands/ban-list/CliCommandBanList.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "..\IServerCliCommand.h"
+
+namespace ServerRuntime
+{
+ /**
+ * **Ban List Command**
+ *
+ * Lists dedicated-server player bans and IP bans in a single command output
+ * 専用サーバーのプレイヤーBANとIP BANをまとめて表示する
+ */
+ class CliCommandBanList : public IServerCliCommand
+ {
+ public:
+ virtual const char *Name() const;
+ virtual const char *Usage() const;
+ virtual const char *Description() const;
+ virtual bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine);
+ virtual void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const;
+ };
+}
+
diff --git a/Minecraft.Server/Console/commands/ban/CliCommandBan.cpp b/Minecraft.Server/Console/commands/ban/CliCommandBan.cpp
new file mode 100644
index 00000000..f9855c0c
--- /dev/null
+++ b/Minecraft.Server/Console/commands/ban/CliCommandBan.cpp
@@ -0,0 +1,145 @@
+#include "stdafx.h"
+
+#include "CliCommandBan.h"
+
+#include "..\..\ServerCliEngine.h"
+#include "..\..\ServerCliParser.h"
+#include "..\..\..\Access\Access.h"
+#include "..\..\..\Common\StringUtils.h"
+#include "..\..\..\..\Minecraft.Client\PlayerConnection.h"
+#include "..\..\..\..\Minecraft.Client\ServerPlayer.h"
+#include "..\..\..\..\Minecraft.World\DisconnectPacket.h"
+
+#include <algorithm>
+
+namespace ServerRuntime
+{
+ namespace
+ {
+ static void AppendUniqueXuid(PlayerUID xuid, std::vector<PlayerUID> *out)
+ {
+ if (out == nullptr || xuid == INVALID_XUID)
+ {
+ return;
+ }
+
+ if (std::find(out->begin(), out->end(), xuid) == out->end())
+ {
+ out->push_back(xuid);
+ }
+ }
+
+ static void CollectPlayerBanXuids(const std::shared_ptr<ServerPlayer> &player, std::vector<PlayerUID> *out)
+ {
+ if (player == nullptr || out == nullptr)
+ {
+ return;
+ }
+
+ // Keep both identity variants because the dedicated server checks login and online XUIDs separately.
+ AppendUniqueXuid(player->getXuid(), out);
+ AppendUniqueXuid(player->getOnlineXuid(), out);
+ }
+ }
+
+ const char *CliCommandBan::Name() const
+ {
+ return "ban";
+ }
+
+ const char *CliCommandBan::Usage() const
+ {
+ return "ban <player> [reason ...]";
+ }
+
+ const char *CliCommandBan::Description() const
+ {
+ return "Ban an online player.";
+ }
+
+ /**
+ * Resolves the live player, writes one or more Access ban entries, and disconnects the target with the banned reason
+ * 対象プレイヤーを解決してBANを保存し切断する
+ */
+ bool CliCommandBan::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
+ {
+ if (line.tokens.size() < 2)
+ {
+ engine->LogWarn("Usage: ban <player> [reason ...]");
+ return false;
+ }
+ if (!ServerRuntime::Access::IsInitialized())
+ {
+ engine->LogWarn("Access manager is not initialized.");
+ return false;
+ }
+
+ const auto target = engine->FindPlayerByNameUtf8(line.tokens[1]);
+ if (target == nullptr)
+ {
+ engine->LogWarn("Unknown player: " + line.tokens[1] + " (this server build can only ban players that are currently online).");
+ return false;
+ }
+
+ std::vector<PlayerUID> xuids;
+ CollectPlayerBanXuids(target, &xuids);
+ if (xuids.empty())
+ {
+ engine->LogWarn("Cannot ban that player because no valid XUID is available.");
+ return false;
+ }
+
+ const bool hasUnbannedIdentity = std::any_of(
+ xuids.begin(),
+ xuids.end(),
+ [](PlayerUID xuid) { return !ServerRuntime::Access::IsPlayerBanned(xuid); });
+ if (!hasUnbannedIdentity)
+ {
+ engine->LogWarn("That player is already banned.");
+ return false;
+ }
+
+ ServerRuntime::Access::BanMetadata metadata = ServerRuntime::Access::BanManager::BuildDefaultMetadata("Console");
+ metadata.reason = StringUtils::JoinTokens(line.tokens, 2);
+ if (metadata.reason.empty())
+ {
+ metadata.reason = "Banned by an operator.";
+ }
+
+ const std::string playerName = StringUtils::WideToUtf8(target->getName());
+ for (const auto xuid : xuids)
+ {
+ if (ServerRuntime::Access::IsPlayerBanned(xuid))
+ {
+ continue;
+ }
+
+ if (!ServerRuntime::Access::AddPlayerBan(xuid, playerName, metadata))
+ {
+ engine->LogError("Failed to write player ban.");
+ return false;
+ }
+ }
+
+ if (target->connection != nullptr)
+ {
+ target->connection->disconnect(DisconnectPacket::eDisconnect_Banned);
+ }
+
+ engine->LogInfo("Banned player " + playerName + ".");
+ return true;
+ }
+
+ /**
+ * Suggests currently connected player names for the Java-style player argument
+ * プレイヤー引数の補完候補を返す
+ */
+ void CliCommandBan::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
+ {
+ if (context.currentTokenIndex == 1)
+ {
+ engine->SuggestPlayers(context.prefix, context.linePrefix, out);
+ }
+ }
+}
+
diff --git a/Minecraft.Server/Console/commands/ban/CliCommandBan.h b/Minecraft.Server/Console/commands/ban/CliCommandBan.h
new file mode 100644
index 00000000..8605474c
--- /dev/null
+++ b/Minecraft.Server/Console/commands/ban/CliCommandBan.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include "..\IServerCliCommand.h"
+
+namespace ServerRuntime
+{
+ /**
+ * Applies a dedicated-server player ban using Java Edition style syntax and Access-backed persistence
+ * Java Edition 風の ban コマンドで永続プレイヤーBANを行う
+ */
+ class CliCommandBan : public IServerCliCommand
+ {
+ public:
+ virtual const char *Name() const;
+ virtual const char *Usage() const;
+ virtual const char *Description() const;
+ virtual bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine);
+ virtual void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const;
+ };
+}
diff --git a/Minecraft.Server/Console/commands/defaultgamemode/CliCommandDefaultGamemode.cpp b/Minecraft.Server/Console/commands/defaultgamemode/CliCommandDefaultGamemode.cpp
new file mode 100644
index 00000000..ee0e35a2
--- /dev/null
+++ b/Minecraft.Server/Console/commands/defaultgamemode/CliCommandDefaultGamemode.cpp
@@ -0,0 +1,117 @@
+#include "stdafx.h"
+
+#include "CliCommandDefaultGamemode.h"
+
+#include "..\..\ServerCliEngine.h"
+#include "..\..\ServerCliParser.h"
+#include "..\..\..\..\Minecraft.Client\MinecraftServer.h"
+#include "..\..\..\..\Minecraft.Client\PlayerList.h"
+#include "..\..\..\..\Minecraft.Client\ServerLevel.h"
+#include "..\..\..\..\Minecraft.Client\ServerPlayer.h"
+#include "..\..\..\..\Minecraft.World\net.minecraft.world.level.storage.h"
+
+namespace ServerRuntime
+{
+ namespace
+ {
+ constexpr const char *kDefaultGamemodeUsage = "defaultgamemode <survival|creative|0|1>";
+
+ static std::string ModeLabel(GameType *mode)
+ {
+ if (mode == GameType::SURVIVAL)
+ {
+ return "survival";
+ }
+ if (mode == GameType::CREATIVE)
+ {
+ return "creative";
+ }
+ if (mode == GameType::ADVENTURE)
+ {
+ return "adventure";
+ }
+
+ return std::to_string(mode != nullptr ? mode->getId() : -1);
+ }
+ }
+
+ const char *CliCommandDefaultGamemode::Name() const
+ {
+ return "defaultgamemode";
+ }
+
+ const char *CliCommandDefaultGamemode::Usage() const
+ {
+ return kDefaultGamemodeUsage;
+ }
+
+ const char *CliCommandDefaultGamemode::Description() const
+ {
+ return "Set the default game mode (server-side implementation).";
+ }
+
+ bool CliCommandDefaultGamemode::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
+ {
+ if (line.tokens.size() != 2)
+ {
+ engine->LogWarn(std::string("Usage: ") + kDefaultGamemodeUsage);
+ return false;
+ }
+
+ GameType *mode = engine->ParseGamemode(line.tokens[1]);
+ if (mode == nullptr)
+ {
+ engine->LogWarn("Unknown gamemode: " + line.tokens[1]);
+ return false;
+ }
+
+ MinecraftServer *server = MinecraftServer::getInstance();
+ if (server == nullptr)
+ {
+ engine->LogWarn("MinecraftServer instance is not available.");
+ return false;
+ }
+
+ PlayerList *players = server->getPlayers();
+ if (players == nullptr)
+ {
+ engine->LogWarn("Player list is not available.");
+ return false;
+ }
+
+ players->setOverrideGameMode(mode);
+
+ for (unsigned int i = 0; i < server->levels.length; ++i)
+ {
+ ServerLevel *level = server->levels[i];
+ if (level != nullptr && level->getLevelData() != nullptr)
+ {
+ level->getLevelData()->setGameType(mode);
+ }
+ }
+
+ if (server->getForceGameType())
+ {
+ for (size_t i = 0; i < players->players.size(); ++i)
+ {
+ std::shared_ptr<ServerPlayer> player = players->players[i];
+ if (player != nullptr)
+ {
+ player->setGameMode(mode);
+ player->fallDistance = 0.0f;
+ }
+ }
+ }
+
+ engine->LogInfo("Default gamemode set to " + ModeLabel(mode) + ".");
+ return true;
+ }
+
+ void CliCommandDefaultGamemode::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
+ {
+ if (context.currentTokenIndex == 1)
+ {
+ engine->SuggestGamemodes(context.prefix, context.linePrefix, out);
+ }
+ }
+}
diff --git a/Minecraft.Server/Console/commands/defaultgamemode/CliCommandDefaultGamemode.h b/Minecraft.Server/Console/commands/defaultgamemode/CliCommandDefaultGamemode.h
new file mode 100644
index 00000000..5cc17b34
--- /dev/null
+++ b/Minecraft.Server/Console/commands/defaultgamemode/CliCommandDefaultGamemode.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include "..\IServerCliCommand.h"
+
+namespace ServerRuntime
+{
+ class CliCommandDefaultGamemode : public IServerCliCommand
+ {
+ public:
+ const char *Name() const override;
+ const char *Usage() const override;
+ const char *Description() const override;
+ bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) override;
+ void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const override;
+ };
+}
diff --git a/Minecraft.Server/Console/commands/enchant/CliCommandEnchant.cpp b/Minecraft.Server/Console/commands/enchant/CliCommandEnchant.cpp
new file mode 100644
index 00000000..70d4d7d6
--- /dev/null
+++ b/Minecraft.Server/Console/commands/enchant/CliCommandEnchant.cpp
@@ -0,0 +1,87 @@
+#include "stdafx.h"
+
+#include "CliCommandEnchant.h"
+
+#include "..\..\ServerCliEngine.h"
+#include "..\..\ServerCliParser.h"
+#include "..\CommandParsing.h"
+#include "..\..\..\..\Minecraft.World\GameCommandPacket.h"
+#include "..\..\..\..\Minecraft.World\EnchantItemCommand.h"
+#include "..\..\..\..\Minecraft.World\net.minecraft.world.entity.player.h"
+#include "..\..\..\..\Minecraft.Client\ServerPlayer.h"
+
+namespace ServerRuntime
+{
+ namespace
+ {
+ constexpr const char *kEnchantUsage = "enchant <player> <enchantId> [level]";
+ }
+
+ const char *CliCommandEnchant::Name() const
+ {
+ return "enchant";
+ }
+
+ const char *CliCommandEnchant::Usage() const
+ {
+ return kEnchantUsage;
+ }
+
+ const char *CliCommandEnchant::Description() const
+ {
+ return "Enchant held item via Minecraft.World command dispatcher.";
+ }
+
+ bool CliCommandEnchant::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
+ {
+ if (line.tokens.size() < 3 || line.tokens.size() > 4)
+ {
+ engine->LogWarn(std::string("Usage: ") + kEnchantUsage);
+ return false;
+ }
+
+ std::shared_ptr<ServerPlayer> target = engine->FindPlayerByNameUtf8(line.tokens[1]);
+ if (target == nullptr)
+ {
+ engine->LogWarn("Unknown player: " + line.tokens[1]);
+ return false;
+ }
+
+ int enchantmentId = 0;
+ int enchantmentLevel = 1;
+ if (!CommandParsing::TryParseInt(line.tokens[2], &enchantmentId))
+ {
+ engine->LogWarn("Invalid enchantment id: " + line.tokens[2]);
+ return false;
+ }
+ if (line.tokens.size() >= 4 && !CommandParsing::TryParseInt(line.tokens[3], &enchantmentLevel))
+ {
+ engine->LogWarn("Invalid enchantment level: " + line.tokens[3]);
+ return false;
+ }
+
+ std::shared_ptr<Player> player = std::dynamic_pointer_cast<Player>(target);
+ if (player == nullptr)
+ {
+ engine->LogWarn("Cannot resolve target player entity.");
+ return false;
+ }
+
+ std::shared_ptr<GameCommandPacket> packet = EnchantItemCommand::preparePacket(player, enchantmentId, enchantmentLevel);
+ if (packet == nullptr)
+ {
+ engine->LogError("Failed to build enchant command packet.");
+ return false;
+ }
+
+ return engine->DispatchWorldCommand(packet->command, packet->data);
+ }
+
+ void CliCommandEnchant::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
+ {
+ if (context.currentTokenIndex == 1)
+ {
+ engine->SuggestPlayers(context.prefix, context.linePrefix, out);
+ }
+ }
+}
diff --git a/Minecraft.Server/Console/commands/enchant/CliCommandEnchant.h b/Minecraft.Server/Console/commands/enchant/CliCommandEnchant.h
new file mode 100644
index 00000000..66e330bd
--- /dev/null
+++ b/Minecraft.Server/Console/commands/enchant/CliCommandEnchant.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include "..\IServerCliCommand.h"
+
+namespace ServerRuntime
+{
+ class CliCommandEnchant : public IServerCliCommand
+ {
+ public:
+ const char *Name() const override;
+ const char *Usage() const override;
+ const char *Description() const override;
+ bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) override;
+ void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const override;
+ };
+}
diff --git a/Minecraft.Server/Console/commands/experience/CliCommandExperience.cpp b/Minecraft.Server/Console/commands/experience/CliCommandExperience.cpp
new file mode 100644
index 00000000..77df99ae
--- /dev/null
+++ b/Minecraft.Server/Console/commands/experience/CliCommandExperience.cpp
@@ -0,0 +1,184 @@
+#include "stdafx.h"
+
+#include "CliCommandExperience.h"
+
+#include "..\..\ServerCliEngine.h"
+#include "..\..\ServerCliParser.h"
+#include "..\CommandParsing.h"
+#include "..\..\..\Common\StringUtils.h"
+#include "..\..\..\..\Minecraft.Client\MinecraftServer.h"
+#include "..\..\..\..\Minecraft.Client\PlayerList.h"
+#include "..\..\..\..\Minecraft.Client\ServerPlayer.h"
+
+#include <limits>
+
+namespace ServerRuntime
+{
+ namespace
+ {
+ constexpr const char *kExperienceUsage = "xp <amount>[L] [player]";
+ constexpr const char *kExperienceUsageWithPlayer = "xp <amount>[L] <player>";
+
+ struct ExperienceAmount
+ {
+ int amount = 0;
+ bool levels = false;
+ bool take = false;
+ };
+
+ static bool TryParseExperienceAmount(const std::string &token, ExperienceAmount *outValue)
+ {
+ if (outValue == nullptr || token.empty())
+ {
+ return false;
+ }
+
+ ExperienceAmount parsed;
+ std::string numericToken = token;
+ const char suffix = token[token.size() - 1];
+ if (suffix == 'l' || suffix == 'L')
+ {
+ parsed.levels = true;
+ numericToken = token.substr(0, token.size() - 1);
+ if (numericToken.empty())
+ {
+ return false;
+ }
+ }
+
+ int signedAmount = 0;
+ if (!CommandParsing::TryParseInt(numericToken, &signedAmount))
+ {
+ return false;
+ }
+ if (signedAmount == (std::numeric_limits<int>::min)())
+ {
+ return false;
+ }
+
+ parsed.take = signedAmount < 0;
+ parsed.amount = parsed.take ? -signedAmount : signedAmount;
+ *outValue = parsed;
+ return true;
+ }
+
+ static std::shared_ptr<ServerPlayer> ResolveTargetPlayer(const ServerCliParsedLine &line, ServerCliEngine *engine)
+ {
+ if (line.tokens.size() >= 3)
+ {
+ return engine->FindPlayerByNameUtf8(line.tokens[2]);
+ }
+
+ MinecraftServer *server = MinecraftServer::getInstance();
+ if (server == nullptr || server->getPlayers() == nullptr)
+ {
+ return nullptr;
+ }
+
+ PlayerList *players = server->getPlayers();
+ if (players->players.size() == 1 && players->players[0] != nullptr)
+ {
+ return players->players[0];
+ }
+
+ return nullptr;
+ }
+ }
+
+ const char *CliCommandExperience::Name() const
+ {
+ return "xp";
+ }
+
+ std::vector<std::string> CliCommandExperience::Aliases() const
+ {
+ return { "experience" };
+ }
+
+ const char *CliCommandExperience::Usage() const
+ {
+ return kExperienceUsage;
+ }
+
+ const char *CliCommandExperience::Description() const
+ {
+ return "Grant or remove experience (server-side implementation).";
+ }
+
+ bool CliCommandExperience::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
+ {
+ if (line.tokens.size() < 2 || line.tokens.size() > 3)
+ {
+ engine->LogWarn(std::string("Usage: ") + kExperienceUsage);
+ return false;
+ }
+
+ ExperienceAmount amount;
+ if (!TryParseExperienceAmount(line.tokens[1], &amount))
+ {
+ engine->LogWarn(std::string("Usage: ") + kExperienceUsage);
+ return false;
+ }
+
+ std::shared_ptr<ServerPlayer> target = ResolveTargetPlayer(line, engine);
+ if (target == nullptr)
+ {
+ if (line.tokens.size() >= 3)
+ {
+ engine->LogWarn("Unknown player: " + line.tokens[2]);
+ }
+ else
+ {
+ engine->LogWarn(std::string("Usage: ") + kExperienceUsageWithPlayer);
+ }
+ return false;
+ }
+
+ if (amount.levels)
+ {
+ target->giveExperienceLevels(amount.take ? -amount.amount : amount.amount);
+ if (amount.take)
+ {
+ engine->LogInfo("Removed " + std::to_string(amount.amount) + " level(s) from " + StringUtils::WideToUtf8(target->getName()) + ".");
+ }
+ else
+ {
+ engine->LogInfo("Added " + std::to_string(amount.amount) + " level(s) to " + StringUtils::WideToUtf8(target->getName()) + ".");
+ }
+ return true;
+ }
+
+ if (amount.take)
+ {
+ engine->LogWarn("Removing raw experience points is not supported. Use negative levels (example: xp -5L <player>).");
+ return false;
+ }
+
+ target->increaseXp(amount.amount);
+ engine->LogInfo("Added " + std::to_string(amount.amount) + " experience points to " + StringUtils::WideToUtf8(target->getName()) + ".");
+ return true;
+ }
+
+ void CliCommandExperience::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
+ {
+ if (context.currentTokenIndex == 1)
+ {
+ if (StringUtils::StartsWithIgnoreCase("10", context.prefix))
+ {
+ out->push_back(context.linePrefix + "10");
+ }
+ if (StringUtils::StartsWithIgnoreCase("10L", context.prefix))
+ {
+ out->push_back(context.linePrefix + "10L");
+ }
+ if (StringUtils::StartsWithIgnoreCase("-5L", context.prefix))
+ {
+ out->push_back(context.linePrefix + "-5L");
+ }
+ }
+ else if (context.currentTokenIndex == 2)
+ {
+ engine->SuggestPlayers(context.prefix, context.linePrefix, out);
+ }
+ }
+}
diff --git a/Minecraft.Server/Console/commands/experience/CliCommandExperience.h b/Minecraft.Server/Console/commands/experience/CliCommandExperience.h
new file mode 100644
index 00000000..3fddb218
--- /dev/null
+++ b/Minecraft.Server/Console/commands/experience/CliCommandExperience.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include "..\IServerCliCommand.h"
+
+namespace ServerRuntime
+{
+ class CliCommandExperience : public IServerCliCommand
+ {
+ public:
+ const char *Name() const override;
+ std::vector<std::string> Aliases() const override;
+ const char *Usage() const override;
+ const char *Description() const override;
+ bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) override;
+ void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const override;
+ };
+}
diff --git a/Minecraft.Server/Console/commands/gamemode/CliCommandGamemode.cpp b/Minecraft.Server/Console/commands/gamemode/CliCommandGamemode.cpp
new file mode 100644
index 00000000..f41660e6
--- /dev/null
+++ b/Minecraft.Server/Console/commands/gamemode/CliCommandGamemode.cpp
@@ -0,0 +1,109 @@
+#include "stdafx.h"
+
+#include "CliCommandGamemode.h"
+
+#include "..\..\ServerCliEngine.h"
+#include "..\..\ServerCliParser.h"
+#include "..\..\..\..\Minecraft.Client\MinecraftServer.h"
+#include "..\..\..\..\Minecraft.Client\PlayerList.h"
+#include "..\..\..\..\Minecraft.Client\ServerPlayer.h"
+
+namespace ServerRuntime
+{
+ namespace
+ {
+ constexpr const char *kGamemodeUsage = "gamemode <survival|creative|0|1> [player]";
+ constexpr const char *kGamemodeUsageWithPlayer = "gamemode <survival|creative|0|1> <player>";
+ }
+
+ const char *CliCommandGamemode::Name() const
+ {
+ return "gamemode";
+ }
+
+ std::vector<std::string> CliCommandGamemode::Aliases() const
+ {
+ return { "gm" };
+ }
+
+ const char *CliCommandGamemode::Usage() const
+ {
+ return kGamemodeUsage;
+ }
+
+ const char *CliCommandGamemode::Description() const
+ {
+ return "Set a player's game mode.";
+ }
+
+ bool CliCommandGamemode::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
+ {
+ if (line.tokens.size() < 2 || line.tokens.size() > 3)
+ {
+ engine->LogWarn(std::string("Usage: ") + kGamemodeUsage);
+ return false;
+ }
+
+ GameType *mode = engine->ParseGamemode(line.tokens[1]);
+ if (mode == nullptr)
+ {
+ engine->LogWarn("Unknown gamemode: " + line.tokens[1]);
+ return false;
+ }
+
+ std::shared_ptr<ServerPlayer> target = nullptr;
+ if (line.tokens.size() >= 3)
+ {
+ target = engine->FindPlayerByNameUtf8(line.tokens[2]);
+ if (target == nullptr)
+ {
+ engine->LogWarn("Unknown player: " + line.tokens[2]);
+ return false;
+ }
+ }
+ else
+ {
+ MinecraftServer *server = MinecraftServer::getInstance();
+ if (server == nullptr || server->getPlayers() == nullptr)
+ {
+ engine->LogWarn("Player list is not available.");
+ return false;
+ }
+
+ PlayerList *players = server->getPlayers();
+ if (players->players.size() != 1 || players->players[0] == nullptr)
+ {
+ engine->LogWarn(std::string("Usage: ") + kGamemodeUsageWithPlayer);
+ return false;
+ }
+ target = players->players[0];
+ }
+
+ target->setGameMode(mode);
+ target->fallDistance = 0.0f;
+
+ if (line.tokens.size() >= 3)
+ {
+ engine->LogInfo("Set " + line.tokens[2] + " gamemode to " + line.tokens[1] + ".");
+ }
+ else
+ {
+ engine->LogInfo("Set gamemode to " + line.tokens[1] + ".");
+ }
+ return true;
+ }
+
+ void CliCommandGamemode::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
+ {
+ if (context.currentTokenIndex == 1)
+ {
+ engine->SuggestGamemodes(context.prefix, context.linePrefix, out);
+ }
+ else if (context.currentTokenIndex == 2)
+ {
+ engine->SuggestPlayers(context.prefix, context.linePrefix, out);
+ }
+ }
+}
+
+
diff --git a/Minecraft.Server/Console/commands/gamemode/CliCommandGamemode.h b/Minecraft.Server/Console/commands/gamemode/CliCommandGamemode.h
new file mode 100644
index 00000000..527bb1f9
--- /dev/null
+++ b/Minecraft.Server/Console/commands/gamemode/CliCommandGamemode.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "..\IServerCliCommand.h"
+
+namespace ServerRuntime
+{
+ class CliCommandGamemode : public IServerCliCommand
+ {
+ public:
+ const char *Name() const override;
+ std::vector<std::string> Aliases() const override;
+ const char *Usage() const override;
+ const char *Description() const override;
+ bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) override;
+ void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const override;
+ };
+}
+
diff --git a/Minecraft.Server/Console/commands/give/CliCommandGive.cpp b/Minecraft.Server/Console/commands/give/CliCommandGive.cpp
new file mode 100644
index 00000000..20c09497
--- /dev/null
+++ b/Minecraft.Server/Console/commands/give/CliCommandGive.cpp
@@ -0,0 +1,103 @@
+#include "stdafx.h"
+
+#include "CliCommandGive.h"
+
+#include "..\..\ServerCliEngine.h"
+#include "..\..\ServerCliParser.h"
+#include "..\CommandParsing.h"
+#include "..\..\..\..\Minecraft.World\GameCommandPacket.h"
+#include "..\..\..\..\Minecraft.World\GiveItemCommand.h"
+#include "..\..\..\..\Minecraft.World\net.minecraft.world.entity.player.h"
+#include "..\..\..\..\Minecraft.Client\ServerPlayer.h"
+
+namespace ServerRuntime
+{
+ namespace
+ {
+ constexpr const char *kGiveUsage = "give <player> <itemId> [amount] [aux]";
+ }
+
+ const char *CliCommandGive::Name() const
+ {
+ return "give";
+ }
+
+ const char *CliCommandGive::Usage() const
+ {
+ return kGiveUsage;
+ }
+
+ const char *CliCommandGive::Description() const
+ {
+ return "Give an item via Minecraft.World command dispatcher.";
+ }
+
+ bool CliCommandGive::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
+ {
+ if (line.tokens.size() < 3 || line.tokens.size() > 5)
+ {
+ engine->LogWarn(std::string("Usage: ") + kGiveUsage);
+ return false;
+ }
+
+ std::shared_ptr<ServerPlayer> target = engine->FindPlayerByNameUtf8(line.tokens[1]);
+ if (target == nullptr)
+ {
+ engine->LogWarn("Unknown player: " + line.tokens[1]);
+ return false;
+ }
+
+ int itemId = 0;
+ int amount = 1;
+ int aux = 0;
+ if (!CommandParsing::TryParseInt(line.tokens[2], &itemId))
+ {
+ engine->LogWarn("Invalid item id: " + line.tokens[2]);
+ return false;
+ }
+ if (itemId <= 0)
+ {
+ engine->LogWarn("Item id must be greater than 0.");
+ return false;
+ }
+ if (line.tokens.size() >= 4 && !CommandParsing::TryParseInt(line.tokens[3], &amount))
+ {
+ engine->LogWarn("Invalid amount: " + line.tokens[3]);
+ return false;
+ }
+ if (line.tokens.size() >= 5 && !CommandParsing::TryParseInt(line.tokens[4], &aux))
+ {
+ engine->LogWarn("Invalid aux value: " + line.tokens[4]);
+ return false;
+ }
+ if (amount <= 0)
+ {
+ engine->LogWarn("Amount must be greater than 0.");
+ return false;
+ }
+
+ std::shared_ptr<Player> player = std::dynamic_pointer_cast<Player>(target);
+ if (player == nullptr)
+ {
+ engine->LogWarn("Cannot resolve target player entity.");
+ return false;
+ }
+
+ std::shared_ptr<GameCommandPacket> packet = GiveItemCommand::preparePacket(player, itemId, amount, aux);
+ if (packet == nullptr)
+ {
+ engine->LogError("Failed to build give command packet.");
+ return false;
+ }
+
+ return engine->DispatchWorldCommand(packet->command, packet->data);
+ }
+
+ void CliCommandGive::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
+ {
+ if (context.currentTokenIndex == 1)
+ {
+ engine->SuggestPlayers(context.prefix, context.linePrefix, out);
+ }
+ }
+}
diff --git a/Minecraft.Server/Console/commands/give/CliCommandGive.h b/Minecraft.Server/Console/commands/give/CliCommandGive.h
new file mode 100644
index 00000000..7c21d997
--- /dev/null
+++ b/Minecraft.Server/Console/commands/give/CliCommandGive.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include "..\IServerCliCommand.h"
+
+namespace ServerRuntime
+{
+ class CliCommandGive : public IServerCliCommand
+ {
+ public:
+ const char *Name() const override;
+ const char *Usage() const override;
+ const char *Description() const override;
+ bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) override;
+ void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const override;
+ };
+}
diff --git a/Minecraft.Server/Console/commands/help/CliCommandHelp.cpp b/Minecraft.Server/Console/commands/help/CliCommandHelp.cpp
new file mode 100644
index 00000000..d4106a9c
--- /dev/null
+++ b/Minecraft.Server/Console/commands/help/CliCommandHelp.cpp
@@ -0,0 +1,46 @@
+#include "stdafx.h"
+
+#include "CliCommandHelp.h"
+
+#include "..\..\ServerCliEngine.h"
+#include "..\..\ServerCliRegistry.h"
+
+namespace ServerRuntime
+{
+ const char *CliCommandHelp::Name() const
+ {
+ return "help";
+ }
+
+ std::vector<std::string> CliCommandHelp::Aliases() const
+ {
+ return { "?" };
+ }
+
+ const char *CliCommandHelp::Usage() const
+ {
+ return "help";
+ }
+
+ const char *CliCommandHelp::Description() const
+ {
+ return "Show available server console commands.";
+ }
+
+ bool CliCommandHelp::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
+ {
+ (void)line;
+ const std::vector<std::unique_ptr<IServerCliCommand>> &commands = engine->Registry().Commands();
+ engine->LogInfo("Available commands:");
+ for (size_t i = 0; i < commands.size(); ++i)
+ {
+ std::string row = " ";
+ row += commands[i]->Usage();
+ row += " - ";
+ row += commands[i]->Description();
+ engine->LogInfo(row);
+ }
+ return true;
+ }
+}
+
diff --git a/Minecraft.Server/Console/commands/help/CliCommandHelp.h b/Minecraft.Server/Console/commands/help/CliCommandHelp.h
new file mode 100644
index 00000000..3612442f
--- /dev/null
+++ b/Minecraft.Server/Console/commands/help/CliCommandHelp.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include "..\IServerCliCommand.h"
+
+namespace ServerRuntime
+{
+ class CliCommandHelp : public IServerCliCommand
+ {
+ public:
+ virtual const char *Name() const;
+ virtual std::vector<std::string> Aliases() const;
+ virtual const char *Usage() const;
+ virtual const char *Description() const;
+ virtual bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine);
+ };
+}
+
diff --git a/Minecraft.Server/Console/commands/kill/CliCommandKill.cpp b/Minecraft.Server/Console/commands/kill/CliCommandKill.cpp
new file mode 100644
index 00000000..04b2c419
--- /dev/null
+++ b/Minecraft.Server/Console/commands/kill/CliCommandKill.cpp
@@ -0,0 +1,64 @@
+#include "stdafx.h"
+
+#include "CliCommandKill.h"
+
+#include "..\..\ServerCliEngine.h"
+#include "..\..\ServerCliParser.h"
+#include "..\..\..\..\Minecraft.World\CommandSender.h"
+#include "..\..\..\..\Minecraft.Client\ServerPlayer.h"
+
+namespace ServerRuntime
+{
+ namespace
+ {
+ constexpr const char *kKillUsage = "kill <player>";
+ }
+
+ const char *CliCommandKill::Name() const
+ {
+ return "kill";
+ }
+
+ const char *CliCommandKill::Usage() const
+ {
+ return kKillUsage;
+ }
+
+ const char *CliCommandKill::Description() const
+ {
+ return "Kill a player via Minecraft.World command dispatcher.";
+ }
+
+ bool CliCommandKill::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
+ {
+ if (line.tokens.size() != 2)
+ {
+ engine->LogWarn(std::string("Usage: ") + kKillUsage);
+ return false;
+ }
+
+ std::shared_ptr<ServerPlayer> target = engine->FindPlayerByNameUtf8(line.tokens[1]);
+ if (target == nullptr)
+ {
+ engine->LogWarn("Unknown player: " + line.tokens[1]);
+ return false;
+ }
+
+ std::shared_ptr<CommandSender> sender = std::dynamic_pointer_cast<CommandSender>(target);
+ if (sender == nullptr)
+ {
+ engine->LogWarn("Cannot resolve target command sender.");
+ return false;
+ }
+
+ return engine->DispatchWorldCommand(eGameCommand_Kill, byteArray(), sender);
+ }
+
+ void CliCommandKill::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
+ {
+ if (context.currentTokenIndex == 1)
+ {
+ engine->SuggestPlayers(context.prefix, context.linePrefix, out);
+ }
+ }
+}
diff --git a/Minecraft.Server/Console/commands/kill/CliCommandKill.h b/Minecraft.Server/Console/commands/kill/CliCommandKill.h
new file mode 100644
index 00000000..e558fac0
--- /dev/null
+++ b/Minecraft.Server/Console/commands/kill/CliCommandKill.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include "..\IServerCliCommand.h"
+
+namespace ServerRuntime
+{
+ class CliCommandKill : public IServerCliCommand
+ {
+ public:
+ const char *Name() const override;
+ const char *Usage() const override;
+ const char *Description() const override;
+ bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) override;
+ void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const override;
+ };
+}
diff --git a/Minecraft.Server/Console/commands/list/CliCommandList.cpp b/Minecraft.Server/Console/commands/list/CliCommandList.cpp
new file mode 100644
index 00000000..a9c5a212
--- /dev/null
+++ b/Minecraft.Server/Console/commands/list/CliCommandList.cpp
@@ -0,0 +1,49 @@
+#include "stdafx.h"
+
+#include "CliCommandList.h"
+
+#include "..\..\ServerCliEngine.h"
+#include "..\..\..\Common\StringUtils.h"
+#include "..\..\..\..\Minecraft.Client\MinecraftServer.h"
+#include "..\..\..\..\Minecraft.Client\PlayerList.h"
+
+namespace ServerRuntime
+{
+ const char *CliCommandList::Name() const
+ {
+ return "list";
+ }
+
+ const char *CliCommandList::Usage() const
+ {
+ return "list";
+ }
+
+ const char *CliCommandList::Description() const
+ {
+ return "List connected players.";
+ }
+
+ bool CliCommandList::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
+ {
+ (void)line;
+ MinecraftServer *server = MinecraftServer::getInstance();
+ if (server == NULL || server->getPlayers() == NULL)
+ {
+ engine->LogWarn("Player list is not available.");
+ return false;
+ }
+
+ PlayerList *players = server->getPlayers();
+ std::string names = StringUtils::WideToUtf8(players->getPlayerNames());
+ if (names.empty())
+ {
+ names = "(none)";
+ }
+
+ engine->LogInfo("Players (" + std::to_string(players->getPlayerCount()) + "): " + names);
+ return true;
+ }
+}
+
+
diff --git a/Minecraft.Server/Console/commands/list/CliCommandList.h b/Minecraft.Server/Console/commands/list/CliCommandList.h
new file mode 100644
index 00000000..ad26dcbc
--- /dev/null
+++ b/Minecraft.Server/Console/commands/list/CliCommandList.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include "..\IServerCliCommand.h"
+
+namespace ServerRuntime
+{
+ class CliCommandList : public IServerCliCommand
+ {
+ public:
+ virtual const char *Name() const;
+ virtual const char *Usage() const;
+ virtual const char *Description() const;
+ virtual bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine);
+ };
+}
+
diff --git a/Minecraft.Server/Console/commands/pardon-ip/CliCommandPardonIp.cpp b/Minecraft.Server/Console/commands/pardon-ip/CliCommandPardonIp.cpp
new file mode 100644
index 00000000..3517dbd8
--- /dev/null
+++ b/Minecraft.Server/Console/commands/pardon-ip/CliCommandPardonIp.cpp
@@ -0,0 +1,98 @@
+#include "stdafx.h"
+
+#include "CliCommandPardonIp.h"
+
+#include "..\..\ServerCliEngine.h"
+#include "..\..\ServerCliParser.h"
+#include "..\..\..\Access\Access.h"
+#include "..\..\..\Common\NetworkUtils.h"
+#include "..\..\..\Common\StringUtils.h"
+
+namespace ServerRuntime
+{
+ const char *CliCommandPardonIp::Name() const
+ {
+ return "pardon-ip";
+ }
+
+ const char *CliCommandPardonIp::Usage() const
+ {
+ return "pardon-ip <address>";
+ }
+
+ const char *CliCommandPardonIp::Description() const
+ {
+ return "Remove an IP ban.";
+ }
+
+ /**
+ * Validates the literal IP argument and removes the matching Access IP ban entry
+ * リテラルIPを検証して一致するIP BANを解除する
+ */
+ bool CliCommandPardonIp::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
+ {
+ if (line.tokens.size() != 2)
+ {
+ engine->LogWarn("Usage: pardon-ip <address>");
+ return false;
+ }
+ if (!ServerRuntime::Access::IsInitialized())
+ {
+ engine->LogWarn("Access manager is not initialized.");
+ return false;
+ }
+
+ // Java Edition pardon-ip only operates on a literal address, so do not resolve player names here.
+ const std::string ip = StringUtils::TrimAscii(line.tokens[1]);
+ if (!NetworkUtils::IsIpLiteral(ip))
+ {
+ engine->LogWarn("Invalid IP address: " + line.tokens[1]);
+ return false;
+ }
+ // Distinguish invalid input from a valid but currently unbanned address for clearer operator feedback.
+ if (!ServerRuntime::Access::IsIpBanned(ip))
+ {
+ engine->LogWarn("That IP address is not banned.");
+ return false;
+ }
+ if (!ServerRuntime::Access::RemoveIpBan(ip))
+ {
+ engine->LogError("Failed to remove IP ban.");
+ return false;
+ }
+
+ engine->LogInfo("Unbanned IP address " + ip + ".");
+ return true;
+ }
+
+ /**
+ * Suggests currently banned IP addresses for the Java Edition literal-IP argument
+ * BAN済みIPの補完候補を返す
+ */
+ void CliCommandPardonIp::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
+ {
+ (void)engine;
+ // Complete from the persisted IP-ban snapshot because this command only accepts already-banned literals.
+ if (context.currentTokenIndex != 1 || out == nullptr)
+ {
+ return;
+ }
+
+ std::vector<ServerRuntime::Access::BannedIpEntry> entries;
+ if (!ServerRuntime::Access::SnapshotBannedIps(&entries))
+ {
+ return;
+ }
+
+ // Reuse the normalized prefix match used by other commands so completion stays case-insensitive.
+ for (const auto &entry : entries)
+ {
+ const std::string &candidate = entry.ip;
+ if (StringUtils::StartsWithIgnoreCase(candidate, context.prefix))
+ {
+ out->push_back(context.linePrefix + candidate);
+ }
+ }
+ }
+}
+
diff --git a/Minecraft.Server/Console/commands/pardon-ip/CliCommandPardonIp.h b/Minecraft.Server/Console/commands/pardon-ip/CliCommandPardonIp.h
new file mode 100644
index 00000000..96f4c7fc
--- /dev/null
+++ b/Minecraft.Server/Console/commands/pardon-ip/CliCommandPardonIp.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "..\IServerCliCommand.h"
+
+namespace ServerRuntime
+{
+ /**
+ * Removes a dedicated-server IP ban using Java Edition style syntax and Access-backed persistence
+ */
+ class CliCommandPardonIp : public IServerCliCommand
+ {
+ public:
+ virtual const char *Name() const;
+ virtual const char *Usage() const;
+ virtual const char *Description() const;
+ virtual bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine);
+ virtual void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const;
+ };
+}
diff --git a/Minecraft.Server/Console/commands/pardon/CliCommandPardon.cpp b/Minecraft.Server/Console/commands/pardon/CliCommandPardon.cpp
new file mode 100644
index 00000000..d1e995e9
--- /dev/null
+++ b/Minecraft.Server/Console/commands/pardon/CliCommandPardon.cpp
@@ -0,0 +1,173 @@
+#include "stdafx.h"
+
+#include "CliCommandPardon.h"
+
+#include "..\..\ServerCliEngine.h"
+#include "..\..\ServerCliParser.h"
+#include "..\..\..\Access\Access.h"
+#include "..\..\..\Common\StringUtils.h"
+#include "..\..\..\..\Minecraft.Client\ServerPlayer.h"
+
+#include <algorithm>
+
+namespace ServerRuntime
+{
+ namespace
+ {
+ static void AppendUniqueText(const std::string &text, std::vector<std::string> *out)
+ {
+ if (out == nullptr || text.empty())
+ {
+ return;
+ }
+
+ if (std::find(out->begin(), out->end(), text) == out->end())
+ {
+ out->push_back(text);
+ }
+ }
+
+ static void AppendUniqueXuid(PlayerUID xuid, std::vector<PlayerUID> *out)
+ {
+ if (out == nullptr || xuid == INVALID_XUID)
+ {
+ return;
+ }
+
+ if (std::find(out->begin(), out->end(), xuid) == out->end())
+ {
+ out->push_back(xuid);
+ }
+ }
+ }
+
+ const char *CliCommandPardon::Name() const
+ {
+ return "pardon";
+ }
+
+ const char *CliCommandPardon::Usage() const
+ {
+ return "pardon <player>";
+ }
+
+ const char *CliCommandPardon::Description() const
+ {
+ return "Remove a player ban.";
+ }
+
+ /**
+ * Removes every Access ban entry that matches the requested player name so dual-XUID entries are cleared together
+ * 名前に一致するBANをまとめて解除する
+ */
+ bool CliCommandPardon::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
+ {
+ if (line.tokens.size() != 2)
+ {
+ engine->LogWarn("Usage: pardon <player>");
+ return false;
+ }
+ if (!ServerRuntime::Access::IsInitialized())
+ {
+ engine->LogWarn("Access manager is not initialized.");
+ return false;
+ }
+
+ std::vector<PlayerUID> xuidsToRemove;
+ std::vector<std::string> matchedNames;
+ std::shared_ptr<ServerPlayer> onlineTarget = engine->FindPlayerByNameUtf8(line.tokens[1]);
+ if (onlineTarget != nullptr)
+ {
+ if (ServerRuntime::Access::IsPlayerBanned(onlineTarget->getXuid()))
+ {
+ AppendUniqueXuid(onlineTarget->getXuid(), &xuidsToRemove);
+ }
+ if (ServerRuntime::Access::IsPlayerBanned(onlineTarget->getOnlineXuid()))
+ {
+ AppendUniqueXuid(onlineTarget->getOnlineXuid(), &xuidsToRemove);
+ }
+ }
+
+ std::vector<ServerRuntime::Access::BannedPlayerEntry> entries;
+ if (!ServerRuntime::Access::SnapshotBannedPlayers(&entries))
+ {
+ engine->LogError("Failed to read banned players.");
+ return false;
+ }
+
+ const std::string loweredTarget = StringUtils::ToLowerAscii(line.tokens[1]);
+ for (const auto &entry : entries)
+ {
+ if (StringUtils::ToLowerAscii(entry.name) == loweredTarget)
+ {
+ PlayerUID parsedXuid = INVALID_XUID;
+ if (ServerRuntime::Access::TryParseXuid(entry.xuid, &parsedXuid))
+ {
+ AppendUniqueXuid(parsedXuid, &xuidsToRemove);
+ }
+ AppendUniqueText(entry.name, &matchedNames);
+ }
+ }
+
+ if (xuidsToRemove.empty())
+ {
+ engine->LogWarn("That player is not banned.");
+ return false;
+ }
+
+ for (const auto xuid : xuidsToRemove)
+ {
+ if (!ServerRuntime::Access::RemovePlayerBan(xuid))
+ {
+ engine->LogError("Failed to remove player ban.");
+ return false;
+ }
+ }
+
+ std::string playerName = line.tokens[1];
+ if (!matchedNames.empty())
+ {
+ playerName = matchedNames[0];
+ }
+ else if (onlineTarget != nullptr)
+ {
+ playerName = StringUtils::WideToUtf8(onlineTarget->getName());
+ }
+
+ engine->LogInfo("Unbanned player " + playerName + ".");
+ return true;
+ }
+
+ /**
+ * Suggests currently banned player names first and then online names for convenience
+ * BAN済み名とオンライン名を補完候補に出す
+ */
+ void CliCommandPardon::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
+ {
+ if (context.currentTokenIndex != 1 || out == nullptr)
+ {
+ return;
+ }
+
+ std::vector<ServerRuntime::Access::BannedPlayerEntry> entries;
+ if (ServerRuntime::Access::SnapshotBannedPlayers(&entries))
+ {
+ std::vector<std::string> names;
+ for (const auto &entry : entries)
+ {
+ AppendUniqueText(entry.name, &names);
+ }
+
+ for (const auto &name : names)
+ {
+ if (StringUtils::StartsWithIgnoreCase(name, context.prefix))
+ {
+ out->push_back(context.linePrefix + name);
+ }
+ }
+ }
+
+ engine->SuggestPlayers(context.prefix, context.linePrefix, out);
+ }
+}
+
diff --git a/Minecraft.Server/Console/commands/pardon/CliCommandPardon.h b/Minecraft.Server/Console/commands/pardon/CliCommandPardon.h
new file mode 100644
index 00000000..a171d428
--- /dev/null
+++ b/Minecraft.Server/Console/commands/pardon/CliCommandPardon.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "..\IServerCliCommand.h"
+
+namespace ServerRuntime
+{
+ /**
+ * Removes dedicated-server player bans using Java Edition style syntax and Access-backed persistence
+ */
+ class CliCommandPardon : public IServerCliCommand
+ {
+ public:
+ virtual const char *Name() const;
+ virtual const char *Usage() const;
+ virtual const char *Description() const;
+ virtual bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine);
+ virtual void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const;
+ };
+}
diff --git a/Minecraft.Server/Console/commands/stop/CliCommandStop.cpp b/Minecraft.Server/Console/commands/stop/CliCommandStop.cpp
new file mode 100644
index 00000000..29e42cd9
--- /dev/null
+++ b/Minecraft.Server/Console/commands/stop/CliCommandStop.cpp
@@ -0,0 +1,32 @@
+#include "stdafx.h"
+
+#include "CliCommandStop.h"
+
+#include "..\..\ServerCliEngine.h"
+
+namespace ServerRuntime
+{
+ const char *CliCommandStop::Name() const
+ {
+ return "stop";
+ }
+
+ const char *CliCommandStop::Usage() const
+ {
+ return "stop";
+ }
+
+ const char *CliCommandStop::Description() const
+ {
+ return "Stop the dedicated server.";
+ }
+
+ bool CliCommandStop::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
+ {
+ (void)line;
+ engine->LogInfo("Stopping server...");
+ engine->RequestShutdown();
+ return true;
+ }
+}
+
diff --git a/Minecraft.Server/Console/commands/stop/CliCommandStop.h b/Minecraft.Server/Console/commands/stop/CliCommandStop.h
new file mode 100644
index 00000000..2297c673
--- /dev/null
+++ b/Minecraft.Server/Console/commands/stop/CliCommandStop.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include "..\IServerCliCommand.h"
+
+namespace ServerRuntime
+{
+ class CliCommandStop : public IServerCliCommand
+ {
+ public:
+ virtual const char *Name() const;
+ virtual const char *Usage() const;
+ virtual const char *Description() const;
+ virtual bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine);
+ };
+}
+
diff --git a/Minecraft.Server/Console/commands/time/CliCommandTime.cpp b/Minecraft.Server/Console/commands/time/CliCommandTime.cpp
new file mode 100644
index 00000000..d274993c
--- /dev/null
+++ b/Minecraft.Server/Console/commands/time/CliCommandTime.cpp
@@ -0,0 +1,118 @@
+#include "stdafx.h"
+
+#include "CliCommandTime.h"
+
+#include "..\..\ServerCliEngine.h"
+#include "..\..\ServerCliParser.h"
+#include "..\..\..\Common\StringUtils.h"
+#include "..\..\..\..\Minecraft.World\GameCommandPacket.h"
+#include "..\..\..\..\Minecraft.World\TimeCommand.h"
+
+namespace ServerRuntime
+{
+ namespace
+ {
+ constexpr const char *kTimeUsage = "time <day|night|set day|set night>";
+
+ static bool TryResolveNightFlag(const std::vector<std::string> &tokens, bool *outNight)
+ {
+ if (outNight == nullptr)
+ {
+ return false;
+ }
+
+ std::string value;
+ if (tokens.size() == 2)
+ {
+ value = StringUtils::ToLowerAscii(tokens[1]);
+ }
+ else if (tokens.size() == 3 && StringUtils::ToLowerAscii(tokens[1]) == "set")
+ {
+ value = StringUtils::ToLowerAscii(tokens[2]);
+ }
+ else
+ {
+ return false;
+ }
+
+ if (value == "day")
+ {
+ *outNight = false;
+ return true;
+ }
+ if (value == "night")
+ {
+ *outNight = true;
+ return true;
+ }
+
+ return false;
+ }
+
+ static void SuggestLiteral(const char *candidate, const ServerCliCompletionContext &context, std::vector<std::string> *out)
+ {
+ if (candidate == nullptr || out == nullptr)
+ {
+ return;
+ }
+
+ const std::string text(candidate);
+ if (StringUtils::StartsWithIgnoreCase(text, context.prefix))
+ {
+ out->push_back(context.linePrefix + text);
+ }
+ }
+ }
+
+ const char *CliCommandTime::Name() const
+ {
+ return "time";
+ }
+
+ const char *CliCommandTime::Usage() const
+ {
+ return kTimeUsage;
+ }
+
+ const char *CliCommandTime::Description() const
+ {
+ return "Set day or night via Minecraft.World command dispatcher.";
+ }
+
+ bool CliCommandTime::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
+ {
+ bool night = false;
+ if (!TryResolveNightFlag(line.tokens, &night))
+ {
+ engine->LogWarn(std::string("Usage: ") + kTimeUsage);
+ return false;
+ }
+
+ std::shared_ptr<GameCommandPacket> packet = TimeCommand::preparePacket(night);
+ if (packet == nullptr)
+ {
+ engine->LogError("Failed to build time command packet.");
+ return false;
+ }
+
+ return engine->DispatchWorldCommand(packet->command, packet->data);
+ }
+
+ void CliCommandTime::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
+ {
+ (void)engine;
+ if (context.currentTokenIndex == 1)
+ {
+ SuggestLiteral("day", context, out);
+ SuggestLiteral("night", context, out);
+ SuggestLiteral("set", context, out);
+ }
+ else if (context.currentTokenIndex == 2 &&
+ context.parsed.tokens.size() >= 2 &&
+ StringUtils::ToLowerAscii(context.parsed.tokens[1]) == "set")
+ {
+ SuggestLiteral("day", context, out);
+ SuggestLiteral("night", context, out);
+ }
+ }
+}
diff --git a/Minecraft.Server/Console/commands/time/CliCommandTime.h b/Minecraft.Server/Console/commands/time/CliCommandTime.h
new file mode 100644
index 00000000..28cf5e5a
--- /dev/null
+++ b/Minecraft.Server/Console/commands/time/CliCommandTime.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include "..\IServerCliCommand.h"
+
+namespace ServerRuntime
+{
+ class CliCommandTime : public IServerCliCommand
+ {
+ public:
+ const char *Name() const override;
+ const char *Usage() const override;
+ const char *Description() const override;
+ bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) override;
+ void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const override;
+ };
+}
diff --git a/Minecraft.Server/Console/commands/tp/CliCommandTp.cpp b/Minecraft.Server/Console/commands/tp/CliCommandTp.cpp
new file mode 100644
index 00000000..45dbb284
--- /dev/null
+++ b/Minecraft.Server/Console/commands/tp/CliCommandTp.cpp
@@ -0,0 +1,82 @@
+#include "stdafx.h"
+
+#include "CliCommandTp.h"
+
+#include "..\..\ServerCliEngine.h"
+#include "..\..\ServerCliParser.h"
+#include "..\..\..\..\Minecraft.Client\PlayerConnection.h"
+#include "..\..\..\..\Minecraft.Client\TeleportCommand.h"
+#include "..\..\..\..\Minecraft.Client\ServerPlayer.h"
+#include "..\..\..\..\Minecraft.World\GameCommandPacket.h"
+
+namespace ServerRuntime
+{
+ namespace
+ {
+ constexpr const char *kTpUsage = "tp <player> <target>";
+ }
+
+ const char *CliCommandTp::Name() const
+ {
+ return "tp";
+ }
+
+ std::vector<std::string> CliCommandTp::Aliases() const
+ {
+ return { "teleport" };
+ }
+
+ const char *CliCommandTp::Usage() const
+ {
+ return kTpUsage;
+ }
+
+ const char *CliCommandTp::Description() const
+ {
+ return "Teleport one player to another via Minecraft.World command dispatcher.";
+ }
+
+ bool CliCommandTp::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
+ {
+ if (line.tokens.size() != 3)
+ {
+ engine->LogWarn(std::string("Usage: ") + kTpUsage);
+ return false;
+ }
+
+ std::shared_ptr<ServerPlayer> subject = engine->FindPlayerByNameUtf8(line.tokens[1]);
+ std::shared_ptr<ServerPlayer> destination = engine->FindPlayerByNameUtf8(line.tokens[2]);
+ if (subject == nullptr)
+ {
+ engine->LogWarn("Unknown player: " + line.tokens[1]);
+ return false;
+ }
+ if (destination == nullptr)
+ {
+ engine->LogWarn("Unknown player: " + line.tokens[2]);
+ return false;
+ }
+ if (subject->connection == nullptr)
+ {
+ engine->LogWarn("Cannot teleport because source player connection is inactive.");
+ return false;
+ }
+ std::shared_ptr<GameCommandPacket> packet = TeleportCommand::preparePacket(subject->getXuid(), destination->getXuid());
+ if (packet == nullptr)
+ {
+ engine->LogError("Failed to build teleport command packet.");
+ return false;
+ }
+
+ return engine->DispatchWorldCommand(packet->command, packet->data);
+ }
+
+ void CliCommandTp::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
+ {
+ if (context.currentTokenIndex == 1 || context.currentTokenIndex == 2)
+ {
+ engine->SuggestPlayers(context.prefix, context.linePrefix, out);
+ }
+ }
+}
+
diff --git a/Minecraft.Server/Console/commands/tp/CliCommandTp.h b/Minecraft.Server/Console/commands/tp/CliCommandTp.h
new file mode 100644
index 00000000..6e9ffdd7
--- /dev/null
+++ b/Minecraft.Server/Console/commands/tp/CliCommandTp.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "..\IServerCliCommand.h"
+
+namespace ServerRuntime
+{
+ class CliCommandTp : public IServerCliCommand
+ {
+ public:
+ const char *Name() const override;
+ std::vector<std::string> Aliases() const override;
+ const char *Usage() const override;
+ const char *Description() const override;
+ bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) override;
+ void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const override;
+ };
+}
+
diff --git a/Minecraft.Server/Console/commands/weather/CliCommandWeather.cpp b/Minecraft.Server/Console/commands/weather/CliCommandWeather.cpp
new file mode 100644
index 00000000..e7f01954
--- /dev/null
+++ b/Minecraft.Server/Console/commands/weather/CliCommandWeather.cpp
@@ -0,0 +1,49 @@
+#include "stdafx.h"
+
+#include "CliCommandWeather.h"
+
+#include "..\..\ServerCliEngine.h"
+#include "..\..\ServerCliParser.h"
+#include "..\..\..\..\Minecraft.World\GameCommandPacket.h"
+#include "..\..\..\..\Minecraft.World\ToggleDownfallCommand.h"
+
+namespace ServerRuntime
+{
+ namespace
+ {
+ constexpr const char *kWeatherUsage = "weather";
+ }
+
+ const char *CliCommandWeather::Name() const
+ {
+ return "weather";
+ }
+
+ const char *CliCommandWeather::Usage() const
+ {
+ return kWeatherUsage;
+ }
+
+ const char *CliCommandWeather::Description() const
+ {
+ return "Toggle weather via Minecraft.World command dispatcher.";
+ }
+
+ bool CliCommandWeather::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
+ {
+ if (line.tokens.size() != 1)
+ {
+ engine->LogWarn(std::string("Usage: ") + kWeatherUsage);
+ return false;
+ }
+
+ std::shared_ptr<GameCommandPacket> packet = ToggleDownfallCommand::preparePacket();
+ if (packet == nullptr)
+ {
+ engine->LogError("Failed to build weather command packet.");
+ return false;
+ }
+
+ return engine->DispatchWorldCommand(packet->command, packet->data);
+ }
+}
diff --git a/Minecraft.Server/Console/commands/weather/CliCommandWeather.h b/Minecraft.Server/Console/commands/weather/CliCommandWeather.h
new file mode 100644
index 00000000..03498b47
--- /dev/null
+++ b/Minecraft.Server/Console/commands/weather/CliCommandWeather.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "..\IServerCliCommand.h"
+
+namespace ServerRuntime
+{
+ class CliCommandWeather : public IServerCliCommand
+ {
+ public:
+ const char *Name() const override;
+ const char *Usage() const override;
+ const char *Description() const override;
+ bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) override;
+ };
+}
diff --git a/Minecraft.Server/Console/commands/whitelist/CliCommandWhitelist.cpp b/Minecraft.Server/Console/commands/whitelist/CliCommandWhitelist.cpp
new file mode 100644
index 00000000..03724278
--- /dev/null
+++ b/Minecraft.Server/Console/commands/whitelist/CliCommandWhitelist.cpp
@@ -0,0 +1,285 @@
+#include "stdafx.h"
+
+#include "CliCommandWhitelist.h"
+
+#include "..\..\ServerCliEngine.h"
+#include "..\..\ServerCliParser.h"
+#include "..\..\..\Access\Access.h"
+#include "..\..\..\Common\StringUtils.h"
+#include "..\..\..\ServerProperties.h"
+
+#include <algorithm>
+#include <array>
+
+namespace ServerRuntime
+{
+ namespace
+ {
+ static const char *kWhitelistUsage = "whitelist <on|off|list|add|remove|reload> [...]";
+
+ static bool CompareWhitelistEntries(const ServerRuntime::Access::WhitelistedPlayerEntry &left, const ServerRuntime::Access::WhitelistedPlayerEntry &right)
+ {
+ const auto leftName = StringUtils::ToLowerAscii(left.name);
+ const auto rightName = StringUtils::ToLowerAscii(right.name);
+ if (leftName != rightName)
+ {
+ return leftName < rightName;
+ }
+
+ return StringUtils::ToLowerAscii(left.xuid) < StringUtils::ToLowerAscii(right.xuid);
+ }
+
+ static bool PersistWhitelistToggle(bool enabled)
+ {
+ auto config = LoadServerPropertiesConfig();
+ config.whiteListEnabled = enabled;
+ return SaveServerPropertiesConfig(config);
+ }
+
+ static std::string BuildWhitelistEntryRow(const ServerRuntime::Access::WhitelistedPlayerEntry &entry)
+ {
+ std::string row = " ";
+ row += entry.xuid;
+ if (!entry.name.empty())
+ {
+ row += " - ";
+ row += entry.name;
+ }
+ return row;
+ }
+
+ static void LogWhitelistMode(ServerCliEngine *engine)
+ {
+ engine->LogInfo(std::string("Whitelist is ") + (ServerRuntime::Access::IsWhitelistEnabled() ? "enabled." : "disabled."));
+ }
+
+ static bool LogWhitelistEntries(ServerCliEngine *engine)
+ {
+ std::vector<ServerRuntime::Access::WhitelistedPlayerEntry> entries;
+ if (!ServerRuntime::Access::SnapshotWhitelistedPlayers(&entries))
+ {
+ engine->LogError("Failed to read whitelist entries.");
+ return false;
+ }
+
+ std::sort(entries.begin(), entries.end(), CompareWhitelistEntries);
+ LogWhitelistMode(engine);
+ engine->LogInfo("There are " + std::to_string(entries.size()) + " whitelisted player(s).");
+ for (const auto &entry : entries)
+ {
+ engine->LogInfo(BuildWhitelistEntryRow(entry));
+ }
+ return true;
+ }
+
+ static bool TryParseWhitelistXuid(const std::string &text, ServerCliEngine *engine, PlayerUID *outXuid)
+ {
+ if (ServerRuntime::Access::TryParseXuid(text, outXuid))
+ {
+ return true;
+ }
+
+ engine->LogWarn("Invalid XUID: " + text);
+ return false;
+ }
+
+ static void SuggestLiteral(const std::string &candidate, const ServerCliCompletionContext &context, std::vector<std::string> *out)
+ {
+ if (out == nullptr)
+ {
+ return;
+ }
+
+ if (StringUtils::StartsWithIgnoreCase(candidate, context.prefix))
+ {
+ out->push_back(context.linePrefix + candidate);
+ }
+ }
+ }
+
+ const char *CliCommandWhitelist::Name() const
+ {
+ return "whitelist";
+ }
+
+ const char *CliCommandWhitelist::Usage() const
+ {
+ return kWhitelistUsage;
+ }
+
+ const char *CliCommandWhitelist::Description() const
+ {
+ return "Manage the dedicated-server XUID whitelist.";
+ }
+
+ bool CliCommandWhitelist::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
+ {
+ if (line.tokens.size() < 2)
+ {
+ engine->LogWarn(std::string("Usage: ") + kWhitelistUsage);
+ return false;
+ }
+ if (!ServerRuntime::Access::IsInitialized())
+ {
+ engine->LogWarn("Access manager is not initialized.");
+ return false;
+ }
+
+ const auto subcommand = StringUtils::ToLowerAscii(line.tokens[1]);
+ if (subcommand == "on" || subcommand == "off")
+ {
+ if (line.tokens.size() != 2)
+ {
+ engine->LogWarn("Usage: whitelist <on|off>");
+ return false;
+ }
+
+ const bool enabled = (subcommand == "on");
+ if (!PersistWhitelistToggle(enabled))
+ {
+ engine->LogError("Failed to persist whitelist mode to server.properties.");
+ return false;
+ }
+
+ ServerRuntime::Access::SetWhitelistEnabled(enabled);
+ engine->LogInfo(std::string("Whitelist ") + (enabled ? "enabled." : "disabled."));
+ return true;
+ }
+
+ if (subcommand == "list")
+ {
+ if (line.tokens.size() != 2)
+ {
+ engine->LogWarn("Usage: whitelist list");
+ return false;
+ }
+
+ return LogWhitelistEntries(engine);
+ }
+
+ if (subcommand == "reload")
+ {
+ if (line.tokens.size() != 2)
+ {
+ engine->LogWarn("Usage: whitelist reload");
+ return false;
+ }
+ if (!ServerRuntime::Access::ReloadWhitelist())
+ {
+ engine->LogError("Failed to reload whitelist.");
+ return false;
+ }
+
+ const auto config = LoadServerPropertiesConfig();
+ ServerRuntime::Access::SetWhitelistEnabled(config.whiteListEnabled);
+ engine->LogInfo("Reloaded whitelist from disk.");
+ LogWhitelistMode(engine);
+ return true;
+ }
+
+ if (subcommand == "add")
+ {
+ if (line.tokens.size() < 3)
+ {
+ engine->LogWarn("Usage: whitelist add <xuid> [name ...]");
+ return false;
+ }
+
+ PlayerUID xuid = INVALID_XUID;
+ if (!TryParseWhitelistXuid(line.tokens[2], engine, &xuid))
+ {
+ return false;
+ }
+
+ if (ServerRuntime::Access::IsPlayerWhitelisted(xuid))
+ {
+ engine->LogWarn("That XUID is already whitelisted.");
+ return false;
+ }
+
+ const auto metadata = ServerRuntime::Access::WhitelistManager::BuildDefaultMetadata("Console");
+ const auto name = StringUtils::JoinTokens(line.tokens, 3);
+ if (!ServerRuntime::Access::AddWhitelistedPlayer(xuid, name, metadata))
+ {
+ engine->LogError("Failed to write whitelist entry.");
+ return false;
+ }
+
+ std::string message = "Whitelisted XUID " + ServerRuntime::Access::FormatXuid(xuid) + ".";
+ if (!name.empty())
+ {
+ message += " Name: " + name;
+ }
+ engine->LogInfo(message);
+ return true;
+ }
+
+ if (subcommand == "remove")
+ {
+ if (line.tokens.size() != 3)
+ {
+ engine->LogWarn("Usage: whitelist remove <xuid>");
+ return false;
+ }
+
+ PlayerUID xuid = INVALID_XUID;
+ if (!TryParseWhitelistXuid(line.tokens[2], engine, &xuid))
+ {
+ return false;
+ }
+
+ if (!ServerRuntime::Access::IsPlayerWhitelisted(xuid))
+ {
+ engine->LogWarn("That XUID is not whitelisted.");
+ return false;
+ }
+
+ if (!ServerRuntime::Access::RemoveWhitelistedPlayer(xuid))
+ {
+ engine->LogError("Failed to remove whitelist entry.");
+ return false;
+ }
+
+ engine->LogInfo("Removed XUID " + ServerRuntime::Access::FormatXuid(xuid) + " from the whitelist.");
+ return true;
+ }
+
+ engine->LogWarn(std::string("Usage: ") + kWhitelistUsage);
+ return false;
+ }
+
+ void CliCommandWhitelist::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
+ {
+ (void)engine;
+ if (out == nullptr)
+ {
+ return;
+ }
+
+ if (context.currentTokenIndex == 1)
+ {
+ SuggestLiteral("on", context, out);
+ SuggestLiteral("off", context, out);
+ SuggestLiteral("list", context, out);
+ SuggestLiteral("add", context, out);
+ SuggestLiteral("remove", context, out);
+ SuggestLiteral("reload", context, out);
+ return;
+ }
+
+ if (context.currentTokenIndex == 2 && context.parsed.tokens.size() >= 2 && StringUtils::ToLowerAscii(context.parsed.tokens[1]) == "remove")
+ {
+ std::vector<ServerRuntime::Access::WhitelistedPlayerEntry> entries;
+ if (!ServerRuntime::Access::SnapshotWhitelistedPlayers(&entries))
+ {
+ return;
+ }
+
+ for (const auto &entry : entries)
+ {
+ SuggestLiteral(entry.xuid, context, out);
+ }
+ }
+ }
+}
+
diff --git a/Minecraft.Server/Console/commands/whitelist/CliCommandWhitelist.h b/Minecraft.Server/Console/commands/whitelist/CliCommandWhitelist.h
new file mode 100644
index 00000000..45e21a5e
--- /dev/null
+++ b/Minecraft.Server/Console/commands/whitelist/CliCommandWhitelist.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include "..\IServerCliCommand.h"
+
+namespace ServerRuntime
+{
+ class CliCommandWhitelist : public IServerCliCommand
+ {
+ public:
+ const char *Name() const override;
+ const char *Usage() const override;
+ const char *Description() const override;
+ bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) override;
+ void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const override;
+ };
+}
+