Dedicated Server Software - Minecraft.Server.exe (#498)

* add: Dedicated Server implementation

- Introduced `ServerMain.cpp` for the dedicated server logic, handling command-line arguments, server initialization, and network management.
- Created `postbuild_server.ps1` script for post-build tasks, including copying necessary resources and DLLs for the dedicated server.
- Added `CopyServerAssets.cmake` to manage the copying of server assets during the build process, ensuring required files are available for the dedicated server.
- Defined project filters in `Minecraft.Server.vcxproj.filters` for better organization of server-related files.

* add: refactor world loader & add server properties

- Introduced ServerLogger for logging startup steps and world I/O operations.
- Implemented ServerProperties for loading and saving server configuration from `server.properties`.
- Added WorldManager to handle world loading and creation based on server properties.
- Updated ServerMain to integrate server properties loading and world management.
- Enhanced project files to include new source and header files for the server components.

* update: implement enhanced logging functionality with configurable log levels

* update: update keyboard and mouse input initialization 1dc8a005ed

* fix: change virtual screen resolution to 1920x1080(HD)

Since 31881af56936aeef38ff322b975fd0 , `skinHud.swf` for 720 is not included in `MediaWindows64.arc`,
the app crashes unless the virtual screen is set to HD.

* fix: dedicated server build settings for miniaudio migration and missing sources

- remove stale Windows64 Miles (mss64) link/copy references from server build
- add Common/Filesystem/Filesystem.cpp to Minecraft.Server.vcxproj
- add Windows64/PostProcesser.cpp to Minecraft.Server.vcxproj
- fix unresolved externals (PostProcesser::*, FileExists) in dedicated server build

* update: changed the virtual screen to 720p

Since the crash caused by the 720p `skinHud.swf` not being included in `MediaWindows64.arc` has been resolved, switching back to 720p to reduce resource usage.

* add: add Docker support for Dedicated Server

add with entrypoint and build scripts

* fix: add initial save for newly created worlds in dedicated server

on the server side, I fixed the behavior introduced after commit aadb511, where newly created worlds are intentionally not saved to disk immediately.

* update: add basically all configuration options that are implemented in the classes to `server.properties`

* update: add LAN advertising configuration for server.properties

LAN-Discovery, which isn’t needed in server mode and could potentially be a security risk, has also been disabled(only server mode).

* add: add implementing interactive command line using linenoise

- Integrated linenoise library for line editing and completion in the server console.
- Updated ServerLogger to handle external writes safely during logging.
- Modified ServerMain to initialize and manage the ServerCli for command input.
- The implementation is separate from everything else, so it doesn't affect anything else.
- The command input section and execution section are separated into threads.

* update: enhance command line completion with predictive hints

Like most command line tools, it highlights predictions in gray.

* add: implement `StringUtils` for string manipulation and refactor usages

Unified the scattered utility functions.

* fix: send DisconnectPacket on shutdown and fix Win64 recv-thread teardown race

Before this change, server/host shutdown closed sockets directly in
ServerConnection::stop(), which bypassed the normal disconnect flow.
As a result, clients could be dropped without receiving a proper
DisconnectPacket during stop/kill/world-close paths.

Also, WinsockNetLayer::Shutdown() could destroy synchronization objects
while host-side recv threads were still exiting, causing a crash in
RecvThreadProc (access violation on world close in host mode).

* fix: return client to menus when Win64 host connection drops

- Add client-side host disconnect handling in CPlatformNetworkManagerStub::DoWork() for _WINDOWS64.
- When in QNET_STATE_GAME_PLAY as a non-host and WinsockNetLayer::IsConnected() becomes false, trigger g_NetworkManager.HandleDisconnect(false) to enter the normal disconnect/UI flow.
- Use m_bLeaveGameOnTick as a one-shot guard to prevent repeated disconnect handling while the link remains down.
- Reset m_bLeaveGameOnTick on LeaveGame(), HostGame(), and JoinGame() to avoid stale state across sessions.

* update: converted Japanese comments to English

* add: create `Minecraft.Server` developer guide in English and Japanese

* update: add note about issue

* add: add `nlohmann/json` json lib

* add: add FileUtils

Moved file operations to `utils`.

* add: Dedicated Server BAN access manager with persistent player and IP bans

- add Access frontend that publishes thread-safe ban manager snapshots for dedicated server use
- add BanManager storage for banned-players.json and banned-ips.json with load/save/update flows
- add persistent player and IP ban checks during dedicated server connection handling
- add UTF-8 BOM-safe JSON parsing and shared file helpers backed by nlohmann/json
- add Unicode-safe ban file read/write and safer atomic replacement behavior on Windows
- add active-ban snapshot APIs and expiry-aware filtering for expires metadata
- add RAII-based dedicated access shutdown handling during server startup and teardown

* update: changed file read/write operations to use `FileUtils`.

- As a side effect, saving has become faster!

* fix: Re-added the source that had somehow disappeared.

* add: significantly improved the dedicated server logging system

- add ServerLogManager to Minecraft.Server as the single entry point for dedicated-server log output
- forward CMinecraftApp logger output to the server logger when running with g_Win64DedicatedServer
- add named network logs for incoming, accepted, rejected, and disconnected connections
- cache connection metadata by smallId so player name and remote IP remain available for disconnect logs
- keep Minecraft.Client changes minimal by using lightweight hook points and handling log orchestration on the server side

* fix: added the updated library source

* add: add `ban` and `pardon` commands for Player and IP

* fix: fix stop command shutdown process

add dedicated server shutdown request handling

* fix: fixed the save logic during server shutdown

Removed redundant repeated saves and eliminated the risks of async writes.

* update: added new sever files to Docker entrypoint

* fix: replace shutdown flag with atomic variable for thread safety

* update: update Dedicated Server developer guide

English is machine translated.
Please forgive me.

* update: check for the existence of `GameHDD` and create

* add: add Whitelist to Dedicated Server

* refactor: clean up and refactor the code

- unify duplicated implementations that were copied repeatedly
- update outdated patterns to more modern ones

* fix: include UI header (new update fix)

* fix: fix the detection range for excessive logging

`getHighestNonEmptyY()` returning `-1` occurs normally when the chunk is entirely air.
The caller (`Minecraft.World/LevelChunk.cpp:2400`) normalizes `-1` to `0`.

* update: add world size config to dedicated server properties

* update: update README add explanation of  `server.properties` & launch arguments

* update: add nightly release workflow for dedicated server and client builds to Actions

* fix: update name for workflow

* add random seed generation

* add: add Docker nightly workflow for Dedicated Server publish to GitHub Container Registry

* fix: ghost player when clients disconnect out of order

#4

* fix: fix 7zip option

* fix: fix Docker workflow for Dedicated Server artifact handling

* add: add no build Dedicated Server startup scripts and Docker Compose

* update: add README for Docker Dedicated Server setup with no local build

* refactor: refactor command path structure

As the number of commands has increased and become harder to navigate, each command has been organized into separate folders.

* update: support stream(file stdin) input mode for server CLI

Support for the stream (file stdin) required when attaching a tty to a Docker container on Linux.

* add: add new CLI Console Commands for Dedicated Server

Most of these commands are executed using the command dispatcher implemented on the `Minecraft.World` side. When registering them with the dispatcher, the sender uses a permission-enabled configuration that treats the CLI as a player.

- default game.
- enchant
- experience.
- give
- kill(currently, getting a permission error for some reason)
- time
- weather.
- update tp & gamemode command

* fix: change player map icon to random select

* update: increase the player limit

* add: restore the basic anti-cheat implementation and add spawn protection

Added the following anti-cheat measures and add spawn protection to `server.properties`.
- instant break
- speed
- reach

* fix: fix Docker image tag

---------

Co-authored-by: sylvessa <225480449+sylvessa@users.noreply.github.com>
This commit is contained in:
kuwa
2026-03-15 16:32:50 +09:00
committed by GitHub
parent 4d200a589d
commit f483074cd2
110 changed files with 38957 additions and 147 deletions

View File

@@ -0,0 +1,39 @@
#pragma once
#include <cerrno>
#include <cstdlib>
#include <limits>
#include <string>
namespace ServerRuntime
{
namespace CommandParsing
{
inline bool TryParseInt(const std::string &text, int *outValue)
{
if (outValue == nullptr || text.empty())
{
return false;
}
char *end = nullptr;
errno = 0;
const long parsedValue = std::strtol(text.c_str(), &end, 10);
if (end == text.c_str() || *end != '\0')
{
return false;
}
if (errno == ERANGE)
{
return false;
}
if (parsedValue < (std::numeric_limits<int>::min)() || parsedValue > (std::numeric_limits<int>::max)())
{
return false;
}
*outValue = static_cast<int>(parsedValue);
return true;
}
}
}

View File

@@ -0,0 +1,50 @@
#pragma once
#include <string>
#include <vector>
namespace ServerRuntime
{
class ServerCliEngine;
struct ServerCliParsedLine;
struct ServerCliCompletionContext;
/**
* **Command interface for server CLI**
*
* Implement this contract to add new commands without changing engine internals.
*/
class IServerCliCommand
{
public:
virtual ~IServerCliCommand() = default;
/** Primary command name */
virtual const char *Name() const = 0;
/** Optional aliases */
virtual std::vector<std::string> Aliases() const { return {}; }
/** Usage text for help */
virtual const char *Usage() const = 0;
/** Short command description*/
virtual const char *Description() const = 0;
/**
* **Execute command logic**
*
* Called after tokenization and command lookup.
*/
virtual bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) = 0;
/**
* **Provide argument completion candidates**
*
* Override when command-specific completion is needed.
*/
virtual void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
{
(void)context;
(void)engine;
(void)out;
}
};
}

