diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ecd100a9..ceb15951a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -114,6 +114,13 @@ list(APPEND MINECRAFT_SERVER_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/tp/CliCommandTp.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/whitelist/CliCommandWhitelist.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/gamemode/CliCommandGamemode.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/time/CliCommandTime.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/weather/CliCommandWeather.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/give/CliCommandGive.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/enchant/CliCommandEnchant.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/kill/CliCommandKill.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/defaultgamemode/CliCommandDefaultGamemode.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/experience/CliCommandExperience.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Common/FileUtils.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Common/StringUtils.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/ServerLogger.cpp" diff --git a/Minecraft.Server/Console/ServerCliEngine.cpp b/Minecraft.Server/Console/ServerCliEngine.cpp index 1162f4d85..82bbdcc8e 100644 --- a/Minecraft.Server/Console/ServerCliEngine.cpp +++ b/Minecraft.Server/Console/ServerCliEngine.cpp @@ -8,13 +8,20 @@ #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" @@ -22,6 +29,8 @@ #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" @@ -30,8 +39,48 @@ 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(this)) { RegisterDefaultCommands(); } @@ -52,7 +101,14 @@ namespace ServerRuntime m_registry->Register(std::unique_ptr(new CliCommandBanList())); m_registry->Register(std::unique_ptr(new CliCommandWhitelist())); m_registry->Register(std::unique_ptr(new CliCommandTp())); + m_registry->Register(std::unique_ptr(new CliCommandTime())); + m_registry->Register(std::unique_ptr(new CliCommandWeather())); + m_registry->Register(std::unique_ptr(new CliCommandGive())); + m_registry->Register(std::unique_ptr(new CliCommandEnchant())); + m_registry->Register(std::unique_ptr(new CliCommandKill())); m_registry->Register(std::unique_ptr(new CliCommandGamemode())); + m_registry->Register(std::unique_ptr(new CliCommandDefaultGamemode())); + m_registry->Register(std::unique_ptr(new CliCommandExperience())); } void ServerCliEngine::EnqueueCommandLine(const std::string &line) @@ -300,9 +356,40 @@ namespace ServerRuntime return NULL; } + bool ServerCliEngine::DispatchWorldCommand(EGameCommand command, byteArray commandData, const std::shared_ptr &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 = sender; + if (commandSender == nullptr) + { + // fall back to console sender if caller did not provide one + commandSender = m_consoleSender; + } + if (commandSender == nullptr) + { + LogWarn("No command sender is available."); + return false; + } + + dispatcher->performCommand(commandSender, command, commandData); + return true; + } + const ServerCliRegistry &ServerCliEngine::Registry() const { return *m_registry; } } - diff --git a/Minecraft.Server/Console/ServerCliEngine.h b/Minecraft.Server/Console/ServerCliEngine.h index 32e166674..b2d72bace 100644 --- a/Minecraft.Server/Console/ServerCliEngine.h +++ b/Minecraft.Server/Console/ServerCliEngine.h @@ -6,8 +6,12 @@ #include #include +#include "..\..\Minecraft.World\ArrayWithLength.h" +#include "..\..\Minecraft.World\CommandsEnum.h" + class GameType; class ServerPlayer; +class CommandSender; namespace ServerRuntime { @@ -98,6 +102,17 @@ namespace ServerRuntime */ 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 &sender = nullptr) const; + const ServerCliRegistry &Registry() const; private: @@ -107,5 +122,6 @@ namespace ServerRuntime mutable std::mutex m_queueMutex; std::queue m_pendingLines; std::unique_ptr m_registry; + std::shared_ptr m_consoleSender; }; } diff --git a/Minecraft.Server/Console/commands/CommandParsing.h b/Minecraft.Server/Console/commands/CommandParsing.h new file mode 100644 index 000000000..edef68d0e --- /dev/null +++ b/Minecraft.Server/Console/commands/CommandParsing.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include + +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::min)() || parsedValue > (std::numeric_limits::max)()) + { + return false; + } + + *outValue = static_cast(parsedValue); + return true; + } + } +} diff --git a/Minecraft.Server/Console/commands/IServerCliCommand.h b/Minecraft.Server/Console/commands/IServerCliCommand.h index 5f54242ac..9cf5ef0e3 100644 --- a/Minecraft.Server/Console/commands/IServerCliCommand.h +++ b/Minecraft.Server/Console/commands/IServerCliCommand.h @@ -17,12 +17,12 @@ namespace ServerRuntime class IServerCliCommand { public: - virtual ~IServerCliCommand() {} + virtual ~IServerCliCommand() = default; /** Primary command name */ virtual const char *Name() const = 0; /** Optional aliases */ - virtual std::vector Aliases() const { return std::vector(); } + virtual std::vector Aliases() const { return {}; } /** Usage text for help */ virtual const char *Usage() const = 0; /** Short command description*/ diff --git a/Minecraft.Server/Console/commands/defaultgamemode/CliCommandDefaultGamemode.cpp b/Minecraft.Server/Console/commands/defaultgamemode/CliCommandDefaultGamemode.cpp new file mode 100644 index 000000000..ee0e35a20 --- /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 "; + + 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 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 *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 000000000..5cc17b34d --- /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 *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 000000000..70d4d7d65 --- /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 [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 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 = std::dynamic_pointer_cast(target); + if (player == nullptr) + { + engine->LogWarn("Cannot resolve target player entity."); + return false; + } + + std::shared_ptr 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 *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 000000000..66e330bd0 --- /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 *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 000000000..77df99ae3 --- /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 + +namespace ServerRuntime +{ + namespace + { + constexpr const char *kExperienceUsage = "xp [L] [player]"; + constexpr const char *kExperienceUsageWithPlayer = "xp [L] "; + + 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::min)()) + { + return false; + } + + parsed.take = signedAmount < 0; + parsed.amount = parsed.take ? -signedAmount : signedAmount; + *outValue = parsed; + return true; + } + + static std::shared_ptr 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 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 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 )."); + 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 *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 000000000..3fddb2188 --- /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 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 *out) const override; + }; +} diff --git a/Minecraft.Server/Console/commands/gamemode/CliCommandGamemode.cpp b/Minecraft.Server/Console/commands/gamemode/CliCommandGamemode.cpp index 2d97b3461..f41660e6c 100644 --- a/Minecraft.Server/Console/commands/gamemode/CliCommandGamemode.cpp +++ b/Minecraft.Server/Console/commands/gamemode/CliCommandGamemode.cpp @@ -4,14 +4,18 @@ #include "..\..\ServerCliEngine.h" #include "..\..\ServerCliParser.h" -#include "..\..\..\Common\StringUtils.h" #include "..\..\..\..\Minecraft.Client\MinecraftServer.h" #include "..\..\..\..\Minecraft.Client\PlayerList.h" #include "..\..\..\..\Minecraft.Client\ServerPlayer.h" -#include "..\..\..\..\Minecraft.World\LevelSettings.h" namespace ServerRuntime { + namespace + { + constexpr const char *kGamemodeUsage = "gamemode [player]"; + constexpr const char *kGamemodeUsageWithPlayer = "gamemode "; + } + const char *CliCommandGamemode::Name() const { return "gamemode"; @@ -24,7 +28,7 @@ namespace ServerRuntime const char *CliCommandGamemode::Usage() const { - return "gamemode [player]"; + return kGamemodeUsage; } const char *CliCommandGamemode::Description() const @@ -34,14 +38,14 @@ namespace ServerRuntime bool CliCommandGamemode::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) { - if (line.tokens.size() < 2) + if (line.tokens.size() < 2 || line.tokens.size() > 3) { - engine->LogWarn("Usage: gamemode [player]"); + engine->LogWarn(std::string("Usage: ") + kGamemodeUsage); return false; } GameType *mode = engine->ParseGamemode(line.tokens[1]); - if (mode == NULL) + if (mode == nullptr) { engine->LogWarn("Unknown gamemode: " + line.tokens[1]); return false; @@ -51,7 +55,7 @@ namespace ServerRuntime if (line.tokens.size() >= 3) { target = engine->FindPlayerByNameUtf8(line.tokens[2]); - if (target == NULL) + if (target == nullptr) { engine->LogWarn("Unknown player: " + line.tokens[2]); return false; @@ -60,16 +64,16 @@ namespace ServerRuntime else { MinecraftServer *server = MinecraftServer::getInstance(); - if (server == NULL || server->getPlayers() == NULL) + 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] == NULL) + if (players->players.size() != 1 || players->players[0] == nullptr) { - engine->LogWarn("Usage: gamemode "); + engine->LogWarn(std::string("Usage: ") + kGamemodeUsageWithPlayer); return false; } target = players->players[0]; @@ -77,9 +81,15 @@ namespace ServerRuntime target->setGameMode(mode); target->fallDistance = 0.0f; - engine->LogInfo( - "Set " + StringUtils::WideToUtf8(target->getName()) + " gamemode to " + - StringUtils::WideToUtf8(mode->getName()) + "."); + + 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; } diff --git a/Minecraft.Server/Console/commands/gamemode/CliCommandGamemode.h b/Minecraft.Server/Console/commands/gamemode/CliCommandGamemode.h index 150a46021..527bb1f9e 100644 --- a/Minecraft.Server/Console/commands/gamemode/CliCommandGamemode.h +++ b/Minecraft.Server/Console/commands/gamemode/CliCommandGamemode.h @@ -7,12 +7,12 @@ namespace ServerRuntime class CliCommandGamemode : public IServerCliCommand { public: - virtual const char *Name() const; - virtual std::vector Aliases() const; - virtual const char *Usage() const; - virtual const char *Description() const; - virtual bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine); - virtual void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector *out) const; + const char *Name() const override; + std::vector 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 *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 000000000..20c094972 --- /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 [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 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 = std::dynamic_pointer_cast(target); + if (player == nullptr) + { + engine->LogWarn("Cannot resolve target player entity."); + return false; + } + + std::shared_ptr 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 *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 000000000..7c21d997c --- /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 *out) const override; + }; +} diff --git a/Minecraft.Server/Console/commands/kill/CliCommandKill.cpp b/Minecraft.Server/Console/commands/kill/CliCommandKill.cpp new file mode 100644 index 000000000..04b2c4191 --- /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 "; + } + + 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 target = engine->FindPlayerByNameUtf8(line.tokens[1]); + if (target == nullptr) + { + engine->LogWarn("Unknown player: " + line.tokens[1]); + return false; + } + + std::shared_ptr sender = std::dynamic_pointer_cast(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 *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 000000000..e558fac0c --- /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 *out) const override; + }; +} diff --git a/Minecraft.Server/Console/commands/time/CliCommandTime.cpp b/Minecraft.Server/Console/commands/time/CliCommandTime.cpp new file mode 100644 index 000000000..d274993cc --- /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 "; + + static bool TryResolveNightFlag(const std::vector &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 *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 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 *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 000000000..28cf5e5a9 --- /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 *out) const override; + }; +} diff --git a/Minecraft.Server/Console/commands/tp/CliCommandTp.cpp b/Minecraft.Server/Console/commands/tp/CliCommandTp.cpp index b4306b3b2..45dbb2846 100644 --- a/Minecraft.Server/Console/commands/tp/CliCommandTp.cpp +++ b/Minecraft.Server/Console/commands/tp/CliCommandTp.cpp @@ -5,12 +5,17 @@ #include "..\..\ServerCliEngine.h" #include "..\..\ServerCliParser.h" #include "..\..\..\..\Minecraft.Client\PlayerConnection.h" +#include "..\..\..\..\Minecraft.Client\TeleportCommand.h" #include "..\..\..\..\Minecraft.Client\ServerPlayer.h" -#include "..\..\..\..\Minecraft.World\net.minecraft.world.level.h" -#include "..\..\..\..\Minecraft.World\net.minecraft.world.level.dimension.h" +#include "..\..\..\..\Minecraft.World\GameCommandPacket.h" namespace ServerRuntime { + namespace + { + constexpr const char *kTpUsage = "tp "; + } + const char *CliCommandTp::Name() const { return "tp"; @@ -23,49 +28,47 @@ namespace ServerRuntime const char *CliCommandTp::Usage() const { - return "tp "; + return kTpUsage; } const char *CliCommandTp::Description() const { - return "Teleport one player to another player."; + return "Teleport one player to another via Minecraft.World command dispatcher."; } bool CliCommandTp::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) { - if (line.tokens.size() < 3) + if (line.tokens.size() != 3) { - engine->LogWarn("Usage: tp "); + engine->LogWarn(std::string("Usage: ") + kTpUsage); return false; } std::shared_ptr subject = engine->FindPlayerByNameUtf8(line.tokens[1]); std::shared_ptr destination = engine->FindPlayerByNameUtf8(line.tokens[2]); - if (subject == NULL) + if (subject == nullptr) { engine->LogWarn("Unknown player: " + line.tokens[1]); return false; } - if (destination == NULL) + if (destination == nullptr) { engine->LogWarn("Unknown player: " + line.tokens[2]); return false; } - if (subject->connection == NULL) + if (subject->connection == nullptr) { engine->LogWarn("Cannot teleport because source player connection is inactive."); return false; } - if (subject->level == NULL || destination->level == NULL || subject->level->dimension->id != destination->level->dimension->id || !subject->isAlive()) + std::shared_ptr packet = TeleportCommand::preparePacket(subject->getXuid(), destination->getXuid()); + if (packet == nullptr) { - engine->LogWarn("Teleport failed because players are in different dimensions or source player is dead."); + engine->LogError("Failed to build teleport command packet."); return false; } - subject->ride(nullptr); - subject->connection->teleport(destination->x, destination->y, destination->z, destination->yRot, destination->xRot); - engine->LogInfo("Teleported " + line.tokens[1] + " to " + line.tokens[2] + "."); - return true; + return engine->DispatchWorldCommand(packet->command, packet->data); } void CliCommandTp::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector *out) const diff --git a/Minecraft.Server/Console/commands/tp/CliCommandTp.h b/Minecraft.Server/Console/commands/tp/CliCommandTp.h index 473e2e924..6e9ffdd7f 100644 --- a/Minecraft.Server/Console/commands/tp/CliCommandTp.h +++ b/Minecraft.Server/Console/commands/tp/CliCommandTp.h @@ -7,12 +7,12 @@ namespace ServerRuntime class CliCommandTp : public IServerCliCommand { public: - virtual const char *Name() const; - virtual std::vector Aliases() const; - virtual const char *Usage() const; - virtual const char *Description() const; - virtual bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine); - virtual void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector *out) const; + const char *Name() const override; + std::vector 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 *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 000000000..e7f019542 --- /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 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 000000000..03498b473 --- /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/Minecraft.Server.vcxproj b/Minecraft.Server/Minecraft.Server.vcxproj index cad5f04bf..be2eb80c8 100644 --- a/Minecraft.Server/Minecraft.Server.vcxproj +++ b/Minecraft.Server/Minecraft.Server.vcxproj @@ -656,13 +656,20 @@ + + + + + + + @@ -692,13 +699,21 @@ + + + + + + + + diff --git a/Minecraft.Server/Minecraft.Server.vcxproj.filters b/Minecraft.Server/Minecraft.Server.vcxproj.filters index b7768ac6a..8cd5e26e0 100644 --- a/Minecraft.Server/Minecraft.Server.vcxproj.filters +++ b/Minecraft.Server/Minecraft.Server.vcxproj.filters @@ -570,12 +570,27 @@ Server\Console\Commands + + Server\Console\Commands + + + Server\Console\Commands + + + Server\Console\Commands + Server\Console\Commands + + Server\Console\Commands + Server\Console\Commands + + Server\Console\Commands + Server\Console\Commands @@ -588,9 +603,15 @@ Server\Console\Commands + + Server\Console\Commands + Server\Console\Commands + + Server\Console\Commands + Server\Console\Commands @@ -654,12 +675,30 @@ Server\Console\Commands + + Server\Console\Commands + + + Server\Console\Commands + + + Server\Console\Commands + Server\Console\Commands + + Server\Console\Commands + Server\Console\Commands + + Server\Console\Commands + + + Server\Console\Commands + Server\Console\Commands @@ -672,9 +711,15 @@ Server\Console\Commands + + Server\Console\Commands + Server\Console\Commands + + Server\Console\Commands + Server\Console\Commands