aboutsummaryrefslogtreecommitdiff
path: root/Minecraft.Server/Console
diff options
context:
space:
mode:
authorkuwa <kuwa.com3@gmail.com>2026-03-15 16:32:50 +0900
committerGitHub <noreply@github.com>2026-03-15 02:32:50 -0500
commitf483074cd2ff2cc9e9c3ef1df4430d4a65d1fb2a (patch)
tree9cd094e38b2259ea913fecb7d6fde16046277182 /Minecraft.Server/Console
parent4d200a589dd3d0a8424eaef6c0d6fd454d16e411 (diff)
Dedicated Server Software - Minecraft.Server.exe (#498)
* add: Dedicated Server implementation - Introduced `ServerMain.cpp` for the dedicated server logic, handling command-line arguments, server initialization, and network management. - Created `postbuild_server.ps1` script for post-build tasks, including copying necessary resources and DLLs for the dedicated server. - Added `CopyServerAssets.cmake` to manage the copying of server assets during the build process, ensuring required files are available for the dedicated server. - Defined project filters in `Minecraft.Server.vcxproj.filters` for better organization of server-related files. * add: refactor world loader & add server properties - Introduced ServerLogger for logging startup steps and world I/O operations. - Implemented ServerProperties for loading and saving server configuration from `server.properties`. - Added WorldManager to handle world loading and creation based on server properties. - Updated ServerMain to integrate server properties loading and world management. - Enhanced project files to include new source and header files for the server components. * update: implement enhanced logging functionality with configurable log levels * update: update keyboard and mouse input initialization 1dc8a005ed111463c22c17b487e5ec8a3e2d30f3 * fix: change virtual screen resolution to 1920x1080(HD) Since 31881af56936aeef38ff322b975fd0 , `skinHud.swf` for 720 is not included in `MediaWindows64.arc`, the app crashes unless the virtual screen is set to HD. * fix: dedicated server build settings for miniaudio migration and missing sources - remove stale Windows64 Miles (mss64) link/copy references from server build - add Common/Filesystem/Filesystem.cpp to Minecraft.Server.vcxproj - add Windows64/PostProcesser.cpp to Minecraft.Server.vcxproj - fix unresolved externals (PostProcesser::*, FileExists) in dedicated server build * update: changed the virtual screen to 720p Since the crash caused by the 720p `skinHud.swf` not being included in `MediaWindows64.arc` has been resolved, switching back to 720p to reduce resource usage. * add: add Docker support for Dedicated Server add with entrypoint and build scripts * fix: add initial save for newly created worlds in dedicated server on the server side, I fixed the behavior introduced after commit aadb511, where newly created worlds are intentionally not saved to disk immediately. * update: add basically all configuration options that are implemented in the classes to `server.properties` * update: add LAN advertising configuration for server.properties LAN-Discovery, which isn’t needed in server mode and could potentially be a security risk, has also been disabled(only server mode). * add: add implementing interactive command line using linenoise - Integrated linenoise library for line editing and completion in the server console. - Updated ServerLogger to handle external writes safely during logging. - Modified ServerMain to initialize and manage the ServerCli for command input. - The implementation is separate from everything else, so it doesn't affect anything else. - The command input section and execution section are separated into threads. * update: enhance command line completion with predictive hints Like most command line tools, it highlights predictions in gray. * add: implement `StringUtils` for string manipulation and refactor usages Unified the scattered utility functions. * fix: send DisconnectPacket on shutdown and fix Win64 recv-thread teardown race Before this change, server/host shutdown closed sockets directly in ServerConnection::stop(), which bypassed the normal disconnect flow. As a result, clients could be dropped without receiving a proper DisconnectPacket during stop/kill/world-close paths. Also, WinsockNetLayer::Shutdown() could destroy synchronization objects while host-side recv threads were still exiting, causing a crash in RecvThreadProc (access violation on world close in host mode). * fix: return client to menus when Win64 host connection drops - Add client-side host disconnect handling in CPlatformNetworkManagerStub::DoWork() for _WINDOWS64. - When in QNET_STATE_GAME_PLAY as a non-host and WinsockNetLayer::IsConnected() becomes false, trigger g_NetworkManager.HandleDisconnect(false) to enter the normal disconnect/UI flow. - Use m_bLeaveGameOnTick as a one-shot guard to prevent repeated disconnect handling while the link remains down. - Reset m_bLeaveGameOnTick on LeaveGame(), HostGame(), and JoinGame() to avoid stale state across sessions. * update: converted Japanese comments to English * add: create `Minecraft.Server` developer guide in English and Japanese * update: add note about issue * add: add `nlohmann/json` json lib * add: add FileUtils Moved file operations to `utils`. * add: Dedicated Server BAN access manager with persistent player and IP bans - add Access frontend that publishes thread-safe ban manager snapshots for dedicated server use - add BanManager storage for banned-players.json and banned-ips.json with load/save/update flows - add persistent player and IP ban checks during dedicated server connection handling - add UTF-8 BOM-safe JSON parsing and shared file helpers backed by nlohmann/json - add Unicode-safe ban file read/write and safer atomic replacement behavior on Windows - add active-ban snapshot APIs and expiry-aware filtering for expires metadata - add RAII-based dedicated access shutdown handling during server startup and teardown * update: changed file read/write operations to use `FileUtils`. - As a side effect, saving has become faster! * fix: Re-added the source that had somehow disappeared. * add: significantly improved the dedicated server logging system - add ServerLogManager to Minecraft.Server as the single entry point for dedicated-server log output - forward CMinecraftApp logger output to the server logger when running with g_Win64DedicatedServer - add named network logs for incoming, accepted, rejected, and disconnected connections - cache connection metadata by smallId so player name and remote IP remain available for disconnect logs - keep Minecraft.Client changes minimal by using lightweight hook points and handling log orchestration on the server side * fix: added the updated library source * add: add `ban` and `pardon` commands for Player and IP * fix: fix stop command shutdown process add dedicated server shutdown request handling * fix: fixed the save logic during server shutdown Removed redundant repeated saves and eliminated the risks of async writes. * update: added new sever files to Docker entrypoint * fix: replace shutdown flag with atomic variable for thread safety * update: update Dedicated Server developer guide English is machine translated. Please forgive me. * update: check for the existence of `GameHDD` and create * add: add Whitelist to Dedicated Server * refactor: clean up and refactor the code - unify duplicated implementations that were copied repeatedly - update outdated patterns to more modern ones * fix: include UI header (new update fix) * fix: fix the detection range for excessive logging `getHighestNonEmptyY()` returning `-1` occurs normally when the chunk is entirely air. The caller (`Minecraft.World/LevelChunk.cpp:2400`) normalizes `-1` to `0`. * update: add world size config to dedicated server properties * update: update README add explanation of `server.properties` & launch arguments * update: add nightly release workflow for dedicated server and client builds to Actions * fix: update name for workflow * add random seed generation * add: add Docker nightly workflow for Dedicated Server publish to GitHub Container Registry * fix: ghost player when clients disconnect out of order #4 * fix: fix 7zip option * fix: fix Docker workflow for Dedicated Server artifact handling * add: add no build Dedicated Server startup scripts and Docker Compose * update: add README for Docker Dedicated Server setup with no local build * refactor: refactor command path structure As the number of commands has increased and become harder to navigate, each command has been organized into separate folders. * update: support stream(file stdin) input mode for server CLI Support for the stream (file stdin) required when attaching a tty to a Docker container on Linux. * add: add new CLI Console Commands for Dedicated Server Most of these commands are executed using the command dispatcher implemented on the `Minecraft.World` side. When registering them with the dispatcher, the sender uses a permission-enabled configuration that treats the CLI as a player. - default game. - enchant - experience. - give - kill(currently, getting a permission error for some reason) - time - weather. - update tp & gamemode command * fix: change player map icon to random select * update: increase the player limit * add: restore the basic anti-cheat implementation and add spawn protection Added the following anti-cheat measures and add spawn protection to `server.properties`. - instant break - speed - reach * fix: fix Docker image tag --------- Co-authored-by: sylvessa <225480449+sylvessa@users.noreply.github.com>
Diffstat (limited to 'Minecraft.Server/Console')
-rw-r--r--Minecraft.Server/Console/ServerCli.cpp44
-rw-r--r--Minecraft.Server/Console/ServerCli.h50
-rw-r--r--Minecraft.Server/Console/ServerCliEngine.cpp395
-rw-r--r--Minecraft.Server/Console/ServerCliEngine.h127
-rw-r--r--Minecraft.Server/Console/ServerCliInput.cpp285
-rw-r--r--Minecraft.Server/Console/ServerCliInput.h63
-rw-r--r--Minecraft.Server/Console/ServerCliParser.cpp116
-rw-r--r--Minecraft.Server/Console/ServerCliParser.h63
-rw-r--r--Minecraft.Server/Console/ServerCliRegistry.cpp99
-rw-r--r--Minecraft.Server/Console/ServerCliRegistry.h58
-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
48 files changed, 3751 insertions, 0 deletions
diff --git a/Minecraft.Server/Console/ServerCli.cpp b/Minecraft.Server/Console/ServerCli.cpp
new file mode 100644
index 00000000..b633effd
--- /dev/null
+++ b/Minecraft.Server/Console/ServerCli.cpp
@@ -0,0 +1,44 @@
+#include "stdafx.h"
+
+#include "ServerCli.h"
+
+#include "ServerCliEngine.h"
+#include "ServerCliInput.h"
+
+namespace ServerRuntime
+{
+ ServerCli::ServerCli()
+ : m_engine(new ServerCliEngine())
+ , m_input(new ServerCliInput())
+ {
+ }
+
+ ServerCli::~ServerCli()
+ {
+ Stop();
+ }
+
+ void ServerCli::Start()
+ {
+ if (m_input && m_engine)
+ {
+ m_input->Start(m_engine.get());
+ }
+ }
+
+ void ServerCli::Stop()
+ {
+ if (m_input)
+ {
+ m_input->Stop();
+ }
+ }
+
+ void ServerCli::Poll()
+ {
+ if (m_engine)
+ {
+ m_engine->Poll();
+ }
+ }
+}
diff --git a/Minecraft.Server/Console/ServerCli.h b/Minecraft.Server/Console/ServerCli.h
new file mode 100644
index 00000000..f544450b
--- /dev/null
+++ b/Minecraft.Server/Console/ServerCli.h
@@ -0,0 +1,50 @@
+#pragma once
+
+#include <memory>
+
+namespace ServerRuntime
+{
+ class ServerCliEngine;
+ class ServerCliInput;
+
+ /**
+ * **Server CLI facade**
+ *
+ * Owns the command engine and input component, and exposes a small lifecycle API.
+ * CLI 全体の開始・停止・更新をまとめる窓口クラス
+ */
+ class ServerCli
+ {
+ public:
+ ServerCli();
+ ~ServerCli();
+
+ /**
+ * **Start console input processing**
+ *
+ * Connects input to the engine and starts background reading.
+ * 入力処理を開始してエンジンに接続
+ */
+ void Start();
+
+ /**
+ * **Stop console input processing**
+ *
+ * Stops background input safely and detaches from the engine.
+ * 入力処理を安全に停止
+ */
+ void Stop();
+
+ /**
+ * **Process queued command lines**
+ *
+ * Drains commands collected by input and executes them in the main loop.
+ * 入力キューのコマンドを実行
+ */
+ void Poll();
+
+ private:
+ std::unique_ptr<ServerCliEngine> m_engine;
+ std::unique_ptr<ServerCliInput> m_input;
+ };
+}
diff --git a/Minecraft.Server/Console/ServerCliEngine.cpp b/Minecraft.Server/Console/ServerCliEngine.cpp
new file mode 100644
index 00000000..82bbdcc8
--- /dev/null
+++ b/Minecraft.Server/Console/ServerCliEngine.cpp
@@ -0,0 +1,395 @@
+#include "stdafx.h"
+
+#include "ServerCliEngine.h"
+
+#include "ServerCliParser.h"
+#include "ServerCliRegistry.h"
+#include "commands\IServerCliCommand.h"
+#include "commands\ban\CliCommandBan.h"
+#include "commands\ban-ip\CliCommandBanIp.h"
+#include "commands\ban-list\CliCommandBanList.h"
+#include "commands\defaultgamemode\CliCommandDefaultGamemode.h"
+#include "commands\enchant\CliCommandEnchant.h"
+#include "commands\experience\CliCommandExperience.h"
+#include "commands\gamemode\CliCommandGamemode.h"
+#include "commands\give\CliCommandGive.h"
+#include "commands\help\CliCommandHelp.h"
+#include "commands\kill\CliCommandKill.h"
+#include "commands\list\CliCommandList.h"
+#include "commands\pardon\CliCommandPardon.h"
+#include "commands\pardon-ip\CliCommandPardonIp.h"
+#include "commands\stop\CliCommandStop.h"
+#include "commands\time\CliCommandTime.h"
+#include "commands\tp\CliCommandTp.h"
+#include "commands\weather\CliCommandWeather.h"
+#include "commands\whitelist\CliCommandWhitelist.h"
+#include "..\Common\StringUtils.h"
+#include "..\ServerShutdown.h"
+#include "..\ServerLogger.h"
+#include "..\..\Minecraft.Client\MinecraftServer.h"
+#include "..\..\Minecraft.Client\PlayerList.h"
+#include "..\..\Minecraft.Client\ServerPlayer.h"
+#include "..\..\Minecraft.World\CommandDispatcher.h"
+#include "..\..\Minecraft.World\CommandSender.h"
+#include "..\..\Minecraft.World\LevelSettings.h"
+#include "..\..\Minecraft.World\StringHelpers.h"
+
+#include <stdlib.h>
+#include <unordered_set>
+
+namespace ServerRuntime
+{
+
+ /**
+ * Create an authorized Sender to make the CLI appear as a user.
+ * The return value can also be used to display logs.
+ */
+ namespace
+ {
+ class ServerCliConsoleCommandSender : public CommandSender
+ {
+ public:
+ explicit ServerCliConsoleCommandSender(const ServerCliEngine *engine)
+ : m_engine(engine)
+ {
+ }
+
+ void sendMessage(const wstring &message, ChatPacket::EChatPacketMessage type, int customData, const wstring &additionalMessage) override
+ {
+ (void)type;
+ (void)customData;
+ (void)additionalMessage;
+ if (m_engine == nullptr)
+ {
+ return;
+ }
+
+ m_engine->LogInfo(StringUtils::WideToUtf8(message));
+ }
+
+ bool hasPermission(EGameCommand command) override
+ {
+ (void)command;
+ return true;
+ }
+
+ private:
+ const ServerCliEngine *m_engine;
+ };
+ }
+
+ ServerCliEngine::ServerCliEngine()
+ : m_registry(new ServerCliRegistry())
+ , m_consoleSender(std::make_shared<ServerCliConsoleCommandSender>(this))
+ {
+ RegisterDefaultCommands();
+ }
+
+ ServerCliEngine::~ServerCliEngine()
+ {
+ }
+
+ void ServerCliEngine::RegisterDefaultCommands()
+ {
+ m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandHelp()));
+ m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandStop()));
+ m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandList()));
+ m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandBan()));
+ m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandBanIp()));
+ m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandPardon()));
+ m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandPardonIp()));
+ m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandBanList()));
+ m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandWhitelist()));
+ m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandTp()));
+ m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandTime()));
+ m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandWeather()));
+ m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandGive()));
+ m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandEnchant()));
+ m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandKill()));
+ m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandGamemode()));
+ m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandDefaultGamemode()));
+ m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandExperience()));
+ }
+
+ void ServerCliEngine::EnqueueCommandLine(const std::string &line)
+ {
+ std::lock_guard<std::mutex> lock(m_queueMutex);
+ m_pendingLines.push(line);
+ }
+
+ void ServerCliEngine::Poll()
+ {
+ for (;;)
+ {
+ std::string line;
+ {
+ // Keep the lock scope minimal: dequeue only, execute outside.
+ std::lock_guard<std::mutex> lock(m_queueMutex);
+ if (m_pendingLines.empty())
+ {
+ break;
+ }
+ line = m_pendingLines.front();
+ m_pendingLines.pop();
+ }
+
+ ExecuteCommandLine(line);
+ }
+ }
+
+ bool ServerCliEngine::ExecuteCommandLine(const std::string &line)
+ {
+ // Normalize user input before parsing (trim + optional leading slash).
+ std::wstring wide = trimString(StringUtils::Utf8ToWide(line));
+ if (wide.empty())
+ {
+ return true;
+ }
+
+ std::string normalizedLine = StringUtils::WideToUtf8(wide);
+ if (!normalizedLine.empty() && normalizedLine[0] == '/')
+ {
+ normalizedLine = normalizedLine.substr(1);
+ }
+
+ ServerCliParsedLine parsed = ServerCliParser::Parse(normalizedLine);
+ if (parsed.tokens.empty())
+ {
+ return true;
+ }
+
+ IServerCliCommand *command = m_registry->FindMutable(parsed.tokens[0]);
+ if (command == NULL)
+ {
+ LogWarn("Unknown command: " + parsed.tokens[0]);
+ return false;
+ }
+
+ return command->Execute(parsed, this);
+ }
+
+ void ServerCliEngine::BuildCompletions(const std::string &line, std::vector<std::string> *out) const
+ {
+ if (out == NULL)
+ {
+ return;
+ }
+
+ out->clear();
+ ServerCliCompletionContext context = ServerCliParser::BuildCompletionContext(line);
+ bool slashPrefixedCommand = false;
+ std::string commandToken;
+ if (!context.parsed.tokens.empty())
+ {
+ // Completion accepts both "tp" and "/tp" style command heads.
+ commandToken = context.parsed.tokens[0];
+ if (!commandToken.empty() && commandToken[0] == '/')
+ {
+ commandToken = commandToken.substr(1);
+ slashPrefixedCommand = true;
+ }
+ }
+
+ if (context.currentTokenIndex == 0)
+ {
+ std::string prefix = context.prefix;
+ if (!prefix.empty() && prefix[0] == '/')
+ {
+ prefix = prefix.substr(1);
+ slashPrefixedCommand = true;
+ }
+
+ std::string linePrefix = context.linePrefix;
+ if (slashPrefixedCommand && linePrefix.empty())
+ {
+ // Preserve leading slash when user started with "/".
+ linePrefix = "/";
+ }
+
+ m_registry->SuggestCommandNames(prefix, linePrefix, out);
+ }
+ else
+ {
+ const IServerCliCommand *command = m_registry->Find(commandToken);
+ if (command != NULL)
+ {
+ command->Complete(context, this, out);
+ }
+ }
+
+ std::unordered_set<std::string> seen;
+ std::vector<std::string> unique;
+ for (size_t i = 0; i < out->size(); ++i)
+ {
+ // Remove duplicates while keeping first-seen ordering.
+ if (seen.insert((*out)[i]).second)
+ {
+ unique.push_back((*out)[i]);
+ }
+ }
+ out->swap(unique);
+ }
+
+ void ServerCliEngine::LogInfo(const std::string &message) const
+ {
+ LogInfof("console", "%s", message.c_str());
+ }
+
+ void ServerCliEngine::LogWarn(const std::string &message) const
+ {
+ LogWarnf("console", "%s", message.c_str());
+ }
+
+ void ServerCliEngine::LogError(const std::string &message) const
+ {
+ LogErrorf("console", "%s", message.c_str());
+ }
+
+ void ServerCliEngine::RequestShutdown() const
+ {
+ RequestDedicatedServerShutdown();
+ }
+
+ std::vector<std::string> ServerCliEngine::GetOnlinePlayerNamesUtf8() const
+ {
+ std::vector<std::string> result;
+ MinecraftServer *server = MinecraftServer::getInstance();
+ if (server == NULL)
+ {
+ return result;
+ }
+
+ PlayerList *players = server->getPlayers();
+ if (players == NULL)
+ {
+ return result;
+ }
+
+ for (size_t i = 0; i < players->players.size(); ++i)
+ {
+ std::shared_ptr<ServerPlayer> player = players->players[i];
+ if (player != NULL)
+ {
+ result.push_back(StringUtils::WideToUtf8(player->getName()));
+ }
+ }
+
+ return result;
+ }
+
+ std::shared_ptr<ServerPlayer> ServerCliEngine::FindPlayerByNameUtf8(const std::string &name) const
+ {
+ MinecraftServer *server = MinecraftServer::getInstance();
+ if (server == NULL)
+ {
+ return nullptr;
+ }
+
+ PlayerList *players = server->getPlayers();
+ if (players == NULL)
+ {
+ return nullptr;
+ }
+
+ std::wstring target = StringUtils::Utf8ToWide(name);
+ for (size_t i = 0; i < players->players.size(); ++i)
+ {
+ std::shared_ptr<ServerPlayer> player = players->players[i];
+ if (player != NULL && equalsIgnoreCase(player->getName(), target))
+ {
+ return player;
+ }
+ }
+
+ return nullptr;
+ }
+
+ void ServerCliEngine::SuggestPlayers(const std::string &prefix, const std::string &linePrefix, std::vector<std::string> *out) const
+ {
+ std::vector<std::string> players = GetOnlinePlayerNamesUtf8();
+ std::string loweredPrefix = StringUtils::ToLowerAscii(prefix);
+ for (size_t i = 0; i < players.size(); ++i)
+ {
+ std::string loweredName = StringUtils::ToLowerAscii(players[i]);
+ if (loweredName.compare(0, loweredPrefix.size(), loweredPrefix) == 0)
+ {
+ out->push_back(linePrefix + players[i]);
+ }
+ }
+ }
+
+ void ServerCliEngine::SuggestGamemodes(const std::string &prefix, const std::string &linePrefix, std::vector<std::string> *out) const
+ {
+ static const char *kModes[] = { "survival", "creative", "s", "c", "0", "1" };
+ std::string loweredPrefix = StringUtils::ToLowerAscii(prefix);
+ for (size_t i = 0; i < sizeof(kModes) / sizeof(kModes[0]); ++i)
+ {
+ std::string candidate = kModes[i];
+ std::string loweredCandidate = StringUtils::ToLowerAscii(candidate);
+ if (loweredCandidate.compare(0, loweredPrefix.size(), loweredPrefix) == 0)
+ {
+ out->push_back(linePrefix + candidate);
+ }
+ }
+ }
+
+ GameType *ServerCliEngine::ParseGamemode(const std::string &token) const
+ {
+ std::string lowered = StringUtils::ToLowerAscii(token);
+ if (lowered == "survival" || lowered == "s" || lowered == "0")
+ {
+ return GameType::SURVIVAL;
+ }
+ if (lowered == "creative" || lowered == "c" || lowered == "1")
+ {
+ return GameType::CREATIVE;
+ }
+
+ char *end = NULL;
+ long id = strtol(lowered.c_str(), &end, 10);
+ if (end != NULL && *end == 0)
+ {
+ // Numeric fallback supports extended ids handled by level settings.
+ return LevelSettings::validateGameType((int)id);
+ }
+
+ return NULL;
+ }
+
+ bool ServerCliEngine::DispatchWorldCommand(EGameCommand command, byteArray commandData, const std::shared_ptr<CommandSender> &sender) const
+ {
+ MinecraftServer *server = MinecraftServer::getInstance();
+ if (server == NULL)
+ {
+ LogWarn("MinecraftServer instance is not available.");
+ return false;
+ }
+
+ CommandDispatcher *dispatcher = server->getCommandDispatcher();
+ if (dispatcher == NULL)
+ {
+ LogWarn("Command dispatcher is not available.");
+ return false;
+ }
+
+ std::shared_ptr<CommandSender> commandSender = sender;
+ if (commandSender == nullptr)
+ {
+ // fall back to console sender if caller did not provide one
+ commandSender = m_consoleSender;
+ }
+ if (commandSender == nullptr)
+ {
+ LogWarn("No command sender is available.");
+ return false;
+ }
+
+ dispatcher->performCommand(commandSender, command, commandData);
+ return true;
+ }
+
+ const ServerCliRegistry &ServerCliEngine::Registry() const
+ {
+ return *m_registry;
+ }
+}
diff --git a/Minecraft.Server/Console/ServerCliEngine.h b/Minecraft.Server/Console/ServerCliEngine.h
new file mode 100644
index 00000000..b2d72bac
--- /dev/null
+++ b/Minecraft.Server/Console/ServerCliEngine.h
@@ -0,0 +1,127 @@
+#pragma once
+
+#include <memory>
+#include <mutex>
+#include <queue>
+#include <string>
+#include <vector>
+
+#include "..\..\Minecraft.World\ArrayWithLength.h"
+#include "..\..\Minecraft.World\CommandsEnum.h"
+
+class GameType;
+class ServerPlayer;
+class CommandSender;
+
+namespace ServerRuntime
+{
+ class ServerCliRegistry;
+
+ /**
+ * **CLI execution engine**
+ *
+ * Handles parsing, command dispatch, completion suggestions, and server-side helpers.
+ * 解析・実行・補完エンジン
+ */
+ class ServerCliEngine
+ {
+ public:
+ ServerCliEngine();
+ ~ServerCliEngine();
+
+ /**
+ * **Queue one raw command line**
+ *
+ * Called by input thread; execution is deferred to `Poll()`.
+ * 入力行を実行キューに追加
+ */
+ void EnqueueCommandLine(const std::string &line);
+
+ /**
+ * **Execute queued commands**
+ *
+ * Drains pending lines and dispatches them in order.
+ * キュー済みコマンドを順番に実行
+ */
+ void Poll();
+
+ /**
+ * **Execute one command line immediately**
+ *
+ * Parses and dispatches a normalized line to a registered command.
+ * 1行を直接パースしてコマンド実行
+ */
+ bool ExecuteCommandLine(const std::string &line);
+
+ /**
+ * **Build completion candidates for current line**
+ *
+ * Produces command or argument suggestions based on parser context.
+ * 現在入力に対する補完候補を作成
+ */
+ void BuildCompletions(const std::string &line, std::vector<std::string> *out) const;
+
+ void LogInfo(const std::string &message) const;
+ void LogWarn(const std::string &message) const;
+ void LogError(const std::string &message) const;
+ void RequestShutdown() const;
+
+ /**
+ * **List connected players as UTF-8 names**
+ *
+ * ここら辺は分けてもいいかも
+ */
+ std::vector<std::string> GetOnlinePlayerNamesUtf8() const;
+
+ /**
+ * **Find a player by UTF-8 name**
+ */
+ std::shared_ptr<ServerPlayer> FindPlayerByNameUtf8(const std::string &name) const;
+
+ /**
+ * **Suggest player-name arguments**
+ *
+ * Appends matching player candidates using the given completion prefix.
+ * プレイヤー名の補完候補
+ */
+ void SuggestPlayers(const std::string &prefix, const std::string &linePrefix, std::vector<std::string> *out) const;
+
+ /**
+ * **Suggest gamemode arguments**
+ *
+ * Appends standard gamemode aliases (survival/creative/0/1).
+ * ゲームモードの補完候補
+ */
+ void SuggestGamemodes(const std::string &prefix, const std::string &linePrefix, std::vector<std::string> *out) const;
+
+ /**
+ * **Parse gamemode token**
+ *
+ * Supports names, short aliases, and numeric ids.
+ * 文字列からゲームモードを解決
+ */
+ GameType *ParseGamemode(const std::string &token) const;
+
+ /**
+ * **Dispatch one Minecraft.World game command**
+ *
+ * Uses `Minecraft.World::CommandDispatcher` for actual execution.
+ * When `sender` is null, an internal console command sender is used.
+ *
+ * Minecraft.Worldのコマンドを実行するためのディスパッチャーのラッパー
+ * 内部でsenderがnullの場合はコンソールコマンド送信者を使用
+ */
+ bool DispatchWorldCommand(EGameCommand command, byteArray commandData, const std::shared_ptr<CommandSender> &sender = nullptr) const;
+
+ const ServerCliRegistry &Registry() const;
+
+ private:
+ void RegisterDefaultCommands();
+
+ private:
+ mutable std::mutex m_queueMutex;
+ std::queue<std::string> m_pendingLines;
+ std::unique_ptr<ServerCliRegistry> m_registry;
+ std::shared_ptr<CommandSender> m_consoleSender;
+ };
+}
diff --git a/Minecraft.Server/Console/ServerCliInput.cpp b/Minecraft.Server/Console/ServerCliInput.cpp
new file mode 100644
index 00000000..d873980a
--- /dev/null
+++ b/Minecraft.Server/Console/ServerCliInput.cpp
@@ -0,0 +1,285 @@
+#include "stdafx.h"
+
+#include "ServerCliInput.h"
+
+#include "ServerCliEngine.h"
+#include "..\ServerLogger.h"
+#include "..\vendor\linenoise\linenoise.h"
+
+#include <ctype.h>
+#include <stdlib.h>
+
+namespace
+{
+ bool UseStreamInputMode()
+ {
+ const char *mode = getenv("SERVER_CLI_INPUT_MODE");
+ if (mode != NULL)
+ {
+ return _stricmp(mode, "stream") == 0
+ || _stricmp(mode, "stdin") == 0;
+ }
+
+ return false;
+ }
+
+ int WaitForStdinReadable(HANDLE stdinHandle, DWORD waitMs)
+ {
+ if (stdinHandle == NULL || stdinHandle == INVALID_HANDLE_VALUE)
+ {
+ return -1;
+ }
+
+ DWORD fileType = GetFileType(stdinHandle);
+ if (fileType == FILE_TYPE_PIPE)
+ {
+ DWORD available = 0;
+ if (!PeekNamedPipe(stdinHandle, NULL, 0, NULL, &available, NULL))
+ {
+ return -1;
+ }
+ return available > 0 ? 1 : 0;
+ }
+
+ if (fileType == FILE_TYPE_CHAR)
+ {
+ // console/pty char handles are often not waitable across Wine+Docker.
+ return 1;
+ }
+
+ DWORD waitResult = WaitForSingleObject(stdinHandle, waitMs);
+ if (waitResult == WAIT_OBJECT_0)
+ {
+ return 1;
+ }
+ if (waitResult == WAIT_TIMEOUT)
+ {
+ return 0;
+ }
+
+ return -1;
+ }
+}
+
+namespace ServerRuntime
+{
+ // C-style completion callback bridge requires a static instance pointer.
+ ServerCliInput *ServerCliInput::s_instance = NULL;
+
+ ServerCliInput::ServerCliInput()
+ : m_running(false)
+ , m_engine(NULL)
+ {
+ }
+
+ ServerCliInput::~ServerCliInput()
+ {
+ Stop();
+ }
+
+ void ServerCliInput::Start(ServerCliEngine *engine)
+ {
+ if (engine == NULL || m_running.exchange(true))
+ {
+ return;
+ }
+
+ m_engine = engine;
+ s_instance = this;
+ linenoiseResetStop();
+ linenoiseHistorySetMaxLen(128);
+ linenoiseSetCompletionCallback(&ServerCliInput::CompletionThunk);
+ m_inputThread = std::thread(&ServerCliInput::RunInputLoop, this);
+ LogInfo("console", "CLI input thread started.");
+ }
+
+ void ServerCliInput::Stop()
+ {
+ if (!m_running.exchange(false))
+ {
+ return;
+ }
+
+ // Ask linenoise to break out first, then join thread safely.
+ linenoiseRequestStop();
+ if (m_inputThread.joinable())
+ {
+ CancelSynchronousIo((HANDLE)m_inputThread.native_handle());
+ m_inputThread.join();
+ }
+ linenoiseSetCompletionCallback(NULL);
+
+ if (s_instance == this)
+ {
+ s_instance = NULL;
+ }
+
+ m_engine = NULL;
+ LogInfo("console", "CLI input thread stopped.");
+ }
+
+ bool ServerCliInput::IsRunning() const
+ {
+ return m_running.load();
+ }
+
+ void ServerCliInput::RunInputLoop()
+ {
+ if (UseStreamInputMode())
+ {
+ LogInfo("console", "CLI input mode: stream(file stdin)");
+ RunStreamInputLoop();
+ return;
+ }
+
+ RunLinenoiseLoop();
+ }
+
+ /**
+ * use linenoise for interactive console input, with line editing and history support
+ */
+ void ServerCliInput::RunLinenoiseLoop()
+ {
+ while (m_running)
+ {
+ char *line = linenoise("server> ");
+ if (line == NULL)
+ {
+ // NULL is expected on stop request (or Ctrl+C inside linenoise).
+ if (!m_running)
+ {
+ break;
+ }
+ Sleep(10);
+ continue;
+ }
+
+ EnqueueLine(line);
+
+ linenoiseFree(line);
+ }
+ }
+
+ /**
+ * use file-based stdin reading instead of linenoise when requested or when stdin is not a console/pty (e.g. piped input or non-interactive docker)
+ */
+ void ServerCliInput::RunStreamInputLoop()
+ {
+ HANDLE stdinHandle = GetStdHandle(STD_INPUT_HANDLE);
+ if (stdinHandle == NULL || stdinHandle == INVALID_HANDLE_VALUE)
+ {
+ LogWarn("console", "stream input mode requested but STDIN handle is unavailable; falling back to linenoise.");
+ RunLinenoiseLoop();
+ return;
+ }
+
+ std::string line;
+ bool skipNextLf = false;
+
+ printf("server> ");
+ fflush(stdout);
+
+ while (m_running)
+ {
+ int readable = WaitForStdinReadable(stdinHandle, 50);
+ if (readable <= 0)
+ {
+ Sleep(10);
+ continue;
+ }
+
+ char ch = 0;
+ DWORD bytesRead = 0;
+ if (!ReadFile(stdinHandle, &ch, 1, &bytesRead, NULL) || bytesRead == 0)
+ {
+ Sleep(10);
+ continue;
+ }
+
+ if (skipNextLf && ch == '\n')
+ {
+ skipNextLf = false;
+ continue;
+ }
+
+ if (ch == '\r' || ch == '\n')
+ {
+ if (ch == '\r')
+ {
+ skipNextLf = true;
+ }
+ else
+ {
+ skipNextLf = false;
+ }
+
+ if (!line.empty())
+ {
+ EnqueueLine(line.c_str());
+ line.clear();
+ }
+
+ printf("server> ");
+ fflush(stdout);
+ continue;
+ }
+
+ skipNextLf = false;
+
+ if ((unsigned char)ch == 3)
+ {
+ continue;
+ }
+
+ if ((unsigned char)ch == 8 || (unsigned char)ch == 127)
+ {
+ if (!line.empty())
+ {
+ line.resize(line.size() - 1);
+ }
+ continue;
+ }
+
+ if (isprint((unsigned char)ch) && line.size() < 4096)
+ {
+ line.push_back(ch);
+ }
+ }
+ }
+
+ void ServerCliInput::EnqueueLine(const char *line)
+ {
+ if (line == NULL || line[0] == 0 || m_engine == NULL)
+ {
+ return;
+ }
+
+ // Keep local history and forward command for main-thread execution.
+ linenoiseHistoryAdd(line);
+ m_engine->EnqueueCommandLine(line);
+ }
+
+ void ServerCliInput::CompletionThunk(const char *line, linenoiseCompletions *completions)
+ {
+ // Static thunk forwards callback into instance state.
+ if (s_instance != NULL)
+ {
+ s_instance->BuildCompletions(line, completions);
+ }
+ }
+
+ void ServerCliInput::BuildCompletions(const char *line, linenoiseCompletions *completions)
+ {
+ if (line == NULL || completions == NULL || m_engine == NULL)
+ {
+ return;
+ }
+
+ std::vector<std::string> suggestions;
+ m_engine->BuildCompletions(line, &suggestions);
+ for (size_t i = 0; i < suggestions.size(); ++i)
+ {
+ linenoiseAddCompletion(completions, suggestions[i].c_str());
+ }
+ }
+}
diff --git a/Minecraft.Server/Console/ServerCliInput.h b/Minecraft.Server/Console/ServerCliInput.h
new file mode 100644
index 00000000..83221a2c
--- /dev/null
+++ b/Minecraft.Server/Console/ServerCliInput.h
@@ -0,0 +1,63 @@
+#pragma once
+
+#include <atomic>
+#include <thread>
+
+struct linenoiseCompletions;
+
+namespace ServerRuntime
+{
+ class ServerCliEngine;
+
+ /**
+ * **CLI input worker**
+ *
+ * Owns the interactive input thread and bridges linenoise callbacks to the engine.
+ * 入力スレッドと補完コールバックを管理するクラス
+ */
+ class ServerCliInput
+ {
+ public:
+ ServerCliInput();
+ ~ServerCliInput();
+
+ /**
+ * **Start input loop**
+ *
+ * Binds to an engine and starts reading user input from the console.
+ * エンジンに接続して入力ループを開始
+ */
+ void Start(ServerCliEngine *engine);
+
+ /**
+ * **Stop input loop**
+ *
+ * Requests stop and joins the input thread.
+ * 停止要求を出して入力スレッドを終了
+ */
+ void Stop();
+
+ /**
+ * **Check running state**
+ *
+ * Returns true while the input thread is active.
+ * 入力処理が動作中かどうか
+ */
+ bool IsRunning() const;
+
+ private:
+ void RunInputLoop();
+ void RunLinenoiseLoop();
+ void RunStreamInputLoop();
+ void EnqueueLine(const char *line);
+ static void CompletionThunk(const char *line, linenoiseCompletions *completions);
+ void BuildCompletions(const char *line, linenoiseCompletions *completions);
+
+ private:
+ std::atomic<bool> m_running;
+ std::thread m_inputThread;
+ ServerCliEngine *m_engine;
+
+ static ServerCliInput *s_instance;
+ };
+}
diff --git a/Minecraft.Server/Console/ServerCliParser.cpp b/Minecraft.Server/Console/ServerCliParser.cpp
new file mode 100644
index 00000000..5888153f
--- /dev/null
+++ b/Minecraft.Server/Console/ServerCliParser.cpp
@@ -0,0 +1,116 @@
+#include "stdafx.h"
+
+#include "ServerCliParser.h"
+
+namespace ServerRuntime
+{
+ static void TokenizeLine(const std::string &line, std::vector<std::string> *tokens, bool *trailingSpace)
+ {
+ std::string current;
+ bool inQuotes = false;
+ bool escaped = false;
+
+ tokens->clear();
+ *trailingSpace = false;
+
+ for (size_t i = 0; i < line.size(); ++i)
+ {
+ char ch = line[i];
+ if (escaped)
+ {
+ // Keep escaped character literally (e.g. \" or \ ).
+ current.push_back(ch);
+ escaped = false;
+ continue;
+ }
+
+ if (ch == '\\')
+ {
+ escaped = true;
+ continue;
+ }
+
+ if (ch == '"')
+ {
+ // Double quotes group spaces into one token.
+ inQuotes = !inQuotes;
+ continue;
+ }
+
+ if (!inQuotes && (ch == ' ' || ch == '\t'))
+ {
+ if (!current.empty())
+ {
+ tokens->push_back(current);
+ current.clear();
+ }
+ continue;
+ }
+
+ current.push_back(ch);
+ }
+
+ if (!current.empty())
+ {
+ tokens->push_back(current);
+ }
+
+ if (!line.empty())
+ {
+ char tail = line[line.size() - 1];
+ // Trailing space means completion targets the next token slot.
+ *trailingSpace = (!inQuotes && (tail == ' ' || tail == '\t'));
+ }
+ }
+
+ ServerCliParsedLine ServerCliParser::Parse(const std::string &line)
+ {
+ ServerCliParsedLine parsed;
+ parsed.raw = line;
+ TokenizeLine(line, &parsed.tokens, &parsed.trailingSpace);
+ return parsed;
+ }
+
+ ServerCliCompletionContext ServerCliParser::BuildCompletionContext(const std::string &line)
+ {
+ ServerCliCompletionContext context;
+ context.parsed = Parse(line);
+
+ if (context.parsed.tokens.empty())
+ {
+ context.currentTokenIndex = 0;
+ context.prefix.clear();
+ context.linePrefix.clear();
+ return context;
+ }
+
+ if (context.parsed.trailingSpace)
+ {
+ // Cursor is after a separator, so complete a new token.
+ context.currentTokenIndex = context.parsed.tokens.size();
+ context.prefix.clear();
+ }
+ else
+ {
+ // Cursor is inside current token, so complete by its prefix.
+ context.currentTokenIndex = context.parsed.tokens.size() - 1;
+ context.prefix = context.parsed.tokens.back();
+ }
+
+ for (size_t i = 0; i < context.currentTokenIndex; ++i)
+ {
+ // linePrefix is the immutable left side reused by completion output.
+ if (!context.linePrefix.empty())
+ {
+ context.linePrefix.push_back(' ');
+ }
+ context.linePrefix += context.parsed.tokens[i];
+ }
+ if (!context.linePrefix.empty())
+ {
+ context.linePrefix.push_back(' ');
+ }
+
+ return context;
+ }
+}
diff --git a/Minecraft.Server/Console/ServerCliParser.h b/Minecraft.Server/Console/ServerCliParser.h
new file mode 100644
index 00000000..a84d179b
--- /dev/null
+++ b/Minecraft.Server/Console/ServerCliParser.h
@@ -0,0 +1,63 @@
+#pragma once
+
+#include <string>
+#include <vector>
+
+namespace ServerRuntime
+{
+ /**
+ * **Parsed command line**
+ */
+ struct ServerCliParsedLine
+ {
+ std::string raw;
+ std::vector<std::string> tokens;
+ bool trailingSpace;
+
+ ServerCliParsedLine()
+ : trailingSpace(false)
+ {
+ }
+ };
+
+ /**
+ * **Completion context for one input line**
+ *
+ * Indicates current token index, token prefix, and the fixed line prefix.
+ */
+ struct ServerCliCompletionContext
+ {
+ ServerCliParsedLine parsed;
+ size_t currentTokenIndex;
+ std::string prefix;
+ std::string linePrefix;
+
+ ServerCliCompletionContext()
+ : currentTokenIndex(0)
+ {
+ }
+ };
+
+ /**
+ * **CLI parser helpers**
+ *
+ * Converts raw input text into tokenized data used by execution and completion.
+ */
+ class ServerCliParser
+ {
+ public:
+ /**
+ * **Tokenize one command line**
+ *
+ * Supports quoted segments and escaped characters.
+ */
+ static ServerCliParsedLine Parse(const std::string &line);
+
+ /**
+ * **Build completion metadata**
+ *
+ * Determines active token position and reusable prefix parts.
+ */
+ static ServerCliCompletionContext BuildCompletionContext(const std::string &line);
+ };
+}
diff --git a/Minecraft.Server/Console/ServerCliRegistry.cpp b/Minecraft.Server/Console/ServerCliRegistry.cpp
new file mode 100644
index 00000000..432907b2
--- /dev/null
+++ b/Minecraft.Server/Console/ServerCliRegistry.cpp
@@ -0,0 +1,99 @@
+#include "stdafx.h"
+
+#include "ServerCliRegistry.h"
+
+#include "commands\IServerCliCommand.h"
+#include "..\Common\StringUtils.h"
+
+namespace ServerRuntime
+{
+ bool ServerCliRegistry::Register(std::unique_ptr<IServerCliCommand> command)
+ {
+ if (!command)
+ {
+ return false;
+ }
+
+ IServerCliCommand *raw = command.get();
+ std::string baseName = StringUtils::ToLowerAscii(raw->Name());
+ // Reject empty/duplicate primary command names.
+ if (baseName.empty() || m_lookup.find(baseName) != m_lookup.end())
+ {
+ return false;
+ }
+ std::vector<std::string> aliases = raw->Aliases();
+ std::vector<std::string> normalizedAliases;
+ normalizedAliases.reserve(aliases.size());
+ for (size_t i = 0; i < aliases.size(); ++i)
+ {
+ std::string alias = StringUtils::ToLowerAscii(aliases[i]);
+ // Alias must also be unique across all names and aliases.
+ if (alias.empty() || m_lookup.find(alias) != m_lookup.end())
+ {
+ return false;
+ }
+ normalizedAliases.push_back(alias);
+ }
+
+ m_lookup[baseName] = raw;
+ for (size_t i = 0; i < normalizedAliases.size(); ++i)
+ {
+ m_lookup[normalizedAliases[i]] = raw;
+ }
+
+ // Command objects are owned here; lookup stores non-owning pointers.
+ m_commands.push_back(std::move(command));
+ return true;
+ }
+
+ const IServerCliCommand *ServerCliRegistry::Find(const std::string &name) const
+ {
+ std::string key = StringUtils::ToLowerAscii(name);
+ auto it = m_lookup.find(key);
+ if (it == m_lookup.end())
+ {
+ return NULL;
+ }
+ return it->second;
+ }
+
+ IServerCliCommand *ServerCliRegistry::FindMutable(const std::string &name)
+ {
+ std::string key = StringUtils::ToLowerAscii(name);
+ auto it = m_lookup.find(key);
+ if (it == m_lookup.end())
+ {
+ return NULL;
+ }
+ return it->second;
+ }
+
+ void ServerCliRegistry::SuggestCommandNames(const std::string &prefix, const std::string &linePrefix, std::vector<std::string> *out) const
+ {
+ for (size_t i = 0; i < m_commands.size(); ++i)
+ {
+ const IServerCliCommand *command = m_commands[i].get();
+ std::string name = command->Name();
+ if (StringUtils::StartsWithIgnoreCase(name, prefix))
+ {
+ out->push_back(linePrefix + name);
+ }
+
+ // Include aliases so users can discover shorthand commands.
+ std::vector<std::string> aliases = command->Aliases();
+ for (size_t aliasIndex = 0; aliasIndex < aliases.size(); ++aliasIndex)
+ {
+ if (StringUtils::StartsWithIgnoreCase(aliases[aliasIndex], prefix))
+ {
+ out->push_back(linePrefix + aliases[aliasIndex]);
+ }
+ }
+ }
+ }
+
+ const std::vector<std::unique_ptr<IServerCliCommand>> &ServerCliRegistry::Commands() const
+ {
+ return m_commands;
+ }
+}
+
diff --git a/Minecraft.Server/Console/ServerCliRegistry.h b/Minecraft.Server/Console/ServerCliRegistry.h
new file mode 100644
index 00000000..5104b534
--- /dev/null
+++ b/Minecraft.Server/Console/ServerCliRegistry.h
@@ -0,0 +1,58 @@
+#pragma once
+
+#include <memory>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+namespace ServerRuntime
+{
+ class IServerCliCommand;
+
+ /**
+ * **CLI command registry**
+ */
+ class ServerCliRegistry
+ {
+ public:
+ /**
+ * **Register a command object**
+ *
+ * Validates name/aliases and adds lookup entries.
+ * コマンドの追加
+ */
+ bool Register(std::unique_ptr<IServerCliCommand> command);
+
+ /**
+ * **Find command by name or alias (const)**
+ *
+ * Returns null when no match exists.
+ */
+ const IServerCliCommand *Find(const std::string &name) const;
+
+ /**
+ * **Find mutable command by name or alias**
+ *
+ * Used by runtime dispatch path.
+ */
+ IServerCliCommand *FindMutable(const std::string &name);
+
+ /**
+ * **Suggest top-level command names**
+ *
+ * Adds matching command names and aliases to the output list.
+ */
+ void SuggestCommandNames(const std::string &prefix, const std::string &linePrefix, std::vector<std::string> *out) const;
+
+ /**
+ * **Get registered command list**
+ *
+ * Intended for help output and inspection.
+ */
+ const std::vector<std::unique_ptr<IServerCliCommand>> &Commands() const;
+
+ private:
+ std::vector<std::unique_ptr<IServerCliCommand>> m_commands;
+ std::unordered_map<std::string, IServerCliCommand *> m_lookup;
+ };
+}
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;
+ };
+}
+