View File

@@ -0,0 +1,171 @@
#include "stdafx.h"
#include "CliCommandBanIp.h"
#include "..\..\ServerCliEngine.h"
#include "..\..\ServerCliParser.h"
#include "..\..\..\Access\Access.h"
#include "..\..\..\Common\NetworkUtils.h"
#include "..\..\..\Common\StringUtils.h"
#include "..\..\..\ServerLogManager.h"
#include "..\..\..\..\Minecraft.Client\MinecraftServer.h"
#include "..\..\..\..\Minecraft.Client\PlayerConnection.h"
#include "..\..\..\..\Minecraft.Client\PlayerList.h"
#include "..\..\..\..\Minecraft.Client\ServerPlayer.h"
#include "..\..\..\..\Minecraft.World\Connection.h"
#include "..\..\..\..\Minecraft.World\DisconnectPacket.h"
namespace ServerRuntime
{
namespace
{
// The dedicated server keeps the accepted remote IP in ServerLogManager, keyed by connection smallId.
// It's a bit strange from a responsibility standpoint, so we'll need to implement it separately.
static bool TryGetPlayerRemoteIp(const std::shared_ptr<ServerPlayer> &player, std::string *outIp)
{
if (outIp == nullptr || player == nullptr || player->connection == nullptr || player->connection->connection == nullptr || player->connection->connection->getSocket() == nullptr)
{
return false;
}
const unsigned char smallId = player->connection->connection->getSocket()->getSmallId();
if (smallId == 0)
{
return false;
}
return ServerRuntime::ServerLogManager::TryGetConnectionRemoteIp(smallId, outIp);
}
// After persisting the ban, walk a snapshot of current players so every matching session is removed.
static int DisconnectPlayersByRemoteIp(const std::string &ip)
{
auto *server = MinecraftServer::getInstance();
if (server == nullptr || server->getPlayers() == nullptr)
{
return 0;
}
const std::string normalizedIp = NetworkUtils::NormalizeIpToken(ip);
const std::vector<std::shared_ptr<ServerPlayer>> playerSnapshot = server->getPlayers()->players;
int disconnectedCount = 0;
for (const auto &player : playerSnapshot)
{
std::string playerIp;
if (!TryGetPlayerRemoteIp(player, &playerIp))
{
continue;
}
if (NetworkUtils::NormalizeIpToken(playerIp) == normalizedIp)
{
if (player != nullptr && player->connection != nullptr)
{
player->connection->disconnect(DisconnectPacket::eDisconnect_Banned);
++disconnectedCount;
}
}
}
return disconnectedCount;
}
}
const char *CliCommandBanIp::Name() const
{
return "ban-ip";
}
const char *CliCommandBanIp::Usage() const
{
return "ban-ip <address|player> [reason ...]";
}
const char *CliCommandBanIp::Description() const
{
return "Ban an IP address or a player's current IP.";
}
/**
* Resolves either a literal IP or an online player's current IP, persists the ban, and disconnects every matching connection
* IPまたは接続中プレイヤーの現在IPをBANし一致する接続を切断する
*/
bool CliCommandBanIp::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
{
if (line.tokens.size() < 2)
{
engine->LogWarn("Usage: ban-ip <address|player> [reason ...]");
return false;
}
if (!ServerRuntime::Access::IsInitialized())
{
engine->LogWarn("Access manager is not initialized.");
return false;
}
const std::string targetToken = line.tokens[1];
std::string remoteIp;
// Match Java Edition behavior by accepting either a literal IP or an online player name.
const auto targetPlayer = engine->FindPlayerByNameUtf8(targetToken);
if (targetPlayer != nullptr)
{
if (!TryGetPlayerRemoteIp(targetPlayer, &remoteIp))
{
engine->LogWarn("Cannot ban that player's IP because no current remote IP is available.");
return false;
}
}
else if (NetworkUtils::IsIpLiteral(targetToken))
{
remoteIp = StringUtils::TrimAscii(targetToken);
}
else
{
engine->LogWarn("Unknown player or invalid IP address: " + targetToken);
return false;
}
// Refuse duplicate bans so operators get immediate feedback instead of rewriting the same entry.
if (ServerRuntime::Access::IsIpBanned(remoteIp))
{
engine->LogWarn("That IP address is already banned.");
return false;
}
ServerRuntime::Access::BanMetadata metadata = ServerRuntime::Access::BanManager::BuildDefaultMetadata("Console");
metadata.reason = StringUtils::JoinTokens(line.tokens, 2);
if (metadata.reason.empty())
{
metadata.reason = "Banned by an operator.";
}
// Publish the ban before disconnecting players so reconnect attempts are rejected immediately.
if (!ServerRuntime::Access::AddIpBan(remoteIp, metadata))
{
engine->LogError("Failed to write IP ban.");
return false;
}
const int disconnectedCount = DisconnectPlayersByRemoteIp(remoteIp);
// Report the resolved IP rather than the original token so player-name targets are explicit in the console.
engine->LogInfo("Banned IP address " + remoteIp + ".");
if (disconnectedCount > 0)
{
engine->LogInfo("Disconnected " + std::to_string(disconnectedCount) + " player(s) with that IP.");
}
return true;
}
/**
* Suggests online player names for the player-target form of the Java Edition command
* プレイヤー名指定時の補完候補を返す
*/
void CliCommandBanIp::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
{
if (context.currentTokenIndex == 1)
{
engine->SuggestPlayers(context.prefix, context.linePrefix, out);
}
}
}

View File

@@ -0,0 +1,19 @@
#pragma once
#include "..\IServerCliCommand.h"
namespace ServerRuntime
{
/**
* Applies a dedicated-server IP ban using Java Edition style syntax and Access-backed persistence
*/
class CliCommandBanIp : public IServerCliCommand
{
public:
virtual const char *Name() const;
virtual const char *Usage() const;
virtual const char *Description() const;
virtual bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine);
virtual void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const;
};
}

View File

@@ -0,0 +1,136 @@
#include "stdafx.h"
#include "CliCommandBanList.h"
#include "..\..\ServerCliEngine.h"
#include "..\..\ServerCliParser.h"
#include "..\..\..\Access\Access.h"
#include "..\..\..\Common\StringUtils.h"
#include <algorithm>
namespace ServerRuntime
{
namespace
{
static void AppendUniqueText(const std::string &text, std::vector<std::string> *out)
{
if (out == nullptr || text.empty())
{
return;
}
if (std::find(out->begin(), out->end(), text) == out->end())
{
out->push_back(text);
}
}
static bool CompareLowerAscii(const std::string &left, const std::string &right)
{
return StringUtils::ToLowerAscii(left) < StringUtils::ToLowerAscii(right);
}
static bool LogBannedPlayers(ServerCliEngine *engine)
{
std::vector<ServerRuntime::Access::BannedPlayerEntry> entries;
if (!ServerRuntime::Access::SnapshotBannedPlayers(&entries))
{
engine->LogError("Failed to read banned players.");
return false;
}
std::vector<std::string> names;
for (const auto &entry : entries)
{
AppendUniqueText(entry.name, &names);
}
std::sort(names.begin(), names.end(), CompareLowerAscii);
engine->LogInfo("There are " + std::to_string(names.size()) + " banned player(s).");
for (const auto &name : names)
{
engine->LogInfo(" " + name);
}
return true;
}
static bool LogBannedIps(ServerCliEngine *engine)
{
std::vector<ServerRuntime::Access::BannedIpEntry> entries;
if (!ServerRuntime::Access::SnapshotBannedIps(&entries))
{
engine->LogError("Failed to read banned IPs.");
return false;
}
std::vector<std::string> ips;
for (const auto &entry : entries)
{
AppendUniqueText(entry.ip, &ips);
}
std::sort(ips.begin(), ips.end(), CompareLowerAscii);
engine->LogInfo("There are " + std::to_string(ips.size()) + " banned IP(s).");
for (const auto &ip : ips)
{
engine->LogInfo(" " + ip);
}
return true;
}
static bool LogAllBans(ServerCliEngine *engine)
{
if (!LogBannedPlayers(engine))
{
return false;
}
// Always print the IP snapshot as well so ban-ip entries are visible from the same command output.
return LogBannedIps(engine);
}
}
const char *CliCommandBanList::Name() const
{
return "banlist";
}
const char *CliCommandBanList::Usage() const
{
return "banlist";
}
const char *CliCommandBanList::Description() const
{
return "List all banned players and IPs.";
}
/**
* Reads the current Access snapshots and always prints both banned players and banned IPs
* Access の一覧を読みプレイヤーBANとIP BANをまとめて表示する
*/
bool CliCommandBanList::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
{
if (line.tokens.size() > 1)
{
engine->LogWarn("Usage: banlist");
return false;
}
if (!ServerRuntime::Access::IsInitialized())
{
engine->LogWarn("Access manager is not initialized.");
return false;
}
return LogAllBans(engine);
}
void CliCommandBanList::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
{
(void)context;
(void)engine;
(void)out;
}
}

