From 965a859b40176f4f6283edb2c06a5729ff80b129 Mon Sep 17 00:00:00 2001 From: kuwacom Date: Sat, 7 Mar 2026 16:48:58 +0900 Subject: [PATCH] add: add implementing interactive command line using linenoise - Integrated linenoise library for line editing and completion in the server console. - Updated ServerLogger to handle external writes safely during logging. - Modified ServerMain to initialize and manage the ServerCli for command input. - The implementation is separate from everything else, so it doesn't affect anything else. - The command input section and execution section are separated into threads. --- CMakeLists.txt | 11 + Minecraft.Server/Console/ServerCli.cpp | 44 ++ Minecraft.Server/Console/ServerCli.h | 50 ++ Minecraft.Server/Console/ServerCliEngine.cpp | 314 ++++++++++ Minecraft.Server/Console/ServerCliEngine.h | 114 ++++ Minecraft.Server/Console/ServerCliInput.cpp | 120 ++++ Minecraft.Server/Console/ServerCliInput.h | 60 ++ Minecraft.Server/Console/ServerCliParser.cpp | 116 ++++ Minecraft.Server/Console/ServerCliParser.h | 63 ++ .../Console/ServerCliRegistry.cpp | 127 ++++ Minecraft.Server/Console/ServerCliRegistry.h | 61 ++ .../Console/commands/CliCommandGamemode.cpp | 95 +++ .../Console/commands/CliCommandGamemode.h | 17 + .../Console/commands/CliCommandHelp.cpp | 45 ++ .../Console/commands/CliCommandHelp.h | 16 + .../Console/commands/CliCommandList.cpp | 47 ++ .../Console/commands/CliCommandList.h | 15 + .../Console/commands/CliCommandStop.cpp | 31 + .../Console/commands/CliCommandStop.h | 15 + .../Console/commands/CliCommandTp.cpp | 78 +++ .../Console/commands/CliCommandTp.h | 17 + .../Console/commands/IServerCliCommand.h | 50 ++ Minecraft.Server/Minecraft.Server.vcxproj | 25 + .../Minecraft.Server.vcxproj.filters | 576 +++++++++++++++++- Minecraft.Server/ServerLogger.cpp | 5 + Minecraft.Server/Windows64/ServerMain.cpp | 5 + Minecraft.Server/vendor/linenoise/LICENSE | 25 + Minecraft.Server/vendor/linenoise/linenoise.c | 542 ++++++++++++++++ Minecraft.Server/vendor/linenoise/linenoise.h | 37 ++ 29 files changed, 2720 insertions(+), 1 deletion(-) create mode 100644 Minecraft.Server/Console/ServerCli.cpp create mode 100644 Minecraft.Server/Console/ServerCli.h create mode 100644 Minecraft.Server/Console/ServerCliEngine.cpp create mode 100644 Minecraft.Server/Console/ServerCliEngine.h create mode 100644 Minecraft.Server/Console/ServerCliInput.cpp create mode 100644 Minecraft.Server/Console/ServerCliInput.h create mode 100644 Minecraft.Server/Console/ServerCliParser.cpp create mode 100644 Minecraft.Server/Console/ServerCliParser.h create mode 100644 Minecraft.Server/Console/ServerCliRegistry.cpp create mode 100644 Minecraft.Server/Console/ServerCliRegistry.h create mode 100644 Minecraft.Server/Console/commands/CliCommandGamemode.cpp create mode 100644 Minecraft.Server/Console/commands/CliCommandGamemode.h create mode 100644 Minecraft.Server/Console/commands/CliCommandHelp.cpp create mode 100644 Minecraft.Server/Console/commands/CliCommandHelp.h create mode 100644 Minecraft.Server/Console/commands/CliCommandList.cpp create mode 100644 Minecraft.Server/Console/commands/CliCommandList.h create mode 100644 Minecraft.Server/Console/commands/CliCommandStop.cpp create mode 100644 Minecraft.Server/Console/commands/CliCommandStop.h create mode 100644 Minecraft.Server/Console/commands/CliCommandTp.cpp create mode 100644 Minecraft.Server/Console/commands/CliCommandTp.h create mode 100644 Minecraft.Server/Console/commands/IServerCliCommand.h create mode 100644 Minecraft.Server/vendor/linenoise/LICENSE create mode 100644 Minecraft.Server/vendor/linenoise/linenoise.c create mode 100644 Minecraft.Server/vendor/linenoise/linenoise.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 6614fcd11..ebc7db0c6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,6 +91,17 @@ target_link_libraries(MinecraftClient PRIVATE set(MINECRAFT_SERVER_SOURCES ${MINECRAFT_CLIENT_SOURCES}) list(APPEND MINECRAFT_SERVER_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Windows64/ServerMain.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/ServerCli.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/ServerCliInput.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/ServerCliParser.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/ServerCliEngine.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/ServerCliRegistry.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/CliCommandHelp.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/CliCommandStop.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/CliCommandList.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/CliCommandTp.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/CliCommandGamemode.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/vendor/linenoise/linenoise.c" ) add_executable(MinecraftServer ${MINECRAFT_SERVER_SOURCES}) diff --git a/Minecraft.Server/Console/ServerCli.cpp b/Minecraft.Server/Console/ServerCli.cpp new file mode 100644 index 000000000..b633effda --- /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 000000000..f544450b5 --- /dev/null +++ b/Minecraft.Server/Console/ServerCli.h @@ -0,0 +1,50 @@ +#pragma once + +#include + +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 m_engine; + std::unique_ptr m_input; + }; +} diff --git a/Minecraft.Server/Console/ServerCliEngine.cpp b/Minecraft.Server/Console/ServerCliEngine.cpp new file mode 100644 index 000000000..c1bb21c87 --- /dev/null +++ b/Minecraft.Server/Console/ServerCliEngine.cpp @@ -0,0 +1,314 @@ +#include "stdafx.h" + +#include "ServerCliEngine.h" + +#include "ServerCliParser.h" +#include "ServerCliRegistry.h" +#include "commands\IServerCliCommand.h" +#include "commands\CliCommandGamemode.h" +#include "commands\CliCommandHelp.h" +#include "commands\CliCommandList.h" +#include "commands\CliCommandStop.h" +#include "commands\CliCommandTp.h" +#include "..\ServerLogger.h" +#include "..\..\Minecraft.Client\MinecraftServer.h" +#include "..\..\Minecraft.Client\PlayerList.h" +#include "..\..\Minecraft.Client\ServerPlayer.h" +#include "..\..\Minecraft.World\LevelSettings.h" +#include "..\..\Minecraft.World\StringHelpers.h" + +#include +#include +#include + +namespace ServerRuntime +{ + ServerCliEngine::ServerCliEngine() + : m_registry(new ServerCliRegistry()) + { + RegisterDefaultCommands(); + } + + ServerCliEngine::~ServerCliEngine() + { + } + + void ServerCliEngine::RegisterDefaultCommands() + { + m_registry->Register(std::unique_ptr(new CliCommandHelp())); + m_registry->Register(std::unique_ptr(new CliCommandStop())); + m_registry->Register(std::unique_ptr(new CliCommandList())); + m_registry->Register(std::unique_ptr(new CliCommandTp())); + m_registry->Register(std::unique_ptr(new CliCommandGamemode())); + } + + void ServerCliEngine::EnqueueCommandLine(const std::string &line) + { + std::lock_guard 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 lock(m_queueMutex); + if (m_pendingLines.empty()) + { + break; + } + line = m_pendingLines.front(); + m_pendingLines.pop(); + } + + ExecuteCommandLine(line); + } + } + + std::string ServerCliEngine::Normalize(const std::string &value) + { + std::string lowered = value; + for (size_t i = 0; i < lowered.size(); ++i) + { + lowered[i] = (char)tolower((unsigned char)lowered[i]); + } + return lowered; + } + + std::wstring ServerCliEngine::ToWide(const std::string &value) + { + return convStringToWstring(value); + } + + std::string ServerCliEngine::ToUtf8(const std::wstring &value) + { + return WideToUtf8(value); + } + + bool ServerCliEngine::ExecuteCommandLine(const std::string &line) + { + // Normalize user input before parsing (trim + optional leading slash). + std::wstring wide = trimString(ToWide(line)); + if (wide.empty()) + { + return true; + } + + std::string normalizedLine = ToUtf8(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 *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 seen; + std::vector 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 + { + MinecraftServer::HaltServer(); + } + + std::vector ServerCliEngine::GetOnlinePlayerNamesUtf8() const + { + std::vector 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 player = players->players[i]; + if (player != NULL) + { + result.push_back(ToUtf8(player->getName())); + } + } + + return result; + } + + std::shared_ptr 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 = ToWide(name); + for (size_t i = 0; i < players->players.size(); ++i) + { + std::shared_ptr 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 *out) const + { + std::vector players = GetOnlinePlayerNamesUtf8(); + std::string loweredPrefix = Normalize(prefix); + for (size_t i = 0; i < players.size(); ++i) + { + std::string loweredName = Normalize(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 *out) const + { + static const char *kModes[] = { "survival", "creative", "s", "c", "0", "1" }; + std::string loweredPrefix = Normalize(prefix); + for (size_t i = 0; i < sizeof(kModes) / sizeof(kModes[0]); ++i) + { + std::string candidate = kModes[i]; + std::string loweredCandidate = Normalize(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 = Normalize(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; + } + + 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 000000000..f460f94f8 --- /dev/null +++ b/Minecraft.Server/Console/ServerCliEngine.h @@ -0,0 +1,114 @@ +#pragma once + +#include +#include +#include +#include +#include + +class GameType; +class ServerPlayer; + +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 *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 GetOnlinePlayerNamesUtf8() const; + + /** + * **Find a player by UTF-8 name** + */ + std::shared_ptr 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 *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 *out) const; + + /** + * **Parse gamemode token** + * + * Supports names, short aliases, and numeric ids. + * 文字列からゲームモードを解決 + */ + GameType *ParseGamemode(const std::string &token) const; + + const ServerCliRegistry &Registry() const; + + private: + void RegisterDefaultCommands(); + static std::string Normalize(const std::string &value); + static std::wstring ToWide(const std::string &value); + static std::string ToUtf8(const std::wstring &value); + + private: + mutable std::mutex m_queueMutex; + std::queue m_pendingLines; + std::unique_ptr m_registry; + }; +} diff --git a/Minecraft.Server/Console/ServerCliInput.cpp b/Minecraft.Server/Console/ServerCliInput.cpp new file mode 100644 index 000000000..6c6c548c8 --- /dev/null +++ b/Minecraft.Server/Console/ServerCliInput.cpp @@ -0,0 +1,120 @@ +#include "stdafx.h" + +#include "ServerCliInput.h" + +#include "ServerCliEngine.h" +#include "..\ServerLogger.h" +#include "..\vendor\linenoise\linenoise.h" + +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()) + { + 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() + { + 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; + } + + if (line[0] != 0 && m_engine != NULL) + { + // Keep local history and forward command for main-thread execution. + linenoiseHistoryAdd(line); + m_engine->EnqueueCommandLine(line); + } + + linenoiseFree(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 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 000000000..81575179c --- /dev/null +++ b/Minecraft.Server/Console/ServerCliInput.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include + +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(); + static void CompletionThunk(const char *line, linenoiseCompletions *completions); + void BuildCompletions(const char *line, linenoiseCompletions *completions); + + private: + std::atomic 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 000000000..5888153fa --- /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 *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 000000000..a84d179bf --- /dev/null +++ b/Minecraft.Server/Console/ServerCliParser.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include + +namespace ServerRuntime +{ + /** + * **Parsed command line** + */ + struct ServerCliParsedLine + { + std::string raw; + std::vector 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 000000000..1483c7006 --- /dev/null +++ b/Minecraft.Server/Console/ServerCliRegistry.cpp @@ -0,0 +1,127 @@ +#include "stdafx.h" + +#include "ServerCliRegistry.h" + +#include "commands\IServerCliCommand.h" + +#include + +namespace ServerRuntime +{ + static bool StartsWithIgnoreCase(const std::string &value, const std::string &prefix) + { + if (prefix.size() > value.size()) + { + return false; + } + for (size_t i = 0; i < prefix.size(); ++i) + { + char a = (char)tolower((unsigned char)value[i]); + char b = (char)tolower((unsigned char)prefix[i]); + if (a != b) + { + return false; + } + } + return true; + } + + std::string ServerCliRegistry::Normalize(const std::string &value) + { + std::string normalized = value; + for (size_t i = 0; i < normalized.size(); ++i) + { + normalized[i] = (char)tolower((unsigned char)normalized[i]); + } + return normalized; + } + + bool ServerCliRegistry::Register(std::unique_ptr command) + { + if (!command) + { + return false; + } + + IServerCliCommand *raw = command.get(); + std::string baseName = Normalize(raw->Name()); + // Reject empty/duplicate primary command names. + if (baseName.empty() || m_lookup.find(baseName) != m_lookup.end()) + { + return false; + } + std::vector aliases = raw->Aliases(); + std::vector normalizedAliases; + normalizedAliases.reserve(aliases.size()); + for (size_t i = 0; i < aliases.size(); ++i) + { + std::string alias = Normalize(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 = Normalize(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 = Normalize(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 *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 (StartsWithIgnoreCase(name, prefix)) + { + out->push_back(linePrefix + name); + } + + // Include aliases so users can discover shorthand commands. + std::vector aliases = command->Aliases(); + for (size_t aliasIndex = 0; aliasIndex < aliases.size(); ++aliasIndex) + { + if (StartsWithIgnoreCase(aliases[aliasIndex], prefix)) + { + out->push_back(linePrefix + aliases[aliasIndex]); + } + } + } + } + + const std::vector> &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 000000000..0dc0fb9ad --- /dev/null +++ b/Minecraft.Server/Console/ServerCliRegistry.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include +#include + +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 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 *out) const; + + /** + * **Get registered command list** + * + * Intended for help output and inspection. + */ + const std::vector> &Commands() const; + + private: + static std::string Normalize(const std::string &value); + + private: + std::vector> m_commands; + std::unordered_map m_lookup; + }; +} diff --git a/Minecraft.Server/Console/commands/CliCommandGamemode.cpp b/Minecraft.Server/Console/commands/CliCommandGamemode.cpp new file mode 100644 index 000000000..d2d5ee360 --- /dev/null +++ b/Minecraft.Server/Console/commands/CliCommandGamemode.cpp @@ -0,0 +1,95 @@ +#include "stdafx.h" + +#include "CliCommandGamemode.h" + +#include "..\ServerCliEngine.h" +#include "..\ServerCliParser.h" +#include "..\..\ServerLogger.h" +#include "..\..\..\Minecraft.Client\MinecraftServer.h" +#include "..\..\..\Minecraft.Client\PlayerList.h" +#include "..\..\..\Minecraft.Client\ServerPlayer.h" +#include "..\..\..\Minecraft.World\LevelSettings.h" + +namespace ServerRuntime +{ + const char *CliCommandGamemode::Name() const + { + return "gamemode"; + } + + std::vector CliCommandGamemode::Aliases() const + { + return { "gm" }; + } + + const char *CliCommandGamemode::Usage() const + { + return "gamemode [player]"; + } + + 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) + { + engine->LogWarn("Usage: gamemode [player]"); + return false; + } + + GameType *mode = engine->ParseGamemode(line.tokens[1]); + if (mode == NULL) + { + engine->LogWarn("Unknown gamemode: " + line.tokens[1]); + return false; + } + + std::shared_ptr target = nullptr; + if (line.tokens.size() >= 3) + { + target = engine->FindPlayerByNameUtf8(line.tokens[2]); + if (target == NULL) + { + engine->LogWarn("Unknown player: " + line.tokens[2]); + return false; + } + } + else + { + MinecraftServer *server = MinecraftServer::getInstance(); + if (server == NULL || server->getPlayers() == NULL) + { + engine->LogWarn("Player list is not available."); + return false; + } + + PlayerList *players = server->getPlayers(); + if (players->players.size() != 1 || players->players[0] == NULL) + { + engine->LogWarn("Usage: gamemode "); + return false; + } + target = players->players[0]; + } + + target->setGameMode(mode); + target->fallDistance = 0.0f; + engine->LogInfo("Set " + WideToUtf8(target->getName()) + " gamemode to " + WideToUtf8(mode->getName()) + "."); + return true; + } + + void CliCommandGamemode::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector *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/CliCommandGamemode.h b/Minecraft.Server/Console/commands/CliCommandGamemode.h new file mode 100644 index 000000000..6e75d47c0 --- /dev/null +++ b/Minecraft.Server/Console/commands/CliCommandGamemode.h @@ -0,0 +1,17 @@ +#pragma once + +#include "IServerCliCommand.h" + +namespace ServerRuntime +{ + class CliCommandGamemode : public IServerCliCommand + { + public: + virtual const char *Name() const; + virtual std::vector Aliases() 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 *out) const; + }; +} diff --git a/Minecraft.Server/Console/commands/CliCommandHelp.cpp b/Minecraft.Server/Console/commands/CliCommandHelp.cpp new file mode 100644 index 000000000..be911e71a --- /dev/null +++ b/Minecraft.Server/Console/commands/CliCommandHelp.cpp @@ -0,0 +1,45 @@ +#include "stdafx.h" + +#include "CliCommandHelp.h" + +#include "..\ServerCliEngine.h" +#include "..\ServerCliRegistry.h" + +namespace ServerRuntime +{ + const char *CliCommandHelp::Name() const + { + return "help"; + } + + std::vector 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> &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/CliCommandHelp.h b/Minecraft.Server/Console/commands/CliCommandHelp.h new file mode 100644 index 000000000..6439eb581 --- /dev/null +++ b/Minecraft.Server/Console/commands/CliCommandHelp.h @@ -0,0 +1,16 @@ +#pragma once + +#include "IServerCliCommand.h" + +namespace ServerRuntime +{ + class CliCommandHelp : public IServerCliCommand + { + public: + virtual const char *Name() const; + virtual std::vector 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/CliCommandList.cpp b/Minecraft.Server/Console/commands/CliCommandList.cpp new file mode 100644 index 000000000..8a91c03c0 --- /dev/null +++ b/Minecraft.Server/Console/commands/CliCommandList.cpp @@ -0,0 +1,47 @@ +#include "stdafx.h" + +#include "CliCommandList.h" + +#include "..\ServerCliEngine.h" +#include "..\..\ServerLogger.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 = 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/CliCommandList.h b/Minecraft.Server/Console/commands/CliCommandList.h new file mode 100644 index 000000000..13f404e93 --- /dev/null +++ b/Minecraft.Server/Console/commands/CliCommandList.h @@ -0,0 +1,15 @@ +#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/CliCommandStop.cpp b/Minecraft.Server/Console/commands/CliCommandStop.cpp new file mode 100644 index 000000000..4692aeab5 --- /dev/null +++ b/Minecraft.Server/Console/commands/CliCommandStop.cpp @@ -0,0 +1,31 @@ +#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/CliCommandStop.h b/Minecraft.Server/Console/commands/CliCommandStop.h new file mode 100644 index 000000000..f4910fddf --- /dev/null +++ b/Minecraft.Server/Console/commands/CliCommandStop.h @@ -0,0 +1,15 @@ +#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/CliCommandTp.cpp b/Minecraft.Server/Console/commands/CliCommandTp.cpp new file mode 100644 index 000000000..560222d2e --- /dev/null +++ b/Minecraft.Server/Console/commands/CliCommandTp.cpp @@ -0,0 +1,78 @@ +#include "stdafx.h" + +#include "CliCommandTp.h" + +#include "..\ServerCliEngine.h" +#include "..\ServerCliParser.h" +#include "..\..\..\Minecraft.Client\PlayerConnection.h" +#include "..\..\..\Minecraft.Client\ServerPlayer.h" +#include "..\..\..\Minecraft.World\net.minecraft.world.level.h" +#include "..\..\..\Minecraft.World\net.minecraft.world.level.dimension.h" + +namespace ServerRuntime +{ + const char *CliCommandTp::Name() const + { + return "tp"; + } + + std::vector CliCommandTp::Aliases() const + { + return { "teleport" }; + } + + const char *CliCommandTp::Usage() const + { + return "tp "; + } + + const char *CliCommandTp::Description() const + { + return "Teleport one player to another player."; + } + + bool CliCommandTp::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) + { + if (line.tokens.size() < 3) + { + engine->LogWarn("Usage: tp "); + return false; + } + + std::shared_ptr subject = engine->FindPlayerByNameUtf8(line.tokens[1]); + std::shared_ptr destination = engine->FindPlayerByNameUtf8(line.tokens[2]); + if (subject == NULL) + { + engine->LogWarn("Unknown player: " + line.tokens[1]); + return false; + } + if (destination == NULL) + { + engine->LogWarn("Unknown player: " + line.tokens[2]); + return false; + } + if (subject->connection == NULL) + { + engine->LogWarn("Cannot teleport because source player connection is inactive."); + return false; + } + if (subject->level == NULL || destination->level == NULL || subject->level->dimension->id != destination->level->dimension->id || !subject->isAlive()) + { + engine->LogWarn("Teleport failed because players are in different dimensions or source player is dead."); + return false; + } + + subject->ride(nullptr); + subject->connection->teleport(destination->x, destination->y, destination->z, destination->yRot, destination->xRot); + engine->LogInfo("Teleported " + line.tokens[1] + " to " + line.tokens[2] + "."); + return true; + } + + void CliCommandTp::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector *out) const + { + if (context.currentTokenIndex == 1 || context.currentTokenIndex == 2) + { + engine->SuggestPlayers(context.prefix, context.linePrefix, out); + } + } +} diff --git a/Minecraft.Server/Console/commands/CliCommandTp.h b/Minecraft.Server/Console/commands/CliCommandTp.h new file mode 100644 index 000000000..45975e959 --- /dev/null +++ b/Minecraft.Server/Console/commands/CliCommandTp.h @@ -0,0 +1,17 @@ +#pragma once + +#include "IServerCliCommand.h" + +namespace ServerRuntime +{ + class CliCommandTp : public IServerCliCommand + { + public: + virtual const char *Name() const; + virtual std::vector Aliases() 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 *out) const; + }; +} diff --git a/Minecraft.Server/Console/commands/IServerCliCommand.h b/Minecraft.Server/Console/commands/IServerCliCommand.h new file mode 100644 index 000000000..5f54242ac --- /dev/null +++ b/Minecraft.Server/Console/commands/IServerCliCommand.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include + +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() {} + + /** Primary command name */ + virtual const char *Name() const = 0; + /** Optional aliases */ + virtual std::vector Aliases() const { return std::vector(); } + /** 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 *out) const + { + (void)context; + (void)engine; + (void)out; + } + }; +} diff --git a/Minecraft.Server/Minecraft.Server.vcxproj b/Minecraft.Server/Minecraft.Server.vcxproj index 555457702..cdbd5f540 100644 --- a/Minecraft.Server/Minecraft.Server.vcxproj +++ b/Minecraft.Server/Minecraft.Server.vcxproj @@ -647,9 +647,22 @@ NotUsing + + + + + + + + + + + + NotUsing + Create @@ -659,8 +672,20 @@ + + + + + + + + + + + + diff --git a/Minecraft.Server/Minecraft.Server.vcxproj.filters b/Minecraft.Server/Minecraft.Server.vcxproj.filters index 930213ad9..4cb5008a8 100644 --- a/Minecraft.Server/Minecraft.Server.vcxproj.filters +++ b/Minecraft.Server/Minecraft.Server.vcxproj.filters @@ -4,6 +4,15 @@ {A8A47C24-66C0-4912-9D34-2CBF87F1D707} + + {39B037A0-9B57-454A-AF34-7D9164E22A0F} + + + {7C28D123-0DA3-4B17-84C0-E326F5A75740} + + + {3E4D5A41-CAB8-4A10-82B5-8B2AE2E25CB2} + @@ -15,19 +24,584 @@ Server + + Server\Console + + + Server\Console + + + Server\Console + + + Server\Console + + + Server\Vendor + Server + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Server\Console\Commands + + + Server\Console\Commands + + + Server\Console\Commands + + + Server\Console\Commands + + + Server\Console\Commands + + + Server\Console + + + Server\Console + + + Server\Console + + + Server\Console + Server Server + + Server\Vendor + Server + + + Server\Console\Commands + + + Server\Console\Commands + + + Server\Console\Commands + + + Server\Console\Commands + + + Server\Console\Commands + + + Server\Console\Commands + - + + + + + + + \ No newline at end of file diff --git a/Minecraft.Server/ServerLogger.cpp b/Minecraft.Server/ServerLogger.cpp index b835276ca..9599df0a9 100644 --- a/Minecraft.Server/ServerLogger.cpp +++ b/Minecraft.Server/ServerLogger.cpp @@ -1,6 +1,7 @@ #include "stdafx.h" #include "ServerLogger.h" +#include "vendor\\linenoise\\linenoise.h" #include #include @@ -86,6 +87,8 @@ static void WriteLogLine(EServerLogLevel level, const char *category, const char return; } + linenoiseExternalWriteBegin(); + const char *safeCategory = NormalizeCategory(category); const char *safeMessage = (message != NULL) ? message : ""; @@ -116,6 +119,8 @@ static void WriteLogLine(EServerLogLevel level, const char *category, const char { SetConsoleTextAttribute(stdoutHandle, originalInfo.wAttributes); } + + linenoiseExternalWriteEnd(); } static void WriteLogLineV(EServerLogLevel level, const char *category, const char *format, va_list args) diff --git a/Minecraft.Server/Windows64/ServerMain.cpp b/Minecraft.Server/Windows64/ServerMain.cpp index 2506d14cd..2b1486460 100644 --- a/Minecraft.Server/Windows64/ServerMain.cpp +++ b/Minecraft.Server/Windows64/ServerMain.cpp @@ -8,6 +8,7 @@ #include "..\ServerLogger.h" #include "..\ServerProperties.h" #include "..\WorldManager.h" +#include "..\Console\ServerCli.h" #include "Tesselator.h" #include "Windows64/4JLibs/inc/4J_Render.h" #include "Windows64/GameConfig/Minecraft.spa.h" @@ -536,11 +537,14 @@ int main(int argc, char **argv) } DWORD nextAutosaveTick = GetTickCount() + autosaveIntervalMs; bool autosaveRequested = false; + ServerRuntime::ServerCli serverCli; + serverCli.Start(); while (!g_shutdownRequested && !app.m_bShutdown) { TickCoreSystems(); HandleXuiActions(); + serverCli.Poll(); if (autosaveRequested && app.GetXuiServerAction(kServerActionPad) == eXuiServerAction_Idle) { @@ -567,6 +571,7 @@ int main(int argc, char **argv) Sleep(10); } + serverCli.Stop(); LogStartupStep("stopping dedicated server"); MinecraftServer *server = MinecraftServer::getInstance(); diff --git a/Minecraft.Server/vendor/linenoise/LICENSE b/Minecraft.Server/vendor/linenoise/LICENSE new file mode 100644 index 000000000..d73915664 --- /dev/null +++ b/Minecraft.Server/vendor/linenoise/LICENSE @@ -0,0 +1,25 @@ +This vendored component is based on the linenoise project idea/API. + +Copyright (c) 2010-2014, Salvatore Sanfilippo +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Minecraft.Server/vendor/linenoise/linenoise.c b/Minecraft.Server/vendor/linenoise/linenoise.c new file mode 100644 index 000000000..7e5d8cde2 --- /dev/null +++ b/Minecraft.Server/vendor/linenoise/linenoise.c @@ -0,0 +1,542 @@ +#include "linenoise.h" + +#include +#include +#include +#include +#include +#include + +#define LINENOISE_MAX_LINE 4096 +#define LINENOISE_MAX_PROMPT 128 + +typedef struct linenoiseHistory { + char **items; + int len; + int cap; + int maxLen; +} linenoiseHistory; + +static linenoiseCompletionCallback *g_completionCallback = NULL; +static volatile LONG g_stopRequested = 0; +static linenoiseHistory g_history = { NULL, 0, 0, 128 }; +/* Guards redraw/log interleaving so prompt and log lines do not overlap. */ +static CRITICAL_SECTION g_ioLock; +static volatile LONG g_ioLockState = 0; /* 0=not init, 1=init in progress, 2=ready */ +/* Snapshot of current editor line used to restore prompt after external output. */ +static volatile LONG g_editorActive = 0; +static char g_editorPrompt[LINENOISE_MAX_PROMPT] = { 0 }; +static char g_editorBuf[LINENOISE_MAX_LINE] = { 0 }; +static int g_editorLen = 0; +static int g_editorPos = 0; +static int g_editorPrevLen = 0; + +/** + * Lazily initialize the console I/O critical section. + * This avoids static init order issues and keeps startup cost minimal. + */ +static void linenoiseEnsureIoLockInit(void) +{ + LONG state = InterlockedCompareExchange(&g_ioLockState, 0, 0); + if (state == 2) + return; + + if (state == 0 && InterlockedCompareExchange(&g_ioLockState, 1, 0) == 0) + { + InitializeCriticalSection(&g_ioLock); + InterlockedExchange(&g_ioLockState, 2); + return; + } + + while (InterlockedCompareExchange(&g_ioLockState, 0, 0) != 2) + { + Sleep(0); + } +} + +static void linenoiseLockIo(void) +{ + linenoiseEnsureIoLockInit(); + EnterCriticalSection(&g_ioLock); +} + +static void linenoiseUnlockIo(void) +{ + LeaveCriticalSection(&g_ioLock); +} + +/** + * Save current prompt/buffer/cursor state for later redraw. + * Called after each redraw while editor is active. + */ +static void linenoiseUpdateEditorState(const char *prompt, const char *buf, int len, int pos, int prevLen) +{ + if (prompt == NULL) + prompt = ""; + if (buf == NULL) + buf = ""; + + strncpy_s(g_editorPrompt, sizeof(g_editorPrompt), prompt, _TRUNCATE); + strncpy_s(g_editorBuf, sizeof(g_editorBuf), buf, _TRUNCATE); + g_editorLen = len; + g_editorPos = pos; + g_editorPrevLen = prevLen; + InterlockedExchange(&g_editorActive, 1); +} + +static void linenoiseDeactivateEditorState(void) +{ + InterlockedExchange(&g_editorActive, 0); + g_editorPrompt[0] = 0; + g_editorBuf[0] = 0; + g_editorLen = 0; + g_editorPos = 0; + g_editorPrevLen = 0; +} + +static char *linenoiseStrdup(const char *src) +{ + size_t n = strlen(src) + 1; + char *out = (char *)malloc(n); + if (out == NULL) + return NULL; + memcpy(out, src, n); + return out; +} + +static void linenoiseEnsureHistoryCapacity(int wanted) +{ + if (wanted <= g_history.cap) + return; + + int newCap = g_history.cap == 0 ? 32 : g_history.cap; + while (newCap < wanted) + newCap *= 2; + + char **newItems = (char **)realloc(g_history.items, sizeof(char *) * (size_t)newCap); + if (newItems == NULL) + return; + + g_history.items = newItems; + g_history.cap = newCap; +} + +static void linenoiseClearCompletions(linenoiseCompletions *lc) +{ + size_t i = 0; + for (i = 0; i < lc->len; ++i) + { + free(lc->cvec[i]); + } + free(lc->cvec); + lc->cvec = NULL; + lc->len = 0; +} + +void linenoiseAddCompletion(linenoiseCompletions *lc, const char *str) +{ + char **newVec = (char **)realloc(lc->cvec, sizeof(char *) * (lc->len + 1)); + if (newVec == NULL) + return; + + lc->cvec = newVec; + lc->cvec[lc->len] = linenoiseStrdup(str); + if (lc->cvec[lc->len] == NULL) + return; + + lc->len += 1; +} + +void linenoiseSetCompletionCallback(linenoiseCompletionCallback *fn) +{ + g_completionCallback = fn; +} + +void linenoiseFree(void *ptr) +{ + free(ptr); +} + +int linenoiseHistorySetMaxLen(int len) +{ + if (len <= 0) + return 0; + + g_history.maxLen = len; + while (g_history.len > g_history.maxLen) + { + free(g_history.items[0]); + memmove(g_history.items, g_history.items + 1, sizeof(char *) * (size_t)(g_history.len - 1)); + g_history.len -= 1; + } + + return 1; +} + +int linenoiseHistoryAdd(const char *line) +{ + if (line == NULL || line[0] == 0) + return 0; + + if (g_history.len > 0) + { + const char *last = g_history.items[g_history.len - 1]; + if (last != NULL && strcmp(last, line) == 0) + return 1; + } + + linenoiseEnsureHistoryCapacity(g_history.len + 1); + if (g_history.cap <= g_history.len) + return 0; + + g_history.items[g_history.len] = linenoiseStrdup(line); + if (g_history.items[g_history.len] == NULL) + return 0; + + g_history.len += 1; + + while (g_history.len > g_history.maxLen) + { + free(g_history.items[0]); + memmove(g_history.items, g_history.items + 1, sizeof(char *) * (size_t)(g_history.len - 1)); + g_history.len -= 1; + } + + return 1; +} + +void linenoiseRequestStop(void) +{ + InterlockedExchange(&g_stopRequested, 1); +} + +void linenoiseResetStop(void) +{ + InterlockedExchange(&g_stopRequested, 0); +} + +static int linenoiseIsStopRequested(void) +{ + return InterlockedCompareExchange(&g_stopRequested, 0, 0) != 0; +} + +static void linenoiseRedrawUnsafe(const char *prompt, const char *buf, int len, int pos, int *prevLen) +{ + int i; + + fputc('\r', stdout); + fputs(prompt, stdout); + if (len > 0) + { + fwrite(buf, 1, (size_t)len, stdout); + } + + if (*prevLen > len) + { + for (i = len; i < *prevLen; ++i) + fputc(' ', stdout); + } + + fputc('\r', stdout); + fputs(prompt, stdout); + if (pos > 0) + { + fwrite(buf, 1, (size_t)pos, stdout); + } + + fflush(stdout); + *prevLen = len; + linenoiseUpdateEditorState(prompt, buf, len, pos, *prevLen); +} + +static void linenoiseRedraw(const char *prompt, const char *buf, int len, int pos, int *prevLen) +{ + linenoiseLockIo(); + linenoiseRedrawUnsafe(prompt, buf, len, pos, prevLen); + linenoiseUnlockIo(); +} + +static int linenoiseStartsWith(const char *full, const char *prefix) +{ + while (*prefix != 0) + { + if (*full != *prefix) + return 0; + ++full; + ++prefix; + } + return 1; +} + +static int linenoiseComputeCommonPrefix(const linenoiseCompletions *lc, const char *seed, char *out, size_t outSize) +{ + size_t commonLen = 0; + size_t i; + + if (lc->len == 0 || outSize == 0) + return 0; + + strncpy_s(out, outSize, lc->cvec[0], _TRUNCATE); + commonLen = strlen(out); + + for (i = 1; i < lc->len; ++i) + { + const char *candidate = lc->cvec[i]; + size_t j = 0; + + while (j < commonLen && out[j] != 0 && candidate[j] != 0 && out[j] == candidate[j]) + ++j; + + commonLen = j; + out[commonLen] = 0; + + if (commonLen == 0) + break; + } + + if (strlen(out) <= strlen(seed)) + return 0; + + return linenoiseStartsWith(out, seed); +} + +static void linenoiseApplyCompletion(const char *prompt, char *buf, int *len, int *pos, int *prevLen) +{ + linenoiseCompletions lc; + int i; + + if (g_completionCallback == NULL) + { + Beep(750, 15); + return; + } + + lc.len = 0; + lc.cvec = NULL; + g_completionCallback(buf, &lc); + + if (lc.len == 0) + { + Beep(750, 15); + linenoiseClearCompletions(&lc); + return; + } + + if (lc.len == 1) + { + strncpy_s(buf, LINENOISE_MAX_LINE, lc.cvec[0], _TRUNCATE); + *len = (int)strlen(buf); + *pos = *len; + linenoiseRedraw(prompt, buf, *len, *pos, prevLen); + linenoiseClearCompletions(&lc); + return; + } + + { + char common[LINENOISE_MAX_LINE] = { 0 }; + if (linenoiseComputeCommonPrefix(&lc, buf, common, sizeof(common))) + { + strncpy_s(buf, LINENOISE_MAX_LINE, common, _TRUNCATE); + *len = (int)strlen(buf); + *pos = *len; + linenoiseRedraw(prompt, buf, *len, *pos, prevLen); + } + } + + linenoiseLockIo(); + fputc('\n', stdout); + for (i = 0; i < (int)lc.len; ++i) + { + fputs(lc.cvec[i], stdout); + fputs(" ", stdout); + } + fputc('\n', stdout); + linenoiseRedrawUnsafe(prompt, buf, *len, *pos, prevLen); + linenoiseUnlockIo(); + linenoiseClearCompletions(&lc); +} + +char *linenoise(const char *prompt) +{ + char buf[LINENOISE_MAX_LINE]; + int len = 0; + int pos = 0; + int prevLen = 0; + int historyIndex = g_history.len; + + if (prompt == NULL) + prompt = ""; + + buf[0] = 0; + linenoiseLockIo(); + linenoiseUpdateEditorState(prompt, buf, len, pos, prevLen); + fputs(prompt, stdout); + fflush(stdout); + linenoiseUnlockIo(); + + while (!linenoiseIsStopRequested()) + { + if (!_kbhit()) + { + Sleep(10); + continue; + } + + { + int c = _getwch(); + + if (c == 0 || c == 224) + { + int ext = _getwch(); + if (ext == 72) + { + if (g_history.len > 0 && historyIndex > 0) + { + historyIndex -= 1; + strncpy_s(buf, sizeof(buf), g_history.items[historyIndex], _TRUNCATE); + len = (int)strlen(buf); + pos = len; + linenoiseRedraw(prompt, buf, len, pos, &prevLen); + } + } + else if (ext == 80) + { + if (g_history.len > 0 && historyIndex < g_history.len) + { + historyIndex += 1; + if (historyIndex == g_history.len) + buf[0] = 0; + else + strncpy_s(buf, sizeof(buf), g_history.items[historyIndex], _TRUNCATE); + + len = (int)strlen(buf); + pos = len; + linenoiseRedraw(prompt, buf, len, pos, &prevLen); + } + } + else if (ext == 75) + { + if (pos > 0) + { + pos -= 1; + linenoiseRedraw(prompt, buf, len, pos, &prevLen); + } + } + else if (ext == 77) + { + if (pos < len) + { + pos += 1; + linenoiseRedraw(prompt, buf, len, pos, &prevLen); + } + } + continue; + } + + if (c == 3) + { + linenoiseLockIo(); + linenoiseDeactivateEditorState(); + fputc('\n', stdout); + fflush(stdout); + linenoiseUnlockIo(); + return NULL; + } + + if (c == '\r' || c == '\n') + { + char *out; + linenoiseLockIo(); + linenoiseDeactivateEditorState(); + fputc('\n', stdout); + fflush(stdout); + linenoiseUnlockIo(); + + out = linenoiseStrdup(buf); + return out; + } + + if (c == '\t') + { + linenoiseApplyCompletion(prompt, buf, &len, &pos, &prevLen); + continue; + } + + if (c == 8) + { + if (pos > 0 && len > 0) + { + memmove(buf + pos - 1, buf + pos, (size_t)(len - pos + 1)); + pos -= 1; + len -= 1; + linenoiseRedraw(prompt, buf, len, pos, &prevLen); + } + continue; + } + + if (isprint((unsigned char)c) && len < LINENOISE_MAX_LINE - 1) + { + if (pos == len) + { + buf[pos++] = (char)c; + len += 1; + buf[len] = 0; + } + else + { + memmove(buf + pos + 1, buf + pos, (size_t)(len - pos + 1)); + buf[pos] = (char)c; + pos += 1; + len += 1; + } + linenoiseRedraw(prompt, buf, len, pos, &prevLen); + } + } + } + + linenoiseLockIo(); + linenoiseDeactivateEditorState(); + fputc('\n', stdout); + fflush(stdout); + linenoiseUnlockIo(); + return NULL; +} + +void linenoiseExternalWriteBegin(void) +{ + int i; + int totalChars = 0; + + /* Lock shared console state and clear current prompt area before external output. */ + linenoiseLockIo(); + if (InterlockedCompareExchange(&g_editorActive, 0, 0) == 0) + { + return; + } + + totalChars = (int)strlen(g_editorPrompt) + g_editorPrevLen; + if (totalChars < 0) + { + totalChars = 0; + } + + fputc('\r', stdout); + for (i = 0; i < totalChars; ++i) + { + fputc(' ', stdout); + } + fputc('\r', stdout); + fflush(stdout); +} + +void linenoiseExternalWriteEnd(void) +{ + /* Restore prompt line after external output has been printed. */ + if (InterlockedCompareExchange(&g_editorActive, 0, 0) != 0) + { + int prevLen = g_editorPrevLen; + linenoiseRedrawUnsafe(g_editorPrompt, g_editorBuf, g_editorLen, g_editorPos, &prevLen); + g_editorPrevLen = prevLen; + } + linenoiseUnlockIo(); +} diff --git a/Minecraft.Server/vendor/linenoise/linenoise.h b/Minecraft.Server/vendor/linenoise/linenoise.h new file mode 100644 index 000000000..6f7a0d2b3 --- /dev/null +++ b/Minecraft.Server/vendor/linenoise/linenoise.h @@ -0,0 +1,37 @@ +#ifndef VENDORED_LINENOISE_H +#define VENDORED_LINENOISE_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct linenoiseCompletions { + size_t len; + char **cvec; +} linenoiseCompletions; + +typedef void(linenoiseCompletionCallback)(const char *buf, linenoiseCompletions *lc); + +char *linenoise(const char *prompt); +void linenoiseFree(void *ptr); + +void linenoiseSetCompletionCallback(linenoiseCompletionCallback *fn); +void linenoiseAddCompletion(linenoiseCompletions *lc, const char *str); + +int linenoiseHistoryAdd(const char *line); +int linenoiseHistorySetMaxLen(int len); + +void linenoiseRequestStop(void); +void linenoiseResetStop(void); + +/* Wrap external stdout/stderr writes so active prompt can be cleared/restored safely. */ +void linenoiseExternalWriteBegin(void); +void linenoiseExternalWriteEnd(void); + +#ifdef __cplusplus +} +#endif + +#endif