mirror of
https://github.com/smartcmd/MinecraftConsoles.git
synced 2026-03-22 12:58:13 +05:00
Dedicated Server Software - Minecraft.Server.exe (#498)
* add: Dedicated Server implementation - Introduced `ServerMain.cpp` for the dedicated server logic, handling command-line arguments, server initialization, and network management. - Created `postbuild_server.ps1` script for post-build tasks, including copying necessary resources and DLLs for the dedicated server. - Added `CopyServerAssets.cmake` to manage the copying of server assets during the build process, ensuring required files are available for the dedicated server. - Defined project filters in `Minecraft.Server.vcxproj.filters` for better organization of server-related files. * add: refactor world loader & add server properties - Introduced ServerLogger for logging startup steps and world I/O operations. - Implemented ServerProperties for loading and saving server configuration from `server.properties`. - Added WorldManager to handle world loading and creation based on server properties. - Updated ServerMain to integrate server properties loading and world management. - Enhanced project files to include new source and header files for the server components. * update: implement enhanced logging functionality with configurable log levels * update: update keyboard and mouse input initialization1dc8a005ed* fix: change virtual screen resolution to 1920x1080(HD) Since 31881af56936aeef38ff322b975fd0 , `skinHud.swf` for 720 is not included in `MediaWindows64.arc`, the app crashes unless the virtual screen is set to HD. * fix: dedicated server build settings for miniaudio migration and missing sources - remove stale Windows64 Miles (mss64) link/copy references from server build - add Common/Filesystem/Filesystem.cpp to Minecraft.Server.vcxproj - add Windows64/PostProcesser.cpp to Minecraft.Server.vcxproj - fix unresolved externals (PostProcesser::*, FileExists) in dedicated server build * update: changed the virtual screen to 720p Since the crash caused by the 720p `skinHud.swf` not being included in `MediaWindows64.arc` has been resolved, switching back to 720p to reduce resource usage. * add: add Docker support for Dedicated Server add with entrypoint and build scripts * fix: add initial save for newly created worlds in dedicated server on the server side, I fixed the behavior introduced after commitaadb511, where newly created worlds are intentionally not saved to disk immediately. * update: add basically all configuration options that are implemented in the classes to `server.properties` * update: add LAN advertising configuration for server.properties LAN-Discovery, which isn’t needed in server mode and could potentially be a security risk, has also been disabled(only server mode). * add: add implementing interactive command line using linenoise - Integrated linenoise library for line editing and completion in the server console. - Updated ServerLogger to handle external writes safely during logging. - Modified ServerMain to initialize and manage the ServerCli for command input. - The implementation is separate from everything else, so it doesn't affect anything else. - The command input section and execution section are separated into threads. * update: enhance command line completion with predictive hints Like most command line tools, it highlights predictions in gray. * add: implement `StringUtils` for string manipulation and refactor usages Unified the scattered utility functions. * fix: send DisconnectPacket on shutdown and fix Win64 recv-thread teardown race Before this change, server/host shutdown closed sockets directly in ServerConnection::stop(), which bypassed the normal disconnect flow. As a result, clients could be dropped without receiving a proper DisconnectPacket during stop/kill/world-close paths. Also, WinsockNetLayer::Shutdown() could destroy synchronization objects while host-side recv threads were still exiting, causing a crash in RecvThreadProc (access violation on world close in host mode). * fix: return client to menus when Win64 host connection drops - Add client-side host disconnect handling in CPlatformNetworkManagerStub::DoWork() for _WINDOWS64. - When in QNET_STATE_GAME_PLAY as a non-host and WinsockNetLayer::IsConnected() becomes false, trigger g_NetworkManager.HandleDisconnect(false) to enter the normal disconnect/UI flow. - Use m_bLeaveGameOnTick as a one-shot guard to prevent repeated disconnect handling while the link remains down. - Reset m_bLeaveGameOnTick on LeaveGame(), HostGame(), and JoinGame() to avoid stale state across sessions. * update: converted Japanese comments to English * add: create `Minecraft.Server` developer guide in English and Japanese * update: add note about issue * add: add `nlohmann/json` json lib * add: add FileUtils Moved file operations to `utils`. * add: Dedicated Server BAN access manager with persistent player and IP bans - add Access frontend that publishes thread-safe ban manager snapshots for dedicated server use - add BanManager storage for banned-players.json and banned-ips.json with load/save/update flows - add persistent player and IP ban checks during dedicated server connection handling - add UTF-8 BOM-safe JSON parsing and shared file helpers backed by nlohmann/json - add Unicode-safe ban file read/write and safer atomic replacement behavior on Windows - add active-ban snapshot APIs and expiry-aware filtering for expires metadata - add RAII-based dedicated access shutdown handling during server startup and teardown * update: changed file read/write operations to use `FileUtils`. - As a side effect, saving has become faster! * fix: Re-added the source that had somehow disappeared. * add: significantly improved the dedicated server logging system - add ServerLogManager to Minecraft.Server as the single entry point for dedicated-server log output - forward CMinecraftApp logger output to the server logger when running with g_Win64DedicatedServer - add named network logs for incoming, accepted, rejected, and disconnected connections - cache connection metadata by smallId so player name and remote IP remain available for disconnect logs - keep Minecraft.Client changes minimal by using lightweight hook points and handling log orchestration on the server side * fix: added the updated library source * add: add `ban` and `pardon` commands for Player and IP * fix: fix stop command shutdown process add dedicated server shutdown request handling * fix: fixed the save logic during server shutdown Removed redundant repeated saves and eliminated the risks of async writes. * update: added new sever files to Docker entrypoint * fix: replace shutdown flag with atomic variable for thread safety * update: update Dedicated Server developer guide English is machine translated. Please forgive me. * update: check for the existence of `GameHDD` and create * add: add Whitelist to Dedicated Server * refactor: clean up and refactor the code - unify duplicated implementations that were copied repeatedly - update outdated patterns to more modern ones * fix: include UI header (new update fix) * fix: fix the detection range for excessive logging `getHighestNonEmptyY()` returning `-1` occurs normally when the chunk is entirely air. The caller (`Minecraft.World/LevelChunk.cpp:2400`) normalizes `-1` to `0`. * update: add world size config to dedicated server properties * update: update README add explanation of `server.properties` & launch arguments * update: add nightly release workflow for dedicated server and client builds to Actions * fix: update name for workflow * add random seed generation * add: add Docker nightly workflow for Dedicated Server publish to GitHub Container Registry * fix: ghost player when clients disconnect out of order #4 * fix: fix 7zip option * fix: fix Docker workflow for Dedicated Server artifact handling * add: add no build Dedicated Server startup scripts and Docker Compose * update: add README for Docker Dedicated Server setup with no local build * refactor: refactor command path structure As the number of commands has increased and become harder to navigate, each command has been organized into separate folders. * update: support stream(file stdin) input mode for server CLI Support for the stream (file stdin) required when attaching a tty to a Docker container on Linux. * add: add new CLI Console Commands for Dedicated Server Most of these commands are executed using the command dispatcher implemented on the `Minecraft.World` side. When registering them with the dispatcher, the sender uses a permission-enabled configuration that treats the CLI as a player. - default game. - enchant - experience. - give - kill(currently, getting a permission error for some reason) - time - weather. - update tp & gamemode command * fix: change player map icon to random select * update: increase the player limit * add: restore the basic anti-cheat implementation and add spawn protection Added the following anti-cheat measures and add spawn protection to `server.properties`. - instant break - speed - reach * fix: fix Docker image tag --------- Co-authored-by: sylvessa <225480449+sylvessa@users.noreply.github.com>
This commit is contained in:
44
Minecraft.Server/Console/ServerCli.cpp
Normal file
44
Minecraft.Server/Console/ServerCli.cpp
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
50
Minecraft.Server/Console/ServerCli.h
Normal file
50
Minecraft.Server/Console/ServerCli.h
Normal file
@@ -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;
|
||||
};
|
||||
}
|
||||
395
Minecraft.Server/Console/ServerCliEngine.cpp
Normal file
395
Minecraft.Server/Console/ServerCliEngine.cpp
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
127
Minecraft.Server/Console/ServerCliEngine.h
Normal file
127
Minecraft.Server/Console/ServerCliEngine.h
Normal file
@@ -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;
|
||||
};
|
||||
}
|
||||
285
Minecraft.Server/Console/ServerCliInput.cpp
Normal file
285
Minecraft.Server/Console/ServerCliInput.cpp
Normal file
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
63
Minecraft.Server/Console/ServerCliInput.h
Normal file
63
Minecraft.Server/Console/ServerCliInput.h
Normal file
@@ -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;
|
||||
};
|
||||
}
|
||||
116
Minecraft.Server/Console/ServerCliParser.cpp
Normal file
116
Minecraft.Server/Console/ServerCliParser.cpp
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
63
Minecraft.Server/Console/ServerCliParser.h
Normal file
63
Minecraft.Server/Console/ServerCliParser.h
Normal file
@@ -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);
|
||||
};
|
||||
}
|
||||
99
Minecraft.Server/Console/ServerCliRegistry.cpp
Normal file
99
Minecraft.Server/Console/ServerCliRegistry.cpp
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
58
Minecraft.Server/Console/ServerCliRegistry.h
Normal file
58
Minecraft.Server/Console/ServerCliRegistry.h
Normal file
@@ -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;
|
||||
};
|
||||
}
|
||||
39
Minecraft.Server/Console/commands/CommandParsing.h
Normal file
39
Minecraft.Server/Console/commands/CommandParsing.h
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
50
Minecraft.Server/Console/commands/IServerCliCommand.h
Normal file
50
Minecraft.Server/Console/commands/IServerCliCommand.h
Normal file
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
171
Minecraft.Server/Console/commands/ban-ip/CliCommandBanIp.cpp
Normal file
171
Minecraft.Server/Console/commands/ban-ip/CliCommandBanIp.cpp
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
19
Minecraft.Server/Console/commands/ban-ip/CliCommandBanIp.h
Normal file
19
Minecraft.Server/Console/commands/ban-ip/CliCommandBanIp.h
Normal file
@@ -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;
|
||||
};
|
||||
}
|
||||
136
Minecraft.Server/Console/commands/ban-list/CliCommandBanList.cpp
Normal file
136
Minecraft.Server/Console/commands/ban-list/CliCommandBanList.cpp
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
145
Minecraft.Server/Console/commands/ban/CliCommandBan.cpp
Normal file
145
Minecraft.Server/Console/commands/ban/CliCommandBan.cpp
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
20
Minecraft.Server/Console/commands/ban/CliCommandBan.h
Normal file
20
Minecraft.Server/Console/commands/ban/CliCommandBan.h
Normal file
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
103
Minecraft.Server/Console/commands/give/CliCommandGive.cpp
Normal file
103
Minecraft.Server/Console/commands/give/CliCommandGive.cpp
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
16
Minecraft.Server/Console/commands/give/CliCommandGive.h
Normal file
16
Minecraft.Server/Console/commands/give/CliCommandGive.h
Normal file
@@ -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;
|
||||
};
|
||||
}
|
||||
46
Minecraft.Server/Console/commands/help/CliCommandHelp.cpp
Normal file
46
Minecraft.Server/Console/commands/help/CliCommandHelp.cpp
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
17
Minecraft.Server/Console/commands/help/CliCommandHelp.h
Normal file
17
Minecraft.Server/Console/commands/help/CliCommandHelp.h
Normal file
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
64
Minecraft.Server/Console/commands/kill/CliCommandKill.cpp
Normal file
64
Minecraft.Server/Console/commands/kill/CliCommandKill.cpp
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
16
Minecraft.Server/Console/commands/kill/CliCommandKill.h
Normal file
16
Minecraft.Server/Console/commands/kill/CliCommandKill.h
Normal file
@@ -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;
|
||||
};
|
||||
}
|
||||
49
Minecraft.Server/Console/commands/list/CliCommandList.cpp
Normal file
49
Minecraft.Server/Console/commands/list/CliCommandList.cpp
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
16
Minecraft.Server/Console/commands/list/CliCommandList.h
Normal file
16
Minecraft.Server/Console/commands/list/CliCommandList.h
Normal file
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
173
Minecraft.Server/Console/commands/pardon/CliCommandPardon.cpp
Normal file
173
Minecraft.Server/Console/commands/pardon/CliCommandPardon.cpp
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
19
Minecraft.Server/Console/commands/pardon/CliCommandPardon.h
Normal file
19
Minecraft.Server/Console/commands/pardon/CliCommandPardon.h
Normal file
@@ -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;
|
||||
};
|
||||
}
|
||||
32
Minecraft.Server/Console/commands/stop/CliCommandStop.cpp
Normal file
32
Minecraft.Server/Console/commands/stop/CliCommandStop.cpp
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
16
Minecraft.Server/Console/commands/stop/CliCommandStop.h
Normal file
16
Minecraft.Server/Console/commands/stop/CliCommandStop.h
Normal file
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
118
Minecraft.Server/Console/commands/time/CliCommandTime.cpp
Normal file
118
Minecraft.Server/Console/commands/time/CliCommandTime.cpp
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
16
Minecraft.Server/Console/commands/time/CliCommandTime.h
Normal file
16
Minecraft.Server/Console/commands/time/CliCommandTime.h
Normal file
@@ -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;
|
||||
};
|
||||
}
|
||||
82
Minecraft.Server/Console/commands/tp/CliCommandTp.cpp
Normal file
82
Minecraft.Server/Console/commands/tp/CliCommandTp.cpp
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
18
Minecraft.Server/Console/commands/tp/CliCommandTp.h
Normal file
18
Minecraft.Server/Console/commands/tp/CliCommandTp.h
Normal file
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user