View File

@@ -0,0 +1,23 @@
#pragma once
#include "..\IServerCliCommand.h"
namespace ServerRuntime
{
/**
* **Ban List Command**
*
* Lists dedicated-server player bans and IP bans in a single command output
* 専用サーバーのプレイヤーBANとIP BANをまとめて表示する
*/
class CliCommandBanList : public IServerCliCommand
{
public:
virtual const char *Name() const;
virtual const char *Usage() const;
virtual const char *Description() const;
virtual bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine);
virtual void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const;
};
}

View File

@@ -0,0 +1,145 @@
#include "stdafx.h"
#include "CliCommandBan.h"
#include "..\..\ServerCliEngine.h"
#include "..\..\ServerCliParser.h"
#include "..\..\..\Access\Access.h"
#include "..\..\..\Common\StringUtils.h"
#include "..\..\..\..\Minecraft.Client\PlayerConnection.h"
#include "..\..\..\..\Minecraft.Client\ServerPlayer.h"
#include "..\..\..\..\Minecraft.World\DisconnectPacket.h"
#include <algorithm>
namespace ServerRuntime
{
namespace
{
static void AppendUniqueXuid(PlayerUID xuid, std::vector<PlayerUID> *out)
{
if (out == nullptr || xuid == INVALID_XUID)
{
return;
}
if (std::find(out->begin(), out->end(), xuid) == out->end())
{
out->push_back(xuid);
}
}
static void CollectPlayerBanXuids(const std::shared_ptr<ServerPlayer> &player, std::vector<PlayerUID> *out)
{
if (player == nullptr || out == nullptr)
{
return;
}
// Keep both identity variants because the dedicated server checks login and online XUIDs separately.
AppendUniqueXuid(player->getXuid(), out);
AppendUniqueXuid(player->getOnlineXuid(), out);
}
}
const char *CliCommandBan::Name() const
{
return "ban";
}
const char *CliCommandBan::Usage() const
{
return "ban <player> [reason ...]";
}
const char *CliCommandBan::Description() const
{
return "Ban an online player.";
}
/**
* Resolves the live player, writes one or more Access ban entries, and disconnects the target with the banned reason
* 対象プレイヤーを解決してBANを保存し切断する
*/
bool CliCommandBan::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
{
if (line.tokens.size() < 2)
{
engine->LogWarn("Usage: ban <player> [reason ...]");
return false;
}
if (!ServerRuntime::Access::IsInitialized())
{
engine->LogWarn("Access manager is not initialized.");
return false;
}
const auto target = engine->FindPlayerByNameUtf8(line.tokens[1]);
if (target == nullptr)
{
engine->LogWarn("Unknown player: " + line.tokens[1] + " (this server build can only ban players that are currently online).");
return false;
}
std::vector<PlayerUID> xuids;
CollectPlayerBanXuids(target, &xuids);
if (xuids.empty())
{
engine->LogWarn("Cannot ban that player because no valid XUID is available.");
return false;
}
const bool hasUnbannedIdentity = std::any_of(
xuids.begin(),
xuids.end(),
[](PlayerUID xuid) { return !ServerRuntime::Access::IsPlayerBanned(xuid); });
if (!hasUnbannedIdentity)
{
engine->LogWarn("That player is already banned.");
return false;
}
ServerRuntime::Access::BanMetadata metadata = ServerRuntime::Access::BanManager::BuildDefaultMetadata("Console");
metadata.reason = StringUtils::JoinTokens(line.tokens, 2);
if (metadata.reason.empty())
{
metadata.reason = "Banned by an operator.";
}
const std::string playerName = StringUtils::WideToUtf8(target->getName());
for (const auto xuid : xuids)
{
if (ServerRuntime::Access::IsPlayerBanned(xuid))
{
continue;
}
if (!ServerRuntime::Access::AddPlayerBan(xuid, playerName, metadata))
{
engine->LogError("Failed to write player ban.");
return false;
}
}
if (target->connection != nullptr)
{
target->connection->disconnect(DisconnectPacket::eDisconnect_Banned);
}
engine->LogInfo("Banned player " + playerName + ".");
return true;
}
/**
* Suggests currently connected player names for the Java-style player argument
* プレイヤー引数の補完候補を返す
*/
void CliCommandBan::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
{
if (context.currentTokenIndex == 1)
{
engine->SuggestPlayers(context.prefix, context.linePrefix, out);
}
}
}

View File

@@ -0,0 +1,20 @@
#pragma once
#include "..\IServerCliCommand.h"
namespace ServerRuntime
{
/**
* Applies a dedicated-server player ban using Java Edition style syntax and Access-backed persistence
* Java Edition 風の ban コマンドで永続プレイヤーBANを行う
*/
class CliCommandBan : public IServerCliCommand
{
public:
virtual const char *Name() const;
virtual const char *Usage() const;
virtual const char *Description() const;
virtual bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine);
virtual void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const;
};
}

View File

@@ -0,0 +1,117 @@
#include "stdafx.h"
#include "CliCommandDefaultGamemode.h"
#include "..\..\ServerCliEngine.h"
#include "..\..\ServerCliParser.h"
#include "..\..\..\..\Minecraft.Client\MinecraftServer.h"
#include "..\..\..\..\Minecraft.Client\PlayerList.h"
#include "..\..\..\..\Minecraft.Client\ServerLevel.h"
#include "..\..\..\..\Minecraft.Client\ServerPlayer.h"
#include "..\..\..\..\Minecraft.World\net.minecraft.world.level.storage.h"
namespace ServerRuntime
{
namespace
{
constexpr const char *kDefaultGamemodeUsage = "defaultgamemode <survival|creative|0|1>";
static std::string ModeLabel(GameType *mode)
{
if (mode == GameType::SURVIVAL)
{
return "survival";
}
if (mode == GameType::CREATIVE)
{
return "creative";
}
if (mode == GameType::ADVENTURE)
{
return "adventure";
}
return std::to_string(mode != nullptr ? mode->getId() : -1);
}
}
const char *CliCommandDefaultGamemode::Name() const
{
return "defaultgamemode";
}
const char *CliCommandDefaultGamemode::Usage() const
{
return kDefaultGamemodeUsage;
}
const char *CliCommandDefaultGamemode::Description() const
{
return "Set the default game mode (server-side implementation).";
}
bool CliCommandDefaultGamemode::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
{
if (line.tokens.size() != 2)
{
engine->LogWarn(std::string("Usage: ") + kDefaultGamemodeUsage);
return false;
}
GameType *mode = engine->ParseGamemode(line.tokens[1]);
if (mode == nullptr)
{
engine->LogWarn("Unknown gamemode: " + line.tokens[1]);
return false;
}
MinecraftServer *server = MinecraftServer::getInstance();
if (server == nullptr)
{
engine->LogWarn("MinecraftServer instance is not available.");
return false;
}
PlayerList *players = server->getPlayers();
if (players == nullptr)
{
engine->LogWarn("Player list is not available.");
return false;
}
players->setOverrideGameMode(mode);
for (unsigned int i = 0; i < server->levels.length; ++i)
{
ServerLevel *level = server->levels[i];
if (level != nullptr && level->getLevelData() != nullptr)
{
level->getLevelData()->setGameType(mode);
}
}
if (server->getForceGameType())
{
for (size_t i = 0; i < players->players.size(); ++i)
{
std::shared_ptr<ServerPlayer> player = players->players[i];
if (player != nullptr)
{
player->setGameMode(mode);
player->fallDistance = 0.0f;
}
}
}
engine->LogInfo("Default gamemode set to " + ModeLabel(mode) + ".");
return true;
}
void CliCommandDefaultGamemode::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
{
if (context.currentTokenIndex == 1)
{
engine->SuggestGamemodes(context.prefix, context.linePrefix, out);
}
}
}

