diff options
Diffstat (limited to 'Minecraft.Server/Console')
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; + }; +} + |
