feat: add smartcmd dedicated server

major bug: when connecting to the server a second time, it gets softlocked and keeps repeating ticking connection A\n in debug logs until it times out.
This commit is contained in:
2026-08-29 14:01:24 +03:00
parent a1541c590d
commit 4b962fcb21
146 changed files with 32837 additions and 272 deletions
+475 -1
View File
@@ -27,9 +27,19 @@
#include "..\Minecraft.World\Pos.h"
#include "..\Minecraft.World\System.h"
#include "..\Minecraft.World\StringHelpers.h"
#include "../Minecraft.World/net.minecraft.world.entity.item.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"
#ifdef SPLIT_SAVES
#include "..\Minecraft.World\ConsoleSaveFileSplit.h"
#endif
#if defined(_WINDOWS64) || defined(DISABLE_PSN) || defined(_DISABLE_XBLIVE)
#include "Common/Network/NetworkSocketLayer.h"
#endif
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
#include "../Minecraft.Server/ServerLogger.h"
#endif
#include "..\Minecraft.World\ConsoleSaveFileOriginal.h"
#include "..\Minecraft.World\Socket.h"
#include "..\Minecraft.World\net.minecraft.world.entity.h"
@@ -78,6 +88,462 @@ bool MinecraftServer::s_slowQueuePacketSent = false;
unordered_map<wstring, int> MinecraftServer::ironTimers;
static bool ShouldUseDedicatedServerProperties()
{
#if defined(MINECRAFT_SERVER_BUILD)
return true;
#else
return false;
#endif
}
static int GetDedicatedServerInt(Settings *settings, const wchar_t *key, int defaultValue)
{
return (ShouldUseDedicatedServerProperties() && settings != nullptr) ? settings->getInt(key, defaultValue) : defaultValue;
}
static bool GetDedicatedServerBool(Settings *settings, const wchar_t *key, bool defaultValue)
{
return (ShouldUseDedicatedServerProperties() && settings != nullptr) ? settings->getBoolean(key, defaultValue) : defaultValue;
}
static wstring GetDedicatedServerString(Settings *settings, const wchar_t *key, const wstring &defaultValue)
{
return (ShouldUseDedicatedServerProperties() && settings != nullptr) ? settings->getString(key, defaultValue) : defaultValue;
}
static void PrintConsoleLine(const wchar_t *prefix, const wstring &message)
{
wprintf(L"%ls%ls\n", prefix, message.c_str());
fflush(stdout);
}
static bool TryParseIntValue(const wstring &text, int &value)
{
std::wistringstream stream(text);
stream >> value;
return !stream.fail() && stream.eof();
}
static vector<wstring> SplitConsoleCommand(const wstring &command)
{
vector<wstring> tokens;
std::wistringstream stream(command);
wstring token;
while (stream >> token)
{
tokens.push_back(token);
}
return tokens;
}
static wstring JoinConsoleCommandTokens(const vector<wstring> &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;
}
static shared_ptr<ServerPlayer> FindPlayerByName(PlayerList *playerList, const wstring &name)
{
if (playerList == nullptr) return nullptr;
for (size_t i = 0; i < playerList->players.size(); ++i)
{
shared_ptr<ServerPlayer> player = playerList->players[i];
if (player != nullptr && equalsIgnoreCase(player->getName(), name))
{
return player;
}
}
return nullptr;
}
static void SetAllLevelTimes(MinecraftServer *server, int value)
{
for (unsigned int i = 0; i < server->levels.length; ++i)
{
if (server->levels[i] != nullptr)
{
server->levels[i]->setDayTime(value);
}
}
}
static bool ExecuteConsoleCommand(MinecraftServer *server, const wstring &rawCommand)
{
if (server == nullptr)
return false;
wstring command = trimString(rawCommand);
if (command.empty())
return true;
if (command[0] == L'/')
{
command = trimString(command.substr(1));
}
vector<wstring> tokens = SplitConsoleCommand(command);
if (tokens.empty())
return true;
const wstring action = toLower(tokens[0]);
PlayerList *playerList = server->getPlayers();
if (action == L"help" || action == L"?")
{
server->info(L"Commands: help, stop, list, say <message>, save-all, time <set day|night|ticks|add ticks>, weather <clear|rain|thunder> [seconds], tp <player> <target>, give <player> <itemId> [amount] [aux], enchant <player> <enchantId> [level], kill <player>");
return true;
}
if (action == L"stop")
{
server->info(L"Stopping server...");
MinecraftServer::HaltServer();
return true;
}
if (action == L"list")
{
wstring playerNames = (playerList != nullptr) ? playerList->getPlayerNames() : L"";
if (playerNames.empty()) playerNames = L"(none)";
server->info(L"Players (" + std::to_wstring((playerList != nullptr) ? playerList->getPlayerCount() : 0) + L"): " + playerNames);
return true;
}
if (action == L"say")
{
if (tokens.size() < 2)
{
server->warn(L"Usage: say <message>");
return false;
}
wstring message = L"[Server] " + JoinConsoleCommandTokens(tokens, 1);
if (playerList != nullptr)
{
playerList->broadcastAll(std::make_shared<ChatPacket>(message));
}
server->info(message);
return true;
}
if (action == L"save-all")
{
if (playerList != nullptr)
{
playerList->saveAll(nullptr, false);
}
server->info(L"World saved.");
return true;
}
if (action == L"time")
{
if (tokens.size() < 2)
{
server->warn(L"Usage: time set <day|night|ticks> | time add <ticks>");
return false;
}
if (toLower(tokens[1]) == L"add")
{
if (tokens.size() < 3)
{
server->warn(L"Usage: time add <ticks>");
return false;
}
int delta = 0;
if (!TryParseIntValue(tokens[2], delta))
{
server->warn(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);
}
}
server->info(L"Added " + std::to_wstring(delta) + L" ticks.");
return true;
}
wstring timeValue = toLower(tokens[1]);
if (timeValue == L"set")
{
if (tokens.size() < 3)
{
server->warn(L"Usage: time set <day|night|ticks>");
return false;
}
timeValue = toLower(tokens[2]);
}
int targetTime = 0;
if (timeValue == L"day")
{
targetTime = 0;
}
else if (timeValue == L"night")
{
targetTime = 12500;
}
else if (!TryParseIntValue(timeValue, targetTime))
{
server->warn(L"Invalid time value: " + timeValue);
return false;
}
SetAllLevelTimes(server, targetTime);
server->info(L"Time set to " + std::to_wstring(targetTime) + L".");
return true;
}
if (action == L"weather")
{
if (tokens.size() < 2)
{
server->warn(L"Usage: weather <clear|rain|thunder> [seconds]");
return false;
}
int durationSeconds = 600;
if (tokens.size() >= 3 && !TryParseIntValue(tokens[2], durationSeconds))
{
server->warn(L"Invalid duration: " + tokens[2]);
return false;
}
if (server->levels[0] == nullptr)
{
server->warn(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
{
server->warn(L"Usage: weather <clear|rain|thunder> [seconds]");
return false;
}
server->info(L"Weather set to " + weather + L".");
return true;
}
if (action == L"tp" || action == L"teleport")
{
if (tokens.size() < 3)
{
server->warn(L"Usage: tp <player> <target>");
return false;
}
shared_ptr<ServerPlayer> subject = FindPlayerByName(playerList, tokens[1]);
shared_ptr<ServerPlayer> destination = FindPlayerByName(playerList, tokens[2]);
if (subject == nullptr)
{
server->warn(L"Unknown player: " + tokens[1]);
return false;
}
if (destination == nullptr)
{
server->warn(L"Unknown player: " + tokens[2]);
return false;
}
if (subject->level->dimension->id != destination->level->dimension->id || !subject->isAlive())
{
server->warn(L"Teleport failed because the players are not in the same dimension or the source player is dead.");
return false;
}
subject->ride(nullptr);
subject->connection->teleport(destination->x, destination->y, destination->z, destination->yRot, destination->xRot);
server->info(L"Teleported " + subject->getName() + L" to " + destination->getName() + L".");
return true;
}
if (action == L"give")
{
if (tokens.size() < 3)
{
server->warn(L"Usage: give <player> <itemId> [amount] [aux]");
return false;
}
shared_ptr<ServerPlayer> player = FindPlayerByName(playerList, tokens[1]);
if (player == nullptr)
{
server->warn(L"Unknown player: " + tokens[1]);
return false;
}
int itemId = 0;
int amount = 1;
int aux = 0;
if (!TryParseIntValue(tokens[2], itemId))
{
server->warn(L"Invalid item id: " + tokens[2]);
return false;
}
if (tokens.size() >= 4 && !TryParseIntValue(tokens[3], amount))
{
server->warn(L"Invalid amount: " + tokens[3]);
return false;
}
if (tokens.size() >= 5 && !TryParseIntValue(tokens[4], aux))
{
server->warn(L"Invalid aux value: " + tokens[4]);
return false;
}
if (itemId <= 0 || Item::items[itemId] == nullptr)
{
server->warn(L"Unknown item id: " + std::to_wstring(itemId));
return false;
}
if (amount <= 0)
{
server->warn(L"Amount must be positive.");
return false;
}
shared_ptr<ItemInstance> itemInstance(new ItemInstance(itemId, amount, aux));
shared_ptr<ItemEntity> drop = player->drop(itemInstance);
if (drop != nullptr)
{
drop->throwTime = 0;
}
server->info(L"Gave item " + std::to_wstring(itemId) + L" x" + std::to_wstring(amount) + L" to " + player->getName() + L".");
return true;
}
if (action == L"enchant")
{
if (tokens.size() < 3)
{
server->warn(L"Usage: enchant <player> <enchantId> [level]");
return false;
}
shared_ptr<ServerPlayer> player = FindPlayerByName(playerList, tokens[1]);
if (player == nullptr)
{
server->warn(L"Unknown player: " + tokens[1]);
return false;
}
int enchantmentId = 0;
int enchantmentLevel = 1;
if (!TryParseIntValue(tokens[2], enchantmentId))
{
server->warn(L"Invalid enchantment id: " + tokens[2]);
return false;
}
if (tokens.size() >= 4 && !TryParseIntValue(tokens[3], enchantmentLevel))
{
server->warn(L"Invalid enchantment level: " + tokens[3]);
return false;
}
shared_ptr<ItemInstance> selectedItem = player->getSelectedItem();
if (selectedItem == nullptr)
{
server->warn(L"The player is not holding an item.");
return false;
}
Enchantment *enchantment = Enchantment::enchantments[enchantmentId];
if (enchantment == nullptr)
{
server->warn(L"Unknown enchantment id: " + std::to_wstring(enchantmentId));
return false;
}
if (!enchantment->canEnchant(selectedItem))
{
server->warn(L"That enchantment cannot be applied to the selected item.");
return false;
}
if (enchantmentLevel < enchantment->getMinLevel()) enchantmentLevel = enchantment->getMinLevel();
if (enchantmentLevel > enchantment->getMaxLevel()) enchantmentLevel = enchantment->getMaxLevel();
if (selectedItem->hasTag())
{
ListTag<CompoundTag> *enchantmentTags = selectedItem->getEnchantmentTags();
if (enchantmentTags != nullptr)
{
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))
{
server->warn(L"That enchantment conflicts with an existing enchantment on the selected item.");
return false;
}
}
}
}
selectedItem->enchant(enchantment, enchantmentLevel);
server->info(L"Enchanted " + player->getName() + L"'s held item with " + std::to_wstring(enchantmentId) + L" " + std::to_wstring(enchantmentLevel) + L".");
return true;
}
if (action == L"kill")
{
if (tokens.size() < 2)
{
server->warn(L"Usage: kill <player>");
return false;
}
shared_ptr<ServerPlayer> player = FindPlayerByName(playerList, tokens[1]);
if (player == nullptr)
{
server->warn(L"Unknown player: " + tokens[1]);
return false;
}
player->hurt(DamageSource::outOfWorld, 3.4e38f);
server->info(L"Killed " + player->getName() + L".");
return true;
}
server->warn(L"Unknown command: " + command);
return false;
}
MinecraftServer::MinecraftServer()
{
// 4J - added initialisers
@@ -105,6 +571,7 @@ MinecraftServer::MinecraftServer()
playerIdleTimeout = 0;
m_postUpdateThread = NULL;
forceGameType = false;
m_spawnProtectionRadius = 0;
commandDispatcher = new ServerCommandDispatcher();
@@ -149,6 +616,10 @@ bool MinecraftServer::initServer(__int64 seed, NetworkGameInitData *initData, DW
logger.info("Loading properties");
#endif
settings = new Settings(new File(L"server.properties"));
// Dedicated-only: spawn-protection radius in blocks; 0 disables protection.
m_spawnProtectionRadius = GetDedicatedServerInt(settings, L"spawn-protection", 0);
if (m_spawnProtectionRadius < 0) m_spawnProtectionRadius = 0;
if (m_spawnProtectionRadius > 256) m_spawnProtectionRadius = 256;
app.DebugPrintf("\n*** SERVER SETTINGS ***\n");
app.DebugPrintf("ServerSettings: host-friends-only is %s\n",(app.GetGameHostOption(eGameHostOption_FriendsOfFriends)>0)?"on":"off");
@@ -156,6 +627,7 @@ bool MinecraftServer::initServer(__int64 seed, NetworkGameInitData *initData, DW
app.DebugPrintf("ServerSettings: pvp is %s\n",(app.GetGameHostOption(eGameHostOption_PvP)>0)?"on":"off");
app.DebugPrintf("ServerSettings: fire spreads is %s\n",(app.GetGameHostOption(eGameHostOption_FireSpreads)>0)?"on":"off");
app.DebugPrintf("ServerSettings: tnt explodes is %s\n",(app.GetGameHostOption(eGameHostOption_TNT)>0)?"on":"off");
app.DebugPrintf("ServerSettings: spawn protection radius is %d\n", m_spawnProtectionRadius);
app.DebugPrintf("\n");
// TODO 4J Stu - Init a load of settings based on data passed as params
@@ -1172,7 +1644,9 @@ Level *MinecraftServer::getCommandSenderWorld()
int MinecraftServer::getSpawnProtectionRadius()
{
return 16;
// Client-host mode must never apply dedicated-server spawn protection settings.
if (!ShouldUseDedicatedServerProperties()) return 0;
return m_spawnProtectionRadius;
}
bool MinecraftServer::isUnderSpawnProtection(Level *level, int x, int y, int z, shared_ptr<Player> player)