View File

@@ -0,0 +1,16 @@
#pragma once
#include "..\IServerCliCommand.h"
namespace ServerRuntime
{
class CliCommandDefaultGamemode : public IServerCliCommand
{
public:
const char *Name() const override;
const char *Usage() const override;
const char *Description() const override;
bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) override;
void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const override;
};
}

View File

@@ -0,0 +1,87 @@
#include "stdafx.h"
#include "CliCommandEnchant.h"
#include "..\..\ServerCliEngine.h"
#include "..\..\ServerCliParser.h"
#include "..\CommandParsing.h"
#include "..\..\..\..\Minecraft.World\GameCommandPacket.h"
#include "..\..\..\..\Minecraft.World\EnchantItemCommand.h"
#include "..\..\..\..\Minecraft.World\net.minecraft.world.entity.player.h"
#include "..\..\..\..\Minecraft.Client\ServerPlayer.h"
namespace ServerRuntime
{
namespace
{
constexpr const char *kEnchantUsage = "enchant <player> <enchantId> [level]";
}
const char *CliCommandEnchant::Name() const
{
return "enchant";
}
const char *CliCommandEnchant::Usage() const
{
return kEnchantUsage;
}
const char *CliCommandEnchant::Description() const
{
return "Enchant held item via Minecraft.World command dispatcher.";
}
bool CliCommandEnchant::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
{
if (line.tokens.size() < 3 || line.tokens.size() > 4)
{
engine->LogWarn(std::string("Usage: ") + kEnchantUsage);
return false;
}
std::shared_ptr<ServerPlayer> target = engine->FindPlayerByNameUtf8(line.tokens[1]);
if (target == nullptr)
{
engine->LogWarn("Unknown player: " + line.tokens[1]);
return false;
}
int enchantmentId = 0;
int enchantmentLevel = 1;
if (!CommandParsing::TryParseInt(line.tokens[2], &enchantmentId))
{
engine->LogWarn("Invalid enchantment id: " + line.tokens[2]);
return false;
}
if (line.tokens.size() >= 4 && !CommandParsing::TryParseInt(line.tokens[3], &enchantmentLevel))
{
engine->LogWarn("Invalid enchantment level: " + line.tokens[3]);
return false;
}
std::shared_ptr<Player> player = std::dynamic_pointer_cast<Player>(target);
if (player == nullptr)
{
engine->LogWarn("Cannot resolve target player entity.");
return false;
}
std::shared_ptr<GameCommandPacket> packet = EnchantItemCommand::preparePacket(player, enchantmentId, enchantmentLevel);
if (packet == nullptr)
{
engine->LogError("Failed to build enchant command packet.");
return false;
}
return engine->DispatchWorldCommand(packet->command, packet->data);
}
void CliCommandEnchant::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
{
if (context.currentTokenIndex == 1)
{
engine->SuggestPlayers(context.prefix, context.linePrefix, out);
}
}
}

View File

@@ -0,0 +1,16 @@
#pragma once
#include "..\IServerCliCommand.h"
namespace ServerRuntime
{
class CliCommandEnchant : public IServerCliCommand
{
public:
const char *Name() const override;
const char *Usage() const override;
const char *Description() const override;
bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) override;
void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const override;
};
}

View File

@@ -0,0 +1,184 @@
#include "stdafx.h"
#include "CliCommandExperience.h"
#include "..\..\ServerCliEngine.h"
#include "..\..\ServerCliParser.h"
#include "..\CommandParsing.h"
#include "..\..\..\Common\StringUtils.h"
#include "..\..\..\..\Minecraft.Client\MinecraftServer.h"
#include "..\..\..\..\Minecraft.Client\PlayerList.h"
#include "..\..\..\..\Minecraft.Client\ServerPlayer.h"
#include <limits>
namespace ServerRuntime
{
namespace
{
constexpr const char *kExperienceUsage = "xp <amount>[L] [player]";
constexpr const char *kExperienceUsageWithPlayer = "xp <amount>[L] <player>";
struct ExperienceAmount
{
int amount = 0;
bool levels = false;
bool take = false;
};
static bool TryParseExperienceAmount(const std::string &token, ExperienceAmount *outValue)
{
if (outValue == nullptr || token.empty())
{
return false;
}
ExperienceAmount parsed;
std::string numericToken = token;
const char suffix = token[token.size() - 1];
if (suffix == 'l' || suffix == 'L')
{
parsed.levels = true;
numericToken = token.substr(0, token.size() - 1);
if (numericToken.empty())
{
return false;
}
}
int signedAmount = 0;
if (!CommandParsing::TryParseInt(numericToken, &signedAmount))
{
return false;
}
if (signedAmount == (std::numeric_limits<int>::min)())
{
return false;
}
parsed.take = signedAmount < 0;
parsed.amount = parsed.take ? -signedAmount : signedAmount;
*outValue = parsed;
return true;
}
static std::shared_ptr<ServerPlayer> ResolveTargetPlayer(const ServerCliParsedLine &line, ServerCliEngine *engine)
{
if (line.tokens.size() >= 3)
{
return engine->FindPlayerByNameUtf8(line.tokens[2]);
}
MinecraftServer *server = MinecraftServer::getInstance();
if (server == nullptr || server->getPlayers() == nullptr)
{
return nullptr;
}
PlayerList *players = server->getPlayers();
if (players->players.size() == 1 && players->players[0] != nullptr)
{
return players->players[0];
}
return nullptr;
}
}
const char *CliCommandExperience::Name() const
{
return "xp";
}
std::vector<std::string> CliCommandExperience::Aliases() const
{
return { "experience" };
}
const char *CliCommandExperience::Usage() const
{
return kExperienceUsage;
}
const char *CliCommandExperience::Description() const
{
return "Grant or remove experience (server-side implementation).";
}
bool CliCommandExperience::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
{
if (line.tokens.size() < 2 || line.tokens.size() > 3)
{
engine->LogWarn(std::string("Usage: ") + kExperienceUsage);
return false;
}
ExperienceAmount amount;
if (!TryParseExperienceAmount(line.tokens[1], &amount))
{
engine->LogWarn(std::string("Usage: ") + kExperienceUsage);
return false;
}
std::shared_ptr<ServerPlayer> target = ResolveTargetPlayer(line, engine);
if (target == nullptr)
{
if (line.tokens.size() >= 3)
{
engine->LogWarn("Unknown player: " + line.tokens[2]);
}
else
{
engine->LogWarn(std::string("Usage: ") + kExperienceUsageWithPlayer);
}
return false;
}
if (amount.levels)
{
target->giveExperienceLevels(amount.take ? -amount.amount : amount.amount);
if (amount.take)
{
engine->LogInfo("Removed " + std::to_string(amount.amount) + " level(s) from " + StringUtils::WideToUtf8(target->getName()) + ".");
}
else
{
engine->LogInfo("Added " + std::to_string(amount.amount) + " level(s) to " + StringUtils::WideToUtf8(target->getName()) + ".");
}
return true;
}
if (amount.take)
{
engine->LogWarn("Removing raw experience points is not supported. Use negative levels (example: xp -5L <player>).");
return false;
}
target->increaseXp(amount.amount);
engine->LogInfo("Added " + std::to_string(amount.amount) + " experience points to " + StringUtils::WideToUtf8(target->getName()) + ".");
return true;
}
void CliCommandExperience::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
{
if (context.currentTokenIndex == 1)
{
if (StringUtils::StartsWithIgnoreCase("10", context.prefix))
{
out->push_back(context.linePrefix + "10");
}
if (StringUtils::StartsWithIgnoreCase("10L", context.prefix))
{
out->push_back(context.linePrefix + "10L");
}
if (StringUtils::StartsWithIgnoreCase("-5L", context.prefix))
{
out->push_back(context.linePrefix + "-5L");
}
}
else if (context.currentTokenIndex == 2)
{
engine->SuggestPlayers(context.prefix, context.linePrefix, out);
}
}
}

View File

@@ -0,0 +1,17 @@
#pragma once
#include "..\IServerCliCommand.h"
namespace ServerRuntime
{
class CliCommandExperience : public IServerCliCommand
{
public:
const char *Name() const override;
std::vector<std::string> Aliases() const override;
const char *Usage() const override;
const char *Description() const override;
bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) override;
void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const override;
};
}

