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