From 58bc70652067d7be2bfb0597b8b0800d35d364de Mon Sep 17 00:00:00 2001 From: la <76826837+3UR@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:30:01 +1000 Subject: [PATCH 1/3] feat: dedicated server command addition and enhancement for some parity with java/bedrock, console prettification(?), you can FINALLY have a proper server seed, tools for moderation. --- Minecraft.Client/Common/Consoles_App.cpp | 6 + Minecraft.Client/Common/UI/IUIScene_HUD.cpp | 4 +- Minecraft.Client/MinecraftServer.cpp | 42 +- Minecraft.Client/MinecraftServer.h | 2 +- Minecraft.Client/PendingConnection.cpp | 5 + Minecraft.Client/PlayerConnection.cpp | 8 +- Minecraft.Client/ServerCommands.cpp | 1642 +++++++++++++++++ Minecraft.Client/ServerCommands.h | 144 ++ Minecraft.Client/ServerConsole.cpp | 766 ++++++++ Minecraft.Client/ServerConsole.h | 92 + Minecraft.Client/Settings.cpp | 6 + Minecraft.Client/Settings.h | 1 + Minecraft.Client/TexturePackRepository.cpp | 8 + .../Windows64/Network/WinsockNetLayer.cpp | 1 + .../Windows64/Network/WinsockNetLayer.h | 1 + .../Windows64/Windows64_Minecraft.cpp | 37 +- cmake/ClientSources.cmake | 2 + 17 files changed, 2738 insertions(+), 29 deletions(-) create mode 100644 Minecraft.Client/ServerCommands.cpp create mode 100644 Minecraft.Client/ServerCommands.h create mode 100644 Minecraft.Client/ServerConsole.cpp create mode 100644 Minecraft.Client/ServerConsole.h diff --git a/Minecraft.Client/Common/Consoles_App.cpp b/Minecraft.Client/Common/Consoles_App.cpp index c3a623d5f..d073a116e 100644 --- a/Minecraft.Client/Common/Consoles_App.cpp +++ b/Minecraft.Client/Common/Consoles_App.cpp @@ -5699,6 +5699,9 @@ bool CMinecraftApp::isXuidDeadmau5(PlayerUID xuid) void CMinecraftApp::AddMemoryTextureFile(const wstring &wName,PBYTE pbData,DWORD dwBytes) { +#ifdef _WINDOWS64 + extern bool g_Win64Verbose; +#endif EnterCriticalSection(&csMemFilesLock); // check it's not already in PMEMDATA pData=nullptr; @@ -5706,6 +5709,9 @@ void CMinecraftApp::AddMemoryTextureFile(const wstring &wName,PBYTE pbData,DWORD if(it != m_MEM_Files.end()) { #ifndef _CONTENT_PACKAGE +#ifdef _WINDOWS64 + if (g_Win64Verbose) +#endif wprintf(L"Incrementing the memory texture file count for %ls\n", wName.c_str()); #endif pData = (*it).second; diff --git a/Minecraft.Client/Common/UI/IUIScene_HUD.cpp b/Minecraft.Client/Common/UI/IUIScene_HUD.cpp index fd9779665..d2754789c 100644 --- a/Minecraft.Client/Common/UI/IUIScene_HUD.cpp +++ b/Minecraft.Client/Common/UI/IUIScene_HUD.cpp @@ -195,8 +195,8 @@ void IUIScene_HUD::renderPlayerHealth() // Update health bool blink = pMinecraft->localplayers[iPad]->invulnerableTime / 3 % 2 == 1; if (pMinecraft->localplayers[iPad]->invulnerableTime < 10) blink = false; - int currentHealth = pMinecraft->localplayers[iPad]->getHealth(); - int oldHealth = pMinecraft->localplayers[iPad]->lastHealth; + int currentHealth = static_cast(ceil(pMinecraft->localplayers[iPad]->getHealth())); + int oldHealth = static_cast(ceil(pMinecraft->localplayers[iPad]->lastHealth)); bool bHasPoison = pMinecraft->localplayers[iPad]->hasEffect(MobEffect::poison); bool bHasWither = pMinecraft->localplayers[iPad]->hasEffect(MobEffect::wither); AttributeInstance *maxHealthAttribute = pMinecraft->localplayers[iPad]->getAttribute(SharedMonsterAttributes::MAX_HEALTH); diff --git a/Minecraft.Client/MinecraftServer.cpp b/Minecraft.Client/MinecraftServer.cpp index bdcc9f813..ffaac4869 100644 --- a/Minecraft.Client/MinecraftServer.cpp +++ b/Minecraft.Client/MinecraftServer.cpp @@ -8,6 +8,9 @@ #include "DispenserBootstrap.h" #include "EntityTracker.h" #include "MinecraftServer.h" +#ifdef _WINDOWS64 +#include "ServerConsole.h" +#endif #include "Options.h" #include "PlayerList.h" #include "ServerChunkCache.h" @@ -58,6 +61,9 @@ #include "..\Minecraft.World\BiomeSource.h" #include "PlayerChunkMap.h" #include "Common\Telemetry\TelemetryManager.h" +#include "ServerCommands.h" +#ifdef _WINDOWS64 +#endif #include "PlayerConnection.h" #ifdef _XBOX_ONE #include "Durango\Network\NetworkPlayerDurango.h" @@ -726,6 +732,19 @@ bool MinecraftServer::initServer(int64_t seed, NetworkGameInitData *initData, DW ProgressRenderer *mcprogress = Minecraft::GetInstance()->progressRenderer; mcprogress->progressStart(IDS_PROGRESS_INITIALISING_SERVER); + if (ShouldUseDedicatedServerProperties()) + { + wstring seedStr = GetDedicatedServerString(settings, L"seed", L""); + if (!seedStr.empty()) + { + int64_t parsedSeed = _wtoi64(seedStr.c_str()); + if (parsedSeed != 0) + seed = parsedSeed; + } + if (seed == 0 && !findSeed) + findSeed = true; + } + if( findSeed ) { #ifdef __PSVITA__ @@ -733,6 +752,10 @@ bool MinecraftServer::initServer(int64_t seed, NetworkGameInitData *initData, DW #else seed = BiomeSource::findSeed(pLevelType); #endif + if (ShouldUseDedicatedServerProperties() && settings != nullptr) + { + settings->setStringAndSave(L"seed", std::to_wstring(seed)); + } } setMaxBuildHeight(GetDedicatedServerInt(settings, L"max-build-height", Level::maxBuildHeight)); @@ -2208,10 +2231,13 @@ void MinecraftServer::handleConsoleInputs() pendingInputs.swap(consoleInput); LeaveCriticalSection(&m_consoleInputCS); + ServerCommands::initialize(); + for (size_t i = 0; i < pendingInputs.size(); ++i) { ConsoleInput *input = pendingInputs[i]; - ExecuteConsoleCommand(this, input->msg); + CommandSource source(this, input->source, CommandSource::CONSOLE, L"CONSOLE"); + ServerCommands::execute(this, input->msg, source); delete input; } } @@ -2245,11 +2271,25 @@ File *MinecraftServer::getFile(const wstring& name) void MinecraftServer::info(const wstring& string) { +#ifdef _WINDOWS64 + if (ServerConsole::getInstance()) + { + ServerConsole::logInfo(string.c_str()); + return; + } +#endif PrintConsoleLine(L"[INFO] ", string); } void MinecraftServer::warn(const wstring& string) { +#ifdef _WINDOWS64 + if (ServerConsole::getInstance()) + { + ServerConsole::logWarn(string.c_str()); + return; + } +#endif PrintConsoleLine(L"[WARN] ", string); } diff --git a/Minecraft.Client/MinecraftServer.h b/Minecraft.Client/MinecraftServer.h index a33888bcc..b19a01860 100644 --- a/Minecraft.Client/MinecraftServer.h +++ b/Minecraft.Client/MinecraftServer.h @@ -142,7 +142,7 @@ class MinecraftServer : public ConsoleInputSource bool loadLevel(LevelStorageSource *storageSource, const wstring& name, int64_t levelSeed, LevelType *pLevelType, NetworkGameInitData *initData); void setProgress(const wstring& status, int progress); void endProgress(); - void saveAllChunks(); + void saveAllChunks(); void saveGameRules(); void stopServer(bool didInit); #ifdef _LARGE_WORLDS diff --git a/Minecraft.Client/PendingConnection.cpp b/Minecraft.Client/PendingConnection.cpp index 6d5497f02..9624a4c7c 100644 --- a/Minecraft.Client/PendingConnection.cpp +++ b/Minecraft.Client/PendingConnection.cpp @@ -7,6 +7,7 @@ #include "ServerLevel.h" #include "PlayerList.h" #include "MinecraftServer.h" +#include "ServerCommands.h" #include "..\Minecraft.World\net.minecraft.network.h" #include "..\Minecraft.World\pos.h" #include "..\Minecraft.World\net.minecraft.world.level.dimension.h" @@ -188,6 +189,10 @@ void PendingConnection::handleLogin(shared_ptr packet) { disconnect(DisconnectPacket::eDisconnect_Banned); } + else if (ServerCommands::getBanList().isBanned(name)) + { + disconnect(DisconnectPacket::eDisconnect_Banned); + } else if (duplicateXuid) { // Reject the incoming connection — a player with this UID is already diff --git a/Minecraft.Client/PlayerConnection.cpp b/Minecraft.Client/PlayerConnection.cpp index d9915cf61..1cb8514be 100644 --- a/Minecraft.Client/PlayerConnection.cpp +++ b/Minecraft.Client/PlayerConnection.cpp @@ -34,6 +34,7 @@ // 4J Added #include "..\Minecraft.World\net.minecraft.world.item.crafting.h" #include "Options.h" +#include "ServerCommands.h" Random PlayerConnection::random; @@ -633,10 +634,9 @@ void PlayerConnection::handleChat(shared_ptr packet) void PlayerConnection::handleCommand(const wstring& message) { - // 4J - TODO -#if 0 - server.getCommandDispatcher().performCommand(player, message); -#endif + ServerCommands::initialize(); + CommandSource source(server, player); + ServerCommands::execute(server, message, source); } void PlayerConnection::handleAnimate(shared_ptr packet) diff --git a/Minecraft.Client/ServerCommands.cpp b/Minecraft.Client/ServerCommands.cpp new file mode 100644 index 000000000..f004ef84c --- /dev/null +++ b/Minecraft.Client/ServerCommands.cpp @@ -0,0 +1,1642 @@ +#include "stdafx.h" +#include "ServerCommands.h" +#include "MinecraftServer.h" +#include "PlayerList.h" +#include "ServerPlayer.h" +#include "ServerLevel.h" +#include "PlayerConnection.h" +#include "EntityTracker.h" +#include "..\Minecraft.World\StringHelpers.h" +#include "..\Minecraft.World\EntityIO.h" +#include "..\Minecraft.World\Entity.h" +#include "..\Minecraft.World\Tile.h" +#include "..\Minecraft.World\Dimension.h" +#include "..\Minecraft.World\LevelData.h" +#include "..\Minecraft.World\MobEffect.h" +#include "..\Minecraft.World\MobEffectInstance.h" +#include "..\Minecraft.World\LevelSettings.h" +#include "..\Minecraft.World\net.minecraft.world.h" +#include "..\Minecraft.World\net.minecraft.world.item.h" +#include "..\Minecraft.World\net.minecraft.world.item.enchantment.h" +#include "..\Minecraft.World\net.minecraft.world.damagesource.h" +#include "..\Minecraft.World\net.minecraft.world.entity.item.h" +#include "..\Minecraft.World\net.minecraft.world.level.h" +#include "..\Minecraft.World\net.minecraft.network.h" +#include "..\Minecraft.World\SharedConstants.h" +#ifdef _WINDOWS64 +#include "ServerConsole.h" +#endif +#include +#include +#include + +unordered_map ServerCommands::s_commands; +unordered_set ServerCommands::s_opOnlyCommands; +OpList ServerCommands::s_opList; +BanList ServerCommands::s_banList; +VanishManager ServerCommands::s_vanishManager; +bool ServerCommands::s_initialized = false; + +CommandSource::CommandSource(MinecraftServer* server, ConsoleInputSource* source, Type type, const wstring& name) + : m_server(server), m_consoleSource(source), m_player(nullptr), m_type(type), m_name(name) +{ +} + +CommandSource::CommandSource(MinecraftServer* server, shared_ptr player) + : m_server(server), m_consoleSource(nullptr), m_player(player), m_type(PLAYER), m_name(player->getName()) +{ +} + +void CommandSource::sendMessage(const wstring& message) +{ + if (m_type == CONSOLE) + { +#ifdef _WINDOWS64 + if (ServerConsole::getInstance()) + ServerConsole::logInfo(message.c_str()); + else +#endif + if (m_consoleSource) + m_consoleSource->info(message); + } + else if (m_player != nullptr) + { + if (m_player->connection != nullptr) + { + m_player->connection->send(std::make_shared(message)); + } + } +} + +void CommandSource::sendSuccess(const wstring& message) +{ + if (m_type == CONSOLE) + { +#ifdef _WINDOWS64 + if (ServerConsole::getInstance()) + ServerConsole::logSuccess("%ls", message.c_str()); + else +#endif + if (m_consoleSource) + m_consoleSource->info(message); + } + else if (m_player != nullptr) + { + if (m_player->connection != nullptr) + { + m_player->connection->send(std::make_shared(message)); + } + } +} + +void CommandSource::sendError(const wstring& message) +{ + if (m_type == CONSOLE) + { +#ifdef _WINDOWS64 + if (ServerConsole::getInstance()) + ServerConsole::logError("%ls", message.c_str()); + else +#endif + if (m_consoleSource) + m_consoleSource->warn(message); + } + else if (m_player != nullptr) + { + if (m_player->connection != nullptr) + { + m_player->connection->send(std::make_shared(message)); + } + } +} + +bool CommandSource::isOp() const +{ + if (m_type == CONSOLE) return true; + return ServerCommands::getOpList().isOp(m_name); +} + +static std::string getExeDirFilePath(const char* filename) +{ + char path[MAX_PATH]; + GetModuleFileNameA(nullptr, path, MAX_PATH); + char* p = strrchr(path, '\\'); + if (p) *(p + 1) = '\0'; + strncat_s(path, sizeof(path), filename, _TRUNCATE); + return std::string(path); +} + +void OpList::addOp(const wstring& name) +{ + m_ops.insert(toLower(name)); +} + +void OpList::removeOp(const wstring& name) +{ + m_ops.erase(toLower(name)); +} + +bool OpList::isOp(const wstring& name) const +{ + return m_ops.find(toLower(name)) != m_ops.end(); +} + +void OpList::save() const +{ + std::string path = getExeDirFilePath("ops.txt"); + std::wofstream file(path, std::ios::out | std::ios::trunc); + if (!file.is_open()) return; + for (const wstring& op : m_ops) + file << op << L"\n"; +} + +void OpList::load() +{ + std::string path = getExeDirFilePath("ops.txt"); + std::wifstream file(path); + if (!file.is_open()) return; + wstring line; + while (std::getline(file, line)) + { + if (!line.empty()) + m_ops.insert(toLower(line)); + } +} + + +void BanList::ban(const wstring& name, const wstring& reason) +{ + m_bans[toLower(name)] = reason; +} + +void BanList::unban(const wstring& name) +{ + m_bans.erase(toLower(name)); +} + +bool BanList::isBanned(const wstring& name) const +{ + return m_bans.find(toLower(name)) != m_bans.end(); +} + +wstring BanList::getBanReason(const wstring& name) const +{ + auto it = m_bans.find(toLower(name)); + if (it != m_bans.end()) + return it->second; + return L""; +} + +void BanList::save() const +{ + std::string path = getExeDirFilePath("bans.txt"); + std::wofstream file(path, std::ios::out | std::ios::trunc); + if (!file.is_open()) return; + for (const auto& entry : m_bans) + { + file << entry.first << L"|" << entry.second << L"\n"; + } +} + +void BanList::load() +{ + std::string path = getExeDirFilePath("bans.txt"); + std::wifstream file(path); + if (!file.is_open()) return; + wstring line; + while (std::getline(file, line)) + { + if (line.empty()) continue; + size_t sep = line.find(L'|'); + if (sep != wstring::npos) + m_bans[toLower(line.substr(0, sep))] = line.substr(sep + 1); + else + m_bans[toLower(line)] = L""; + } +} + + +void VanishManager::setVanished(const wstring& name, bool vanished) +{ + wstring lower = toLower(name); + if (vanished) + m_vanished.insert(lower); + else + m_vanished.erase(lower); +} + +bool VanishManager::isVanished(const wstring& name) const +{ + return m_vanished.find(toLower(name)) != m_vanished.end(); +} + + +vector ServerCommands::splitCommand(const wstring& command) +{ + vector tokens; + std::wistringstream stream(command); + wstring token; + while (stream >> token) + tokens.push_back(token); + return tokens; +} + +wstring ServerCommands::joinTokens(const vector& tokens, size_t startIndex) +{ + wstring joined; + for (size_t i = startIndex; i < tokens.size(); ++i) + { + if (!joined.empty()) joined += L" "; + joined += tokens[i]; + } + return joined; +} + +bool ServerCommands::tryParseInt(const wstring& text, int& value) +{ + std::wistringstream stream(text); + stream >> value; + return !stream.fail() && stream.eof(); +} + +bool ServerCommands::tryParseDouble(const wstring& text, double& value) +{ + std::wistringstream stream(text); + stream >> value; + return !stream.fail() && stream.eof(); +} + +shared_ptr ServerCommands::findPlayer(PlayerList* playerList, const wstring& name) +{ + if (playerList == nullptr) return nullptr; + + for (size_t i = 0; i < playerList->players.size(); ++i) + { + shared_ptr player = playerList->players[i]; + if (player != nullptr && equalsIgnoreCase(player->getName(), name)) + return player; + } + return nullptr; +} + +vector> ServerCommands::resolvePlayerSelector(MinecraftServer* server, const wstring& selector, CommandSource& source) +{ + vector> result; + PlayerList* playerList = server->getPlayers(); + if (playerList == nullptr) return result; + + if (selector == L"@a") + { + for (size_t i = 0; i < playerList->players.size(); ++i) + { + if (playerList->players[i] != nullptr) + result.push_back(playerList->players[i]); + } + } + else if (selector == L"@p") + { + if (source.isPlayer() && source.getPlayer() != nullptr) + { + result.push_back(source.getPlayer()); + } + else if (!playerList->players.empty() && playerList->players[0] != nullptr) + { + result.push_back(playerList->players[0]); + } + } + else if (selector == L"@r") + { + if (!playerList->players.empty()) + { + int idx = rand() % (int)playerList->players.size(); + if (playerList->players[idx] != nullptr) + result.push_back(playerList->players[idx]); + } + } + else if (selector == L"@s") + { + if (source.isPlayer() && source.getPlayer() != nullptr) + result.push_back(source.getPlayer()); + } + else + { + shared_ptr player = findPlayer(playerList, selector); + if (player != nullptr) + result.push_back(player); + } + + return result; +} + +void ServerCommands::initialize() +{ + if (s_initialized) return; + s_initialized = true; + + s_commands[L"help"] = cmdHelp; + s_commands[L"?"] = cmdHelp; + s_commands[L"stop"] = cmdStop; + s_commands[L"list"] = cmdList; + s_commands[L"say"] = cmdSay; + s_commands[L"save-all"] = cmdSaveAll; + s_commands[L"kill"] = cmdKill; + s_commands[L"gamemode"] = cmdGamemode; + s_commands[L"teleport"] = cmdTeleport; + s_commands[L"tp"] = cmdTeleport; + s_commands[L"give"] = cmdGive; + s_commands[L"giveitem"] = cmdGive; + s_commands[L"enchant"] = cmdEnchant; + s_commands[L"enchantitem"] = cmdEnchant; + s_commands[L"time"] = cmdTime; + s_commands[L"weather"] = cmdWeather; + s_commands[L"summon"] = cmdSummon; + s_commands[L"setblock"] = cmdSetblock; + s_commands[L"fill"] = cmdFill; + s_commands[L"effect"] = cmdEffect; + s_commands[L"clear"] = cmdClear; + s_commands[L"vanish"] = cmdVanish; + s_commands[L"op"] = cmdOp; + s_commands[L"deop"] = cmdDeop; + s_commands[L"kick"] = cmdKick; + s_commands[L"ban"] = cmdBan; + s_commands[L"unban"] = cmdUnban; + s_commands[L"pardon"] = cmdUnban; + s_commands[L"banlist"] = cmdBanList; + + s_opOnlyCommands = { + L"stop", L"save-all", L"say", L"kill", L"gamemode", + L"teleport", L"tp", L"give", L"giveitem", + L"enchant", L"enchantitem", L"time", L"weather", + L"summon", L"setblock", L"fill", L"effect", + L"clear", L"vanish", L"op", L"deop", + L"kick", L"ban", L"unban", L"pardon", L"banlist" + }; + + s_opList.load(); + s_banList.load(); +} + +bool ServerCommands::execute(MinecraftServer* server, const wstring& rawCommand, CommandSource& source) +{ + if (server == nullptr) return false; + + wstring command = trimString(rawCommand); + if (command.empty()) return true; + + if (command[0] == L'/') command = trimString(command.substr(1)); + + vector tokens = splitCommand(command); + if (tokens.empty()) return true; + + wstring action = toLower(tokens[0]); + + auto it = s_commands.find(action); + if (it == s_commands.end()) + { + source.sendError(L"Unknown command: " + command + L". Type 'help' for a list of commands."); + return false; + } + + if (source.isPlayer() && !source.isOp() && s_opOnlyCommands.count(action)) + { + source.sendError(L"You do not have permission to use this command."); + return false; + } + + return it->second(server, tokens, source); +} + + +bool ServerCommands::cmdHelp(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (!source.isOp()) + { + source.sendMessage(L" - Server Commands - "); + source.sendMessage(L"help - Show this help message"); + source.sendMessage(L"list - List online players"); + return true; + } + + int page = 1; + if (tokens.size() >= 2) + { + if (tokens[1] == L"2") + page = 2; + } + + if (page == 1) + { + source.sendMessage(L" - Server Commands (Page 1/2) - "); + source.sendMessage(L"help [1|2] - Show this help message"); + source.sendMessage(L"list - List online players"); + source.sendMessage(L"stop - Stop the server"); + source.sendMessage(L"save-all - Save the world"); + source.sendMessage(L"say - Broadcast a message"); + source.sendMessage(L"kill - Kill a player"); + source.sendMessage(L"gamemode [player] - Set game mode"); + source.sendMessage(L"tp | tp - Teleport"); + source.sendMessage(L"give [amount] [data] - Give items"); + source.sendMessage(L"enchant [level] - Enchant held item"); + source.sendMessage(L"time - Manage world time"); + source.sendMessage(L"weather [duration] - Set weather"); + source.sendMessage(L"Type /help 2 for more commands."); + } + else + { + source.sendMessage(L" - Server Commands (Page 2/2) - "); + source.sendMessage(L"summon [x] [y] [z] - Summon an entity"); + source.sendMessage(L"setblock [data] - Set a block"); + source.sendMessage(L"fill [data] [mode] - Fill blocks"); + source.sendMessage(L"effect [effectId] [seconds] [amplifier] - Manage effects"); + source.sendMessage(L"clear [itemId] [data] - Clear inventory"); + source.sendMessage(L"vanish - Toggle vanish (fake disconnect)"); + source.sendMessage(L"op - Grant operator status"); + source.sendMessage(L"deop - Revoke operator status"); + source.sendMessage(L"kick [reason] - Kick a player"); + source.sendMessage(L"ban [reason] - Ban a player"); + source.sendMessage(L"unban - Unban a player (alias: pardon)"); + source.sendMessage(L"banlist - List all banned players"); + } + return true; +} + +bool ServerCommands::cmdStop(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + source.sendMessage(L"Stopping server..."); + server->setSaveOnExit(true); + app.m_bShutdown = true; + MinecraftServer::HaltServer(); + return true; +} + +bool ServerCommands::cmdList(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + PlayerList* playerList = server->getPlayers(); + int count = (playerList != nullptr) ? playerList->getPlayerCount() : 0; + + wstring names; + if (playerList != nullptr) + { + for (size_t i = 0; i < playerList->players.size(); ++i) + { + if (playerList->players[i] != nullptr) + { + if (!names.empty()) names += L", "; + wstring pname = playerList->players[i]->getName(); + if (s_vanishManager.isVanished(pname)) + names += pname + L" (vanished)"; + else + names += pname; + } + } + } + if (names.empty()) names = L"(none)"; + + source.sendMessage(L"Players (" + std::to_wstring(count) + L"): " + names); + return true; +} + + +bool ServerCommands::cmdSay(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 2) + { + source.sendError(L"Usage: say "); + return false; + } + + wstring message = L"[Server] " + joinTokens(tokens, 1); + PlayerList* playerList = server->getPlayers(); + if (playerList != nullptr) + playerList->broadcastAll(std::make_shared(message)); + source.sendMessage(message); + return true; +} + + +bool ServerCommands::cmdSaveAll(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + // stub. non-functional for now. + return true; +} + + +bool ServerCommands::cmdKill(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 2) + { + if (source.isPlayer() && source.getPlayer() != nullptr) + { + source.getPlayer()->hurt(DamageSource::outOfWorld, 3.4e38f); + source.sendSuccess(L"Killed " + source.getName()); + return true; + } + source.sendError(L"Usage: kill "); + return false; + } + + auto targets = resolvePlayerSelector(server, tokens[1], source); + if (targets.empty()) + { + source.sendError(L"No player found: " + tokens[1]); + return false; + } + + for (auto& player : targets) + { + player->hurt(DamageSource::outOfWorld, 3.4e38f); + source.sendSuccess(L"Killed " + player->getName()); + } + return true; +} + + +bool ServerCommands::cmdGamemode(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 2) + { + source.sendError(L"Usage: gamemode [player]"); + return false; + } + + wstring modeName = toLower(tokens[1]); + GameType* gameType = nullptr; + + if (modeName == L"survival" || modeName == L"s" || modeName == L"0") + gameType = GameType::SURVIVAL; + else if (modeName == L"creative" || modeName == L"c" || modeName == L"1") + gameType = GameType::CREATIVE; + else if (modeName == L"adventure" || modeName == L"a" || modeName == L"2") + gameType = GameType::ADVENTURE; + else + { + source.sendError(L"Unknown game mode: " + tokens[1]); + return false; + } + + vector> targets; + if (tokens.size() >= 3) + { + targets = resolvePlayerSelector(server, tokens[2], source); + } + else if (source.isPlayer()) + { + targets.push_back(source.getPlayer()); + } + else + { + source.sendError(L"Usage: gamemode "); + return false; + } + + if (targets.empty()) + { + source.sendError(L"No player found."); + return false; + } + + for (auto& player : targets) + { + player->setGameMode(gameType); + source.sendSuccess(L"Set " + player->getName() + L"'s game mode to " + gameType->getName()); + } + return true; +} + + +bool ServerCommands::cmdTeleport(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + PlayerList* playerList = server->getPlayers(); + + if (tokens.size() == 3) + { + auto subjects = resolvePlayerSelector(server, tokens[1], source); + shared_ptr destination = findPlayer(playerList, tokens[2]); + + if (subjects.empty()) + { + source.sendError(L"Unknown player: " + tokens[1]); + return false; + } + if (destination == nullptr) + { + source.sendError(L"Unknown player: " + tokens[2]); + return false; + } + + for (auto& subject : subjects) + { + if (subject->level->dimension->id != destination->level->dimension->id || !subject->isAlive()) + { + source.sendError(L"Cannot teleport " + subject->getName() + L" (different dimension or dead)."); + continue; + } + subject->ride(nullptr); + subject->connection->teleport(destination->x, destination->y, destination->z, destination->yRot, destination->xRot); + source.sendSuccess(L"Teleported " + subject->getName() + L" to " + destination->getName()); + } + return true; + } + + if (tokens.size() == 5) + { + auto subjects = resolvePlayerSelector(server, tokens[1], source); + if (subjects.empty()) + { + source.sendError(L"Unknown player: " + tokens[1]); + return false; + } + + double x, y, z; + if (!tryParseDouble(tokens[2], x) || !tryParseDouble(tokens[3], y) || !tryParseDouble(tokens[4], z)) + { + source.sendError(L"Invalid coordinates."); + return false; + } + + for (auto& subject : subjects) + { + subject->ride(nullptr); + subject->connection->teleport(x, y, z, subject->yRot, subject->xRot); + source.sendSuccess(L"Teleported " + subject->getName() + L" to " + + std::to_wstring(x) + L", " + std::to_wstring(y) + L", " + std::to_wstring(z)); + } + return true; + } + + if (tokens.size() == 4 && source.isPlayer()) + { + double x, y, z; + if (!tryParseDouble(tokens[1], x) || !tryParseDouble(tokens[2], y) || !tryParseDouble(tokens[3], z)) + { + source.sendError(L"Invalid coordinates."); + return false; + } + auto player = source.getPlayer(); + player->ride(nullptr); + player->connection->teleport(x, y, z, player->yRot, player->xRot); + source.sendSuccess(L"Teleported to " + + std::to_wstring(x) + L", " + std::to_wstring(y) + L", " + std::to_wstring(z)); + return true; + } + + source.sendError(L"Usage: tp | tp "); + return false; +} + +bool ServerCommands::cmdGive(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 3) + { + source.sendError(L"Usage: give [amount] [data]"); + return false; + } + + auto targets = resolvePlayerSelector(server, tokens[1], source); + if (targets.empty()) + { + source.sendError(L"Unknown player: " + tokens[1]); + return false; + } + + int itemId = 0, amount = 1, data = 0; + if (!tryParseInt(tokens[2], itemId)) + { + source.sendError(L"Invalid item id: " + tokens[2]); + return false; + } + if (tokens.size() >= 4 && !tryParseInt(tokens[3], amount)) + { + source.sendError(L"Invalid amount: " + tokens[3]); + return false; + } + if (tokens.size() >= 5 && !tryParseInt(tokens[4], data)) + { + source.sendError(L"Invalid data value: " + tokens[4]); + return false; + } + + if (itemId <= 0 || Item::items[itemId] == nullptr) + { + source.sendError(L"Unknown item id: " + std::to_wstring(itemId)); + return false; + } + if (amount <= 0) amount = 1; + if (amount > 64) amount = 64; + + for (auto& player : targets) + { + shared_ptr itemInstance(new ItemInstance(itemId, amount, data)); + shared_ptr drop = player->drop(itemInstance); + if (drop != nullptr) + drop->throwTime = 0; + source.sendSuccess(L"Gave " + std::to_wstring(amount) + L" x [" + std::to_wstring(itemId) + L":" + std::to_wstring(data) + L"] to " + player->getName()); + } + return true; +} + +bool ServerCommands::cmdEnchant(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 3) + { + source.sendError(L"Usage: enchant [level]"); + return false; + } + + auto targets = resolvePlayerSelector(server, tokens[1], source); + if (targets.empty()) + { + source.sendError(L"Unknown player: " + tokens[1]); + return false; + } + + int enchantmentId = 0, enchantmentLevel = 1; + if (!tryParseInt(tokens[2], enchantmentId)) + { + source.sendError(L"Invalid enchantment id: " + tokens[2]); + return false; + } + if (tokens.size() >= 4 && !tryParseInt(tokens[3], enchantmentLevel)) + { + source.sendError(L"Invalid enchantment level: " + tokens[3]); + return false; + } + + for (auto& player : targets) + { + shared_ptr selectedItem = player->getSelectedItem(); + if (selectedItem == nullptr) + { + source.sendError(player->getName() + L" is not holding an item."); + continue; + } + + Enchantment* enchantment = Enchantment::enchantments[enchantmentId]; + if (enchantment == nullptr) + { + source.sendError(L"Unknown enchantment id: " + std::to_wstring(enchantmentId)); + return false; + } + if (!enchantment->canEnchant(selectedItem)) + { + source.sendError(L"That enchantment cannot be applied to the selected item."); + continue; + } + + if (enchantmentLevel < enchantment->getMinLevel()) enchantmentLevel = enchantment->getMinLevel(); + if (enchantmentLevel > enchantment->getMaxLevel()) enchantmentLevel = enchantment->getMaxLevel(); + + if (selectedItem->hasTag()) + { + ListTag* enchantmentTags = selectedItem->getEnchantmentTags(); + if (enchantmentTags != nullptr) + { + bool conflict = false; + for (int i = 0; i < enchantmentTags->size(); i++) + { + int type = enchantmentTags->get(i)->getShort((wchar_t*)ItemInstance::TAG_ENCH_ID); + if (Enchantment::enchantments[type] != nullptr && !Enchantment::enchantments[type]->isCompatibleWith(enchantment)) + { + source.sendError(L"Enchantment conflicts with existing enchantment on " + player->getName() + L"'s item."); + conflict = true; + break; + } + } + if (conflict) continue; + } + } + + selectedItem->enchant(enchantment, enchantmentLevel); + source.sendSuccess(L"Enchanted " + player->getName() + L"'s held item with enchantment " + + std::to_wstring(enchantmentId) + L" level " + std::to_wstring(enchantmentLevel)); + } + return true; +} + + +bool ServerCommands::cmdTime(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 2) + { + source.sendError(L"Usage: time "); + return false; + } + + wstring subCmd = toLower(tokens[1]); + + if (subCmd == L"query") + { + if (server->levels[0] != nullptr) + { + int64_t dayTime = server->levels[0]->getDayTime(); + int64_t gameTime = server->levels[0]->getGameTime(); + source.sendMessage(L"Day time: " + std::to_wstring(dayTime) + L", Game time: " + std::to_wstring(gameTime)); + } + return true; + } + + if (subCmd == L"add") + { + if (tokens.size() < 3) + { + source.sendError(L"Usage: time add "); + return false; + } + + int delta = 0; + if (!tryParseInt(tokens[2], delta)) + { + source.sendError(L"Invalid tick value: " + tokens[2]); + return false; + } + + for (unsigned int i = 0; i < server->levels.length; ++i) + { + if (server->levels[i] != nullptr) + server->levels[i]->setDayTime(server->levels[i]->getDayTime() + delta); + } + source.sendSuccess(L"Added " + std::to_wstring(delta) + L" ticks to the time."); + return true; + } + wstring timeValue; + if (subCmd == L"set") + { + if (tokens.size() < 3) + { + source.sendError(L"Usage: time set "); + return false; + } + timeValue = toLower(tokens[2]); + } + else + { + timeValue = subCmd; + } + + int targetTime = 0; + if (timeValue == L"day") + targetTime = 1000; + else if (timeValue == L"noon") + targetTime = 6000; + else if (timeValue == L"night") + targetTime = 13000; + else if (timeValue == L"midnight") + targetTime = 18000; + else if (!tryParseInt(timeValue, targetTime)) + { + source.sendError(L"Invalid time value: " + timeValue); + return false; + } + + for (unsigned int i = 0; i < server->levels.length; ++i) + { + if (server->levels[i] != nullptr) + server->levels[i]->setDayTime(targetTime); + } + source.sendSuccess(L"Set the time to " + std::to_wstring(targetTime)); + return true; +} + + +bool ServerCommands::cmdWeather(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 2) + { + source.sendError(L"Usage: weather [duration_seconds]"); + return false; + } + + int durationSeconds = 600; + if (tokens.size() >= 3 && !tryParseInt(tokens[2], durationSeconds)) + { + source.sendError(L"Invalid duration: " + tokens[2]); + return false; + } + + if (server->levels[0] == nullptr) + { + source.sendError(L"The overworld is not loaded."); + return false; + } + + LevelData* levelData = server->levels[0]->getLevelData(); + int duration = durationSeconds * SharedConstants::TICKS_PER_SECOND; + levelData->setRainTime(duration); + levelData->setThunderTime(duration); + + wstring weather = toLower(tokens[1]); + if (weather == L"clear") + { + levelData->setRaining(false); + levelData->setThundering(false); + } + else if (weather == L"rain") + { + levelData->setRaining(true); + levelData->setThundering(false); + } + else if (weather == L"thunder") + { + levelData->setRaining(true); + levelData->setThundering(true); + } + else + { + source.sendError(L"Unknown weather type: " + tokens[1] + L". Use clear, rain, or thunder."); + return false; + } + + source.sendSuccess(L"Set weather to " + weather + L" for " + std::to_wstring(durationSeconds) + L" seconds."); + return true; +} + +bool ServerCommands::cmdSummon(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 2) + { + source.sendError(L"Usage: summon [x] [y] [z]"); + return false; + } + + wstring entityName = tokens[1]; + + double x = 0, y = 64, z = 0; + bool hasPos = false; + + if (tokens.size() >= 5) + { + if (!tryParseDouble(tokens[2], x) || !tryParseDouble(tokens[3], y) || !tryParseDouble(tokens[4], z)) + { + source.sendError(L"Invalid coordinates."); + return false; + } + hasPos = true; + } + else if (source.isPlayer() && source.getPlayer() != nullptr) + { + x = source.getPlayer()->x; + y = source.getPlayer()->y; + z = source.getPlayer()->z; + hasPos = true; + } + + if (!hasPos && server->levels[0] != nullptr) + { + LevelData* ld = server->levels[0]->getLevelData(); + if (ld != nullptr) + { + x = ld->getXSpawn(); + y = ld->getYSpawn(); + z = ld->getZSpawn(); + } + } + ServerLevel* level = server->levels[0]; + if (source.isPlayer() && source.getPlayer() != nullptr) + { + level = (ServerLevel*)source.getPlayer()->level; + } + + if (level == nullptr) + { + source.sendError(L"No level available."); + return false; + } + + shared_ptr entity = EntityIO::newEntity(entityName, level); + if (entity == nullptr) + { + wstring lowerName = toLower(entityName); + for (auto& pair : EntityIO::idsSpawnableInCreative) + { + wstring knownName = EntityIO::getEncodeId(pair.first); + if (toLower(knownName) == lowerName) + { + entity = EntityIO::newEntity(knownName, level); + entityName = knownName; + break; + } + } + } + + if (entity == nullptr) + { + source.sendError(L"Unknown entity type: " + tokens[1]); + return false; + } + + entity->moveTo(x, y, z, 0.0f, 0.0f); + level->addEntity(entity); + + source.sendSuccess(L"Summoned " + entityName + L" at " + + std::to_wstring((int)x) + L", " + std::to_wstring((int)y) + L", " + std::to_wstring((int)z)); + return true; +} + + +bool ServerCommands::cmdSetblock(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 5) + { + source.sendError(L"Usage: setblock [data]"); + return false; + } + + int x, y, z, tileId, data = 0; + if (!tryParseInt(tokens[1], x) || !tryParseInt(tokens[2], y) || !tryParseInt(tokens[3], z)) + { + source.sendError(L"Invalid coordinates."); + return false; + } + if (!tryParseInt(tokens[4], tileId)) + { + source.sendError(L"Invalid tile id: " + tokens[4]); + return false; + } + if (tokens.size() >= 6 && !tryParseInt(tokens[5], data)) + { + source.sendError(L"Invalid data value: " + tokens[5]); + return false; + } + + if (tileId < 0 || tileId >= Tile::TILE_NUM_COUNT) + { + source.sendError(L"Tile id out of range (0-" + std::to_wstring(Tile::TILE_NUM_COUNT - 1) + L")."); + return false; + } + + if (tileId != 0 && Tile::tiles[tileId] == nullptr) + { + source.sendError(L"Unknown tile id: " + std::to_wstring(tileId)); + return false; + } + + ServerLevel* level = server->levels[0]; + if (source.isPlayer() && source.getPlayer() != nullptr) + level = (ServerLevel*)source.getPlayer()->level; + + if (level == nullptr) + { + source.sendError(L"No level available."); + return false; + } + + level->setTileAndData(x, y, z, tileId, data, Tile::UPDATE_ALL); + + source.sendSuccess(L"Set block at " + std::to_wstring(x) + L", " + std::to_wstring(y) + L", " + std::to_wstring(z) + + L" to " + std::to_wstring(tileId) + L":" + std::to_wstring(data)); + return true; +} + + + +bool ServerCommands::cmdFill(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 8) + { + source.sendError(L"Usage: fill [data] [destroy|hollow|keep|outline|replace]"); + return false; + } + + int x1, y1, z1, x2, y2, z2, tileId, data = 0; + if (!tryParseInt(tokens[1], x1) || !tryParseInt(tokens[2], y1) || !tryParseInt(tokens[3], z1) || + !tryParseInt(tokens[4], x2) || !tryParseInt(tokens[5], y2) || !tryParseInt(tokens[6], z2)) + { + source.sendError(L"Invalid coordinates."); + return false; + } + if (!tryParseInt(tokens[7], tileId)) + { + source.sendError(L"Invalid tile id: " + tokens[7]); + return false; + } + if (tokens.size() >= 9 && !tryParseInt(tokens[8], data)) + { + source.sendError(L"Invalid data value: " + tokens[8]); + return false; + } + + if (tileId < 0 || tileId >= Tile::TILE_NUM_COUNT) + { + source.sendError(L"Tile id out of range."); + return false; + } + if (tileId != 0 && Tile::tiles[tileId] == nullptr) + { + source.sendError(L"Unknown tile id: " + std::to_wstring(tileId)); + return false; + } + + wstring mode = L"replace"; + if (tokens.size() >= 10) mode = toLower(tokens[9]); + + int minX = (x1 < x2) ? x1 : x2; + int minY = (y1 < y2) ? y1 : y2; + int minZ = (z1 < z2) ? z1 : z2; + int maxX = (x1 > x2) ? x1 : x2; + int maxY = (y1 > y2) ? y1 : y2; + int maxZ = (z1 > z2) ? z1 : z2; + + int64_t volume = (int64_t)(maxX - minX + 1) * (maxY - minY + 1) * (maxZ - minZ + 1); + if (volume > 32768) + { + source.sendError(L"Too many blocks in the fill region (" + std::to_wstring(volume) + L"). Maximum is 32768."); + return false; + } + + ServerLevel* level = server->levels[0]; + if (source.isPlayer() && source.getPlayer() != nullptr) + level = (ServerLevel*)source.getPlayer()->level; + + if (level == nullptr) + { + source.sendError(L"No level available."); + return false; + } + + int blocksChanged = 0; + + for (int bx = minX; bx <= maxX; ++bx) + { + for (int by = minY; by <= maxY; ++by) + { + for (int bz = minZ; bz <= maxZ; ++bz) + { + bool shouldPlace = true; + + if (mode == L"keep") + { + if (level->getTile(bx, by, bz) != 0) shouldPlace = false; + } + else if (mode == L"hollow") + { + bool isEdge = (bx == minX || bx == maxX || by == minY || by == maxY || bz == minZ || bz == maxZ); + if (isEdge) + shouldPlace = true; + else + { + level->setTileAndData(bx, by, bz, 0, 0, Tile::UPDATE_ALL); + blocksChanged++; + shouldPlace = false; + } + } + else if (mode == L"outline") + { + bool isEdge = (bx == minX || bx == maxX || by == minY || by == maxY || bz == minZ || bz == maxZ); + if (!isEdge) shouldPlace = false; + } + + if (shouldPlace) + { + level->setTileAndData(bx, by, bz, tileId, data, Tile::UPDATE_ALL); + blocksChanged++; + } + } + } + } + + source.sendSuccess(L"Filled " + std::to_wstring(blocksChanged) + L" blocks."); + return true; +} + + +bool ServerCommands::cmdEffect(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 3) + { + source.sendError(L"Usage: effect [effectId] [seconds] [amplifier] | effect clear "); + return false; + } + + wstring subCmd = toLower(tokens[1]); + + if (subCmd == L"clear") + { + auto targets = resolvePlayerSelector(server, tokens[2], source); + if (targets.empty()) + { + source.sendError(L"Unknown player: " + tokens[2]); + return false; + } + + for (auto& player : targets) + { + player->removeAllEffects(); + source.sendSuccess(L"Cleared all effects from " + player->getName()); + } + return true; + } + + if (subCmd == L"give") + { + if (tokens.size() < 4) + { + source.sendError(L"Usage: effect give [seconds] [amplifier]"); + return false; + } + + auto targets = resolvePlayerSelector(server, tokens[2], source); + if (targets.empty()) + { + source.sendError(L"Unknown player: " + tokens[2]); + return false; + } + + int effectId = 0, seconds = 30, amplifier = 0; + if (!tryParseInt(tokens[3], effectId)) + { + source.sendError(L"Invalid effect id: " + tokens[3]); + return false; + } + if (tokens.size() >= 5 && !tryParseInt(tokens[4], seconds)) + { + source.sendError(L"Invalid duration: " + tokens[4]); + return false; + } + if (tokens.size() >= 6 && !tryParseInt(tokens[5], amplifier)) + { + source.sendError(L"Invalid amplifier: " + tokens[5]); + return false; + } + + if (effectId < 0 || effectId >= MobEffect::NUM_EFFECTS || MobEffect::effects[effectId] == nullptr) + { + source.sendError(L"Unknown effect id: " + std::to_wstring(effectId) + L" (valid range: 1-" + std::to_wstring(MobEffect::NUM_EFFECTS - 1) + L")"); + return false; + } + + if (seconds <= 0) seconds = 1; + if (seconds > 1000000) seconds = 1000000; + if (amplifier < 0) amplifier = 0; + if (amplifier > 255) amplifier = 255; + + int durationTicks = seconds * SharedConstants::TICKS_PER_SECOND; + + for (auto& player : targets) + { + MobEffectInstance* effectInstance = new MobEffectInstance(effectId, durationTicks, amplifier); + player->addEffect(effectInstance); + source.sendSuccess(L"Applied effect " + std::to_wstring(effectId) + + L" (amplifier " + std::to_wstring(amplifier) + L", " + std::to_wstring(seconds) + L"s) to " + player->getName()); + } + return true; + } + + auto targets = resolvePlayerSelector(server, tokens[1], source); + if (!targets.empty() && tokens.size() >= 3) + { + int effectId = 0, seconds = 30, amplifier = 0; + if (tryParseInt(tokens[2], effectId)) + { + if (tokens.size() >= 4) tryParseInt(tokens[3], seconds); + if (tokens.size() >= 5) tryParseInt(tokens[4], amplifier); + + if (effectId < 0 || effectId >= MobEffect::NUM_EFFECTS || MobEffect::effects[effectId] == nullptr) + { + source.sendError(L"Unknown effect id: " + std::to_wstring(effectId)); + return false; + } + + if (seconds <= 0) seconds = 1; + if (amplifier < 0) amplifier = 0; + if (amplifier > 255) amplifier = 255; + + int durationTicks = seconds * SharedConstants::TICKS_PER_SECOND; + + for (auto& player : targets) + { + MobEffectInstance* effectInstance = new MobEffectInstance(effectId, durationTicks, amplifier); + player->addEffect(effectInstance); + source.sendSuccess(L"Applied effect " + std::to_wstring(effectId) + L" to " + player->getName()); + } + return true; + } + } + + source.sendError(L"Usage: effect [effectId] [seconds] [amplifier]"); + return false; +} + + + +bool ServerCommands::cmdClear(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 2) + { + if (source.isPlayer()) + { + auto player = source.getPlayer(); + player->inventory->clearInventory(-1, -1); + source.sendSuccess(L"Cleared inventory of " + player->getName()); + return true; + } + source.sendError(L"Usage: clear [itemId] [data]"); + return false; + } + + auto targets = resolvePlayerSelector(server, tokens[1], source); + if (targets.empty()) + { + source.sendError(L"Unknown player: " + tokens[1]); + return false; + } + + int filterItemId = -1, filterData = -1; + if (tokens.size() >= 3 && !tryParseInt(tokens[2], filterItemId)) + { + source.sendError(L"Invalid item id: " + tokens[2]); + return false; + } + if (tokens.size() >= 4 && !tryParseInt(tokens[3], filterData)) + { + source.sendError(L"Invalid data value: " + tokens[3]); + return false; + } + + for (auto& player : targets) + { + if (filterItemId < 0) + { + player->inventory->clearInventory(-1, -1); + source.sendSuccess(L"Cleared inventory of " + player->getName()); + } + else + { + int removed = 0; + for (int slot = 0; slot < player->inventory->getContainerSize(); ++slot) + { + shared_ptr item = player->inventory->getItem(slot); + if (item != nullptr && item->id == filterItemId) + { + if (filterData >= 0 && item->getAuxValue() != filterData) continue; + removed += item->count; + player->inventory->setItem(slot, nullptr); + } + } + source.sendSuccess(L"Cleared " + std::to_wstring(removed) + L" items from " + player->getName()); + } + } + return true; +} + + +static void vanishPlayer(shared_ptr player, PlayerList* playerList) +{ + if (playerList != nullptr) + { + auto fakeLeave = std::make_shared(player->getName(), ChatPacket::e_ChatPlayerLeftGame); + for (size_t i = 0; i < playerList->players.size(); ++i) + { + if (playerList->players[i] != nullptr && playerList->players[i] != player) + playerList->players[i]->connection->send(fakeLeave); + } + } + + ServerLevel* level = (ServerLevel*)player->level; + if (level != nullptr) + { + EntityTracker* tracker = level->getTracker(); + if (tracker != nullptr) + tracker->removeEntity(player); + } +} + +static void unvanishPlayer(shared_ptr player, PlayerList* playerList) +{ + ServerLevel* level = (ServerLevel*)player->level; + if (level != nullptr) + { + EntityTracker* tracker = level->getTracker(); + if (tracker != nullptr) + tracker->addEntity(player); + } + + if (playerList != nullptr) + { + auto fakeJoin = std::make_shared(player->getName(), ChatPacket::e_ChatPlayerJoinedGame); + for (size_t i = 0; i < playerList->players.size(); ++i) + { + if (playerList->players[i] != nullptr && playerList->players[i] != player) + playerList->players[i]->connection->send(fakeJoin); + } + } +} + +bool ServerCommands::cmdVanish(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 2) + { + if (source.isPlayer()) + { + wstring name = source.getName(); + bool isVanished = s_vanishManager.isVanished(name); + s_vanishManager.setVanished(name, !isVanished); + + PlayerList* playerList = server->getPlayers(); + if (!isVanished) + { + vanishPlayer(source.getPlayer(), playerList); + source.sendSuccess(L"You are now vanished. Other players think you disconnected."); + } + else + { + unvanishPlayer(source.getPlayer(), playerList); + source.sendSuccess(L"You are no longer vanished."); + } + return true; + } + source.sendError(L"Usage: vanish "); + return false; + } + + auto targets = resolvePlayerSelector(server, tokens[1], source); + if (targets.empty()) + { + source.sendError(L"Unknown player: " + tokens[1]); + return false; + } + + PlayerList* playerList = server->getPlayers(); + + for (auto& player : targets) + { + wstring name = player->getName(); + bool isVanished = s_vanishManager.isVanished(name); + s_vanishManager.setVanished(name, !isVanished); + + if (!isVanished) + { + vanishPlayer(player, playerList); + source.sendSuccess(name + L" is now vanished."); + } + else + { + unvanishPlayer(player, playerList); + source.sendSuccess(name + L" is no longer vanished."); + } + } + return true; +} + + +bool ServerCommands::cmdOp(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 2) + { + source.sendError(L"Usage: op "); + return false; + } + + wstring playerName = tokens[1]; + shared_ptr player = findPlayer(server->getPlayers(), playerName); + if (player != nullptr) + playerName = player->getName(); + + if (s_opList.isOp(playerName)) + { + source.sendMessage(playerName + L" is already an operator."); + return true; + } + + s_opList.addOp(playerName); + s_opList.save(); + source.sendSuccess(L"Made " + playerName + L" a server operator."); + + if (player != nullptr && player->connection != nullptr) + { + player->connection->send(std::make_shared(L"You are now a server operator.")); + } + + return true; +} + +bool ServerCommands::cmdDeop(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 2) + { + source.sendError(L"Usage: deop "); + return false; + } + + wstring playerName = tokens[1]; + shared_ptr player = findPlayer(server->getPlayers(), playerName); + if (player != nullptr) + playerName = player->getName(); + + if (!s_opList.isOp(playerName)) + { + source.sendMessage(playerName + L" is not an operator."); + return true; + } + + s_opList.removeOp(playerName); + s_opList.save(); + source.sendSuccess(L"Removed " + playerName + L" from server operators."); + + if (player != nullptr && player->connection != nullptr) + { + player->connection->send(std::make_shared(L"You are no longer a server operator.")); + } + + return true; +} + + +bool ServerCommands::cmdKick(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 2) + { + source.sendError(L"Usage: kick [reason]"); + return false; + } + + auto targets = resolvePlayerSelector(server, tokens[1], source); + if (targets.empty()) + { + source.sendError(L"Unknown player: " + tokens[1]); + return false; + } + + wstring reason = tokens.size() > 2 ? joinTokens(tokens, 2) : L"Kicked by an operator"; + + for (auto& player : targets) + { + if (player->connection != nullptr) + { + player->connection->send(std::make_shared(L"Kicked: " + reason)); + player->connection->setWasKicked(); + player->connection->disconnect(DisconnectPacket::eDisconnect_Kicked); + } + source.sendSuccess(L"Kicked " + player->getName() + L": " + reason); + } + return true; +} + +bool ServerCommands::cmdBan(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 2) + { + source.sendError(L"Usage: ban [reason]"); + return false; + } + + wstring playerName = tokens[1]; + wstring reason = tokens.size() > 2 ? joinTokens(tokens, 2) : L"Banned by an operator"; + + shared_ptr player = findPlayer(server->getPlayers(), playerName); + if (player != nullptr) + playerName = player->getName(); + + s_banList.ban(playerName, reason); + s_banList.save(); + source.sendSuccess(L"Banned " + playerName + L": " + reason); + + if (player != nullptr && player->connection != nullptr) + { + player->connection->send(std::make_shared(L"Banned: " + reason)); + player->connection->setWasKicked(); + player->connection->disconnect(DisconnectPacket::eDisconnect_Banned); + } + + return true; +} + +bool ServerCommands::cmdUnban(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 2) + { + source.sendError(L"Usage: unban "); + return false; + } + + wstring playerName = tokens[1]; + if (!s_banList.isBanned(playerName)) + { + source.sendError(playerName + L" is not banned."); + return true; + } + + s_banList.unban(playerName); + s_banList.save(); + source.sendSuccess(L"Unbanned " + playerName + L"."); + return true; +} + +bool ServerCommands::cmdBanList(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + const auto& bans = s_banList.getBans(); + if (bans.empty()) + { + source.sendMessage(L"No players are banned."); + return true; + } + + source.sendMessage(L"Banned players (" + std::to_wstring(bans.size()) + L"):"); + for (const auto& entry : bans) + { + if (entry.second.empty()) + source.sendMessage(L" " + entry.first); + else + source.sendMessage(L" " + entry.first + L" - " + entry.second); + } + return true; +} diff --git a/Minecraft.Client/ServerCommands.h b/Minecraft.Client/ServerCommands.h new file mode 100644 index 000000000..72b08ec29 --- /dev/null +++ b/Minecraft.Client/ServerCommands.h @@ -0,0 +1,144 @@ +#pragma once +#include +#include +#include +#include +#include + +class MinecraftServer; +class ServerPlayer; +class ConsoleInputSource; +class PlayerList; + +// Command sender abstraction - can be server console or a player +class CommandSource +{ +public: + enum Type { CONSOLE, PLAYER }; + + CommandSource(MinecraftServer* server, ConsoleInputSource* source, Type type, const wstring& name); + CommandSource(MinecraftServer* server, shared_ptr player); + + void sendMessage(const wstring& message); + void sendSuccess(const wstring& message); + void sendError(const wstring& message); + + bool isConsole() const { return m_type == CONSOLE; } + bool isPlayer() const { return m_type == PLAYER; } + bool isOp() const; + + MinecraftServer* getServer() const { return m_server; } + shared_ptr getPlayer() const { return m_player; } + wstring getName() const { return m_name; } + +private: + MinecraftServer* m_server; + ConsoleInputSource* m_consoleSource; + shared_ptr m_player; + Type m_type; + wstring m_name; +}; + +// Server-side op list management +class OpList +{ +public: + void addOp(const wstring& name); + void removeOp(const wstring& name); + bool isOp(const wstring& name) const; + const unordered_set& getOps() const { return m_ops; } + + void save() const; + void load(); + +private: + unordered_set m_ops; +}; + +// Server-side ban list management (persistent) +class BanList +{ +public: + void ban(const wstring& name, const wstring& reason = L""); + void unban(const wstring& name); + bool isBanned(const wstring& name) const; + wstring getBanReason(const wstring& name) const; + const unordered_map& getBans() const { return m_bans; } + + void save() const; + void load(); + +private: + unordered_map m_bans; // name -> reason +}; + +// Vanish manager +class VanishManager +{ +public: + void setVanished(const wstring& name, bool vanished); + bool isVanished(const wstring& name) const; + const unordered_set& getVanished() const { return m_vanished; } + +private: + unordered_set m_vanished; +}; + +// Main command registry and execution engine +class ServerCommands +{ +public: + static void initialize(); + + // Execute a command string (with or without leading /) + static bool execute(MinecraftServer* server, const wstring& rawCommand, CommandSource& source); + + // Access op and vanish managers + static OpList& getOpList() { return s_opList; } + static VanishManager& getVanishManager() { return s_vanishManager; } + static BanList& getBanList() { return s_banList; } + +private: + // Utility functions + static vector splitCommand(const wstring& command); + static wstring joinTokens(const vector& tokens, size_t startIndex); + static bool tryParseInt(const wstring& text, int& value); + static bool tryParseDouble(const wstring& text, double& value); + static shared_ptr findPlayer(PlayerList* playerList, const wstring& name); + static vector> resolvePlayerSelector(MinecraftServer* server, const wstring& selector, CommandSource& source); + + // Command handlers + static bool cmdHelp(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdStop(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdList(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdSay(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdSaveAll(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdKill(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdGamemode(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdTeleport(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdGive(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdEnchant(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdTime(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdWeather(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdSummon(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdSetblock(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdFill(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdEffect(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdClear(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdVanish(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdOp(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdDeop(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdKick(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdBan(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdUnban(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdBanList(MinecraftServer* server, const vector& tokens, CommandSource& source); + + // Command handler map + typedef bool (*CommandHandler)(MinecraftServer*, const vector&, CommandSource&); + static unordered_map s_commands; + static unordered_set s_opOnlyCommands; + static OpList s_opList; + static BanList s_banList; + static VanishManager s_vanishManager; + static bool s_initialized; +}; diff --git a/Minecraft.Client/ServerConsole.cpp b/Minecraft.Client/ServerConsole.cpp new file mode 100644 index 000000000..25648432f --- /dev/null +++ b/Minecraft.Client/ServerConsole.cpp @@ -0,0 +1,766 @@ +#include "stdafx.h" + +#ifdef _WINDOWS64 + +#include "ServerConsole.h" +#include "MinecraftServer.h" +#include "PlayerList.h" +#include "ServerPlayer.h" +#include "..\Minecraft.World\StringHelpers.h" +#include "..\Minecraft.World\EntityIO.h" +#include "..\Minecraft.World\Tile.h" +#include "..\Minecraft.World\MobEffect.h" +#include "..\Minecraft.World\LevelSettings.h" + +#include +#include +#include +#include + +ServerConsole* ServerConsole::s_instance = nullptr; + +const char* ServerConsole::Color::Reset = "\033[0m"; +const char* ServerConsole::Color::Red = "\033[31m"; +const char* ServerConsole::Color::Green = "\033[32m"; +const char* ServerConsole::Color::Yellow = "\033[33m"; +const char* ServerConsole::Color::Blue = "\033[34m"; +const char* ServerConsole::Color::Magenta = "\033[35m"; +const char* ServerConsole::Color::Cyan = "\033[36m"; +const char* ServerConsole::Color::White = "\033[37m"; +const char* ServerConsole::Color::Gray = "\033[90m"; +const char* ServerConsole::Color::BrightRed = "\033[91m"; +const char* ServerConsole::Color::BrightGreen = "\033[92m"; +const char* ServerConsole::Color::BrightYellow= "\033[93m"; +const char* ServerConsole::Color::BrightBlue = "\033[94m"; +const char* ServerConsole::Color::BrightMagenta="\033[95m"; +const char* ServerConsole::Color::BrightCyan = "\033[96m"; +const char* ServerConsole::Color::BrightWhite = "\033[97m"; + +// Known command names for completion and highlighting +static const char* s_commandNames[] = { + "help", "stop", "list", "say", "save-all", + "kill", "gamemode", "teleport", "tp", "give", "giveitem", + "enchant", "enchantitem", "time", "weather", "summon", + "setblock", "fill", "effect", "clear", "vanish", "op", "deop", + "kick", "ban", "unban", "pardon", "banlist", + nullptr +}; + +// Subcommand completions for specific commands +static const char* s_timeSubcmds[] = { "set", "add", "query", nullptr }; +static const char* s_timeSetValues[] = { "day", "night", "noon", "midnight", nullptr }; +static const char* s_weatherTypes[] = { "clear", "rain", "thunder", nullptr }; +static const char* s_gamemodeNames[] = { "survival", "creative", "adventure", "0", "1", "2", "s", "c", "a", nullptr }; +static const char* s_effectActions[] = { "give", "clear", nullptr }; + +ServerConsole::ServerConsole(MinecraftServer* server) + : m_server(server) + , m_cursorPos(0) + , m_historyIndex(-1) + , m_tabIndex(-1) + , m_running(true) +{ + m_hConsoleIn = GetStdHandle(STD_INPUT_HANDLE); + m_hConsoleOut = GetStdHandle(STD_OUTPUT_HANDLE); + s_instance = this; +} + +ServerConsole::~ServerConsole() +{ + if (s_instance == this) + s_instance = nullptr; +} + +void ServerConsole::enableVirtualTerminal() +{ + DWORD dwMode = 0; + if (GetConsoleMode(m_hConsoleOut, &dwMode)) + { + dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; + SetConsoleMode(m_hConsoleOut, dwMode); + } + + if (GetConsoleMode(m_hConsoleIn, &dwMode)) + { + // Disable line input and echo so we can handle raw key events + dwMode &= ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT); + dwMode |= ENABLE_WINDOW_INPUT; + SetConsoleMode(m_hConsoleIn, dwMode); + } +} + +void ServerConsole::logInfo(const char* fmt, ...) +{ + ServerConsole* sc = getInstance(); + if (sc) sc->clearInputLine(); + + char buf[2048]; + va_list args; + va_start(args, fmt); + vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + + printf("%s[INFO]%s %s\n", Color::BrightCyan, Color::Reset, buf); + fflush(stdout); + + if (sc) sc->redrawInputLine(); +} + +void ServerConsole::logWarn(const char* fmt, ...) +{ + ServerConsole* sc = getInstance(); + if (sc) sc->clearInputLine(); + + char buf[2048]; + va_list args; + va_start(args, fmt); + vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + + printf("%s[WARN]%s %s%s%s\n", Color::BrightYellow, Color::Reset, Color::Yellow, buf, Color::Reset); + fflush(stdout); + + if (sc) sc->redrawInputLine(); +} + +void ServerConsole::logError(const char* fmt, ...) +{ + ServerConsole* sc = getInstance(); + if (sc) sc->clearInputLine(); + + char buf[2048]; + va_list args; + va_start(args, fmt); + vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + + printf("%s[ERROR]%s %s%s%s\n", Color::BrightRed, Color::Reset, Color::Red, buf, Color::Reset); + fflush(stdout); + + if (sc) sc->redrawInputLine(); +} + +void ServerConsole::logSuccess(const char* fmt, ...) +{ + ServerConsole* sc = getInstance(); + if (sc) sc->clearInputLine(); + + char buf[2048]; + va_list args; + va_start(args, fmt); + vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + + printf("%s[INFO]%s %s%s%s\n", Color::BrightGreen, Color::Reset, Color::Green, buf, Color::Reset); + fflush(stdout); + + if (sc) sc->redrawInputLine(); +} + +void ServerConsole::logCommand(const char* label, const char* message) +{ + ServerConsole* sc = getInstance(); + if (sc) sc->clearInputLine(); + + printf("%s[%s]%s %s\n", Color::BrightMagenta, label, Color::Reset, message); + fflush(stdout); + + if (sc) sc->redrawInputLine(); +} + +void ServerConsole::logInfo(const wchar_t* message) +{ + ServerConsole* sc = getInstance(); + if (sc) sc->clearInputLine(); + + printf("%s[INFO]%s %ls\n", Color::BrightCyan, Color::Reset, message); + fflush(stdout); + + if (sc) sc->redrawInputLine(); +} + +void ServerConsole::logWarn(const wchar_t* message) +{ + ServerConsole* sc = getInstance(); + if (sc) sc->clearInputLine(); + + printf("%s[WARN]%s %s%ls%s\n", Color::BrightYellow, Color::Reset, Color::Yellow, message, Color::Reset); + fflush(stdout); + + if (sc) sc->redrawInputLine(); +} + +void ServerConsole::logError(const wchar_t* message) +{ + ServerConsole* sc = getInstance(); + if (sc) sc->clearInputLine(); + + printf("%s[ERROR]%s %s%ls%s\n", Color::BrightRed, Color::Reset, Color::Red, message, Color::Reset); + fflush(stdout); + + if (sc) sc->redrawInputLine(); +} + +void ServerConsole::printPrompt() +{ + printf("%s> %s", Color::BrightGreen, Color::Reset); + fflush(stdout); +} + +void ServerConsole::clearInputLine() +{ + // Move to start of line, clear it + printf("\r\033[K"); + fflush(stdout); +} + +void ServerConsole::redrawInputLine() +{ + printf("\r\033[K"); + printPrompt(); + + if (!m_inputBuffer.empty()) + { + std::string highlighted = highlightCommand(m_inputBuffer); + printf("%s", highlighted.c_str()); + } + + // Position cursor correctly + int promptLen = 2; // "> " + int targetCol = promptLen + m_cursorPos; + printf("\r\033[%dC", targetCol); + fflush(stdout); +} + +std::string ServerConsole::highlightCommand(const std::string& input) +{ + if (input.empty()) return input; + + std::string result; + std::istringstream stream(input); + std::string token; + bool firstToken = true; + size_t pos = 0; + + // Find the first word (command name) + size_t firstSpace = input.find(' '); + std::string cmdName = (firstSpace != std::string::npos) ? input.substr(0, firstSpace) : input; + std::string cmdLower = cmdName; + std::transform(cmdLower.begin(), cmdLower.end(), cmdLower.begin(), ::tolower); + + // Check if it starts with / + size_t cmdStart = 0; + if (!cmdLower.empty() && cmdLower[0] == '/') + { + result += Color::Gray; + result += '/'; + cmdLower = cmdLower.substr(1); + cmdName = cmdName.substr(1); + cmdStart = 1; + } + + // Highlight command name + bool knownCmd = false; + for (int i = 0; s_commandNames[i] != nullptr; ++i) + { + if (cmdLower == s_commandNames[i]) + { + knownCmd = true; + break; + } + } + + if (knownCmd) + result += std::string(Color::BrightGreen) + cmdName + Color::Reset; + else + result += std::string(Color::BrightRed) + cmdName + Color::Reset; + + // Highlight rest (arguments) + if (firstSpace != std::string::npos) + { + std::string rest = input.substr(firstSpace); + // Color numbers differently from strings + std::string argResult; + bool inNumber = false; + for (size_t i = 0; i < rest.size(); ++i) + { + char c = rest[i]; + if (c == ' ') + { + if (inNumber) { argResult += Color::Reset; inNumber = false; } + argResult += c; + } + else if ((c >= '0' && c <= '9') || c == '-' || c == '.') + { + if (!inNumber) { argResult += Color::BrightYellow; inNumber = true; } + argResult += c; + } + else if (c == '~') + { + if (inNumber) { argResult += Color::Reset; inNumber = false; } + argResult += Color::BrightMagenta; + argResult += c; + argResult += Color::Reset; + } + else + { + if (inNumber) { argResult += Color::Reset; inNumber = false; } + argResult += Color::BrightWhite; + argResult += c; + argResult += Color::Reset; + } + } + if (inNumber) argResult += Color::Reset; + result += argResult; + } + + return result; +} + +static std::string toLowerStr(const std::string& s) +{ + std::string r = s; + std::transform(r.begin(), r.end(), r.begin(), ::tolower); + return r; +} + +std::vector ServerConsole::getCompletions(const std::string& partial) +{ + std::vector results; + if (partial.empty()) return results; + + // Parse the input to figure out what we're completing + std::string input = partial; + bool hasSlash = false; + if (!input.empty() && input[0] == '/') + { + hasSlash = true; + input = input.substr(1); + } + + // Split into tokens + std::vector tokens; + { + std::istringstream ss(input); + std::string tok; + while (ss >> tok) tokens.push_back(tok); + } + + // If input ends with a space, we're completing the NEXT token + bool completingNext = (!partial.empty() && partial.back() == ' '); + if (completingNext) tokens.push_back(""); + + if (tokens.empty()) + { + // Complete command names + for (int i = 0; s_commandNames[i] != nullptr; ++i) + { + std::string name = s_commandNames[i]; + results.push_back(hasSlash ? ("/" + name) : name); + } + return results; + } + + if (tokens.size() == 1) + { + // Complete command name + std::string prefix = toLowerStr(tokens[0]); + for (int i = 0; s_commandNames[i] != nullptr; ++i) + { + std::string name = s_commandNames[i]; + if (name.find(prefix) == 0) + { + results.push_back(hasSlash ? ("/" + name) : name); + } + } + return results; + } + + // Token 1+ : subcommand or argument completions + std::string cmdName = toLowerStr(tokens[0]); + std::string argPrefix = toLowerStr(tokens.back()); + + // Subcommand completions + const char** subCmds = nullptr; + if (tokens.size() == 2) + { + if (cmdName == "time") subCmds = s_timeSubcmds; + else if (cmdName == "weather") subCmds = s_weatherTypes; + else if (cmdName == "gamemode") subCmds = s_gamemodeNames; + else if (cmdName == "effect") subCmds = s_effectActions; + } + else if (tokens.size() == 3 && cmdName == "time" && toLowerStr(tokens[1]) == "set") + { + subCmds = s_timeSetValues; + } + + if (subCmds != nullptr) + { + for (int i = 0; subCmds[i] != nullptr; ++i) + { + std::string sub = subCmds[i]; + if (sub.find(argPrefix) == 0) + { + results.push_back(sub); + } + } + // Don't return yet - also try player names + } + + // Player name completions for commands that take player names + bool wantsPlayerName = false; + if (cmdName == "kill" || cmdName == "tp" || cmdName == "teleport" || + cmdName == "give" || cmdName == "giveitem" || cmdName == "enchant" || cmdName == "enchantitem" || + cmdName == "gamemode" || cmdName == "effect" || cmdName == "clear" || + cmdName == "vanish" || cmdName == "op" || cmdName == "deop" || + cmdName == "kick" || cmdName == "ban") + { + // Most commands take a player name as the first or second arg + wantsPlayerName = true; + } + + if (wantsPlayerName) + { + MinecraftServer* server = MinecraftServer::getInstance(); + if (server != nullptr) + { + PlayerList* playerList = server->getPlayers(); + if (playerList != nullptr) + { + for (size_t i = 0; i < playerList->players.size(); ++i) + { + if (playerList->players[i] != nullptr) + { + wstring wname = playerList->players[i]->getName(); + // Convert to narrow string + std::string name(wname.begin(), wname.end()); + std::string nameLower = toLowerStr(name); + if (nameLower.find(argPrefix) == 0) + { + results.push_back(name); + } + } + } + // Add @a, @p, @r, @s selectors + const char* selectors[] = { "@a", "@p", "@r", "@s", nullptr }; + for (int i = 0; selectors[i] != nullptr; ++i) + { + std::string sel = selectors[i]; + if (sel.find(argPrefix) == 0) + results.push_back(sel); + } + } + } + } + + // Entity name completions for summon (use public idsSpawnableInCreative) + if (cmdName == "summon" && tokens.size() == 2) + { + for (auto& pair : EntityIO::idsSpawnableInCreative) + { + wstring wname = EntityIO::getEncodeId(pair.first); + if (!wname.empty()) + { + std::string name(wname.begin(), wname.end()); + std::string nameLower = toLowerStr(name); + if (nameLower.find(argPrefix) == 0) + { + results.push_back(name); + } + } + } + } + + return results; +} + +void ServerConsole::handleTabCompletion() +{ + if (m_tabIndex == -1) + { + // Start new tab completion + m_tabPartial = m_inputBuffer; + m_tabCompletions = getCompletions(m_tabPartial); + if (m_tabCompletions.empty()) return; + m_tabIndex = 0; + } + else + { + m_tabIndex = (m_tabIndex + 1) % (int)m_tabCompletions.size(); + } + + if (m_tabCompletions.empty()) return; + + // Figure out what portion to replace + // Find the last space in the partial to know what word we're completing + std::string completion = m_tabCompletions[m_tabIndex]; + + size_t lastSpace = m_tabPartial.rfind(' '); + if (lastSpace == std::string::npos) + { + // Replacing the whole input + m_inputBuffer = completion; + } + else + { + // Replacing the last word + m_inputBuffer = m_tabPartial.substr(0, lastSpace + 1) + completion; + } + + m_cursorPos = (int)m_inputBuffer.size(); + + // If there are multiple completions, show them + if (m_tabCompletions.size() > 1) + { + clearInputLine(); + printf("\r\n"); + for (size_t i = 0; i < m_tabCompletions.size(); ++i) + { + if ((int)i == m_tabIndex) + printf("%s%s%s ", Color::BrightGreen, m_tabCompletions[i].c_str(), Color::Reset); + else + printf("%s ", m_tabCompletions[i].c_str()); + } + printf("\r\n"); + fflush(stdout); + } + + redrawInputLine(); +} + +void ServerConsole::insertChar(char c) +{ + if ((int)m_inputBuffer.size() >= MAX_INPUT_LENGTH) return; + + m_inputBuffer.insert(m_inputBuffer.begin() + m_cursorPos, c); + m_cursorPos++; + m_tabIndex = -1; // Reset tab completion + redrawInputLine(); +} + +void ServerConsole::deleteCharBack() +{ + if (m_cursorPos > 0) + { + m_inputBuffer.erase(m_cursorPos - 1, 1); + m_cursorPos--; + m_tabIndex = -1; + redrawInputLine(); + } +} + +void ServerConsole::deleteCharForward() +{ + if (m_cursorPos < (int)m_inputBuffer.size()) + { + m_inputBuffer.erase(m_cursorPos, 1); + m_tabIndex = -1; + redrawInputLine(); + } +} + +void ServerConsole::moveCursorLeft() +{ + if (m_cursorPos > 0) + { + m_cursorPos--; + redrawInputLine(); + } +} + +void ServerConsole::moveCursorRight() +{ + if (m_cursorPos < (int)m_inputBuffer.size()) + { + m_cursorPos++; + redrawInputLine(); + } +} + +void ServerConsole::moveCursorHome() +{ + m_cursorPos = 0; + redrawInputLine(); +} + +void ServerConsole::moveCursorEnd() +{ + m_cursorPos = (int)m_inputBuffer.size(); + redrawInputLine(); +} + +void ServerConsole::historyUp() +{ + if (m_history.empty()) return; + + if (m_historyIndex == -1) + { + m_savedInput = m_inputBuffer; + m_historyIndex = 0; + } + else if (m_historyIndex < (int)m_history.size() - 1) + { + m_historyIndex++; + } + else + { + return; // at oldest entry + } + + m_inputBuffer = m_history[m_historyIndex]; + m_cursorPos = (int)m_inputBuffer.size(); + m_tabIndex = -1; + redrawInputLine(); +} + +void ServerConsole::historyDown() +{ + if (m_historyIndex == -1) return; + + m_historyIndex--; + if (m_historyIndex < 0) + { + m_historyIndex = -1; + m_inputBuffer = m_savedInput; + } + else + { + m_inputBuffer = m_history[m_historyIndex]; + } + + m_cursorPos = (int)m_inputBuffer.size(); + m_tabIndex = -1; + redrawInputLine(); +} + +void ServerConsole::readInputLine(std::string& outLine) +{ + m_inputBuffer.clear(); + m_cursorPos = 0; + m_historyIndex = -1; + m_tabIndex = -1; + m_savedInput.clear(); + + printPrompt(); + + while (m_running) + { + INPUT_RECORD ir; + DWORD count = 0; + + if (!ReadConsoleInputA(m_hConsoleIn, &ir, 1, &count) || count == 0) + { + Sleep(10); + continue; + } + + if (ir.EventType != KEY_EVENT || !ir.Event.KeyEvent.bKeyDown) + continue; + + KEY_EVENT_RECORD& key = ir.Event.KeyEvent; + WORD vk = key.wVirtualKeyCode; + char ch = key.uChar.AsciiChar; + + if (vk == VK_RETURN) + { + printf("\r\n"); + fflush(stdout); + outLine = m_inputBuffer; + return; + } + else if (vk == VK_TAB) + { + handleTabCompletion(); + } + else if (vk == VK_BACK) + { + deleteCharBack(); + } + else if (vk == VK_DELETE) + { + deleteCharForward(); + } + else if (vk == VK_LEFT) + { + moveCursorLeft(); + } + else if (vk == VK_RIGHT) + { + moveCursorRight(); + } + else if (vk == VK_UP) + { + historyUp(); + } + else if (vk == VK_DOWN) + { + historyDown(); + } + else if (vk == VK_HOME) + { + moveCursorHome(); + } + else if (vk == VK_END) + { + moveCursorEnd(); + } + else if (vk == VK_ESCAPE) + { + m_inputBuffer.clear(); + m_cursorPos = 0; + m_tabIndex = -1; + redrawInputLine(); + } + else if (ch >= 32 && ch < 127) + { + insertChar(ch); + } + } + + outLine.clear(); +} + +void ServerConsole::run() +{ + enableVirtualTerminal(); + + printf("\n%s========================================%s\n", Color::BrightCyan, Color::Reset); + printf("%s Minecraft Legacy Console Server%s\n", Color::BrightWhite, Color::Reset); + printf("%s========================================%s\n", Color::BrightCyan, Color::Reset); + printf("Type %shelp%s for available commands.\n", Color::BrightGreen, Color::Reset); + printf("Use %sTab%s for auto-completion.\n\n", Color::BrightYellow, Color::Reset); + + while (m_running && !MinecraftServer::serverHalted()) + { + std::string line; + readInputLine(line); + + if (!m_running || MinecraftServer::serverHalted()) break; + + // Trim + size_t start = line.find_first_not_of(" \t"); + size_t end = line.find_last_not_of(" \t"); + if (start == std::string::npos) continue; + line = line.substr(start, end - start + 1); + + if (line.empty()) continue; + + // Add to history (avoid duplicates at front) + if (m_history.empty() || m_history.front() != line) + { + m_history.push_front(line); + if ((int)m_history.size() > MAX_HISTORY) + m_history.pop_back(); + } + + // Convert to wstring and send to server + wstring command = convStringToWstring(line); + + if (m_server != nullptr) + { + m_server->handleConsoleInput(command, m_server); + } + } +} + +#endif // _WINDOWS64 diff --git a/Minecraft.Client/ServerConsole.h b/Minecraft.Client/ServerConsole.h new file mode 100644 index 000000000..49385abfd --- /dev/null +++ b/Minecraft.Client/ServerConsole.h @@ -0,0 +1,92 @@ +#pragma once +#ifdef _WINDOWS64 + +#include +#include +#include +#include +#include + +class MinecraftServer; + +class ServerConsole +{ +public: + static const int MAX_HISTORY = 500; + static const int MAX_INPUT_LENGTH = 512; + + struct Color + { + static const char* Reset; + static const char* Red; + static const char* Green; + static const char* Yellow; + static const char* Blue; + static const char* Magenta; + static const char* Cyan; + static const char* White; + static const char* Gray; + static const char* BrightRed; + static const char* BrightGreen; + static const char* BrightYellow; + static const char* BrightBlue; + static const char* BrightMagenta; + static const char* BrightCyan; + static const char* BrightWhite; + }; + + ServerConsole(MinecraftServer* server); + ~ServerConsole(); + + void enableVirtualTerminal(); + + void run(); + static void logInfo(const char* fmt, ...); + static void logWarn(const char* fmt, ...); + static void logError(const char* fmt, ...); + static void logSuccess(const char* fmt, ...); + static void logCommand(const char* label, const char* message); + static void logInfo(const wchar_t* message); + static void logWarn(const wchar_t* message); + static void logError(const wchar_t* message); + + std::vector getCompletions(const std::string& partial); + + static ServerConsole* getInstance() { return s_instance; } + +private: + void readInputLine(std::string& outLine); + void redrawInputLine(); + void clearInputLine(); + void handleTabCompletion(); + void insertChar(char c); + void deleteCharBack(); + void deleteCharForward(); + void moveCursorLeft(); + void moveCursorRight(); + void moveCursorHome(); + void moveCursorEnd(); + void historyUp(); + void historyDown(); + std::string highlightCommand(const std::string& input); + void printPrompt(); + + MinecraftServer* m_server; + std::string m_inputBuffer; + int m_cursorPos; + std::deque m_history; + int m_historyIndex; + std::string m_savedInput; + + std::vector m_tabCompletions; + int m_tabIndex; + std::string m_tabPartial; + + HANDLE m_hConsoleIn; + HANDLE m_hConsoleOut; + bool m_running; + + static ServerConsole* s_instance; +}; + +#endif // _WINDOWS64 diff --git a/Minecraft.Client/Settings.cpp b/Minecraft.Client/Settings.cpp index 9d93e6cbe..e38a19b7e 100644 --- a/Minecraft.Client/Settings.cpp +++ b/Minecraft.Client/Settings.cpp @@ -124,4 +124,10 @@ void Settings::setBooleanAndSave(const wstring& key, bool value) { properties[key] = value ? L"true" : L"false"; saveProperties(); +} + +void Settings::setStringAndSave(const wstring& key, const wstring& value) +{ + properties[key] = value; + saveProperties(); } \ No newline at end of file diff --git a/Minecraft.Client/Settings.h b/Minecraft.Client/Settings.h index 4a3c130be..573d84d2c 100644 --- a/Minecraft.Client/Settings.h +++ b/Minecraft.Client/Settings.h @@ -18,4 +18,5 @@ class Settings int getInt(const wstring& key, int defaultValue); bool getBoolean(const wstring& key, bool defaultValue); void setBooleanAndSave(const wstring& key, bool value); + void setStringAndSave(const wstring& key, const wstring& value); }; \ No newline at end of file diff --git a/Minecraft.Client/TexturePackRepository.cpp b/Minecraft.Client/TexturePackRepository.cpp index ef926d78b..4c1ef8d1c 100644 --- a/Minecraft.Client/TexturePackRepository.cpp +++ b/Minecraft.Client/TexturePackRepository.cpp @@ -375,6 +375,11 @@ TexturePack *TexturePackRepository::addTexturePackFromDLC(DLCPack *dlcPack, DWOR cacheById[dwParentID] = newPack; #ifndef _CONTENT_PACKAGE +#ifdef _WINDOWS64 + extern bool g_Win64Verbose; + if (g_Win64Verbose) + { +#endif if(dlcPack->hasPurchasedFile(DLCManager::e_DLCType_TexturePack,L"")) { wprintf(L"Added new FULL DLCTexturePack: %ls - id=%d\n", dlcPack->getName().c_str(),dwParentID ); @@ -383,6 +388,9 @@ TexturePack *TexturePackRepository::addTexturePackFromDLC(DLCPack *dlcPack, DWOR { wprintf(L"Added new TRIAL DLCTexturePack: %ls - id=%d\n", dlcPack->getName().c_str(),dwParentID ); } +#ifdef _WINDOWS64 + } +#endif #endif } return newPack; diff --git a/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp b/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp index 28d295049..3b74950e0 100644 --- a/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp +++ b/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp @@ -65,6 +65,7 @@ char g_Win64MultiplayerIP[256] = "127.0.0.1"; bool g_Win64DedicatedServer = false; int g_Win64DedicatedServerPort = WIN64_NET_DEFAULT_PORT; char g_Win64DedicatedServerBindIP[256] = ""; +bool g_Win64Verbose = false; bool WinsockNetLayer::Initialize() { diff --git a/Minecraft.Client/Windows64/Network/WinsockNetLayer.h b/Minecraft.Client/Windows64/Network/WinsockNetLayer.h index c5d689752..9af9bc7a8 100644 --- a/Minecraft.Client/Windows64/Network/WinsockNetLayer.h +++ b/Minecraft.Client/Windows64/Network/WinsockNetLayer.h @@ -170,5 +170,6 @@ extern char g_Win64MultiplayerIP[256]; extern bool g_Win64DedicatedServer; extern int g_Win64DedicatedServerPort; extern char g_Win64DedicatedServerBindIP[256]; +extern bool g_Win64Verbose; #endif diff --git a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp index 70aeb22bf..dde434aa6 100644 --- a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp +++ b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp @@ -30,6 +30,7 @@ #include "..\..\Minecraft.World\ThreadName.h" #include "..\..\Minecraft.Client\StatsCounter.h" #include "..\ConnectScreen.h" +#include "..\ServerConsole.h" //#include "Social\SocialManager.h" //#include "Leaderboards\LeaderboardManager.h" //#include "XUI\XUI_Scene_Container.h" @@ -123,6 +124,7 @@ struct Win64LaunchOptions int screenMode; bool serverMode; bool fullscreen; + bool verbose; }; static void CopyWideArgToAnsi(LPCWSTR source, char* dest, size_t destSize) @@ -208,6 +210,7 @@ static Win64LaunchOptions ParseLaunchOptions() Win64LaunchOptions options = {}; options.screenMode = 0; options.serverMode = false; + options.verbose = false; g_Win64MultiplayerJoin = false; g_Win64MultiplayerPort = WIN64_NET_DEFAULT_PORT; @@ -271,6 +274,8 @@ static Win64LaunchOptions ParseLaunchOptions() } else if (_wcsicmp(argv[i], L"-fullscreen") == 0) options.fullscreen = true; + else if (_wcsicmp(argv[i], L"-v") == 0 || _wcsicmp(argv[i], L"-verbose") == 0) + options.verbose = true; } LocalFree(argv); @@ -1354,30 +1359,19 @@ static int HeadlessServerConsoleThreadProc(void* lpParameter) { UNREFERENCED_PARAMETER(lpParameter); - std::string line; + MinecraftServer* server = nullptr; while (!app.m_bShutdown) { - if (!std::getline(std::cin, line)) - { - if (std::cin.eof()) - { - break; - } - - std::cin.clear(); - Sleep(50); - continue; - } - - wstring command = trimString(convStringToWstring(line)); - if (command.empty()) - continue; - - MinecraftServer* server = MinecraftServer::getInstance(); + server = MinecraftServer::getInstance(); if (server != nullptr) - { - server->handleConsoleInput(command, server); - } + break; + Sleep(100); + } + + if (server != nullptr && !app.m_bShutdown) + { + ServerConsole console(server); + console.run(); } return 0; @@ -1564,6 +1558,7 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, // Load stuff from launch options, including username const Win64LaunchOptions launchOptions = ParseLaunchOptions(); + g_Win64Verbose = launchOptions.verbose; ApplyScreenMode(launchOptions.screenMode); // Ensure uid.dat exists from startup in client mode (before any multiplayer/login path). diff --git a/cmake/ClientSources.cmake b/cmake/ClientSources.cmake index 6467a243c..aebfc51dc 100644 --- a/cmake/ClientSources.cmake +++ b/cmake/ClientSources.cmake @@ -397,7 +397,9 @@ set(MINECRAFT_CLIENT_SOURCES "SelectWorldScreen.cpp" "ServerChunkCache.cpp" "ServerCommandDispatcher.cpp" + "ServerCommands.cpp" "ServerConnection.cpp" + "ServerConsole.cpp" "ServerLevel.cpp" "ServerLevelListener.cpp" "ServerPlayer.cpp" From aabacfa4322c611806da40dbbeefb189e1d859ff Mon Sep 17 00:00:00 2001 From: la <76826837+3UR@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:30:01 +1000 Subject: [PATCH 2/3] feat: dedicated server command addition and enhancement for some parity with java/bedrock, console prettification(?), you can FINALLY have a proper server seed, tools for moderation. --- Minecraft.Client/Common/Consoles_App.cpp | 6 + Minecraft.Client/Common/UI/IUIScene_HUD.cpp | 4 +- Minecraft.Client/MinecraftServer.cpp | 42 +- Minecraft.Client/MinecraftServer.h | 2 +- Minecraft.Client/PendingConnection.cpp | 5 + Minecraft.Client/PlayerConnection.cpp | 8 +- Minecraft.Client/ServerCommands.cpp | 1642 +++++++++++++++++ Minecraft.Client/ServerCommands.h | 144 ++ Minecraft.Client/ServerConsole.cpp | 766 ++++++++ Minecraft.Client/ServerConsole.h | 92 + Minecraft.Client/Settings.cpp | 6 + Minecraft.Client/Settings.h | 1 + Minecraft.Client/TexturePackRepository.cpp | 8 + .../Windows64/Network/WinsockNetLayer.cpp | 1 + .../Windows64/Network/WinsockNetLayer.h | 1 + .../Windows64/Windows64_Minecraft.cpp | 37 +- cmake/ClientSources.cmake | 2 + 17 files changed, 2738 insertions(+), 29 deletions(-) create mode 100644 Minecraft.Client/ServerCommands.cpp create mode 100644 Minecraft.Client/ServerCommands.h create mode 100644 Minecraft.Client/ServerConsole.cpp create mode 100644 Minecraft.Client/ServerConsole.h diff --git a/Minecraft.Client/Common/Consoles_App.cpp b/Minecraft.Client/Common/Consoles_App.cpp index c3a623d5f..d073a116e 100644 --- a/Minecraft.Client/Common/Consoles_App.cpp +++ b/Minecraft.Client/Common/Consoles_App.cpp @@ -5699,6 +5699,9 @@ bool CMinecraftApp::isXuidDeadmau5(PlayerUID xuid) void CMinecraftApp::AddMemoryTextureFile(const wstring &wName,PBYTE pbData,DWORD dwBytes) { +#ifdef _WINDOWS64 + extern bool g_Win64Verbose; +#endif EnterCriticalSection(&csMemFilesLock); // check it's not already in PMEMDATA pData=nullptr; @@ -5706,6 +5709,9 @@ void CMinecraftApp::AddMemoryTextureFile(const wstring &wName,PBYTE pbData,DWORD if(it != m_MEM_Files.end()) { #ifndef _CONTENT_PACKAGE +#ifdef _WINDOWS64 + if (g_Win64Verbose) +#endif wprintf(L"Incrementing the memory texture file count for %ls\n", wName.c_str()); #endif pData = (*it).second; diff --git a/Minecraft.Client/Common/UI/IUIScene_HUD.cpp b/Minecraft.Client/Common/UI/IUIScene_HUD.cpp index fd9779665..d2754789c 100644 --- a/Minecraft.Client/Common/UI/IUIScene_HUD.cpp +++ b/Minecraft.Client/Common/UI/IUIScene_HUD.cpp @@ -195,8 +195,8 @@ void IUIScene_HUD::renderPlayerHealth() // Update health bool blink = pMinecraft->localplayers[iPad]->invulnerableTime / 3 % 2 == 1; if (pMinecraft->localplayers[iPad]->invulnerableTime < 10) blink = false; - int currentHealth = pMinecraft->localplayers[iPad]->getHealth(); - int oldHealth = pMinecraft->localplayers[iPad]->lastHealth; + int currentHealth = static_cast(ceil(pMinecraft->localplayers[iPad]->getHealth())); + int oldHealth = static_cast(ceil(pMinecraft->localplayers[iPad]->lastHealth)); bool bHasPoison = pMinecraft->localplayers[iPad]->hasEffect(MobEffect::poison); bool bHasWither = pMinecraft->localplayers[iPad]->hasEffect(MobEffect::wither); AttributeInstance *maxHealthAttribute = pMinecraft->localplayers[iPad]->getAttribute(SharedMonsterAttributes::MAX_HEALTH); diff --git a/Minecraft.Client/MinecraftServer.cpp b/Minecraft.Client/MinecraftServer.cpp index bdcc9f813..ffaac4869 100644 --- a/Minecraft.Client/MinecraftServer.cpp +++ b/Minecraft.Client/MinecraftServer.cpp @@ -8,6 +8,9 @@ #include "DispenserBootstrap.h" #include "EntityTracker.h" #include "MinecraftServer.h" +#ifdef _WINDOWS64 +#include "ServerConsole.h" +#endif #include "Options.h" #include "PlayerList.h" #include "ServerChunkCache.h" @@ -58,6 +61,9 @@ #include "..\Minecraft.World\BiomeSource.h" #include "PlayerChunkMap.h" #include "Common\Telemetry\TelemetryManager.h" +#include "ServerCommands.h" +#ifdef _WINDOWS64 +#endif #include "PlayerConnection.h" #ifdef _XBOX_ONE #include "Durango\Network\NetworkPlayerDurango.h" @@ -726,6 +732,19 @@ bool MinecraftServer::initServer(int64_t seed, NetworkGameInitData *initData, DW ProgressRenderer *mcprogress = Minecraft::GetInstance()->progressRenderer; mcprogress->progressStart(IDS_PROGRESS_INITIALISING_SERVER); + if (ShouldUseDedicatedServerProperties()) + { + wstring seedStr = GetDedicatedServerString(settings, L"seed", L""); + if (!seedStr.empty()) + { + int64_t parsedSeed = _wtoi64(seedStr.c_str()); + if (parsedSeed != 0) + seed = parsedSeed; + } + if (seed == 0 && !findSeed) + findSeed = true; + } + if( findSeed ) { #ifdef __PSVITA__ @@ -733,6 +752,10 @@ bool MinecraftServer::initServer(int64_t seed, NetworkGameInitData *initData, DW #else seed = BiomeSource::findSeed(pLevelType); #endif + if (ShouldUseDedicatedServerProperties() && settings != nullptr) + { + settings->setStringAndSave(L"seed", std::to_wstring(seed)); + } } setMaxBuildHeight(GetDedicatedServerInt(settings, L"max-build-height", Level::maxBuildHeight)); @@ -2208,10 +2231,13 @@ void MinecraftServer::handleConsoleInputs() pendingInputs.swap(consoleInput); LeaveCriticalSection(&m_consoleInputCS); + ServerCommands::initialize(); + for (size_t i = 0; i < pendingInputs.size(); ++i) { ConsoleInput *input = pendingInputs[i]; - ExecuteConsoleCommand(this, input->msg); + CommandSource source(this, input->source, CommandSource::CONSOLE, L"CONSOLE"); + ServerCommands::execute(this, input->msg, source); delete input; } } @@ -2245,11 +2271,25 @@ File *MinecraftServer::getFile(const wstring& name) void MinecraftServer::info(const wstring& string) { +#ifdef _WINDOWS64 + if (ServerConsole::getInstance()) + { + ServerConsole::logInfo(string.c_str()); + return; + } +#endif PrintConsoleLine(L"[INFO] ", string); } void MinecraftServer::warn(const wstring& string) { +#ifdef _WINDOWS64 + if (ServerConsole::getInstance()) + { + ServerConsole::logWarn(string.c_str()); + return; + } +#endif PrintConsoleLine(L"[WARN] ", string); } diff --git a/Minecraft.Client/MinecraftServer.h b/Minecraft.Client/MinecraftServer.h index a33888bcc..b19a01860 100644 --- a/Minecraft.Client/MinecraftServer.h +++ b/Minecraft.Client/MinecraftServer.h @@ -142,7 +142,7 @@ class MinecraftServer : public ConsoleInputSource bool loadLevel(LevelStorageSource *storageSource, const wstring& name, int64_t levelSeed, LevelType *pLevelType, NetworkGameInitData *initData); void setProgress(const wstring& status, int progress); void endProgress(); - void saveAllChunks(); + void saveAllChunks(); void saveGameRules(); void stopServer(bool didInit); #ifdef _LARGE_WORLDS diff --git a/Minecraft.Client/PendingConnection.cpp b/Minecraft.Client/PendingConnection.cpp index 6d5497f02..9624a4c7c 100644 --- a/Minecraft.Client/PendingConnection.cpp +++ b/Minecraft.Client/PendingConnection.cpp @@ -7,6 +7,7 @@ #include "ServerLevel.h" #include "PlayerList.h" #include "MinecraftServer.h" +#include "ServerCommands.h" #include "..\Minecraft.World\net.minecraft.network.h" #include "..\Minecraft.World\pos.h" #include "..\Minecraft.World\net.minecraft.world.level.dimension.h" @@ -188,6 +189,10 @@ void PendingConnection::handleLogin(shared_ptr packet) { disconnect(DisconnectPacket::eDisconnect_Banned); } + else if (ServerCommands::getBanList().isBanned(name)) + { + disconnect(DisconnectPacket::eDisconnect_Banned); + } else if (duplicateXuid) { // Reject the incoming connection — a player with this UID is already diff --git a/Minecraft.Client/PlayerConnection.cpp b/Minecraft.Client/PlayerConnection.cpp index d9915cf61..1cb8514be 100644 --- a/Minecraft.Client/PlayerConnection.cpp +++ b/Minecraft.Client/PlayerConnection.cpp @@ -34,6 +34,7 @@ // 4J Added #include "..\Minecraft.World\net.minecraft.world.item.crafting.h" #include "Options.h" +#include "ServerCommands.h" Random PlayerConnection::random; @@ -633,10 +634,9 @@ void PlayerConnection::handleChat(shared_ptr packet) void PlayerConnection::handleCommand(const wstring& message) { - // 4J - TODO -#if 0 - server.getCommandDispatcher().performCommand(player, message); -#endif + ServerCommands::initialize(); + CommandSource source(server, player); + ServerCommands::execute(server, message, source); } void PlayerConnection::handleAnimate(shared_ptr packet) diff --git a/Minecraft.Client/ServerCommands.cpp b/Minecraft.Client/ServerCommands.cpp new file mode 100644 index 000000000..f004ef84c --- /dev/null +++ b/Minecraft.Client/ServerCommands.cpp @@ -0,0 +1,1642 @@ +#include "stdafx.h" +#include "ServerCommands.h" +#include "MinecraftServer.h" +#include "PlayerList.h" +#include "ServerPlayer.h" +#include "ServerLevel.h" +#include "PlayerConnection.h" +#include "EntityTracker.h" +#include "..\Minecraft.World\StringHelpers.h" +#include "..\Minecraft.World\EntityIO.h" +#include "..\Minecraft.World\Entity.h" +#include "..\Minecraft.World\Tile.h" +#include "..\Minecraft.World\Dimension.h" +#include "..\Minecraft.World\LevelData.h" +#include "..\Minecraft.World\MobEffect.h" +#include "..\Minecraft.World\MobEffectInstance.h" +#include "..\Minecraft.World\LevelSettings.h" +#include "..\Minecraft.World\net.minecraft.world.h" +#include "..\Minecraft.World\net.minecraft.world.item.h" +#include "..\Minecraft.World\net.minecraft.world.item.enchantment.h" +#include "..\Minecraft.World\net.minecraft.world.damagesource.h" +#include "..\Minecraft.World\net.minecraft.world.entity.item.h" +#include "..\Minecraft.World\net.minecraft.world.level.h" +#include "..\Minecraft.World\net.minecraft.network.h" +#include "..\Minecraft.World\SharedConstants.h" +#ifdef _WINDOWS64 +#include "ServerConsole.h" +#endif +#include +#include +#include + +unordered_map ServerCommands::s_commands; +unordered_set ServerCommands::s_opOnlyCommands; +OpList ServerCommands::s_opList; +BanList ServerCommands::s_banList; +VanishManager ServerCommands::s_vanishManager; +bool ServerCommands::s_initialized = false; + +CommandSource::CommandSource(MinecraftServer* server, ConsoleInputSource* source, Type type, const wstring& name) + : m_server(server), m_consoleSource(source), m_player(nullptr), m_type(type), m_name(name) +{ +} + +CommandSource::CommandSource(MinecraftServer* server, shared_ptr player) + : m_server(server), m_consoleSource(nullptr), m_player(player), m_type(PLAYER), m_name(player->getName()) +{ +} + +void CommandSource::sendMessage(const wstring& message) +{ + if (m_type == CONSOLE) + { +#ifdef _WINDOWS64 + if (ServerConsole::getInstance()) + ServerConsole::logInfo(message.c_str()); + else +#endif + if (m_consoleSource) + m_consoleSource->info(message); + } + else if (m_player != nullptr) + { + if (m_player->connection != nullptr) + { + m_player->connection->send(std::make_shared(message)); + } + } +} + +void CommandSource::sendSuccess(const wstring& message) +{ + if (m_type == CONSOLE) + { +#ifdef _WINDOWS64 + if (ServerConsole::getInstance()) + ServerConsole::logSuccess("%ls", message.c_str()); + else +#endif + if (m_consoleSource) + m_consoleSource->info(message); + } + else if (m_player != nullptr) + { + if (m_player->connection != nullptr) + { + m_player->connection->send(std::make_shared(message)); + } + } +} + +void CommandSource::sendError(const wstring& message) +{ + if (m_type == CONSOLE) + { +#ifdef _WINDOWS64 + if (ServerConsole::getInstance()) + ServerConsole::logError("%ls", message.c_str()); + else +#endif + if (m_consoleSource) + m_consoleSource->warn(message); + } + else if (m_player != nullptr) + { + if (m_player->connection != nullptr) + { + m_player->connection->send(std::make_shared(message)); + } + } +} + +bool CommandSource::isOp() const +{ + if (m_type == CONSOLE) return true; + return ServerCommands::getOpList().isOp(m_name); +} + +static std::string getExeDirFilePath(const char* filename) +{ + char path[MAX_PATH]; + GetModuleFileNameA(nullptr, path, MAX_PATH); + char* p = strrchr(path, '\\'); + if (p) *(p + 1) = '\0'; + strncat_s(path, sizeof(path), filename, _TRUNCATE); + return std::string(path); +} + +void OpList::addOp(const wstring& name) +{ + m_ops.insert(toLower(name)); +} + +void OpList::removeOp(const wstring& name) +{ + m_ops.erase(toLower(name)); +} + +bool OpList::isOp(const wstring& name) const +{ + return m_ops.find(toLower(name)) != m_ops.end(); +} + +void OpList::save() const +{ + std::string path = getExeDirFilePath("ops.txt"); + std::wofstream file(path, std::ios::out | std::ios::trunc); + if (!file.is_open()) return; + for (const wstring& op : m_ops) + file << op << L"\n"; +} + +void OpList::load() +{ + std::string path = getExeDirFilePath("ops.txt"); + std::wifstream file(path); + if (!file.is_open()) return; + wstring line; + while (std::getline(file, line)) + { + if (!line.empty()) + m_ops.insert(toLower(line)); + } +} + + +void BanList::ban(const wstring& name, const wstring& reason) +{ + m_bans[toLower(name)] = reason; +} + +void BanList::unban(const wstring& name) +{ + m_bans.erase(toLower(name)); +} + +bool BanList::isBanned(const wstring& name) const +{ + return m_bans.find(toLower(name)) != m_bans.end(); +} + +wstring BanList::getBanReason(const wstring& name) const +{ + auto it = m_bans.find(toLower(name)); + if (it != m_bans.end()) + return it->second; + return L""; +} + +void BanList::save() const +{ + std::string path = getExeDirFilePath("bans.txt"); + std::wofstream file(path, std::ios::out | std::ios::trunc); + if (!file.is_open()) return; + for (const auto& entry : m_bans) + { + file << entry.first << L"|" << entry.second << L"\n"; + } +} + +void BanList::load() +{ + std::string path = getExeDirFilePath("bans.txt"); + std::wifstream file(path); + if (!file.is_open()) return; + wstring line; + while (std::getline(file, line)) + { + if (line.empty()) continue; + size_t sep = line.find(L'|'); + if (sep != wstring::npos) + m_bans[toLower(line.substr(0, sep))] = line.substr(sep + 1); + else + m_bans[toLower(line)] = L""; + } +} + + +void VanishManager::setVanished(const wstring& name, bool vanished) +{ + wstring lower = toLower(name); + if (vanished) + m_vanished.insert(lower); + else + m_vanished.erase(lower); +} + +bool VanishManager::isVanished(const wstring& name) const +{ + return m_vanished.find(toLower(name)) != m_vanished.end(); +} + + +vector ServerCommands::splitCommand(const wstring& command) +{ + vector tokens; + std::wistringstream stream(command); + wstring token; + while (stream >> token) + tokens.push_back(token); + return tokens; +} + +wstring ServerCommands::joinTokens(const vector& tokens, size_t startIndex) +{ + wstring joined; + for (size_t i = startIndex; i < tokens.size(); ++i) + { + if (!joined.empty()) joined += L" "; + joined += tokens[i]; + } + return joined; +} + +bool ServerCommands::tryParseInt(const wstring& text, int& value) +{ + std::wistringstream stream(text); + stream >> value; + return !stream.fail() && stream.eof(); +} + +bool ServerCommands::tryParseDouble(const wstring& text, double& value) +{ + std::wistringstream stream(text); + stream >> value; + return !stream.fail() && stream.eof(); +} + +shared_ptr ServerCommands::findPlayer(PlayerList* playerList, const wstring& name) +{ + if (playerList == nullptr) return nullptr; + + for (size_t i = 0; i < playerList->players.size(); ++i) + { + shared_ptr player = playerList->players[i]; + if (player != nullptr && equalsIgnoreCase(player->getName(), name)) + return player; + } + return nullptr; +} + +vector> ServerCommands::resolvePlayerSelector(MinecraftServer* server, const wstring& selector, CommandSource& source) +{ + vector> result; + PlayerList* playerList = server->getPlayers(); + if (playerList == nullptr) return result; + + if (selector == L"@a") + { + for (size_t i = 0; i < playerList->players.size(); ++i) + { + if (playerList->players[i] != nullptr) + result.push_back(playerList->players[i]); + } + } + else if (selector == L"@p") + { + if (source.isPlayer() && source.getPlayer() != nullptr) + { + result.push_back(source.getPlayer()); + } + else if (!playerList->players.empty() && playerList->players[0] != nullptr) + { + result.push_back(playerList->players[0]); + } + } + else if (selector == L"@r") + { + if (!playerList->players.empty()) + { + int idx = rand() % (int)playerList->players.size(); + if (playerList->players[idx] != nullptr) + result.push_back(playerList->players[idx]); + } + } + else if (selector == L"@s") + { + if (source.isPlayer() && source.getPlayer() != nullptr) + result.push_back(source.getPlayer()); + } + else + { + shared_ptr player = findPlayer(playerList, selector); + if (player != nullptr) + result.push_back(player); + } + + return result; +} + +void ServerCommands::initialize() +{ + if (s_initialized) return; + s_initialized = true; + + s_commands[L"help"] = cmdHelp; + s_commands[L"?"] = cmdHelp; + s_commands[L"stop"] = cmdStop; + s_commands[L"list"] = cmdList; + s_commands[L"say"] = cmdSay; + s_commands[L"save-all"] = cmdSaveAll; + s_commands[L"kill"] = cmdKill; + s_commands[L"gamemode"] = cmdGamemode; + s_commands[L"teleport"] = cmdTeleport; + s_commands[L"tp"] = cmdTeleport; + s_commands[L"give"] = cmdGive; + s_commands[L"giveitem"] = cmdGive; + s_commands[L"enchant"] = cmdEnchant; + s_commands[L"enchantitem"] = cmdEnchant; + s_commands[L"time"] = cmdTime; + s_commands[L"weather"] = cmdWeather; + s_commands[L"summon"] = cmdSummon; + s_commands[L"setblock"] = cmdSetblock; + s_commands[L"fill"] = cmdFill; + s_commands[L"effect"] = cmdEffect; + s_commands[L"clear"] = cmdClear; + s_commands[L"vanish"] = cmdVanish; + s_commands[L"op"] = cmdOp; + s_commands[L"deop"] = cmdDeop; + s_commands[L"kick"] = cmdKick; + s_commands[L"ban"] = cmdBan; + s_commands[L"unban"] = cmdUnban; + s_commands[L"pardon"] = cmdUnban; + s_commands[L"banlist"] = cmdBanList; + + s_opOnlyCommands = { + L"stop", L"save-all", L"say", L"kill", L"gamemode", + L"teleport", L"tp", L"give", L"giveitem", + L"enchant", L"enchantitem", L"time", L"weather", + L"summon", L"setblock", L"fill", L"effect", + L"clear", L"vanish", L"op", L"deop", + L"kick", L"ban", L"unban", L"pardon", L"banlist" + }; + + s_opList.load(); + s_banList.load(); +} + +bool ServerCommands::execute(MinecraftServer* server, const wstring& rawCommand, CommandSource& source) +{ + if (server == nullptr) return false; + + wstring command = trimString(rawCommand); + if (command.empty()) return true; + + if (command[0] == L'/') command = trimString(command.substr(1)); + + vector tokens = splitCommand(command); + if (tokens.empty()) return true; + + wstring action = toLower(tokens[0]); + + auto it = s_commands.find(action); + if (it == s_commands.end()) + { + source.sendError(L"Unknown command: " + command + L". Type 'help' for a list of commands."); + return false; + } + + if (source.isPlayer() && !source.isOp() && s_opOnlyCommands.count(action)) + { + source.sendError(L"You do not have permission to use this command."); + return false; + } + + return it->second(server, tokens, source); +} + + +bool ServerCommands::cmdHelp(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (!source.isOp()) + { + source.sendMessage(L" - Server Commands - "); + source.sendMessage(L"help - Show this help message"); + source.sendMessage(L"list - List online players"); + return true; + } + + int page = 1; + if (tokens.size() >= 2) + { + if (tokens[1] == L"2") + page = 2; + } + + if (page == 1) + { + source.sendMessage(L" - Server Commands (Page 1/2) - "); + source.sendMessage(L"help [1|2] - Show this help message"); + source.sendMessage(L"list - List online players"); + source.sendMessage(L"stop - Stop the server"); + source.sendMessage(L"save-all - Save the world"); + source.sendMessage(L"say - Broadcast a message"); + source.sendMessage(L"kill - Kill a player"); + source.sendMessage(L"gamemode [player] - Set game mode"); + source.sendMessage(L"tp | tp - Teleport"); + source.sendMessage(L"give [amount] [data] - Give items"); + source.sendMessage(L"enchant [level] - Enchant held item"); + source.sendMessage(L"time - Manage world time"); + source.sendMessage(L"weather [duration] - Set weather"); + source.sendMessage(L"Type /help 2 for more commands."); + } + else + { + source.sendMessage(L" - Server Commands (Page 2/2) - "); + source.sendMessage(L"summon [x] [y] [z] - Summon an entity"); + source.sendMessage(L"setblock [data] - Set a block"); + source.sendMessage(L"fill [data] [mode] - Fill blocks"); + source.sendMessage(L"effect [effectId] [seconds] [amplifier] - Manage effects"); + source.sendMessage(L"clear [itemId] [data] - Clear inventory"); + source.sendMessage(L"vanish - Toggle vanish (fake disconnect)"); + source.sendMessage(L"op - Grant operator status"); + source.sendMessage(L"deop - Revoke operator status"); + source.sendMessage(L"kick [reason] - Kick a player"); + source.sendMessage(L"ban [reason] - Ban a player"); + source.sendMessage(L"unban - Unban a player (alias: pardon)"); + source.sendMessage(L"banlist - List all banned players"); + } + return true; +} + +bool ServerCommands::cmdStop(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + source.sendMessage(L"Stopping server..."); + server->setSaveOnExit(true); + app.m_bShutdown = true; + MinecraftServer::HaltServer(); + return true; +} + +bool ServerCommands::cmdList(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + PlayerList* playerList = server->getPlayers(); + int count = (playerList != nullptr) ? playerList->getPlayerCount() : 0; + + wstring names; + if (playerList != nullptr) + { + for (size_t i = 0; i < playerList->players.size(); ++i) + { + if (playerList->players[i] != nullptr) + { + if (!names.empty()) names += L", "; + wstring pname = playerList->players[i]->getName(); + if (s_vanishManager.isVanished(pname)) + names += pname + L" (vanished)"; + else + names += pname; + } + } + } + if (names.empty()) names = L"(none)"; + + source.sendMessage(L"Players (" + std::to_wstring(count) + L"): " + names); + return true; +} + + +bool ServerCommands::cmdSay(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 2) + { + source.sendError(L"Usage: say "); + return false; + } + + wstring message = L"[Server] " + joinTokens(tokens, 1); + PlayerList* playerList = server->getPlayers(); + if (playerList != nullptr) + playerList->broadcastAll(std::make_shared(message)); + source.sendMessage(message); + return true; +} + + +bool ServerCommands::cmdSaveAll(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + // stub. non-functional for now. + return true; +} + + +bool ServerCommands::cmdKill(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 2) + { + if (source.isPlayer() && source.getPlayer() != nullptr) + { + source.getPlayer()->hurt(DamageSource::outOfWorld, 3.4e38f); + source.sendSuccess(L"Killed " + source.getName()); + return true; + } + source.sendError(L"Usage: kill "); + return false; + } + + auto targets = resolvePlayerSelector(server, tokens[1], source); + if (targets.empty()) + { + source.sendError(L"No player found: " + tokens[1]); + return false; + } + + for (auto& player : targets) + { + player->hurt(DamageSource::outOfWorld, 3.4e38f); + source.sendSuccess(L"Killed " + player->getName()); + } + return true; +} + + +bool ServerCommands::cmdGamemode(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 2) + { + source.sendError(L"Usage: gamemode [player]"); + return false; + } + + wstring modeName = toLower(tokens[1]); + GameType* gameType = nullptr; + + if (modeName == L"survival" || modeName == L"s" || modeName == L"0") + gameType = GameType::SURVIVAL; + else if (modeName == L"creative" || modeName == L"c" || modeName == L"1") + gameType = GameType::CREATIVE; + else if (modeName == L"adventure" || modeName == L"a" || modeName == L"2") + gameType = GameType::ADVENTURE; + else + { + source.sendError(L"Unknown game mode: " + tokens[1]); + return false; + } + + vector> targets; + if (tokens.size() >= 3) + { + targets = resolvePlayerSelector(server, tokens[2], source); + } + else if (source.isPlayer()) + { + targets.push_back(source.getPlayer()); + } + else + { + source.sendError(L"Usage: gamemode "); + return false; + } + + if (targets.empty()) + { + source.sendError(L"No player found."); + return false; + } + + for (auto& player : targets) + { + player->setGameMode(gameType); + source.sendSuccess(L"Set " + player->getName() + L"'s game mode to " + gameType->getName()); + } + return true; +} + + +bool ServerCommands::cmdTeleport(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + PlayerList* playerList = server->getPlayers(); + + if (tokens.size() == 3) + { + auto subjects = resolvePlayerSelector(server, tokens[1], source); + shared_ptr destination = findPlayer(playerList, tokens[2]); + + if (subjects.empty()) + { + source.sendError(L"Unknown player: " + tokens[1]); + return false; + } + if (destination == nullptr) + { + source.sendError(L"Unknown player: " + tokens[2]); + return false; + } + + for (auto& subject : subjects) + { + if (subject->level->dimension->id != destination->level->dimension->id || !subject->isAlive()) + { + source.sendError(L"Cannot teleport " + subject->getName() + L" (different dimension or dead)."); + continue; + } + subject->ride(nullptr); + subject->connection->teleport(destination->x, destination->y, destination->z, destination->yRot, destination->xRot); + source.sendSuccess(L"Teleported " + subject->getName() + L" to " + destination->getName()); + } + return true; + } + + if (tokens.size() == 5) + { + auto subjects = resolvePlayerSelector(server, tokens[1], source); + if (subjects.empty()) + { + source.sendError(L"Unknown player: " + tokens[1]); + return false; + } + + double x, y, z; + if (!tryParseDouble(tokens[2], x) || !tryParseDouble(tokens[3], y) || !tryParseDouble(tokens[4], z)) + { + source.sendError(L"Invalid coordinates."); + return false; + } + + for (auto& subject : subjects) + { + subject->ride(nullptr); + subject->connection->teleport(x, y, z, subject->yRot, subject->xRot); + source.sendSuccess(L"Teleported " + subject->getName() + L" to " + + std::to_wstring(x) + L", " + std::to_wstring(y) + L", " + std::to_wstring(z)); + } + return true; + } + + if (tokens.size() == 4 && source.isPlayer()) + { + double x, y, z; + if (!tryParseDouble(tokens[1], x) || !tryParseDouble(tokens[2], y) || !tryParseDouble(tokens[3], z)) + { + source.sendError(L"Invalid coordinates."); + return false; + } + auto player = source.getPlayer(); + player->ride(nullptr); + player->connection->teleport(x, y, z, player->yRot, player->xRot); + source.sendSuccess(L"Teleported to " + + std::to_wstring(x) + L", " + std::to_wstring(y) + L", " + std::to_wstring(z)); + return true; + } + + source.sendError(L"Usage: tp | tp "); + return false; +} + +bool ServerCommands::cmdGive(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 3) + { + source.sendError(L"Usage: give [amount] [data]"); + return false; + } + + auto targets = resolvePlayerSelector(server, tokens[1], source); + if (targets.empty()) + { + source.sendError(L"Unknown player: " + tokens[1]); + return false; + } + + int itemId = 0, amount = 1, data = 0; + if (!tryParseInt(tokens[2], itemId)) + { + source.sendError(L"Invalid item id: " + tokens[2]); + return false; + } + if (tokens.size() >= 4 && !tryParseInt(tokens[3], amount)) + { + source.sendError(L"Invalid amount: " + tokens[3]); + return false; + } + if (tokens.size() >= 5 && !tryParseInt(tokens[4], data)) + { + source.sendError(L"Invalid data value: " + tokens[4]); + return false; + } + + if (itemId <= 0 || Item::items[itemId] == nullptr) + { + source.sendError(L"Unknown item id: " + std::to_wstring(itemId)); + return false; + } + if (amount <= 0) amount = 1; + if (amount > 64) amount = 64; + + for (auto& player : targets) + { + shared_ptr itemInstance(new ItemInstance(itemId, amount, data)); + shared_ptr drop = player->drop(itemInstance); + if (drop != nullptr) + drop->throwTime = 0; + source.sendSuccess(L"Gave " + std::to_wstring(amount) + L" x [" + std::to_wstring(itemId) + L":" + std::to_wstring(data) + L"] to " + player->getName()); + } + return true; +} + +bool ServerCommands::cmdEnchant(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 3) + { + source.sendError(L"Usage: enchant [level]"); + return false; + } + + auto targets = resolvePlayerSelector(server, tokens[1], source); + if (targets.empty()) + { + source.sendError(L"Unknown player: " + tokens[1]); + return false; + } + + int enchantmentId = 0, enchantmentLevel = 1; + if (!tryParseInt(tokens[2], enchantmentId)) + { + source.sendError(L"Invalid enchantment id: " + tokens[2]); + return false; + } + if (tokens.size() >= 4 && !tryParseInt(tokens[3], enchantmentLevel)) + { + source.sendError(L"Invalid enchantment level: " + tokens[3]); + return false; + } + + for (auto& player : targets) + { + shared_ptr selectedItem = player->getSelectedItem(); + if (selectedItem == nullptr) + { + source.sendError(player->getName() + L" is not holding an item."); + continue; + } + + Enchantment* enchantment = Enchantment::enchantments[enchantmentId]; + if (enchantment == nullptr) + { + source.sendError(L"Unknown enchantment id: " + std::to_wstring(enchantmentId)); + return false; + } + if (!enchantment->canEnchant(selectedItem)) + { + source.sendError(L"That enchantment cannot be applied to the selected item."); + continue; + } + + if (enchantmentLevel < enchantment->getMinLevel()) enchantmentLevel = enchantment->getMinLevel(); + if (enchantmentLevel > enchantment->getMaxLevel()) enchantmentLevel = enchantment->getMaxLevel(); + + if (selectedItem->hasTag()) + { + ListTag* enchantmentTags = selectedItem->getEnchantmentTags(); + if (enchantmentTags != nullptr) + { + bool conflict = false; + for (int i = 0; i < enchantmentTags->size(); i++) + { + int type = enchantmentTags->get(i)->getShort((wchar_t*)ItemInstance::TAG_ENCH_ID); + if (Enchantment::enchantments[type] != nullptr && !Enchantment::enchantments[type]->isCompatibleWith(enchantment)) + { + source.sendError(L"Enchantment conflicts with existing enchantment on " + player->getName() + L"'s item."); + conflict = true; + break; + } + } + if (conflict) continue; + } + } + + selectedItem->enchant(enchantment, enchantmentLevel); + source.sendSuccess(L"Enchanted " + player->getName() + L"'s held item with enchantment " + + std::to_wstring(enchantmentId) + L" level " + std::to_wstring(enchantmentLevel)); + } + return true; +} + + +bool ServerCommands::cmdTime(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 2) + { + source.sendError(L"Usage: time "); + return false; + } + + wstring subCmd = toLower(tokens[1]); + + if (subCmd == L"query") + { + if (server->levels[0] != nullptr) + { + int64_t dayTime = server->levels[0]->getDayTime(); + int64_t gameTime = server->levels[0]->getGameTime(); + source.sendMessage(L"Day time: " + std::to_wstring(dayTime) + L", Game time: " + std::to_wstring(gameTime)); + } + return true; + } + + if (subCmd == L"add") + { + if (tokens.size() < 3) + { + source.sendError(L"Usage: time add "); + return false; + } + + int delta = 0; + if (!tryParseInt(tokens[2], delta)) + { + source.sendError(L"Invalid tick value: " + tokens[2]); + return false; + } + + for (unsigned int i = 0; i < server->levels.length; ++i) + { + if (server->levels[i] != nullptr) + server->levels[i]->setDayTime(server->levels[i]->getDayTime() + delta); + } + source.sendSuccess(L"Added " + std::to_wstring(delta) + L" ticks to the time."); + return true; + } + wstring timeValue; + if (subCmd == L"set") + { + if (tokens.size() < 3) + { + source.sendError(L"Usage: time set "); + return false; + } + timeValue = toLower(tokens[2]); + } + else + { + timeValue = subCmd; + } + + int targetTime = 0; + if (timeValue == L"day") + targetTime = 1000; + else if (timeValue == L"noon") + targetTime = 6000; + else if (timeValue == L"night") + targetTime = 13000; + else if (timeValue == L"midnight") + targetTime = 18000; + else if (!tryParseInt(timeValue, targetTime)) + { + source.sendError(L"Invalid time value: " + timeValue); + return false; + } + + for (unsigned int i = 0; i < server->levels.length; ++i) + { + if (server->levels[i] != nullptr) + server->levels[i]->setDayTime(targetTime); + } + source.sendSuccess(L"Set the time to " + std::to_wstring(targetTime)); + return true; +} + + +bool ServerCommands::cmdWeather(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 2) + { + source.sendError(L"Usage: weather [duration_seconds]"); + return false; + } + + int durationSeconds = 600; + if (tokens.size() >= 3 && !tryParseInt(tokens[2], durationSeconds)) + { + source.sendError(L"Invalid duration: " + tokens[2]); + return false; + } + + if (server->levels[0] == nullptr) + { + source.sendError(L"The overworld is not loaded."); + return false; + } + + LevelData* levelData = server->levels[0]->getLevelData(); + int duration = durationSeconds * SharedConstants::TICKS_PER_SECOND; + levelData->setRainTime(duration); + levelData->setThunderTime(duration); + + wstring weather = toLower(tokens[1]); + if (weather == L"clear") + { + levelData->setRaining(false); + levelData->setThundering(false); + } + else if (weather == L"rain") + { + levelData->setRaining(true); + levelData->setThundering(false); + } + else if (weather == L"thunder") + { + levelData->setRaining(true); + levelData->setThundering(true); + } + else + { + source.sendError(L"Unknown weather type: " + tokens[1] + L". Use clear, rain, or thunder."); + return false; + } + + source.sendSuccess(L"Set weather to " + weather + L" for " + std::to_wstring(durationSeconds) + L" seconds."); + return true; +} + +bool ServerCommands::cmdSummon(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 2) + { + source.sendError(L"Usage: summon [x] [y] [z]"); + return false; + } + + wstring entityName = tokens[1]; + + double x = 0, y = 64, z = 0; + bool hasPos = false; + + if (tokens.size() >= 5) + { + if (!tryParseDouble(tokens[2], x) || !tryParseDouble(tokens[3], y) || !tryParseDouble(tokens[4], z)) + { + source.sendError(L"Invalid coordinates."); + return false; + } + hasPos = true; + } + else if (source.isPlayer() && source.getPlayer() != nullptr) + { + x = source.getPlayer()->x; + y = source.getPlayer()->y; + z = source.getPlayer()->z; + hasPos = true; + } + + if (!hasPos && server->levels[0] != nullptr) + { + LevelData* ld = server->levels[0]->getLevelData(); + if (ld != nullptr) + { + x = ld->getXSpawn(); + y = ld->getYSpawn(); + z = ld->getZSpawn(); + } + } + ServerLevel* level = server->levels[0]; + if (source.isPlayer() && source.getPlayer() != nullptr) + { + level = (ServerLevel*)source.getPlayer()->level; + } + + if (level == nullptr) + { + source.sendError(L"No level available."); + return false; + } + + shared_ptr entity = EntityIO::newEntity(entityName, level); + if (entity == nullptr) + { + wstring lowerName = toLower(entityName); + for (auto& pair : EntityIO::idsSpawnableInCreative) + { + wstring knownName = EntityIO::getEncodeId(pair.first); + if (toLower(knownName) == lowerName) + { + entity = EntityIO::newEntity(knownName, level); + entityName = knownName; + break; + } + } + } + + if (entity == nullptr) + { + source.sendError(L"Unknown entity type: " + tokens[1]); + return false; + } + + entity->moveTo(x, y, z, 0.0f, 0.0f); + level->addEntity(entity); + + source.sendSuccess(L"Summoned " + entityName + L" at " + + std::to_wstring((int)x) + L", " + std::to_wstring((int)y) + L", " + std::to_wstring((int)z)); + return true; +} + + +bool ServerCommands::cmdSetblock(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 5) + { + source.sendError(L"Usage: setblock [data]"); + return false; + } + + int x, y, z, tileId, data = 0; + if (!tryParseInt(tokens[1], x) || !tryParseInt(tokens[2], y) || !tryParseInt(tokens[3], z)) + { + source.sendError(L"Invalid coordinates."); + return false; + } + if (!tryParseInt(tokens[4], tileId)) + { + source.sendError(L"Invalid tile id: " + tokens[4]); + return false; + } + if (tokens.size() >= 6 && !tryParseInt(tokens[5], data)) + { + source.sendError(L"Invalid data value: " + tokens[5]); + return false; + } + + if (tileId < 0 || tileId >= Tile::TILE_NUM_COUNT) + { + source.sendError(L"Tile id out of range (0-" + std::to_wstring(Tile::TILE_NUM_COUNT - 1) + L")."); + return false; + } + + if (tileId != 0 && Tile::tiles[tileId] == nullptr) + { + source.sendError(L"Unknown tile id: " + std::to_wstring(tileId)); + return false; + } + + ServerLevel* level = server->levels[0]; + if (source.isPlayer() && source.getPlayer() != nullptr) + level = (ServerLevel*)source.getPlayer()->level; + + if (level == nullptr) + { + source.sendError(L"No level available."); + return false; + } + + level->setTileAndData(x, y, z, tileId, data, Tile::UPDATE_ALL); + + source.sendSuccess(L"Set block at " + std::to_wstring(x) + L", " + std::to_wstring(y) + L", " + std::to_wstring(z) + + L" to " + std::to_wstring(tileId) + L":" + std::to_wstring(data)); + return true; +} + + + +bool ServerCommands::cmdFill(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 8) + { + source.sendError(L"Usage: fill [data] [destroy|hollow|keep|outline|replace]"); + return false; + } + + int x1, y1, z1, x2, y2, z2, tileId, data = 0; + if (!tryParseInt(tokens[1], x1) || !tryParseInt(tokens[2], y1) || !tryParseInt(tokens[3], z1) || + !tryParseInt(tokens[4], x2) || !tryParseInt(tokens[5], y2) || !tryParseInt(tokens[6], z2)) + { + source.sendError(L"Invalid coordinates."); + return false; + } + if (!tryParseInt(tokens[7], tileId)) + { + source.sendError(L"Invalid tile id: " + tokens[7]); + return false; + } + if (tokens.size() >= 9 && !tryParseInt(tokens[8], data)) + { + source.sendError(L"Invalid data value: " + tokens[8]); + return false; + } + + if (tileId < 0 || tileId >= Tile::TILE_NUM_COUNT) + { + source.sendError(L"Tile id out of range."); + return false; + } + if (tileId != 0 && Tile::tiles[tileId] == nullptr) + { + source.sendError(L"Unknown tile id: " + std::to_wstring(tileId)); + return false; + } + + wstring mode = L"replace"; + if (tokens.size() >= 10) mode = toLower(tokens[9]); + + int minX = (x1 < x2) ? x1 : x2; + int minY = (y1 < y2) ? y1 : y2; + int minZ = (z1 < z2) ? z1 : z2; + int maxX = (x1 > x2) ? x1 : x2; + int maxY = (y1 > y2) ? y1 : y2; + int maxZ = (z1 > z2) ? z1 : z2; + + int64_t volume = (int64_t)(maxX - minX + 1) * (maxY - minY + 1) * (maxZ - minZ + 1); + if (volume > 32768) + { + source.sendError(L"Too many blocks in the fill region (" + std::to_wstring(volume) + L"). Maximum is 32768."); + return false; + } + + ServerLevel* level = server->levels[0]; + if (source.isPlayer() && source.getPlayer() != nullptr) + level = (ServerLevel*)source.getPlayer()->level; + + if (level == nullptr) + { + source.sendError(L"No level available."); + return false; + } + + int blocksChanged = 0; + + for (int bx = minX; bx <= maxX; ++bx) + { + for (int by = minY; by <= maxY; ++by) + { + for (int bz = minZ; bz <= maxZ; ++bz) + { + bool shouldPlace = true; + + if (mode == L"keep") + { + if (level->getTile(bx, by, bz) != 0) shouldPlace = false; + } + else if (mode == L"hollow") + { + bool isEdge = (bx == minX || bx == maxX || by == minY || by == maxY || bz == minZ || bz == maxZ); + if (isEdge) + shouldPlace = true; + else + { + level->setTileAndData(bx, by, bz, 0, 0, Tile::UPDATE_ALL); + blocksChanged++; + shouldPlace = false; + } + } + else if (mode == L"outline") + { + bool isEdge = (bx == minX || bx == maxX || by == minY || by == maxY || bz == minZ || bz == maxZ); + if (!isEdge) shouldPlace = false; + } + + if (shouldPlace) + { + level->setTileAndData(bx, by, bz, tileId, data, Tile::UPDATE_ALL); + blocksChanged++; + } + } + } + } + + source.sendSuccess(L"Filled " + std::to_wstring(blocksChanged) + L" blocks."); + return true; +} + + +bool ServerCommands::cmdEffect(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 3) + { + source.sendError(L"Usage: effect [effectId] [seconds] [amplifier] | effect clear "); + return false; + } + + wstring subCmd = toLower(tokens[1]); + + if (subCmd == L"clear") + { + auto targets = resolvePlayerSelector(server, tokens[2], source); + if (targets.empty()) + { + source.sendError(L"Unknown player: " + tokens[2]); + return false; + } + + for (auto& player : targets) + { + player->removeAllEffects(); + source.sendSuccess(L"Cleared all effects from " + player->getName()); + } + return true; + } + + if (subCmd == L"give") + { + if (tokens.size() < 4) + { + source.sendError(L"Usage: effect give [seconds] [amplifier]"); + return false; + } + + auto targets = resolvePlayerSelector(server, tokens[2], source); + if (targets.empty()) + { + source.sendError(L"Unknown player: " + tokens[2]); + return false; + } + + int effectId = 0, seconds = 30, amplifier = 0; + if (!tryParseInt(tokens[3], effectId)) + { + source.sendError(L"Invalid effect id: " + tokens[3]); + return false; + } + if (tokens.size() >= 5 && !tryParseInt(tokens[4], seconds)) + { + source.sendError(L"Invalid duration: " + tokens[4]); + return false; + } + if (tokens.size() >= 6 && !tryParseInt(tokens[5], amplifier)) + { + source.sendError(L"Invalid amplifier: " + tokens[5]); + return false; + } + + if (effectId < 0 || effectId >= MobEffect::NUM_EFFECTS || MobEffect::effects[effectId] == nullptr) + { + source.sendError(L"Unknown effect id: " + std::to_wstring(effectId) + L" (valid range: 1-" + std::to_wstring(MobEffect::NUM_EFFECTS - 1) + L")"); + return false; + } + + if (seconds <= 0) seconds = 1; + if (seconds > 1000000) seconds = 1000000; + if (amplifier < 0) amplifier = 0; + if (amplifier > 255) amplifier = 255; + + int durationTicks = seconds * SharedConstants::TICKS_PER_SECOND; + + for (auto& player : targets) + { + MobEffectInstance* effectInstance = new MobEffectInstance(effectId, durationTicks, amplifier); + player->addEffect(effectInstance); + source.sendSuccess(L"Applied effect " + std::to_wstring(effectId) + + L" (amplifier " + std::to_wstring(amplifier) + L", " + std::to_wstring(seconds) + L"s) to " + player->getName()); + } + return true; + } + + auto targets = resolvePlayerSelector(server, tokens[1], source); + if (!targets.empty() && tokens.size() >= 3) + { + int effectId = 0, seconds = 30, amplifier = 0; + if (tryParseInt(tokens[2], effectId)) + { + if (tokens.size() >= 4) tryParseInt(tokens[3], seconds); + if (tokens.size() >= 5) tryParseInt(tokens[4], amplifier); + + if (effectId < 0 || effectId >= MobEffect::NUM_EFFECTS || MobEffect::effects[effectId] == nullptr) + { + source.sendError(L"Unknown effect id: " + std::to_wstring(effectId)); + return false; + } + + if (seconds <= 0) seconds = 1; + if (amplifier < 0) amplifier = 0; + if (amplifier > 255) amplifier = 255; + + int durationTicks = seconds * SharedConstants::TICKS_PER_SECOND; + + for (auto& player : targets) + { + MobEffectInstance* effectInstance = new MobEffectInstance(effectId, durationTicks, amplifier); + player->addEffect(effectInstance); + source.sendSuccess(L"Applied effect " + std::to_wstring(effectId) + L" to " + player->getName()); + } + return true; + } + } + + source.sendError(L"Usage: effect [effectId] [seconds] [amplifier]"); + return false; +} + + + +bool ServerCommands::cmdClear(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 2) + { + if (source.isPlayer()) + { + auto player = source.getPlayer(); + player->inventory->clearInventory(-1, -1); + source.sendSuccess(L"Cleared inventory of " + player->getName()); + return true; + } + source.sendError(L"Usage: clear [itemId] [data]"); + return false; + } + + auto targets = resolvePlayerSelector(server, tokens[1], source); + if (targets.empty()) + { + source.sendError(L"Unknown player: " + tokens[1]); + return false; + } + + int filterItemId = -1, filterData = -1; + if (tokens.size() >= 3 && !tryParseInt(tokens[2], filterItemId)) + { + source.sendError(L"Invalid item id: " + tokens[2]); + return false; + } + if (tokens.size() >= 4 && !tryParseInt(tokens[3], filterData)) + { + source.sendError(L"Invalid data value: " + tokens[3]); + return false; + } + + for (auto& player : targets) + { + if (filterItemId < 0) + { + player->inventory->clearInventory(-1, -1); + source.sendSuccess(L"Cleared inventory of " + player->getName()); + } + else + { + int removed = 0; + for (int slot = 0; slot < player->inventory->getContainerSize(); ++slot) + { + shared_ptr item = player->inventory->getItem(slot); + if (item != nullptr && item->id == filterItemId) + { + if (filterData >= 0 && item->getAuxValue() != filterData) continue; + removed += item->count; + player->inventory->setItem(slot, nullptr); + } + } + source.sendSuccess(L"Cleared " + std::to_wstring(removed) + L" items from " + player->getName()); + } + } + return true; +} + + +static void vanishPlayer(shared_ptr player, PlayerList* playerList) +{ + if (playerList != nullptr) + { + auto fakeLeave = std::make_shared(player->getName(), ChatPacket::e_ChatPlayerLeftGame); + for (size_t i = 0; i < playerList->players.size(); ++i) + { + if (playerList->players[i] != nullptr && playerList->players[i] != player) + playerList->players[i]->connection->send(fakeLeave); + } + } + + ServerLevel* level = (ServerLevel*)player->level; + if (level != nullptr) + { + EntityTracker* tracker = level->getTracker(); + if (tracker != nullptr) + tracker->removeEntity(player); + } +} + +static void unvanishPlayer(shared_ptr player, PlayerList* playerList) +{ + ServerLevel* level = (ServerLevel*)player->level; + if (level != nullptr) + { + EntityTracker* tracker = level->getTracker(); + if (tracker != nullptr) + tracker->addEntity(player); + } + + if (playerList != nullptr) + { + auto fakeJoin = std::make_shared(player->getName(), ChatPacket::e_ChatPlayerJoinedGame); + for (size_t i = 0; i < playerList->players.size(); ++i) + { + if (playerList->players[i] != nullptr && playerList->players[i] != player) + playerList->players[i]->connection->send(fakeJoin); + } + } +} + +bool ServerCommands::cmdVanish(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 2) + { + if (source.isPlayer()) + { + wstring name = source.getName(); + bool isVanished = s_vanishManager.isVanished(name); + s_vanishManager.setVanished(name, !isVanished); + + PlayerList* playerList = server->getPlayers(); + if (!isVanished) + { + vanishPlayer(source.getPlayer(), playerList); + source.sendSuccess(L"You are now vanished. Other players think you disconnected."); + } + else + { + unvanishPlayer(source.getPlayer(), playerList); + source.sendSuccess(L"You are no longer vanished."); + } + return true; + } + source.sendError(L"Usage: vanish "); + return false; + } + + auto targets = resolvePlayerSelector(server, tokens[1], source); + if (targets.empty()) + { + source.sendError(L"Unknown player: " + tokens[1]); + return false; + } + + PlayerList* playerList = server->getPlayers(); + + for (auto& player : targets) + { + wstring name = player->getName(); + bool isVanished = s_vanishManager.isVanished(name); + s_vanishManager.setVanished(name, !isVanished); + + if (!isVanished) + { + vanishPlayer(player, playerList); + source.sendSuccess(name + L" is now vanished."); + } + else + { + unvanishPlayer(player, playerList); + source.sendSuccess(name + L" is no longer vanished."); + } + } + return true; +} + + +bool ServerCommands::cmdOp(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 2) + { + source.sendError(L"Usage: op "); + return false; + } + + wstring playerName = tokens[1]; + shared_ptr player = findPlayer(server->getPlayers(), playerName); + if (player != nullptr) + playerName = player->getName(); + + if (s_opList.isOp(playerName)) + { + source.sendMessage(playerName + L" is already an operator."); + return true; + } + + s_opList.addOp(playerName); + s_opList.save(); + source.sendSuccess(L"Made " + playerName + L" a server operator."); + + if (player != nullptr && player->connection != nullptr) + { + player->connection->send(std::make_shared(L"You are now a server operator.")); + } + + return true; +} + +bool ServerCommands::cmdDeop(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 2) + { + source.sendError(L"Usage: deop "); + return false; + } + + wstring playerName = tokens[1]; + shared_ptr player = findPlayer(server->getPlayers(), playerName); + if (player != nullptr) + playerName = player->getName(); + + if (!s_opList.isOp(playerName)) + { + source.sendMessage(playerName + L" is not an operator."); + return true; + } + + s_opList.removeOp(playerName); + s_opList.save(); + source.sendSuccess(L"Removed " + playerName + L" from server operators."); + + if (player != nullptr && player->connection != nullptr) + { + player->connection->send(std::make_shared(L"You are no longer a server operator.")); + } + + return true; +} + + +bool ServerCommands::cmdKick(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 2) + { + source.sendError(L"Usage: kick [reason]"); + return false; + } + + auto targets = resolvePlayerSelector(server, tokens[1], source); + if (targets.empty()) + { + source.sendError(L"Unknown player: " + tokens[1]); + return false; + } + + wstring reason = tokens.size() > 2 ? joinTokens(tokens, 2) : L"Kicked by an operator"; + + for (auto& player : targets) + { + if (player->connection != nullptr) + { + player->connection->send(std::make_shared(L"Kicked: " + reason)); + player->connection->setWasKicked(); + player->connection->disconnect(DisconnectPacket::eDisconnect_Kicked); + } + source.sendSuccess(L"Kicked " + player->getName() + L": " + reason); + } + return true; +} + +bool ServerCommands::cmdBan(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 2) + { + source.sendError(L"Usage: ban [reason]"); + return false; + } + + wstring playerName = tokens[1]; + wstring reason = tokens.size() > 2 ? joinTokens(tokens, 2) : L"Banned by an operator"; + + shared_ptr player = findPlayer(server->getPlayers(), playerName); + if (player != nullptr) + playerName = player->getName(); + + s_banList.ban(playerName, reason); + s_banList.save(); + source.sendSuccess(L"Banned " + playerName + L": " + reason); + + if (player != nullptr && player->connection != nullptr) + { + player->connection->send(std::make_shared(L"Banned: " + reason)); + player->connection->setWasKicked(); + player->connection->disconnect(DisconnectPacket::eDisconnect_Banned); + } + + return true; +} + +bool ServerCommands::cmdUnban(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + if (tokens.size() < 2) + { + source.sendError(L"Usage: unban "); + return false; + } + + wstring playerName = tokens[1]; + if (!s_banList.isBanned(playerName)) + { + source.sendError(playerName + L" is not banned."); + return true; + } + + s_banList.unban(playerName); + s_banList.save(); + source.sendSuccess(L"Unbanned " + playerName + L"."); + return true; +} + +bool ServerCommands::cmdBanList(MinecraftServer* server, const vector& tokens, CommandSource& source) +{ + const auto& bans = s_banList.getBans(); + if (bans.empty()) + { + source.sendMessage(L"No players are banned."); + return true; + } + + source.sendMessage(L"Banned players (" + std::to_wstring(bans.size()) + L"):"); + for (const auto& entry : bans) + { + if (entry.second.empty()) + source.sendMessage(L" " + entry.first); + else + source.sendMessage(L" " + entry.first + L" - " + entry.second); + } + return true; +} diff --git a/Minecraft.Client/ServerCommands.h b/Minecraft.Client/ServerCommands.h new file mode 100644 index 000000000..72b08ec29 --- /dev/null +++ b/Minecraft.Client/ServerCommands.h @@ -0,0 +1,144 @@ +#pragma once +#include +#include +#include +#include +#include + +class MinecraftServer; +class ServerPlayer; +class ConsoleInputSource; +class PlayerList; + +// Command sender abstraction - can be server console or a player +class CommandSource +{ +public: + enum Type { CONSOLE, PLAYER }; + + CommandSource(MinecraftServer* server, ConsoleInputSource* source, Type type, const wstring& name); + CommandSource(MinecraftServer* server, shared_ptr player); + + void sendMessage(const wstring& message); + void sendSuccess(const wstring& message); + void sendError(const wstring& message); + + bool isConsole() const { return m_type == CONSOLE; } + bool isPlayer() const { return m_type == PLAYER; } + bool isOp() const; + + MinecraftServer* getServer() const { return m_server; } + shared_ptr getPlayer() const { return m_player; } + wstring getName() const { return m_name; } + +private: + MinecraftServer* m_server; + ConsoleInputSource* m_consoleSource; + shared_ptr m_player; + Type m_type; + wstring m_name; +}; + +// Server-side op list management +class OpList +{ +public: + void addOp(const wstring& name); + void removeOp(const wstring& name); + bool isOp(const wstring& name) const; + const unordered_set& getOps() const { return m_ops; } + + void save() const; + void load(); + +private: + unordered_set m_ops; +}; + +// Server-side ban list management (persistent) +class BanList +{ +public: + void ban(const wstring& name, const wstring& reason = L""); + void unban(const wstring& name); + bool isBanned(const wstring& name) const; + wstring getBanReason(const wstring& name) const; + const unordered_map& getBans() const { return m_bans; } + + void save() const; + void load(); + +private: + unordered_map m_bans; // name -> reason +}; + +// Vanish manager +class VanishManager +{ +public: + void setVanished(const wstring& name, bool vanished); + bool isVanished(const wstring& name) const; + const unordered_set& getVanished() const { return m_vanished; } + +private: + unordered_set m_vanished; +}; + +// Main command registry and execution engine +class ServerCommands +{ +public: + static void initialize(); + + // Execute a command string (with or without leading /) + static bool execute(MinecraftServer* server, const wstring& rawCommand, CommandSource& source); + + // Access op and vanish managers + static OpList& getOpList() { return s_opList; } + static VanishManager& getVanishManager() { return s_vanishManager; } + static BanList& getBanList() { return s_banList; } + +private: + // Utility functions + static vector splitCommand(const wstring& command); + static wstring joinTokens(const vector& tokens, size_t startIndex); + static bool tryParseInt(const wstring& text, int& value); + static bool tryParseDouble(const wstring& text, double& value); + static shared_ptr findPlayer(PlayerList* playerList, const wstring& name); + static vector> resolvePlayerSelector(MinecraftServer* server, const wstring& selector, CommandSource& source); + + // Command handlers + static bool cmdHelp(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdStop(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdList(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdSay(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdSaveAll(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdKill(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdGamemode(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdTeleport(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdGive(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdEnchant(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdTime(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdWeather(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdSummon(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdSetblock(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdFill(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdEffect(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdClear(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdVanish(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdOp(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdDeop(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdKick(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdBan(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdUnban(MinecraftServer* server, const vector& tokens, CommandSource& source); + static bool cmdBanList(MinecraftServer* server, const vector& tokens, CommandSource& source); + + // Command handler map + typedef bool (*CommandHandler)(MinecraftServer*, const vector&, CommandSource&); + static unordered_map s_commands; + static unordered_set s_opOnlyCommands; + static OpList s_opList; + static BanList s_banList; + static VanishManager s_vanishManager; + static bool s_initialized; +}; diff --git a/Minecraft.Client/ServerConsole.cpp b/Minecraft.Client/ServerConsole.cpp new file mode 100644 index 000000000..25648432f --- /dev/null +++ b/Minecraft.Client/ServerConsole.cpp @@ -0,0 +1,766 @@ +#include "stdafx.h" + +#ifdef _WINDOWS64 + +#include "ServerConsole.h" +#include "MinecraftServer.h" +#include "PlayerList.h" +#include "ServerPlayer.h" +#include "..\Minecraft.World\StringHelpers.h" +#include "..\Minecraft.World\EntityIO.h" +#include "..\Minecraft.World\Tile.h" +#include "..\Minecraft.World\MobEffect.h" +#include "..\Minecraft.World\LevelSettings.h" + +#include +#include +#include +#include + +ServerConsole* ServerConsole::s_instance = nullptr; + +const char* ServerConsole::Color::Reset = "\033[0m"; +const char* ServerConsole::Color::Red = "\033[31m"; +const char* ServerConsole::Color::Green = "\033[32m"; +const char* ServerConsole::Color::Yellow = "\033[33m"; +const char* ServerConsole::Color::Blue = "\033[34m"; +const char* ServerConsole::Color::Magenta = "\033[35m"; +const char* ServerConsole::Color::Cyan = "\033[36m"; +const char* ServerConsole::Color::White = "\033[37m"; +const char* ServerConsole::Color::Gray = "\033[90m"; +const char* ServerConsole::Color::BrightRed = "\033[91m"; +const char* ServerConsole::Color::BrightGreen = "\033[92m"; +const char* ServerConsole::Color::BrightYellow= "\033[93m"; +const char* ServerConsole::Color::BrightBlue = "\033[94m"; +const char* ServerConsole::Color::BrightMagenta="\033[95m"; +const char* ServerConsole::Color::BrightCyan = "\033[96m"; +const char* ServerConsole::Color::BrightWhite = "\033[97m"; + +// Known command names for completion and highlighting +static const char* s_commandNames[] = { + "help", "stop", "list", "say", "save-all", + "kill", "gamemode", "teleport", "tp", "give", "giveitem", + "enchant", "enchantitem", "time", "weather", "summon", + "setblock", "fill", "effect", "clear", "vanish", "op", "deop", + "kick", "ban", "unban", "pardon", "banlist", + nullptr +}; + +// Subcommand completions for specific commands +static const char* s_timeSubcmds[] = { "set", "add", "query", nullptr }; +static const char* s_timeSetValues[] = { "day", "night", "noon", "midnight", nullptr }; +static const char* s_weatherTypes[] = { "clear", "rain", "thunder", nullptr }; +static const char* s_gamemodeNames[] = { "survival", "creative", "adventure", "0", "1", "2", "s", "c", "a", nullptr }; +static const char* s_effectActions[] = { "give", "clear", nullptr }; + +ServerConsole::ServerConsole(MinecraftServer* server) + : m_server(server) + , m_cursorPos(0) + , m_historyIndex(-1) + , m_tabIndex(-1) + , m_running(true) +{ + m_hConsoleIn = GetStdHandle(STD_INPUT_HANDLE); + m_hConsoleOut = GetStdHandle(STD_OUTPUT_HANDLE); + s_instance = this; +} + +ServerConsole::~ServerConsole() +{ + if (s_instance == this) + s_instance = nullptr; +} + +void ServerConsole::enableVirtualTerminal() +{ + DWORD dwMode = 0; + if (GetConsoleMode(m_hConsoleOut, &dwMode)) + { + dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; + SetConsoleMode(m_hConsoleOut, dwMode); + } + + if (GetConsoleMode(m_hConsoleIn, &dwMode)) + { + // Disable line input and echo so we can handle raw key events + dwMode &= ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT); + dwMode |= ENABLE_WINDOW_INPUT; + SetConsoleMode(m_hConsoleIn, dwMode); + } +} + +void ServerConsole::logInfo(const char* fmt, ...) +{ + ServerConsole* sc = getInstance(); + if (sc) sc->clearInputLine(); + + char buf[2048]; + va_list args; + va_start(args, fmt); + vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + + printf("%s[INFO]%s %s\n", Color::BrightCyan, Color::Reset, buf); + fflush(stdout); + + if (sc) sc->redrawInputLine(); +} + +void ServerConsole::logWarn(const char* fmt, ...) +{ + ServerConsole* sc = getInstance(); + if (sc) sc->clearInputLine(); + + char buf[2048]; + va_list args; + va_start(args, fmt); + vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + + printf("%s[WARN]%s %s%s%s\n", Color::BrightYellow, Color::Reset, Color::Yellow, buf, Color::Reset); + fflush(stdout); + + if (sc) sc->redrawInputLine(); +} + +void ServerConsole::logError(const char* fmt, ...) +{ + ServerConsole* sc = getInstance(); + if (sc) sc->clearInputLine(); + + char buf[2048]; + va_list args; + va_start(args, fmt); + vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + + printf("%s[ERROR]%s %s%s%s\n", Color::BrightRed, Color::Reset, Color::Red, buf, Color::Reset); + fflush(stdout); + + if (sc) sc->redrawInputLine(); +} + +void ServerConsole::logSuccess(const char* fmt, ...) +{ + ServerConsole* sc = getInstance(); + if (sc) sc->clearInputLine(); + + char buf[2048]; + va_list args; + va_start(args, fmt); + vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + + printf("%s[INFO]%s %s%s%s\n", Color::BrightGreen, Color::Reset, Color::Green, buf, Color::Reset); + fflush(stdout); + + if (sc) sc->redrawInputLine(); +} + +void ServerConsole::logCommand(const char* label, const char* message) +{ + ServerConsole* sc = getInstance(); + if (sc) sc->clearInputLine(); + + printf("%s[%s]%s %s\n", Color::BrightMagenta, label, Color::Reset, message); + fflush(stdout); + + if (sc) sc->redrawInputLine(); +} + +void ServerConsole::logInfo(const wchar_t* message) +{ + ServerConsole* sc = getInstance(); + if (sc) sc->clearInputLine(); + + printf("%s[INFO]%s %ls\n", Color::BrightCyan, Color::Reset, message); + fflush(stdout); + + if (sc) sc->redrawInputLine(); +} + +void ServerConsole::logWarn(const wchar_t* message) +{ + ServerConsole* sc = getInstance(); + if (sc) sc->clearInputLine(); + + printf("%s[WARN]%s %s%ls%s\n", Color::BrightYellow, Color::Reset, Color::Yellow, message, Color::Reset); + fflush(stdout); + + if (sc) sc->redrawInputLine(); +} + +void ServerConsole::logError(const wchar_t* message) +{ + ServerConsole* sc = getInstance(); + if (sc) sc->clearInputLine(); + + printf("%s[ERROR]%s %s%ls%s\n", Color::BrightRed, Color::Reset, Color::Red, message, Color::Reset); + fflush(stdout); + + if (sc) sc->redrawInputLine(); +} + +void ServerConsole::printPrompt() +{ + printf("%s> %s", Color::BrightGreen, Color::Reset); + fflush(stdout); +} + +void ServerConsole::clearInputLine() +{ + // Move to start of line, clear it + printf("\r\033[K"); + fflush(stdout); +} + +void ServerConsole::redrawInputLine() +{ + printf("\r\033[K"); + printPrompt(); + + if (!m_inputBuffer.empty()) + { + std::string highlighted = highlightCommand(m_inputBuffer); + printf("%s", highlighted.c_str()); + } + + // Position cursor correctly + int promptLen = 2; // "> " + int targetCol = promptLen + m_cursorPos; + printf("\r\033[%dC", targetCol); + fflush(stdout); +} + +std::string ServerConsole::highlightCommand(const std::string& input) +{ + if (input.empty()) return input; + + std::string result; + std::istringstream stream(input); + std::string token; + bool firstToken = true; + size_t pos = 0; + + // Find the first word (command name) + size_t firstSpace = input.find(' '); + std::string cmdName = (firstSpace != std::string::npos) ? input.substr(0, firstSpace) : input; + std::string cmdLower = cmdName; + std::transform(cmdLower.begin(), cmdLower.end(), cmdLower.begin(), ::tolower); + + // Check if it starts with / + size_t cmdStart = 0; + if (!cmdLower.empty() && cmdLower[0] == '/') + { + result += Color::Gray; + result += '/'; + cmdLower = cmdLower.substr(1); + cmdName = cmdName.substr(1); + cmdStart = 1; + } + + // Highlight command name + bool knownCmd = false; + for (int i = 0; s_commandNames[i] != nullptr; ++i) + { + if (cmdLower == s_commandNames[i]) + { + knownCmd = true; + break; + } + } + + if (knownCmd) + result += std::string(Color::BrightGreen) + cmdName + Color::Reset; + else + result += std::string(Color::BrightRed) + cmdName + Color::Reset; + + // Highlight rest (arguments) + if (firstSpace != std::string::npos) + { + std::string rest = input.substr(firstSpace); + // Color numbers differently from strings + std::string argResult; + bool inNumber = false; + for (size_t i = 0; i < rest.size(); ++i) + { + char c = rest[i]; + if (c == ' ') + { + if (inNumber) { argResult += Color::Reset; inNumber = false; } + argResult += c; + } + else if ((c >= '0' && c <= '9') || c == '-' || c == '.') + { + if (!inNumber) { argResult += Color::BrightYellow; inNumber = true; } + argResult += c; + } + else if (c == '~') + { + if (inNumber) { argResult += Color::Reset; inNumber = false; } + argResult += Color::BrightMagenta; + argResult += c; + argResult += Color::Reset; + } + else + { + if (inNumber) { argResult += Color::Reset; inNumber = false; } + argResult += Color::BrightWhite; + argResult += c; + argResult += Color::Reset; + } + } + if (inNumber) argResult += Color::Reset; + result += argResult; + } + + return result; +} + +static std::string toLowerStr(const std::string& s) +{ + std::string r = s; + std::transform(r.begin(), r.end(), r.begin(), ::tolower); + return r; +} + +std::vector ServerConsole::getCompletions(const std::string& partial) +{ + std::vector results; + if (partial.empty()) return results; + + // Parse the input to figure out what we're completing + std::string input = partial; + bool hasSlash = false; + if (!input.empty() && input[0] == '/') + { + hasSlash = true; + input = input.substr(1); + } + + // Split into tokens + std::vector tokens; + { + std::istringstream ss(input); + std::string tok; + while (ss >> tok) tokens.push_back(tok); + } + + // If input ends with a space, we're completing the NEXT token + bool completingNext = (!partial.empty() && partial.back() == ' '); + if (completingNext) tokens.push_back(""); + + if (tokens.empty()) + { + // Complete command names + for (int i = 0; s_commandNames[i] != nullptr; ++i) + { + std::string name = s_commandNames[i]; + results.push_back(hasSlash ? ("/" + name) : name); + } + return results; + } + + if (tokens.size() == 1) + { + // Complete command name + std::string prefix = toLowerStr(tokens[0]); + for (int i = 0; s_commandNames[i] != nullptr; ++i) + { + std::string name = s_commandNames[i]; + if (name.find(prefix) == 0) + { + results.push_back(hasSlash ? ("/" + name) : name); + } + } + return results; + } + + // Token 1+ : subcommand or argument completions + std::string cmdName = toLowerStr(tokens[0]); + std::string argPrefix = toLowerStr(tokens.back()); + + // Subcommand completions + const char** subCmds = nullptr; + if (tokens.size() == 2) + { + if (cmdName == "time") subCmds = s_timeSubcmds; + else if (cmdName == "weather") subCmds = s_weatherTypes; + else if (cmdName == "gamemode") subCmds = s_gamemodeNames; + else if (cmdName == "effect") subCmds = s_effectActions; + } + else if (tokens.size() == 3 && cmdName == "time" && toLowerStr(tokens[1]) == "set") + { + subCmds = s_timeSetValues; + } + + if (subCmds != nullptr) + { + for (int i = 0; subCmds[i] != nullptr; ++i) + { + std::string sub = subCmds[i]; + if (sub.find(argPrefix) == 0) + { + results.push_back(sub); + } + } + // Don't return yet - also try player names + } + + // Player name completions for commands that take player names + bool wantsPlayerName = false; + if (cmdName == "kill" || cmdName == "tp" || cmdName == "teleport" || + cmdName == "give" || cmdName == "giveitem" || cmdName == "enchant" || cmdName == "enchantitem" || + cmdName == "gamemode" || cmdName == "effect" || cmdName == "clear" || + cmdName == "vanish" || cmdName == "op" || cmdName == "deop" || + cmdName == "kick" || cmdName == "ban") + { + // Most commands take a player name as the first or second arg + wantsPlayerName = true; + } + + if (wantsPlayerName) + { + MinecraftServer* server = MinecraftServer::getInstance(); + if (server != nullptr) + { + PlayerList* playerList = server->getPlayers(); + if (playerList != nullptr) + { + for (size_t i = 0; i < playerList->players.size(); ++i) + { + if (playerList->players[i] != nullptr) + { + wstring wname = playerList->players[i]->getName(); + // Convert to narrow string + std::string name(wname.begin(), wname.end()); + std::string nameLower = toLowerStr(name); + if (nameLower.find(argPrefix) == 0) + { + results.push_back(name); + } + } + } + // Add @a, @p, @r, @s selectors + const char* selectors[] = { "@a", "@p", "@r", "@s", nullptr }; + for (int i = 0; selectors[i] != nullptr; ++i) + { + std::string sel = selectors[i]; + if (sel.find(argPrefix) == 0) + results.push_back(sel); + } + } + } + } + + // Entity name completions for summon (use public idsSpawnableInCreative) + if (cmdName == "summon" && tokens.size() == 2) + { + for (auto& pair : EntityIO::idsSpawnableInCreative) + { + wstring wname = EntityIO::getEncodeId(pair.first); + if (!wname.empty()) + { + std::string name(wname.begin(), wname.end()); + std::string nameLower = toLowerStr(name); + if (nameLower.find(argPrefix) == 0) + { + results.push_back(name); + } + } + } + } + + return results; +} + +void ServerConsole::handleTabCompletion() +{ + if (m_tabIndex == -1) + { + // Start new tab completion + m_tabPartial = m_inputBuffer; + m_tabCompletions = getCompletions(m_tabPartial); + if (m_tabCompletions.empty()) return; + m_tabIndex = 0; + } + else + { + m_tabIndex = (m_tabIndex + 1) % (int)m_tabCompletions.size(); + } + + if (m_tabCompletions.empty()) return; + + // Figure out what portion to replace + // Find the last space in the partial to know what word we're completing + std::string completion = m_tabCompletions[m_tabIndex]; + + size_t lastSpace = m_tabPartial.rfind(' '); + if (lastSpace == std::string::npos) + { + // Replacing the whole input + m_inputBuffer = completion; + } + else + { + // Replacing the last word + m_inputBuffer = m_tabPartial.substr(0, lastSpace + 1) + completion; + } + + m_cursorPos = (int)m_inputBuffer.size(); + + // If there are multiple completions, show them + if (m_tabCompletions.size() > 1) + { + clearInputLine(); + printf("\r\n"); + for (size_t i = 0; i < m_tabCompletions.size(); ++i) + { + if ((int)i == m_tabIndex) + printf("%s%s%s ", Color::BrightGreen, m_tabCompletions[i].c_str(), Color::Reset); + else + printf("%s ", m_tabCompletions[i].c_str()); + } + printf("\r\n"); + fflush(stdout); + } + + redrawInputLine(); +} + +void ServerConsole::insertChar(char c) +{ + if ((int)m_inputBuffer.size() >= MAX_INPUT_LENGTH) return; + + m_inputBuffer.insert(m_inputBuffer.begin() + m_cursorPos, c); + m_cursorPos++; + m_tabIndex = -1; // Reset tab completion + redrawInputLine(); +} + +void ServerConsole::deleteCharBack() +{ + if (m_cursorPos > 0) + { + m_inputBuffer.erase(m_cursorPos - 1, 1); + m_cursorPos--; + m_tabIndex = -1; + redrawInputLine(); + } +} + +void ServerConsole::deleteCharForward() +{ + if (m_cursorPos < (int)m_inputBuffer.size()) + { + m_inputBuffer.erase(m_cursorPos, 1); + m_tabIndex = -1; + redrawInputLine(); + } +} + +void ServerConsole::moveCursorLeft() +{ + if (m_cursorPos > 0) + { + m_cursorPos--; + redrawInputLine(); + } +} + +void ServerConsole::moveCursorRight() +{ + if (m_cursorPos < (int)m_inputBuffer.size()) + { + m_cursorPos++; + redrawInputLine(); + } +} + +void ServerConsole::moveCursorHome() +{ + m_cursorPos = 0; + redrawInputLine(); +} + +void ServerConsole::moveCursorEnd() +{ + m_cursorPos = (int)m_inputBuffer.size(); + redrawInputLine(); +} + +void ServerConsole::historyUp() +{ + if (m_history.empty()) return; + + if (m_historyIndex == -1) + { + m_savedInput = m_inputBuffer; + m_historyIndex = 0; + } + else if (m_historyIndex < (int)m_history.size() - 1) + { + m_historyIndex++; + } + else + { + return; // at oldest entry + } + + m_inputBuffer = m_history[m_historyIndex]; + m_cursorPos = (int)m_inputBuffer.size(); + m_tabIndex = -1; + redrawInputLine(); +} + +void ServerConsole::historyDown() +{ + if (m_historyIndex == -1) return; + + m_historyIndex--; + if (m_historyIndex < 0) + { + m_historyIndex = -1; + m_inputBuffer = m_savedInput; + } + else + { + m_inputBuffer = m_history[m_historyIndex]; + } + + m_cursorPos = (int)m_inputBuffer.size(); + m_tabIndex = -1; + redrawInputLine(); +} + +void ServerConsole::readInputLine(std::string& outLine) +{ + m_inputBuffer.clear(); + m_cursorPos = 0; + m_historyIndex = -1; + m_tabIndex = -1; + m_savedInput.clear(); + + printPrompt(); + + while (m_running) + { + INPUT_RECORD ir; + DWORD count = 0; + + if (!ReadConsoleInputA(m_hConsoleIn, &ir, 1, &count) || count == 0) + { + Sleep(10); + continue; + } + + if (ir.EventType != KEY_EVENT || !ir.Event.KeyEvent.bKeyDown) + continue; + + KEY_EVENT_RECORD& key = ir.Event.KeyEvent; + WORD vk = key.wVirtualKeyCode; + char ch = key.uChar.AsciiChar; + + if (vk == VK_RETURN) + { + printf("\r\n"); + fflush(stdout); + outLine = m_inputBuffer; + return; + } + else if (vk == VK_TAB) + { + handleTabCompletion(); + } + else if (vk == VK_BACK) + { + deleteCharBack(); + } + else if (vk == VK_DELETE) + { + deleteCharForward(); + } + else if (vk == VK_LEFT) + { + moveCursorLeft(); + } + else if (vk == VK_RIGHT) + { + moveCursorRight(); + } + else if (vk == VK_UP) + { + historyUp(); + } + else if (vk == VK_DOWN) + { + historyDown(); + } + else if (vk == VK_HOME) + { + moveCursorHome(); + } + else if (vk == VK_END) + { + moveCursorEnd(); + } + else if (vk == VK_ESCAPE) + { + m_inputBuffer.clear(); + m_cursorPos = 0; + m_tabIndex = -1; + redrawInputLine(); + } + else if (ch >= 32 && ch < 127) + { + insertChar(ch); + } + } + + outLine.clear(); +} + +void ServerConsole::run() +{ + enableVirtualTerminal(); + + printf("\n%s========================================%s\n", Color::BrightCyan, Color::Reset); + printf("%s Minecraft Legacy Console Server%s\n", Color::BrightWhite, Color::Reset); + printf("%s========================================%s\n", Color::BrightCyan, Color::Reset); + printf("Type %shelp%s for available commands.\n", Color::BrightGreen, Color::Reset); + printf("Use %sTab%s for auto-completion.\n\n", Color::BrightYellow, Color::Reset); + + while (m_running && !MinecraftServer::serverHalted()) + { + std::string line; + readInputLine(line); + + if (!m_running || MinecraftServer::serverHalted()) break; + + // Trim + size_t start = line.find_first_not_of(" \t"); + size_t end = line.find_last_not_of(" \t"); + if (start == std::string::npos) continue; + line = line.substr(start, end - start + 1); + + if (line.empty()) continue; + + // Add to history (avoid duplicates at front) + if (m_history.empty() || m_history.front() != line) + { + m_history.push_front(line); + if ((int)m_history.size() > MAX_HISTORY) + m_history.pop_back(); + } + + // Convert to wstring and send to server + wstring command = convStringToWstring(line); + + if (m_server != nullptr) + { + m_server->handleConsoleInput(command, m_server); + } + } +} + +#endif // _WINDOWS64 diff --git a/Minecraft.Client/ServerConsole.h b/Minecraft.Client/ServerConsole.h new file mode 100644 index 000000000..49385abfd --- /dev/null +++ b/Minecraft.Client/ServerConsole.h @@ -0,0 +1,92 @@ +#pragma once +#ifdef _WINDOWS64 + +#include +#include +#include +#include +#include + +class MinecraftServer; + +class ServerConsole +{ +public: + static const int MAX_HISTORY = 500; + static const int MAX_INPUT_LENGTH = 512; + + struct Color + { + static const char* Reset; + static const char* Red; + static const char* Green; + static const char* Yellow; + static const char* Blue; + static const char* Magenta; + static const char* Cyan; + static const char* White; + static const char* Gray; + static const char* BrightRed; + static const char* BrightGreen; + static const char* BrightYellow; + static const char* BrightBlue; + static const char* BrightMagenta; + static const char* BrightCyan; + static const char* BrightWhite; + }; + + ServerConsole(MinecraftServer* server); + ~ServerConsole(); + + void enableVirtualTerminal(); + + void run(); + static void logInfo(const char* fmt, ...); + static void logWarn(const char* fmt, ...); + static void logError(const char* fmt, ...); + static void logSuccess(const char* fmt, ...); + static void logCommand(const char* label, const char* message); + static void logInfo(const wchar_t* message); + static void logWarn(const wchar_t* message); + static void logError(const wchar_t* message); + + std::vector getCompletions(const std::string& partial); + + static ServerConsole* getInstance() { return s_instance; } + +private: + void readInputLine(std::string& outLine); + void redrawInputLine(); + void clearInputLine(); + void handleTabCompletion(); + void insertChar(char c); + void deleteCharBack(); + void deleteCharForward(); + void moveCursorLeft(); + void moveCursorRight(); + void moveCursorHome(); + void moveCursorEnd(); + void historyUp(); + void historyDown(); + std::string highlightCommand(const std::string& input); + void printPrompt(); + + MinecraftServer* m_server; + std::string m_inputBuffer; + int m_cursorPos; + std::deque m_history; + int m_historyIndex; + std::string m_savedInput; + + std::vector m_tabCompletions; + int m_tabIndex; + std::string m_tabPartial; + + HANDLE m_hConsoleIn; + HANDLE m_hConsoleOut; + bool m_running; + + static ServerConsole* s_instance; +}; + +#endif // _WINDOWS64 diff --git a/Minecraft.Client/Settings.cpp b/Minecraft.Client/Settings.cpp index 9d93e6cbe..e38a19b7e 100644 --- a/Minecraft.Client/Settings.cpp +++ b/Minecraft.Client/Settings.cpp @@ -124,4 +124,10 @@ void Settings::setBooleanAndSave(const wstring& key, bool value) { properties[key] = value ? L"true" : L"false"; saveProperties(); +} + +void Settings::setStringAndSave(const wstring& key, const wstring& value) +{ + properties[key] = value; + saveProperties(); } \ No newline at end of file diff --git a/Minecraft.Client/Settings.h b/Minecraft.Client/Settings.h index 4a3c130be..573d84d2c 100644 --- a/Minecraft.Client/Settings.h +++ b/Minecraft.Client/Settings.h @@ -18,4 +18,5 @@ class Settings int getInt(const wstring& key, int defaultValue); bool getBoolean(const wstring& key, bool defaultValue); void setBooleanAndSave(const wstring& key, bool value); + void setStringAndSave(const wstring& key, const wstring& value); }; \ No newline at end of file diff --git a/Minecraft.Client/TexturePackRepository.cpp b/Minecraft.Client/TexturePackRepository.cpp index ef926d78b..4c1ef8d1c 100644 --- a/Minecraft.Client/TexturePackRepository.cpp +++ b/Minecraft.Client/TexturePackRepository.cpp @@ -375,6 +375,11 @@ TexturePack *TexturePackRepository::addTexturePackFromDLC(DLCPack *dlcPack, DWOR cacheById[dwParentID] = newPack; #ifndef _CONTENT_PACKAGE +#ifdef _WINDOWS64 + extern bool g_Win64Verbose; + if (g_Win64Verbose) + { +#endif if(dlcPack->hasPurchasedFile(DLCManager::e_DLCType_TexturePack,L"")) { wprintf(L"Added new FULL DLCTexturePack: %ls - id=%d\n", dlcPack->getName().c_str(),dwParentID ); @@ -383,6 +388,9 @@ TexturePack *TexturePackRepository::addTexturePackFromDLC(DLCPack *dlcPack, DWOR { wprintf(L"Added new TRIAL DLCTexturePack: %ls - id=%d\n", dlcPack->getName().c_str(),dwParentID ); } +#ifdef _WINDOWS64 + } +#endif #endif } return newPack; diff --git a/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp b/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp index 28d295049..3b74950e0 100644 --- a/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp +++ b/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp @@ -65,6 +65,7 @@ char g_Win64MultiplayerIP[256] = "127.0.0.1"; bool g_Win64DedicatedServer = false; int g_Win64DedicatedServerPort = WIN64_NET_DEFAULT_PORT; char g_Win64DedicatedServerBindIP[256] = ""; +bool g_Win64Verbose = false; bool WinsockNetLayer::Initialize() { diff --git a/Minecraft.Client/Windows64/Network/WinsockNetLayer.h b/Minecraft.Client/Windows64/Network/WinsockNetLayer.h index c5d689752..9af9bc7a8 100644 --- a/Minecraft.Client/Windows64/Network/WinsockNetLayer.h +++ b/Minecraft.Client/Windows64/Network/WinsockNetLayer.h @@ -170,5 +170,6 @@ extern char g_Win64MultiplayerIP[256]; extern bool g_Win64DedicatedServer; extern int g_Win64DedicatedServerPort; extern char g_Win64DedicatedServerBindIP[256]; +extern bool g_Win64Verbose; #endif diff --git a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp index 70aeb22bf..dde434aa6 100644 --- a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp +++ b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp @@ -30,6 +30,7 @@ #include "..\..\Minecraft.World\ThreadName.h" #include "..\..\Minecraft.Client\StatsCounter.h" #include "..\ConnectScreen.h" +#include "..\ServerConsole.h" //#include "Social\SocialManager.h" //#include "Leaderboards\LeaderboardManager.h" //#include "XUI\XUI_Scene_Container.h" @@ -123,6 +124,7 @@ struct Win64LaunchOptions int screenMode; bool serverMode; bool fullscreen; + bool verbose; }; static void CopyWideArgToAnsi(LPCWSTR source, char* dest, size_t destSize) @@ -208,6 +210,7 @@ static Win64LaunchOptions ParseLaunchOptions() Win64LaunchOptions options = {}; options.screenMode = 0; options.serverMode = false; + options.verbose = false; g_Win64MultiplayerJoin = false; g_Win64MultiplayerPort = WIN64_NET_DEFAULT_PORT; @@ -271,6 +274,8 @@ static Win64LaunchOptions ParseLaunchOptions() } else if (_wcsicmp(argv[i], L"-fullscreen") == 0) options.fullscreen = true; + else if (_wcsicmp(argv[i], L"-v") == 0 || _wcsicmp(argv[i], L"-verbose") == 0) + options.verbose = true; } LocalFree(argv); @@ -1354,30 +1359,19 @@ static int HeadlessServerConsoleThreadProc(void* lpParameter) { UNREFERENCED_PARAMETER(lpParameter); - std::string line; + MinecraftServer* server = nullptr; while (!app.m_bShutdown) { - if (!std::getline(std::cin, line)) - { - if (std::cin.eof()) - { - break; - } - - std::cin.clear(); - Sleep(50); - continue; - } - - wstring command = trimString(convStringToWstring(line)); - if (command.empty()) - continue; - - MinecraftServer* server = MinecraftServer::getInstance(); + server = MinecraftServer::getInstance(); if (server != nullptr) - { - server->handleConsoleInput(command, server); - } + break; + Sleep(100); + } + + if (server != nullptr && !app.m_bShutdown) + { + ServerConsole console(server); + console.run(); } return 0; @@ -1564,6 +1558,7 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, // Load stuff from launch options, including username const Win64LaunchOptions launchOptions = ParseLaunchOptions(); + g_Win64Verbose = launchOptions.verbose; ApplyScreenMode(launchOptions.screenMode); // Ensure uid.dat exists from startup in client mode (before any multiplayer/login path). diff --git a/cmake/ClientSources.cmake b/cmake/ClientSources.cmake index 6467a243c..aebfc51dc 100644 --- a/cmake/ClientSources.cmake +++ b/cmake/ClientSources.cmake @@ -397,7 +397,9 @@ set(MINECRAFT_CLIENT_SOURCES "SelectWorldScreen.cpp" "ServerChunkCache.cpp" "ServerCommandDispatcher.cpp" + "ServerCommands.cpp" "ServerConnection.cpp" + "ServerConsole.cpp" "ServerLevel.cpp" "ServerLevelListener.cpp" "ServerPlayer.cpp" From 739d20764bfc861da84b83b4eec83147c009afc6 Mon Sep 17 00:00:00 2001 From: Matthew Toro Date: Tue, 10 Mar 2026 07:18:38 -0400 Subject: [PATCH 3/3] fix. --- Minecraft.Client/Minecraft.Client.vcxproj | 4 ++++ Minecraft.Client/Minecraft.Client.vcxproj.filters | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/Minecraft.Client/Minecraft.Client.vcxproj b/Minecraft.Client/Minecraft.Client.vcxproj index d97cbc383..41f765bb5 100644 --- a/Minecraft.Client/Minecraft.Client.vcxproj +++ b/Minecraft.Client/Minecraft.Client.vcxproj @@ -20728,7 +20728,9 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CU + + @@ -37956,7 +37958,9 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CU + + diff --git a/Minecraft.Client/Minecraft.Client.vcxproj.filters b/Minecraft.Client/Minecraft.Client.vcxproj.filters index 23b754fa9..410db0adf 100644 --- a/Minecraft.Client/Minecraft.Client.vcxproj.filters +++ b/Minecraft.Client/Minecraft.Client.vcxproj.filters @@ -1684,6 +1684,12 @@ Xbox\Source Files\XUI\Menu screens\Debug + + net\minecraft\server + + + net\minecraft\server + net\minecraft\server\network @@ -4254,6 +4260,12 @@ net\minecraft\server + + net\minecraft\server + + + net\minecraft\server + net\minecraft\server