View File

@@ -0,0 +1,109 @@
#include "stdafx.h"
#include "CliCommandGamemode.h"
#include "..\..\ServerCliEngine.h"
#include "..\..\ServerCliParser.h"
#include "..\..\..\..\Minecraft.Client\MinecraftServer.h"
#include "..\..\..\..\Minecraft.Client\PlayerList.h"
#include "..\..\..\..\Minecraft.Client\ServerPlayer.h"
namespace ServerRuntime
{
namespace
{
constexpr const char *kGamemodeUsage = "gamemode <survival|creative|0|1> [player]";
constexpr const char *kGamemodeUsageWithPlayer = "gamemode <survival|creative|0|1> <player>";
}
const char *CliCommandGamemode::Name() const
{
return "gamemode";
}
std::vector<std::string> CliCommandGamemode::Aliases() const
{
return { "gm" };
}
const char *CliCommandGamemode::Usage() const
{
return kGamemodeUsage;
}
const char *CliCommandGamemode::Description() const
{
return "Set a player's game mode.";
}
bool CliCommandGamemode::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
{
if (line.tokens.size() < 2 || line.tokens.size() > 3)
{
engine->LogWarn(std::string("Usage: ") + kGamemodeUsage);
return false;
}
GameType *mode = engine->ParseGamemode(line.tokens[1]);
if (mode == nullptr)
{
engine->LogWarn("Unknown gamemode: " + line.tokens[1]);
return false;
}
std::shared_ptr<ServerPlayer> target = nullptr;
if (line.tokens.size() >= 3)
{
target = engine->FindPlayerByNameUtf8(line.tokens[2]);
if (target == nullptr)
{
engine->LogWarn("Unknown player: " + line.tokens[2]);
return false;
}
}
else
{
MinecraftServer *server = MinecraftServer::getInstance();
if (server == nullptr || server->getPlayers() == nullptr)
{
engine->LogWarn("Player list is not available.");
return false;
}
PlayerList *players = server->getPlayers();
if (players->players.size() != 1 || players->players[0] == nullptr)
{
engine->LogWarn(std::string("Usage: ") + kGamemodeUsageWithPlayer);
return false;
}
target = players->players[0];
}
target->setGameMode(mode);
target->fallDistance = 0.0f;
if (line.tokens.size() >= 3)
{
engine->LogInfo("Set " + line.tokens[2] + " gamemode to " + line.tokens[1] + ".");
}
else
{
engine->LogInfo("Set gamemode to " + line.tokens[1] + ".");
}
return true;
}
void CliCommandGamemode::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
{
if (context.currentTokenIndex == 1)
{
engine->SuggestGamemodes(context.prefix, context.linePrefix, out);
}
else if (context.currentTokenIndex == 2)
{
engine->SuggestPlayers(context.prefix, context.linePrefix, out);
}
}
}

View File

@@ -0,0 +1,18 @@
#pragma once
#include "..\IServerCliCommand.h"
namespace ServerRuntime
{
class CliCommandGamemode : public IServerCliCommand
{
public:
const char *Name() const override;
std::vector<std::string> Aliases() const override;
const char *Usage() const override;
const char *Description() const override;
bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) override;
void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const override;
};
}

View File

@@ -0,0 +1,103 @@
#include "stdafx.h"
#include "CliCommandGive.h"
#include "..\..\ServerCliEngine.h"
#include "..\..\ServerCliParser.h"
#include "..\CommandParsing.h"
#include "..\..\..\..\Minecraft.World\GameCommandPacket.h"
#include "..\..\..\..\Minecraft.World\GiveItemCommand.h"
#include "..\..\..\..\Minecraft.World\net.minecraft.world.entity.player.h"
#include "..\..\..\..\Minecraft.Client\ServerPlayer.h"
namespace ServerRuntime
{
namespace
{
constexpr const char *kGiveUsage = "give <player> <itemId> [amount] [aux]";
}
const char *CliCommandGive::Name() const
{
return "give";
}
const char *CliCommandGive::Usage() const
{
return kGiveUsage;
}
const char *CliCommandGive::Description() const
{
return "Give an item via Minecraft.World command dispatcher.";
}
bool CliCommandGive::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
{
if (line.tokens.size() < 3 || line.tokens.size() > 5)
{
engine->LogWarn(std::string("Usage: ") + kGiveUsage);
return false;
}
std::shared_ptr<ServerPlayer> target = engine->FindPlayerByNameUtf8(line.tokens[1]);
if (target == nullptr)
{
engine->LogWarn("Unknown player: " + line.tokens[1]);
return false;
}
int itemId = 0;
int amount = 1;
int aux = 0;
if (!CommandParsing::TryParseInt(line.tokens[2], &itemId))
{
engine->LogWarn("Invalid item id: " + line.tokens[2]);
return false;
}
if (itemId <= 0)
{
engine->LogWarn("Item id must be greater than 0.");
return false;
}
if (line.tokens.size() >= 4 && !CommandParsing::TryParseInt(line.tokens[3], &amount))
{
engine->LogWarn("Invalid amount: " + line.tokens[3]);
return false;
}
if (line.tokens.size() >= 5 && !CommandParsing::TryParseInt(line.tokens[4], &aux))
{
engine->LogWarn("Invalid aux value: " + line.tokens[4]);
return false;
}
if (amount <= 0)
{
engine->LogWarn("Amount must be greater than 0.");
return false;
}
std::shared_ptr<Player> player = std::dynamic_pointer_cast<Player>(target);
if (player == nullptr)
{
engine->LogWarn("Cannot resolve target player entity.");
return false;
}
std::shared_ptr<GameCommandPacket> packet = GiveItemCommand::preparePacket(player, itemId, amount, aux);
if (packet == nullptr)
{
engine->LogError("Failed to build give command packet.");
return false;
}
return engine->DispatchWorldCommand(packet->command, packet->data);
}
void CliCommandGive::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
{
if (context.currentTokenIndex == 1)
{
engine->SuggestPlayers(context.prefix, context.linePrefix, out);
}
}
}

View File

@@ -0,0 +1,16 @@
#pragma once
#include "..\IServerCliCommand.h"
namespace ServerRuntime
{
class CliCommandGive : public IServerCliCommand
{
public:
const char *Name() const override;
const char *Usage() const override;
const char *Description() const override;
bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) override;
void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const override;
};
}

View File

@@ -0,0 +1,46 @@
#include "stdafx.h"
#include "CliCommandHelp.h"
#include "..\..\ServerCliEngine.h"
#include "..\..\ServerCliRegistry.h"
namespace ServerRuntime
{
const char *CliCommandHelp::Name() const
{
return "help";
}
std::vector<std::string> CliCommandHelp::Aliases() const
{
return { "?" };
}
const char *CliCommandHelp::Usage() const
{
return "help";
}
const char *CliCommandHelp::Description() const
{
return "Show available server console commands.";
}
bool CliCommandHelp::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
{
(void)line;
const std::vector<std::unique_ptr<IServerCliCommand>> &commands = engine->Registry().Commands();
engine->LogInfo("Available commands:");
for (size_t i = 0; i < commands.size(); ++i)
{
std::string row = " ";
row += commands[i]->Usage();
row += " - ";
row += commands[i]->Description();
engine->LogInfo(row);
}
return true;
}
}

View File

@@ -0,0 +1,17 @@
#pragma once
#include "..\IServerCliCommand.h"
namespace ServerRuntime
{
class CliCommandHelp : public IServerCliCommand
{
public:
virtual const char *Name() const;
virtual std::vector<std::string> Aliases() const;
virtual const char *Usage() const;
virtual const char *Description() const;
virtual bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine);
};
}

View File

@@ -0,0 +1,64 @@
#include "stdafx.h"
#include "CliCommandKill.h"
#include "..\..\ServerCliEngine.h"
#include "..\..\ServerCliParser.h"
#include "..\..\..\..\Minecraft.World\CommandSender.h"
#include "..\..\..\..\Minecraft.Client\ServerPlayer.h"
namespace ServerRuntime
{
namespace
{
constexpr const char *kKillUsage = "kill <player>";
}
const char *CliCommandKill::Name() const
{
return "kill";
}
const char *CliCommandKill::Usage() const
{
return kKillUsage;
}
const char *CliCommandKill::Description() const
{
return "Kill a player via Minecraft.World command dispatcher.";
}
bool CliCommandKill::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
{
if (line.tokens.size() != 2)
{
engine->LogWarn(std::string("Usage: ") + kKillUsage);
return false;
}
std::shared_ptr<ServerPlayer> target = engine->FindPlayerByNameUtf8(line.tokens[1]);
if (target == nullptr)
{
engine->LogWarn("Unknown player: " + line.tokens[1]);
return false;
}
std::shared_ptr<CommandSender> sender = std::dynamic_pointer_cast<CommandSender>(target);
if (sender == nullptr)
{
engine->LogWarn("Cannot resolve target command sender.");
return false;
}
return engine->DispatchWorldCommand(eGameCommand_Kill, byteArray(), sender);
}
void CliCommandKill::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
{
if (context.currentTokenIndex == 1)
{
engine->SuggestPlayers(context.prefix, context.linePrefix, out);
}
}
}

View File

@@ -0,0 +1,16 @@
#pragma once
#include "..\IServerCliCommand.h"
namespace ServerRuntime
{
class CliCommandKill : public IServerCliCommand
{
public:
const char *Name() const override;
const char *Usage() const override;
const char *Description() const override;
bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) override;
void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const override;
};
}

View File

@@ -0,0 +1,49 @@
#include "stdafx.h"
#include "CliCommandList.h"
#include "..\..\ServerCliEngine.h"
#include "..\..\..\Common\StringUtils.h"
#include "..\..\..\..\Minecraft.Client\MinecraftServer.h"
#include "..\..\..\..\Minecraft.Client\PlayerList.h"
namespace ServerRuntime
{
const char *CliCommandList::Name() const
{
return "list";
}
const char *CliCommandList::Usage() const
{
return "list";
}
const char *CliCommandList::Description() const
{
return "List connected players.";
}
bool CliCommandList::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
{
(void)line;
MinecraftServer *server = MinecraftServer::getInstance();
if (server == NULL || server->getPlayers() == NULL)
{
engine->LogWarn("Player list is not available.");
return false;
}
PlayerList *players = server->getPlayers();
std::string names = StringUtils::WideToUtf8(players->getPlayerNames());
if (names.empty())
{
names = "(none)";
}
engine->LogInfo("Players (" + std::to_string(players->getPlayerCount()) + "): " + names);
return true;
}
}

View File

@@ -0,0 +1,16 @@
#pragma once
#include "..\IServerCliCommand.h"
namespace ServerRuntime
{
class CliCommandList : public IServerCliCommand
{
public:
virtual const char *Name() const;
virtual const char *Usage() const;
virtual const char *Description() const;
virtual bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine);
};
}

View File

@@ -0,0 +1,98 @@
#include "stdafx.h"
#include "CliCommandPardonIp.h"
#include "..\..\ServerCliEngine.h"
#include "..\..\ServerCliParser.h"
#include "..\..\..\Access\Access.h"
#include "..\..\..\Common\NetworkUtils.h"
#include "..\..\..\Common\StringUtils.h"
namespace ServerRuntime
{
const char *CliCommandPardonIp::Name() const
{
return "pardon-ip";
}
const char *CliCommandPardonIp::Usage() const
{
return "pardon-ip <address>";
}
const char *CliCommandPardonIp::Description() const
{
return "Remove an IP ban.";
}
/**
* Validates the literal IP argument and removes the matching Access IP ban entry
* リテラルIPを検証して一致するIP BANを解除する
*/
bool CliCommandPardonIp::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
{
if (line.tokens.size() != 2)
{
engine->LogWarn("Usage: pardon-ip <address>");
return false;
}
if (!ServerRuntime::Access::IsInitialized())
{
engine->LogWarn("Access manager is not initialized.");
return false;
}
// Java Edition pardon-ip only operates on a literal address, so do not resolve player names here.
const std::string ip = StringUtils::TrimAscii(line.tokens[1]);
if (!NetworkUtils::IsIpLiteral(ip))
{
engine->LogWarn("Invalid IP address: " + line.tokens[1]);
return false;
}
// Distinguish invalid input from a valid but currently unbanned address for clearer operator feedback.
if (!ServerRuntime::Access::IsIpBanned(ip))
{
engine->LogWarn("That IP address is not banned.");
return false;
}
if (!ServerRuntime::Access::RemoveIpBan(ip))
{
engine->LogError("Failed to remove IP ban.");
return false;
}
engine->LogInfo("Unbanned IP address " + ip + ".");
return true;
}
/**
* Suggests currently banned IP addresses for the Java Edition literal-IP argument
* BAN済みIPの補完候補を返す
*/
void CliCommandPardonIp::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
{
(void)engine;
// Complete from the persisted IP-ban snapshot because this command only accepts already-banned literals.
if (context.currentTokenIndex != 1 || out == nullptr)
{
return;
}
std::vector<ServerRuntime::Access::BannedIpEntry> entries;
if (!ServerRuntime::Access::SnapshotBannedIps(&entries))
{
return;
}
// Reuse the normalized prefix match used by other commands so completion stays case-insensitive.
for (const auto &entry : entries)
{
const std::string &candidate = entry.ip;
if (StringUtils::StartsWithIgnoreCase(candidate, context.prefix))
{
out->push_back(context.linePrefix + candidate);
}
}
}
}

View File

@@ -0,0 +1,19 @@
#pragma once
#include "..\IServerCliCommand.h"
namespace ServerRuntime
{
/**
* Removes a dedicated-server IP ban using Java Edition style syntax and Access-backed persistence
*/
class CliCommandPardonIp : public IServerCliCommand
{
public:
virtual const char *Name() const;
virtual const char *Usage() const;
virtual const char *Description() const;
virtual bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine);
virtual void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const;
};
}

View File

@@ -0,0 +1,173 @@
#include "stdafx.h"
#include "CliCommandPardon.h"
#include "..\..\ServerCliEngine.h"
#include "..\..\ServerCliParser.h"
#include "..\..\..\Access\Access.h"
#include "..\..\..\Common\StringUtils.h"
#include "..\..\..\..\Minecraft.Client\ServerPlayer.h"
#include <algorithm>
namespace ServerRuntime
{
namespace
{
static void AppendUniqueText(const std::string &text, std::vector<std::string> *out)
{
if (out == nullptr || text.empty())
{
return;
}
if (std::find(out->begin(), out->end(), text) == out->end())
{
out->push_back(text);
}
}
static void AppendUniqueXuid(PlayerUID xuid, std::vector<PlayerUID> *out)
{
if (out == nullptr || xuid == INVALID_XUID)
{
return;
}
if (std::find(out->begin(), out->end(), xuid) == out->end())
{
out->push_back(xuid);
}
}
}
const char *CliCommandPardon::Name() const
{
return "pardon";
}
const char *CliCommandPardon::Usage() const
{
return "pardon <player>";
}
const char *CliCommandPardon::Description() const
{
return "Remove a player ban.";
}
/**
* Removes every Access ban entry that matches the requested player name so dual-XUID entries are cleared together
* 名前に一致するBANをまとめて解除する
*/
bool CliCommandPardon::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
{
if (line.tokens.size() != 2)
{
engine->LogWarn("Usage: pardon <player>");
return false;
}
if (!ServerRuntime::Access::IsInitialized())
{
engine->LogWarn("Access manager is not initialized.");
return false;
}
std::vector<PlayerUID> xuidsToRemove;
std::vector<std::string> matchedNames;
std::shared_ptr<ServerPlayer> onlineTarget = engine->FindPlayerByNameUtf8(line.tokens[1]);
if (onlineTarget != nullptr)
{
if (ServerRuntime::Access::IsPlayerBanned(onlineTarget->getXuid()))
{
AppendUniqueXuid(onlineTarget->getXuid(), &xuidsToRemove);
}
if (ServerRuntime::Access::IsPlayerBanned(onlineTarget->getOnlineXuid()))
{
AppendUniqueXuid(onlineTarget->getOnlineXuid(), &xuidsToRemove);
}
}
std::vector<ServerRuntime::Access::BannedPlayerEntry> entries;
if (!ServerRuntime::Access::SnapshotBannedPlayers(&entries))
{
engine->LogError("Failed to read banned players.");
return false;
}
const std::string loweredTarget = StringUtils::ToLowerAscii(line.tokens[1]);
for (const auto &entry : entries)
{
if (StringUtils::ToLowerAscii(entry.name) == loweredTarget)
{
PlayerUID parsedXuid = INVALID_XUID;
if (ServerRuntime::Access::TryParseXuid(entry.xuid, &parsedXuid))
{
AppendUniqueXuid(parsedXuid, &xuidsToRemove);
}
AppendUniqueText(entry.name, &matchedNames);
}
}
if (xuidsToRemove.empty())
{
engine->LogWarn("That player is not banned.");
return false;
}
for (const auto xuid : xuidsToRemove)
{
if (!ServerRuntime::Access::RemovePlayerBan(xuid))
{
engine->LogError("Failed to remove player ban.");
return false;
}
}
std::string playerName = line.tokens[1];
if (!matchedNames.empty())
{
playerName = matchedNames[0];
}
else if (onlineTarget != nullptr)
{
playerName = StringUtils::WideToUtf8(onlineTarget->getName());
}
engine->LogInfo("Unbanned player " + playerName + ".");
return true;
}
/**
* Suggests currently banned player names first and then online names for convenience
* BAN済み名とオンライン名を補完候補に出す
*/
void CliCommandPardon::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
{
if (context.currentTokenIndex != 1 || out == nullptr)
{
return;
}
std::vector<ServerRuntime::Access::BannedPlayerEntry> entries;
if (ServerRuntime::Access::SnapshotBannedPlayers(&entries))
{
std::vector<std::string> names;
for (const auto &entry : entries)
{
AppendUniqueText(entry.name, &names);
}
for (const auto &name : names)
{
if (StringUtils::StartsWithIgnoreCase(name, context.prefix))
{
out->push_back(context.linePrefix + name);
}
}
}
engine->SuggestPlayers(context.prefix, context.linePrefix, out);
}
}

View File

@@ -0,0 +1,19 @@
#pragma once
#include "..\IServerCliCommand.h"
namespace ServerRuntime
{
/**
* Removes dedicated-server player bans using Java Edition style syntax and Access-backed persistence
*/
class CliCommandPardon : public IServerCliCommand
{
public:
virtual const char *Name() const;
virtual const char *Usage() const;
virtual const char *Description() const;
virtual bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine);
virtual void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const;
};
}

View File

@@ -0,0 +1,32 @@
#include "stdafx.h"
#include "CliCommandStop.h"
#include "..\..\ServerCliEngine.h"
namespace ServerRuntime
{
const char *CliCommandStop::Name() const
{
return "stop";
}
const char *CliCommandStop::Usage() const
{
return "stop";
}
const char *CliCommandStop::Description() const
{
return "Stop the dedicated server.";
}
bool CliCommandStop::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
{
(void)line;
engine->LogInfo("Stopping server...");
engine->RequestShutdown();
return true;
}
}

View File

@@ -0,0 +1,16 @@
#pragma once
#include "..\IServerCliCommand.h"
namespace ServerRuntime
{
class CliCommandStop : public IServerCliCommand
{
public:
virtual const char *Name() const;
virtual const char *Usage() const;
virtual const char *Description() const;
virtual bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine);
};
}

View File

@@ -0,0 +1,118 @@
#include "stdafx.h"
#include "CliCommandTime.h"
#include "..\..\ServerCliEngine.h"
#include "..\..\ServerCliParser.h"
#include "..\..\..\Common\StringUtils.h"
#include "..\..\..\..\Minecraft.World\GameCommandPacket.h"
#include "..\..\..\..\Minecraft.World\TimeCommand.h"
namespace ServerRuntime
{
namespace
{
constexpr const char *kTimeUsage = "time <day|night|set day|set night>";
static bool TryResolveNightFlag(const std::vector<std::string> &tokens, bool *outNight)
{
if (outNight == nullptr)
{
return false;
}
std::string value;
if (tokens.size() == 2)
{
value = StringUtils::ToLowerAscii(tokens[1]);
}
else if (tokens.size() == 3 && StringUtils::ToLowerAscii(tokens[1]) == "set")
{
value = StringUtils::ToLowerAscii(tokens[2]);
}
else
{
return false;
}
if (value == "day")
{
*outNight = false;
return true;
}
if (value == "night")
{
*outNight = true;
return true;
}
return false;
}
static void SuggestLiteral(const char *candidate, const ServerCliCompletionContext &context, std::vector<std::string> *out)
{
if (candidate == nullptr || out == nullptr)
{
return;
}
const std::string text(candidate);
if (StringUtils::StartsWithIgnoreCase(text, context.prefix))
{
out->push_back(context.linePrefix + text);
}
}
}
const char *CliCommandTime::Name() const
{
return "time";
}
const char *CliCommandTime::Usage() const
{
return kTimeUsage;
}
const char *CliCommandTime::Description() const
{
return "Set day or night via Minecraft.World command dispatcher.";
}
bool CliCommandTime::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
{
bool night = false;
if (!TryResolveNightFlag(line.tokens, &night))
{
engine->LogWarn(std::string("Usage: ") + kTimeUsage);
return false;
}
std::shared_ptr<GameCommandPacket> packet = TimeCommand::preparePacket(night);
if (packet == nullptr)
{
engine->LogError("Failed to build time command packet.");
return false;
}
return engine->DispatchWorldCommand(packet->command, packet->data);
}
void CliCommandTime::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
{
(void)engine;
if (context.currentTokenIndex == 1)
{
SuggestLiteral("day", context, out);
SuggestLiteral("night", context, out);
SuggestLiteral("set", context, out);
}
else if (context.currentTokenIndex == 2 &&
context.parsed.tokens.size() >= 2 &&
StringUtils::ToLowerAscii(context.parsed.tokens[1]) == "set")
{
SuggestLiteral("day", context, out);
SuggestLiteral("night", context, out);
}
}
}

View File

@@ -0,0 +1,16 @@
#pragma once
#include "..\IServerCliCommand.h"
namespace ServerRuntime
{
class CliCommandTime : public IServerCliCommand
{
public:
const char *Name() const override;
const char *Usage() const override;
const char *Description() const override;
bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) override;
void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const override;
};
}

View File

@@ -0,0 +1,82 @@
#include "stdafx.h"
#include "CliCommandTp.h"
#include "..\..\ServerCliEngine.h"
#include "..\..\ServerCliParser.h"
#include "..\..\..\..\Minecraft.Client\PlayerConnection.h"
#include "..\..\..\..\Minecraft.Client\TeleportCommand.h"
#include "..\..\..\..\Minecraft.Client\ServerPlayer.h"
#include "..\..\..\..\Minecraft.World\GameCommandPacket.h"
namespace ServerRuntime
{
namespace
{
constexpr const char *kTpUsage = "tp <player> <target>";
}
const char *CliCommandTp::Name() const
{
return "tp";
}
std::vector<std::string> CliCommandTp::Aliases() const
{
return { "teleport" };
}
const char *CliCommandTp::Usage() const
{
return kTpUsage;
}
const char *CliCommandTp::Description() const
{
return "Teleport one player to another via Minecraft.World command dispatcher.";
}
bool CliCommandTp::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
{
if (line.tokens.size() != 3)
{
engine->LogWarn(std::string("Usage: ") + kTpUsage);
return false;
}
std::shared_ptr<ServerPlayer> subject = engine->FindPlayerByNameUtf8(line.tokens[1]);
std::shared_ptr<ServerPlayer> destination = engine->FindPlayerByNameUtf8(line.tokens[2]);
if (subject == nullptr)
{
engine->LogWarn("Unknown player: " + line.tokens[1]);
return false;
}
if (destination == nullptr)
{
engine->LogWarn("Unknown player: " + line.tokens[2]);
return false;
}
if (subject->connection == nullptr)
{
engine->LogWarn("Cannot teleport because source player connection is inactive.");
return false;
}
std::shared_ptr<GameCommandPacket> packet = TeleportCommand::preparePacket(subject->getXuid(), destination->getXuid());
if (packet == nullptr)
{
engine->LogError("Failed to build teleport command packet.");
return false;
}
return engine->DispatchWorldCommand(packet->command, packet->data);
}
void CliCommandTp::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
{
if (context.currentTokenIndex == 1 || context.currentTokenIndex == 2)
{
engine->SuggestPlayers(context.prefix, context.linePrefix, out);
}
}
}

View File

@@ -0,0 +1,18 @@
#pragma once
#include "..\IServerCliCommand.h"
namespace ServerRuntime
{
class CliCommandTp : public IServerCliCommand
{
public:
const char *Name() const override;
std::vector<std::string> Aliases() const override;
const char *Usage() const override;
const char *Description() const override;
bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) override;
void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const override;
};
}

View File

@@ -0,0 +1,49 @@
#include "stdafx.h"
#include "CliCommandWeather.h"
#include "..\..\ServerCliEngine.h"
#include "..\..\ServerCliParser.h"
#include "..\..\..\..\Minecraft.World\GameCommandPacket.h"
#include "..\..\..\..\Minecraft.World\ToggleDownfallCommand.h"
namespace ServerRuntime
{
namespace
{
constexpr const char *kWeatherUsage = "weather";
}
const char *CliCommandWeather::Name() const
{
return "weather";
}
const char *CliCommandWeather::Usage() const
{
return kWeatherUsage;
}
const char *CliCommandWeather::Description() const
{
return "Toggle weather via Minecraft.World command dispatcher.";
}
bool CliCommandWeather::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
{
if (line.tokens.size() != 1)
{
engine->LogWarn(std::string("Usage: ") + kWeatherUsage);
return false;
}
std::shared_ptr<GameCommandPacket> packet = ToggleDownfallCommand::preparePacket();
if (packet == nullptr)
{
engine->LogError("Failed to build weather command packet.");
return false;
}
return engine->DispatchWorldCommand(packet->command, packet->data);
}
}

View File

@@ -0,0 +1,15 @@
#pragma once
#include "..\IServerCliCommand.h"
namespace ServerRuntime
{
class CliCommandWeather : public IServerCliCommand
{
public:
const char *Name() const override;
const char *Usage() const override;
const char *Description() const override;
bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) override;
};
}

View File

@@ -0,0 +1,285 @@
#include "stdafx.h"
#include "CliCommandWhitelist.h"
#include "..\..\ServerCliEngine.h"
#include "..\..\ServerCliParser.h"
#include "..\..\..\Access\Access.h"
#include "..\..\..\Common\StringUtils.h"
#include "..\..\..\ServerProperties.h"
#include <algorithm>
#include <array>
namespace ServerRuntime
{
namespace
{
static const char *kWhitelistUsage = "whitelist <on|off|list|add|remove|reload> [...]";
static bool CompareWhitelistEntries(const ServerRuntime::Access::WhitelistedPlayerEntry &left, const ServerRuntime::Access::WhitelistedPlayerEntry &right)
{
const auto leftName = StringUtils::ToLowerAscii(left.name);
const auto rightName = StringUtils::ToLowerAscii(right.name);
if (leftName != rightName)
{
return leftName < rightName;
}
return StringUtils::ToLowerAscii(left.xuid) < StringUtils::ToLowerAscii(right.xuid);
}
static bool PersistWhitelistToggle(bool enabled)
{
auto config = LoadServerPropertiesConfig();
config.whiteListEnabled = enabled;
return SaveServerPropertiesConfig(config);
}
static std::string BuildWhitelistEntryRow(const ServerRuntime::Access::WhitelistedPlayerEntry &entry)
{
std::string row = " ";
row += entry.xuid;
if (!entry.name.empty())
{
row += " - ";
row += entry.name;
}
return row;
}
static void LogWhitelistMode(ServerCliEngine *engine)
{
engine->LogInfo(std::string("Whitelist is ") + (ServerRuntime::Access::IsWhitelistEnabled() ? "enabled." : "disabled."));
}
static bool LogWhitelistEntries(ServerCliEngine *engine)
{
std::vector<ServerRuntime::Access::WhitelistedPlayerEntry> entries;
if (!ServerRuntime::Access::SnapshotWhitelistedPlayers(&entries))
{
engine->LogError("Failed to read whitelist entries.");
return false;
}
std::sort(entries.begin(), entries.end(), CompareWhitelistEntries);
LogWhitelistMode(engine);
engine->LogInfo("There are " + std::to_string(entries.size()) + " whitelisted player(s).");
for (const auto &entry : entries)
{
engine->LogInfo(BuildWhitelistEntryRow(entry));
}
return true;
}
static bool TryParseWhitelistXuid(const std::string &text, ServerCliEngine *engine, PlayerUID *outXuid)
{
if (ServerRuntime::Access::TryParseXuid(text, outXuid))
{
return true;
}
engine->LogWarn("Invalid XUID: " + text);
return false;
}
static void SuggestLiteral(const std::string &candidate, const ServerCliCompletionContext &context, std::vector<std::string> *out)
{
if (out == nullptr)
{
return;
}
if (StringUtils::StartsWithIgnoreCase(candidate, context.prefix))
{
out->push_back(context.linePrefix + candidate);
}
}
}
const char *CliCommandWhitelist::Name() const
{
return "whitelist";
}
const char *CliCommandWhitelist::Usage() const
{
return kWhitelistUsage;
}
const char *CliCommandWhitelist::Description() const
{
return "Manage the dedicated-server XUID whitelist.";
}
bool CliCommandWhitelist::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine)
{
if (line.tokens.size() < 2)
{
engine->LogWarn(std::string("Usage: ") + kWhitelistUsage);
return false;
}
if (!ServerRuntime::Access::IsInitialized())
{
engine->LogWarn("Access manager is not initialized.");
return false;
}
const auto subcommand = StringUtils::ToLowerAscii(line.tokens[1]);
if (subcommand == "on" || subcommand == "off")
{
if (line.tokens.size() != 2)
{
engine->LogWarn("Usage: whitelist <on|off>");
return false;
}
const bool enabled = (subcommand == "on");
if (!PersistWhitelistToggle(enabled))
{
engine->LogError("Failed to persist whitelist mode to server.properties.");
return false;
}
ServerRuntime::Access::SetWhitelistEnabled(enabled);
engine->LogInfo(std::string("Whitelist ") + (enabled ? "enabled." : "disabled."));
return true;
}
if (subcommand == "list")
{
if (line.tokens.size() != 2)
{
engine->LogWarn("Usage: whitelist list");
return false;
}
return LogWhitelistEntries(engine);
}
if (subcommand == "reload")
{
if (line.tokens.size() != 2)
{
engine->LogWarn("Usage: whitelist reload");
return false;
}
if (!ServerRuntime::Access::ReloadWhitelist())
{
engine->LogError("Failed to reload whitelist.");
return false;
}
const auto config = LoadServerPropertiesConfig();
ServerRuntime::Access::SetWhitelistEnabled(config.whiteListEnabled);
engine->LogInfo("Reloaded whitelist from disk.");
LogWhitelistMode(engine);
return true;
}
if (subcommand == "add")
{
if (line.tokens.size() < 3)
{
engine->LogWarn("Usage: whitelist add <xuid> [name ...]");
return false;
}
PlayerUID xuid = INVALID_XUID;
if (!TryParseWhitelistXuid(line.tokens[2], engine, &xuid))
{
return false;
}
if (ServerRuntime::Access::IsPlayerWhitelisted(xuid))
{
engine->LogWarn("That XUID is already whitelisted.");
return false;
}
const auto metadata = ServerRuntime::Access::WhitelistManager::BuildDefaultMetadata("Console");
const auto name = StringUtils::JoinTokens(line.tokens, 3);
if (!ServerRuntime::Access::AddWhitelistedPlayer(xuid, name, metadata))
{
engine->LogError("Failed to write whitelist entry.");
return false;
}
std::string message = "Whitelisted XUID " + ServerRuntime::Access::FormatXuid(xuid) + ".";
if (!name.empty())
{
message += " Name: " + name;
}
engine->LogInfo(message);
return true;
}
if (subcommand == "remove")
{
if (line.tokens.size() != 3)
{
engine->LogWarn("Usage: whitelist remove <xuid>");
return false;
}
PlayerUID xuid = INVALID_XUID;
if (!TryParseWhitelistXuid(line.tokens[2], engine, &xuid))
{
return false;
}
if (!ServerRuntime::Access::IsPlayerWhitelisted(xuid))
{
engine->LogWarn("That XUID is not whitelisted.");
return false;
}
if (!ServerRuntime::Access::RemoveWhitelistedPlayer(xuid))
{
engine->LogError("Failed to remove whitelist entry.");
return false;
}
engine->LogInfo("Removed XUID " + ServerRuntime::Access::FormatXuid(xuid) + " from the whitelist.");
return true;
}
engine->LogWarn(std::string("Usage: ") + kWhitelistUsage);
return false;
}
void CliCommandWhitelist::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
{
(void)engine;
if (out == nullptr)
{
return;
}
if (context.currentTokenIndex == 1)
{
SuggestLiteral("on", context, out);
SuggestLiteral("off", context, out);
SuggestLiteral("list", context, out);
SuggestLiteral("add", context, out);
SuggestLiteral("remove", context, out);
SuggestLiteral("reload", context, out);
return;
}
if (context.currentTokenIndex == 2 && context.parsed.tokens.size() >= 2 && StringUtils::ToLowerAscii(context.parsed.tokens[1]) == "remove")
{
std::vector<ServerRuntime::Access::WhitelistedPlayerEntry> entries;
if (!ServerRuntime::Access::SnapshotWhitelistedPlayers(&entries))
{
return;
}
for (const auto &entry : entries)
{
SuggestLiteral(entry.xuid, context, out);
}
}
}
}

View File

@@ -0,0 +1,17 @@
#pragma once
#include "..\IServerCliCommand.h"
namespace ServerRuntime
{
class CliCommandWhitelist : public IServerCliCommand
{
public:
const char *Name() const override;
const char *Usage() const override;
const char *Description() const override;
bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) override;
void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const override;
};
}