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
+9
View File
@@ -11,6 +11,7 @@
/Minecraft.Client/x64_*
/Minecraft.Client/*Debug
/Minecraft.Client/*Release*
/Minecraft.Client/*Server*
/Minecraft.Client/*ContentPackage*
/Minecraft.Client/Windows64/GameHDD
@@ -28,6 +29,14 @@
/Minecraft.Client/PS3/SPU_Tasks/*/Release
/Minecraft.Client/PS3/SPU_Tasks/*/ContentPackage
# Minecraft.Server
/Minecraft.Server/x64
/Minecraft.Server/x64_*
/Minecraft.Server/*Debug
/Minecraft.Server/*Release*
/Minecraft.Server/*Server*
/Minecraft.Server/*ContentPackage*
# Misc
*.ppu.o
*.pkg
+4
View File
@@ -303,6 +303,10 @@ void ClientConnection::handleLogin(shared_ptr<LoginPacket> packet)
level->isClientSide = true;
minecraft->setLevel(level);
}
else
{
level = (MultiPlayerLevel *)dimensionLevel;
}
minecraft->player->setPlayerIndex( packet->m_playerIndex );
minecraft->player->setCustomSkin( app.GetPlayerSkinId(m_userIndex) );
+13 -1
View File
@@ -1,15 +1,27 @@
#include "stdafx.h"
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
#include "../../Minecraft.Server/ServerLogManager.h"
#endif
//--------------------------------------------------------------------------------------
// Name: DebugSpewV()
// Desc: Internal helper function
//--------------------------------------------------------------------------------------
#ifndef _CONTENT_PACKAGE
static VOID DebugSpewV( const CHAR* strFormat, const va_list pArgList )
static VOID DebugSpewV( const CHAR* strFormat, va_list pArgList )
{
#if defined __PS3__ || defined __ORBIS__ || defined __PSVITA__
assert(0);
#else
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
// Dedicated server routes legacy debug spew through ServerLogger to preserve CLI prompt handling.
if (ServerRuntime::ServerLogManager::ShouldForwardClientDebugLogs())
{
ServerRuntime::ServerLogManager::ForwardClientDebugSpewLogV(strFormat, pArgList);
return;
}
#endif
CHAR str[2048];
// Use the secure CRT to avoid buffer overruns. Specify a count of
// _TRUNCATE so that too long strings will be silently truncated
+34 -13
View File
@@ -30,6 +30,9 @@
#include "GameRules\ConsoleGameRules.h"
#include "GameRules\ConsoleSchematicFile.h"
#include "..\User.h"
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
#include "..\..\Minecraft.Server\ServerLogManager.h"
#endif
#include "..\\EntityRenderDispatcher.h"
#include "..\TexturePackRepository.h"
#include "..\DLCTexturePack.h"
@@ -230,12 +233,21 @@ void CMinecraftApp::DebugPrintf(const char *szFormat, ...)
{
#ifndef _FINAL_BUILD
char buf[1024];
va_list ap;
va_start(ap, szFormat);
vsnprintf(buf, sizeof(buf), szFormat, ap);
va_end(ap);
OutputDebugStringA(buf);
va_list ap;
va_start(ap, szFormat);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
// Dedicated server routes client debug spew through ServerLogger so CLI output stays prompt-safe.
if (ServerRuntime::ServerLogManager::ShouldForwardClientDebugLogs())
{
ServerRuntime::ServerLogManager::ForwardClientAppDebugLogV(szFormat, ap);
va_end(ap);
return;
}
#endif
char buf[1024];
vsnprintf(buf, sizeof(buf), szFormat, ap);
va_end(ap);
OutputDebugStringA(buf);
#endif
}
@@ -243,13 +255,22 @@ void CMinecraftApp::DebugPrintf(const char *szFormat, ...)
void CMinecraftApp::DebugPrintf(int user, const char *szFormat, ...)
{
#ifndef _FINAL_BUILD
if(user == USER_NONE)
return;
char buf[1024];
va_list ap;
va_start(ap, szFormat);
vsnprintf(buf, sizeof(buf), szFormat, ap);
va_end(ap);
if(user == USER_NONE)
return;
va_list ap;
va_start(ap, szFormat);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
// Dedicated server routes client debug spew through ServerLogger so CLI output stays prompt-safe.
if (ServerRuntime::ServerLogManager::ShouldForwardClientDebugLogs())
{
ServerRuntime::ServerLogManager::ForwardClientUserDebugLogV(user, szFormat, ap);
va_end(ap);
return;
}
#endif
char buf[1024];
vsnprintf(buf, sizeof(buf), szFormat, ap);
va_end(ap);
#ifdef __PS3__
unsigned int writelen;
sys_tty_write(SYS_TTYP_USER1 + ( user - 1 ), buf, strlen(buf), &writelen );
@@ -358,12 +358,24 @@ bool CGameNetworkManager::StartNetworkGame(Minecraft *minecraft, LPVOID lpParame
// PRIMARY PLAYER
vector<ClientConnection *> createdConnections;
ClientConnection *connection;
ClientConnection *connection = NULL;
if( g_NetworkManager.IsHost() )
#ifdef MINECRAFT_SERVER_BUILD
{
app.DebugPrintf("Dedicated server mode: skipping local host client connection\n");
// Keep telemetry behavior consistent with the host path.
INT multiplayerInstanceId = TelemetryManager->GenerateMultiplayerInstanceId();
TelemetryManager->SetMultiplayerInstanceId(multiplayerInstanceId);
app.SetGameMode( eMode_Multiplayer );
}
#else
{
connection = new ClientConnection(minecraft, NULL);
}
#endif
else
{
INetworkPlayer *pNetworkPlayer = g_NetworkManager.GetLocalPlayerByUserIndex(ProfileManager.GetLockedProfile());
@@ -389,155 +401,159 @@ bool CGameNetworkManager::StartNetworkGame(Minecraft *minecraft, LPVOID lpParame
connection = new ClientConnection(minecraft, socket);
}
if( !connection->createdOk )
if (connection != NULL)
{
assert(false);
delete connection;
connection = NULL;
MinecraftServer::HaltServer();
return false;
}
connection->send( shared_ptr<PreLoginPacket>( new PreLoginPacket(minecraft->user->name) ) );
// Tick connection until we're ready to go. The stages involved in this are:
// (1) Creating the ClientConnection sends a prelogin packet to the server
// (2) the server sends a prelogin back, which is handled by the clientConnection, and returns a login packet
// (3) the server sends a login back, which is handled by the client connection to start the game
if( !g_NetworkManager.IsHost() )
{
Minecraft::GetInstance()->progressRenderer->progressStart(IDS_PROGRESS_CONNECTING);
}
else
{
// 4J Stu - Host needs to generate a unique multiplayer id for sentient telemetry reporting
INT multiplayerInstanceId = TelemetryManager->GenerateMultiplayerInstanceId();
TelemetryManager->SetMultiplayerInstanceId(multiplayerInstanceId);
}
TexturePack *tPack = Minecraft::GetInstance()->skins->getSelected();
do
{
app.DebugPrintf("ticking connection A\n");
connection->tick();
// 4J Stu - We were ticking this way too fast which could cause the connection to time out
// The connections should tick at 20 per second
Sleep(50);
} while ( (IsInSession() && !connection->isStarted() && !connection->isClosed() && !g_NetworkManager.IsLeavingGame()) || tPack->isLoadingData() || (Minecraft::GetInstance()->skins->needsUIUpdate() || ui.IsReloadingSkin()) );
ui.CleanUpSkinReload();
// 4J Stu - Fix for #11279 - CRASH: TCR 001: BAS Game Stability: Signing out of game will cause title to crash
// We need to break out of the above loop if m_bLeavingGame is set, and close the connection
if( g_NetworkManager.IsLeavingGame() || !IsInSession() )
{
connection->close();
}
if( connection->isStarted() && !connection->isClosed() )
{
createdConnections.push_back( connection );
int primaryPad = ProfileManager.GetPrimaryPad();
app.SetRichPresenceContext(primaryPad,CONTEXT_GAME_STATE_BLANK);
if (GetPlayerCount() > 1) // Are we offline or online, and how many players are there
if( !connection->createdOk )
{
if (IsLocalGame()) ProfileManager.SetCurrentGameActivity(primaryPad,CONTEXT_PRESENCE_MULTIPLAYEROFFLINE,false);
else ProfileManager.SetCurrentGameActivity(primaryPad,CONTEXT_PRESENCE_MULTIPLAYER,false);
assert(false);
delete connection;
connection = NULL;
MinecraftServer::HaltServer();
return false;
}
connection->send( shared_ptr<PreLoginPacket>( new PreLoginPacket(minecraft->user->name) ) );
// Tick connection until we're ready to go. The stages involved in this are:
// (1) Creating the ClientConnection sends a prelogin packet to the server
// (2) the server sends a prelogin back, which is handled by the clientConnection, and returns a login packet
// (3) the server sends a login back, which is handled by the client connection to start the game
if( !g_NetworkManager.IsHost() )
{
Minecraft::GetInstance()->progressRenderer->progressStart(IDS_PROGRESS_CONNECTING);
}
else
{
if(IsLocalGame()) ProfileManager.SetCurrentGameActivity(primaryPad,CONTEXT_PRESENCE_MULTIPLAYER_1POFFLINE,false);
else ProfileManager.SetCurrentGameActivity(primaryPad,CONTEXT_PRESENCE_MULTIPLAYER_1P,false);
// 4J Stu - Host needs to generate a unique multiplayer id for sentient telemetry reporting
INT multiplayerInstanceId = TelemetryManager->GenerateMultiplayerInstanceId();
TelemetryManager->SetMultiplayerInstanceId(multiplayerInstanceId);
}
TexturePack *tPack = Minecraft::GetInstance()->skins->getSelected();
do
{
app.DebugPrintf("ticking connection A\n");
connection->tick();
// 4J Stu - We were ticking this way too fast which could cause the connection to time out
// The connections should tick at 20 per second
Sleep(50);
} while ( (IsInSession() && !connection->isStarted() && !connection->isClosed() && !g_NetworkManager.IsLeavingGame()) || tPack->isLoadingData() || (Minecraft::GetInstance()->skins->needsUIUpdate() || ui.IsReloadingSkin()) );
ui.CleanUpSkinReload();
// 4J Stu - Fix for #11279 - CRASH: TCR 001: BAS Game Stability: Signing out of game will cause title to crash
// We need to break out of the above loop if m_bLeavingGame is set, and close the connection
if( g_NetworkManager.IsLeavingGame() || !IsInSession() )
{
connection->close();
}
// ALL OTHER LOCAL PLAYERS
for(int idx = 0; idx < XUSER_MAX_COUNT; ++idx)
if( connection->isStarted() && !connection->isClosed() )
{
// Already have setup the primary pad
if(idx == ProfileManager.GetPrimaryPad() ) continue;
if( GetLocalPlayerByUserIndex(idx) != NULL && !ProfileManager.IsSignedIn(idx) )
{
INetworkPlayer *pNetworkPlayer = g_NetworkManager.GetLocalPlayerByUserIndex(idx);
Socket *socket = pNetworkPlayer->GetSocket();
app.DebugPrintf("Closing socket due to player %d not being signed in any more\n");
if( !socket->close(false) ) socket->close(true);
continue;
}
// By default when we host we only have the local player, but currently allow multiple local players to join
// when joining any other way, so just because they are signed in doesn't mean they are in the session
// 4J Stu - If they are in the session, then we should add them to the game. Otherwise we won't be able to add them later
INetworkPlayer *pNetworkPlayer = g_NetworkManager.GetLocalPlayerByUserIndex(idx);
if( pNetworkPlayer == NULL )
continue;
ClientConnection *connection;
Socket *socket = pNetworkPlayer->GetSocket();
connection = new ClientConnection(minecraft, socket, idx);
minecraft->addPendingLocalConnection(idx, connection);
//minecraft->createExtraLocalPlayer(idx, (convStringToWstring( ProfileManager.GetGamertag(idx) )).c_str(), idx, connection);
// Open the socket on the server end to accept incoming data
Socket::addIncomingSocket(socket);
connection->send( shared_ptr<PreLoginPacket>( new PreLoginPacket(convStringToWstring( ProfileManager.GetGamertag(idx) )) ) );
createdConnections.push_back( connection );
// Tick connection until we're ready to go. The stages involved in this are:
// (1) Creating the ClientConnection sends a prelogin packet to the server
// (2) the server sends a prelogin back, which is handled by the clientConnection, and returns a login packet
// (3) the server sends a login back, which is handled by the client connection to start the game
do
int primaryPad = ProfileManager.GetPrimaryPad();
app.SetRichPresenceContext(primaryPad,CONTEXT_GAME_STATE_BLANK);
if (GetPlayerCount() > 1) // Are we offline or online, and how many players are there
{
// We need to keep ticking the connections for players that already logged in
for(AUTO_VAR(it, createdConnections.begin()); it < createdConnections.end(); ++it)
{
(*it)->tick();
}
// 4J Stu - We were ticking this way too fast which could cause the connection to time out
// The connections should tick at 20 per second
Sleep(50);
app.DebugPrintf("<***> %d %d %d %d %d\n",IsInSession(), !connection->isStarted(),!connection->isClosed(),ProfileManager.IsSignedIn(idx),!g_NetworkManager.IsLeavingGame());
#if defined _XBOX || defined __PS3__ && !defined DISABLE_PSN
} while (IsInSession() && !connection->isStarted() && !connection->isClosed() && ProfileManager.IsSignedIn(idx) && !g_NetworkManager.IsLeavingGame() );
#else
// TODO - This SHOULD be something just like the code above but temporarily changing here so that we don't have to depend on the profilemanager behaviour
} while (IsInSession() && !connection->isStarted() && !connection->isClosed() && !g_NetworkManager.IsLeavingGame() );
#endif
// 4J Stu - Fix for #11279 - CRASH: TCR 001: BAS Game Stability: Signing out of game will cause title to crash
// We need to break out of the above loop if m_bLeavingGame is set, and stop creating new connections
// The connections in the createdConnections vector get closed at the end of the thread
if( g_NetworkManager.IsLeavingGame() || !IsInSession() ) break;
if( ProfileManager.IsSignedIn(idx) && !connection->isClosed() )
{
app.SetRichPresenceContext(idx,CONTEXT_GAME_STATE_BLANK);
if (IsLocalGame()) ProfileManager.SetCurrentGameActivity(idx,CONTEXT_PRESENCE_MULTIPLAYEROFFLINE,false);
else ProfileManager.SetCurrentGameActivity(idx,CONTEXT_PRESENCE_MULTIPLAYER,false);
if (IsLocalGame()) ProfileManager.SetCurrentGameActivity(primaryPad,CONTEXT_PRESENCE_MULTIPLAYEROFFLINE,false);
else ProfileManager.SetCurrentGameActivity(primaryPad,CONTEXT_PRESENCE_MULTIPLAYER,false);
}
else
{
connection->close();
AUTO_VAR(it, find( createdConnections.begin(), createdConnections.end(), connection ));
if(it != createdConnections.end() ) createdConnections.erase( it );
if(IsLocalGame()) ProfileManager.SetCurrentGameActivity(primaryPad,CONTEXT_PRESENCE_MULTIPLAYER_1POFFLINE,false);
else ProfileManager.SetCurrentGameActivity(primaryPad,CONTEXT_PRESENCE_MULTIPLAYER_1P,false);
}
}
app.SetGameMode( eMode_Multiplayer );
}
else if ( connection->isClosed() || !IsInSession())
{
// assert(false);
MinecraftServer::HaltServer();
return false;
// ALL OTHER LOCAL PLAYERS
for(int idx = 0; idx < XUSER_MAX_COUNT; ++idx)
{
// Already have setup the primary pad
if(idx == ProfileManager.GetPrimaryPad() ) continue;
if( GetLocalPlayerByUserIndex(idx) != NULL && !ProfileManager.IsSignedIn(idx) )
{
INetworkPlayer *pNetworkPlayer = g_NetworkManager.GetLocalPlayerByUserIndex(idx);
Socket *socket = pNetworkPlayer->GetSocket();
app.DebugPrintf("Closing socket due to player %d not being signed in any more\n");
if( !socket->close(false) ) socket->close(true);
continue;
}
// By default when we host we only have the local player, but currently allow multiple local players to join
// when joining any other way, so just because they are signed in doesn't mean they are in the session
// 4J Stu - If they are in the session, then we should add them to the game. Otherwise we won't be able to add them later
INetworkPlayer *pNetworkPlayer = g_NetworkManager.GetLocalPlayerByUserIndex(idx);
if( pNetworkPlayer == NULL )
continue;
ClientConnection *connection;
Socket *socket = pNetworkPlayer->GetSocket();
connection = new ClientConnection(minecraft, socket, idx);
minecraft->addPendingLocalConnection(idx, connection);
//minecraft->createExtraLocalPlayer(idx, (convStringToWstring( ProfileManager.GetGamertag(idx) )).c_str(), idx, connection);
// Open the socket on the server end to accept incoming data
Socket::addIncomingSocket(socket);
connection->send( shared_ptr<PreLoginPacket>( new PreLoginPacket(convStringToWstring( ProfileManager.GetGamertag(idx) )) ) );
createdConnections.push_back( connection );
// Tick connection until we're ready to go. The stages involved in this are:
// (1) Creating the ClientConnection sends a prelogin packet to the server
// (2) the server sends a prelogin back, which is handled by the clientConnection, and returns a login packet
// (3) the server sends a login back, which is handled by the client connection to start the game
do
{
// We need to keep ticking the connections for players that already logged in
for(AUTO_VAR(it, createdConnections.begin()); it < createdConnections.end(); ++it)
{
(*it)->tick();
}
// 4J Stu - We were ticking this way too fast which could cause the connection to time out
// The connections should tick at 20 per second
Sleep(50);
app.DebugPrintf("<***> %d %d %d %d %d\n",IsInSession(), !connection->isStarted(),!connection->isClosed(),ProfileManager.IsSignedIn(idx),!g_NetworkManager.IsLeavingGame());
#if defined _XBOX || defined __PS3__ && !defined DISABLE_PSN
} while (IsInSession() && !connection->isStarted() && !connection->isClosed() && ProfileManager.IsSignedIn(idx) && !g_NetworkManager.IsLeavingGame() );
#else
// TODO - This SHOULD be something just like the code above but temporarily changing here so that we don't have to depend on the profilemanager behaviour
} while (IsInSession() && !connection->isStarted() && !connection->isClosed() && !g_NetworkManager.IsLeavingGame() );
#endif
// 4J Stu - Fix for #11279 - CRASH: TCR 001: BAS Game Stability: Signing out of game will cause title to crash
// We need to break out of the above loop if m_bLeavingGame is set, and stop creating new connections
// The connections in the createdConnections vector get closed at the end of the thread
if( g_NetworkManager.IsLeavingGame() || !IsInSession() ) break;
if( ProfileManager.IsSignedIn(idx) && !connection->isClosed() )
{
app.SetRichPresenceContext(idx,CONTEXT_GAME_STATE_BLANK);
if (IsLocalGame()) ProfileManager.SetCurrentGameActivity(idx,CONTEXT_PRESENCE_MULTIPLAYEROFFLINE,false);
else ProfileManager.SetCurrentGameActivity(idx,CONTEXT_PRESENCE_MULTIPLAYER,false);
}
else
{
connection->close();
AUTO_VAR(it, find( createdConnections.begin(), createdConnections.end(), connection ));
if(it != createdConnections.end() ) createdConnections.erase( it );
}
}
app.SetGameMode( eMode_Multiplayer );
}
else if ( connection->isClosed() || !IsInSession())
{
// assert(false);
MinecraftServer::HaltServer();
return false;
}
}
@@ -547,7 +563,7 @@ bool CGameNetworkManager::StartNetworkGame(Minecraft *minecraft, LPVOID lpParame
{
(*it)->close();
}
// assert(false);
//assert(false);
MinecraftServer::HaltServer();
return false;
}
@@ -1,8 +1,17 @@
#include "stdafx.h"
#include <string>
#include "NetworkSocketLayer.h"
#include "..\..\Common\Network\PlatformNetworkManager.h"
#include "..\..\..\Minecraft.World\Socket.h"
#if defined(MINECRAFT_SERVER_BUILD)
#include "..\..\..\Minecraft.Server\Access\Access.h"
#include "..\..\..\Minecraft.Server\ServerLogManager.h"
#endif
#if defined(MINECRAFT_SERVER_BUILD)
static bool TryGetNumericRemoteIp(const sockaddr_in &remoteAddress, std::string *outIp);
#endif
SOCKET NetworkSocketLayer::s_listenSocket = INVALID_SOCKET;
SOCKET NetworkSocketLayer::s_hostConnectionSocket = INVALID_SOCKET;
@@ -55,23 +64,26 @@ std::vector<LANSession> NetworkSocketLayer::s_discoveredSessions;
CRITICAL_SECTION NetworkSocketLayer::s_disconnectLock;
std::vector<BYTE> NetworkSocketLayer::s_disconnectedSmallIds;
CRITICAL_SECTION NetworkSocketLayer::s_pendingJoinLock;
std::vector<BYTE> NetworkSocketLayer::s_pendingJoinSmallIds;
CRITICAL_SECTION NetworkSocketLayer::s_freeSmallIdLock;
std::vector<BYTE> NetworkSocketLayer::s_freeSmallIds;
// only goes true on a successful Initialize(), and a failed one gets retried
static bool s_locksCreated = false;
#if defined _WINDOWS64
#if !defined _WINDOWS64
bool g_MultiplayerHost = true;
bool g_MultiplayerJoin = false;
#else
bool g_MultiplayerHost = false;
bool g_MultiplayerJoin = true;
#endif
#ifdef MINECRAFT_SERVER_BUILD
int g_DedicatedServerPort = NETWORK_LAN_DEFAULT_PORT;
char g_DedicatedServerBindIP[256] = "";
bool g_DedicatedServerLanAdvertise = true;
#endif
int g_MultiplayerPort = NETWORK_LAN_DEFAULT_PORT;
char g_MultiplayerIP[256] = "192.168.0.69";
char g_MultiplayerIP[256] = "192.168.8.27";
bool NetworkSocketLayer::Initialize()
@@ -86,7 +98,6 @@ bool NetworkSocketLayer::Initialize()
InitializeCriticalSection(&s_advertiseLock);
InitializeCriticalSection(&s_discoveryLock);
InitializeCriticalSection(&s_disconnectLock);
InitializeCriticalSection(&s_pendingJoinLock);
InitializeCriticalSection(&s_freeSmallIdLock);
for (int i = 0; i < NETWORK_LAN_MAX_CLIENTS + 1; i++)
@@ -139,7 +150,10 @@ bool NetworkSocketLayer::Initialize()
s_initialized = true;
// Dedicated Server does not use LAN session discovery and therefore does not initiate discovery.
#ifndef MINECRAFT_SERVER_BUILD
StartDiscovery();
#endif
return true;
}
@@ -224,8 +238,6 @@ void NetworkSocketLayer::Shutdown()
DeleteCriticalSection(&s_discoveryLock);
DeleteCriticalSection(&s_disconnectLock);
s_disconnectedSmallIds.clear();
DeleteCriticalSection(&s_pendingJoinLock);
s_pendingJoinSmallIds.clear();
DeleteCriticalSection(&s_freeSmallIdLock);
s_freeSmallIds.clear();
s_locksCreated = false;
@@ -799,6 +811,12 @@ bool NetworkSocketLayer::JoinGame(const char *ip, int port)
{
return false;
}
// Clear the recv timeout now that the handshake is complete.
// The recv thread should block indefinitely waiting for data.
DWORD noTimeout = 0;
setsockopt(s_hostConnectionSocket, SOL_SOCKET, SO_RCVTIMEO, (const char*)&noTimeout, sizeof(noTimeout));
s_localSmallId = assignedSmallId;
app.DebugPrintf("LAN: Connected to %s:%d, assigned smallId=%d\n", ip, port, s_localSmallId);
@@ -907,6 +925,20 @@ SOCKET NetworkSocketLayer::GetSocketForSmallId(BYTE smallId)
#endif
}
// Send reject handshake: sentinel 0xFF + DisconnectPacket wire format (1 byte id 255 + 4 byte big-endian reason). Then caller closes socket.
static void SendRejectWithReason(SOCKET clientSocket, DisconnectPacket::eDisconnectReason reason)
{
BYTE buf[6];
buf[0] = NETWORK_LAN_SMALLID_REJECT;
buf[1] = (BYTE)255; // DisconnectPacket packet id
int r = (int)reason;
buf[2] = (BYTE)((r >> 24) & 0xff);
buf[3] = (BYTE)((r >> 16) & 0xff);
buf[4] = (BYTE)((r >> 8) & 0xff);
buf[5] = (BYTE)(r & 0xff);
send(clientSocket, (const char*)buf, sizeof(buf), 0);
}
static bool RecvExact(SOCKET sock, BYTE *buf, int len)
{
int totalRecv = 0;
@@ -919,6 +951,27 @@ static bool RecvExact(SOCKET sock, BYTE *buf, int len)
return true;
}
#if defined(MINECRAFT_SERVER_BUILD)
static bool TryGetNumericRemoteIp(const sockaddr_in &remoteAddress, std::string *outIp)
{
if (outIp == nullptr)
{
return false;
}
outIp->clear();
char ipBuffer[64] = {};
const char *ip = inet_ntop(AF_INET, (void *)&remoteAddress.sin_addr, ipBuffer, sizeof(ipBuffer));
if (ip == nullptr || ip[0] == 0)
{
return false;
}
*outIp = ip;
return true;
}
#endif
void NetworkSocketLayer::HandleDataReceived(BYTE fromSmallId, BYTE toSmallId, unsigned char *data, unsigned int dataSize)
{
INetworkPlayer *pPlayerFrom = g_NetworkManager.GetPlayerBySmallId(fromSmallId);
@@ -951,9 +1004,15 @@ int NetworkSocketLayer::AcceptThreadProc(LPVOID param)
while (s_active)
{
#ifdef __PSVITA__
SOCKET clientSocket = accept(s_listenSocket, (SceNetSockaddr *)NULL, NULL);
sockaddr_in remoteAddress;
ZeroMemory(&remoteAddress, sizeof(remoteAddress));
int remoteAddressLength = sizeof(remoteAddress);
SOCKET clientSocket = accept(s_listenSocket, (SceNetSockaddr *)&remoteAddress, &remoteAddressLength);
#else
SOCKET clientSocket = accept(s_listenSocket, NULL, NULL);
sockaddr_in remoteAddress;
ZeroMemory(&remoteAddress, sizeof(remoteAddress));
int remoteAddressLength = sizeof(remoteAddress);
SOCKET clientSocket = accept(s_listenSocket, (sockaddr*)&remoteAddress, &remoteAddressLength);
#endif
#if defined _WINDOWS64 || defined _XBOX
if (clientSocket == INVALID_SOCKET)
@@ -974,10 +1033,28 @@ int NetworkSocketLayer::AcceptThreadProc(LPVOID param)
int noDelay = 1;
setsockopt(clientSocket, IPPROTO_TCP, TCP_NODELAY, (const char *)&noDelay, sizeof(noDelay));
extern QNET_STATE _iQNetStubState;
if (_iQNetStubState != QNET_STATE_GAME_PLAY && _iQNetStubState != QNET_STATE_SESSION_STARTING)
#if defined(MINECRAFT_SERVER_BUILD)
std::string remoteIp;
const bool hasRemoteIp = TryGetNumericRemoteIp(remoteAddress, &remoteIp);
const char *remoteIpForLog = hasRemoteIp ? remoteIp.c_str() : "unknown";
ServerRuntime::ServerLogManager::OnIncomingTcpConnection(remoteIpForLog);
if (hasRemoteIp && ServerRuntime::Access::IsIpBanned(remoteIp))
{
ServerRuntime::ServerLogManager::OnRejectedTcpConnection(remoteIpForLog, ServerRuntime::ServerLogManager::eTcpRejectReason_BannedIp);
SendRejectWithReason(clientSocket, DisconnectPacket::eDisconnect_Banned);
closesocket(clientSocket);
continue;
}
#endif
extern QNET_STATE _iQNetStubState;
if (_iQNetStubState != QNET_STATE_GAME_PLAY)
{
#if defined(MINECRAFT_SERVER_BUILD)
ServerRuntime::ServerLogManager::OnRejectedTcpConnection(remoteIpForLog, ServerRuntime::ServerLogManager::eTcpRejectReason_GameNotReady);
#else
app.DebugPrintf("Win64 LAN: Rejecting connection, game not ready\n");
#endif
closesocket(clientSocket);
continue;
}
@@ -996,7 +1073,11 @@ int NetworkSocketLayer::AcceptThreadProc(LPVOID param)
else
{
LeaveCriticalSection(&s_freeSmallIdLock);
#if defined(MINECRAFT_SERVER_BUILD)
ServerRuntime::ServerLogManager::OnRejectedTcpConnection(remoteIpForLog, ServerRuntime::ServerLogManager::eTcpRejectReason_ServerFull);
#else
app.DebugPrintf("Win64 LAN: Server full, rejecting connection\n");
#endif
closesocket(clientSocket);
continue;
}
@@ -1026,16 +1107,19 @@ int NetworkSocketLayer::AcceptThreadProc(LPVOID param)
conn.active = true;
LeaveCriticalSection(&s_connectionsLock);
#if defined(MINECRAFT_SERVER_BUILD)
ServerRuntime::ServerLogManager::OnAcceptedTcpConnection(assignedSmallId, remoteIpForLog);
#else
app.DebugPrintf("Win64 LAN: Client connected, assigned smallId=%d\n", assignedSmallId);
#endif
IQNetPlayer *qnetPlayer = &IQNet::m_player[assignedSmallId];
IQNetPlayer* qnetPlayer = &IQNet::m_player[assignedSmallId];
extern void Win64_SetupRemoteQNetPlayer(IQNetPlayer *player, BYTE smallId, bool isHost, bool isLocal);
extern void Win64_SetupRemoteQNetPlayer(IQNetPlayer * player, BYTE smallId, bool isHost, bool isLocal);
Win64_SetupRemoteQNetPlayer(qnetPlayer, assignedSmallId, false, false);
EnterCriticalSection(&s_pendingJoinLock);
s_pendingJoinSmallIds.push_back(assignedSmallId);
LeaveCriticalSection(&s_pendingJoinLock);
extern CPlatformNetworkManagerStub* g_pPlatformNetworkManager;
g_pPlatformNetworkManager->NotifyPlayerJoined(qnetPlayer);
DWORD *threadParam = new DWORD;
*threadParam = assignedSmallId;
@@ -1142,20 +1226,6 @@ void NetworkSocketLayer::PushFreeSmallId(BYTE smallId)
LeaveCriticalSection(&s_freeSmallIdLock);
}
bool NetworkSocketLayer::PopPendingJoinSmallId(BYTE *outSmallId)
{
bool found = false;
EnterCriticalSection(&s_pendingJoinLock);
if (!s_pendingJoinSmallIds.empty())
{
*outSmallId = s_pendingJoinSmallIds.back();
s_pendingJoinSmallIds.pop_back();
found = true;
}
LeaveCriticalSection(&s_pendingJoinLock);
return found;
}
bool NetworkSocketLayer::IsSmallIdConnected(BYTE smallId)
{
if (smallId >= NETWORK_LAN_MAX_CLIENTS + 1) return false;
@@ -34,6 +34,7 @@
#define NETWORK_LAN_DEFAULT_PORT 25565
#define NETWORK_LAN_MAX_CLIENTS 7
#define NETWORK_LAN_SMALLID_REJECT 0xFF
#define NETWORK_LAN_RECV_BUFFER_SIZE 65536
#define NETWORK_LAN_MAX_PACKET_SIZE (3 * 1024 * 1024)
#define NETWORK_LAN_DISCOVERY_PORT 25566
@@ -172,8 +173,6 @@ public:
static void PushFreeSmallId(BYTE smallId);
static void CloseConnectionBySmallId(BYTE smallId);
static bool PopPendingJoinSmallId(BYTE *outSmallId);
static bool IsSmallIdConnected(BYTE smallId);
static bool StartAdvertising(int gamePort, const wchar_t *hostName, unsigned int gameSettings, unsigned int texPackId, unsigned char subTexId, unsigned short netVer);
@@ -230,9 +229,6 @@ private:
static CRITICAL_SECTION s_disconnectLock;
static std::vector<BYTE> s_disconnectedSmallIds;
static CRITICAL_SECTION s_pendingJoinLock;
static std::vector<BYTE> s_pendingJoinSmallIds;
static CRITICAL_SECTION s_freeSmallIdLock;
static std::vector<BYTE> s_freeSmallIds;
};
@@ -241,3 +237,9 @@ extern bool g_MultiplayerHost;
extern bool g_MultiplayerJoin;
extern int g_MultiplayerPort;
extern char g_MultiplayerIP[256];
#ifdef MINECRAFT_SERVER_BUILD
extern int g_DedicatedServerPort;
extern char g_DedicatedServerBindIP[256];
extern bool g_DedicatedServerLanAdvertise;
#endif
@@ -171,6 +171,7 @@ bool CPlatformNetworkManagerStub::Initialise(CGameNetworkManager *pGameNetworkMa
m_bLeavingGame = false;
m_bLeaveGameOnTick = false;
m_bLeaveGameOnTick = false;
m_bHostChanged = false;
m_bSearchResultsReady = false;
@@ -233,53 +234,56 @@ void CPlatformNetworkManagerStub::DoWork()
BYTE disconnectedSmallId;
while (NetworkSocketLayer::PopDisconnectedSmallId(&disconnectedSmallId))
{
if (disconnectedSmallId == 0) continue;
if (NetworkSocketLayer::IsSmallIdConnected(disconnectedSmallId)) continue;
IQNetPlayer *qnetPlayer = m_pIQNet->GetPlayerBySmallId(disconnectedSmallId);
if (qnetPlayer != NULL && qnetPlayer->m_smallId == disconnectedSmallId)
IQNetPlayer* qnetPlayer = m_pIQNet->GetPlayerBySmallId(disconnectedSmallId);
if (qnetPlayer != nullptr && qnetPlayer->m_smallId == disconnectedSmallId)
{
NotifyPlayerLeaving(qnetPlayer);
qnetPlayer->m_smallId = 0;
qnetPlayer->m_isRemote = false;
qnetPlayer->m_isHostPlayer = false;
//qnetPlayer->m_resolvedXuid = INVALID_XUID; // TODO: implement
qnetPlayer->m_gamertag[0] = 0;
qnetPlayer->SetCustomDataValue(0);
NetworkSocketLayer::PushFreeSmallId(disconnectedSmallId);
if (IQNet::s_playerCount > 1)
// Recalculate s_playerCount as the highest active slot + 1.
// A blind decrement would hide players at higher-indexed slots when a
// lower-indexed player disconnects first: GetPlayerBySmallId scans
// [0, s_playerCount) so any slot at or above the decremented count
// becomes invisible, causing its disconnect to be missed (ghost player).
while (IQNet::s_playerCount > 1 &&
IQNet::m_player[IQNet::s_playerCount - 1].GetCustomDataValue() == 0)
IQNet::s_playerCount--;
}
}
BYTE joinedSmallId;
while (NetworkSocketLayer::PopPendingJoinSmallId(&joinedSmallId))
{
IQNetPlayer *qnetPlayer = m_pIQNet->GetPlayerBySmallId(joinedSmallId);
if (qnetPlayer != NULL && qnetPlayer->m_smallId == joinedSmallId)
{
NotifyPlayerJoined(qnetPlayer);
}
}
for (int i = 1; i < MINECRAFT_NET_MAX_PLAYERS; i++)
{
IQNetPlayer *qp = &IQNet::m_player[i];
if (qp->GetCustomDataValue() == 0)
continue;
INetworkPlayer *np = (INetworkPlayer *)qp->GetCustomDataValue();
Socket *sock = np->GetSocket();
if (sock != NULL && sock->isClosing())
{
NetworkSocketLayer::CloseConnectionBySmallId((BYTE)i);
}
// NOTE: Do NOT call PushFreeSmallId here. The old PlayerConnection's
// write thread may still be alive (it dies in PlayerList::tick when
// m_smallIdsToClose is processed). If we recycle the smallId now,
// AcceptThread can reuse it for a new connection, and the old write
// thread's getPlayer() lookup will resolve to the NEW player, sending
// stale game packets to the new client's TCP socket — corrupting its
// login handshake (bad packet id crash). PushFreeSmallId and
// ClearSocketForSmallId are called from PlayerList::tick after the
// old Connection threads are dead.
//
// Clear chunk visibility flags for this system so rejoin gets fresh chunk state.
SystemFlagRemoveBySmallId(disconnectedSmallId);
}
}
if (_iQNetStubState == QNET_STATE_GAME_PLAY && !m_pIQNet->IsHost())
// Client-side host disconnect detection:
// if TCP is gone, propagate through normal network-disconnect flow so UI returns to menus.
// The processing from the Xbox version will be reused.
if (_iQNetStubState == QNET_STATE_GAME_PLAY && !m_pIQNet->IsHost() && !m_bLeavingGame)
{
if (!NetworkSocketLayer::IsConnected() && !g_NetworkManager.IsLeavingGame())
if (!NetworkSocketLayer::IsConnected())
{
if (app.GetDisconnectReason() == DisconnectPacket::eDisconnect_None)
app.SetDisconnectReason(DisconnectPacket::eDisconnect_Quitting);
app.SetAction(ProfileManager.GetPrimaryPad(), eAppAction_ExitWorld, (void *)TRUE);
if (!m_bLeaveGameOnTick)
{
m_bLeaveGameOnTick = true;
g_NetworkManager.HandleDisconnect(false);
}
}
else
{
m_bLeaveGameOnTick = false;
}
}
}
@@ -354,6 +358,7 @@ bool CPlatformNetworkManagerStub::LeaveGame(bool bMigrateHost)
if( m_bLeavingGame ) return true;
m_bLeavingGame = true;
m_bLeaveGameOnTick = false;
NetworkSocketLayer::StopAdvertising();
// If we are the host wait for the game server to end
@@ -397,6 +402,7 @@ void CPlatformNetworkManagerStub::HostGame(int localUsersMask, bool bOnlineGame,
localUsersMask |= GetLocalPlayerMask( g_NetworkManager.GetPrimaryPad() );
m_bLeavingGame = false;
m_bLeaveGameOnTick = false;
m_pIQNet->HostGame();
@@ -418,9 +424,25 @@ void CPlatformNetworkManagerStub::HostGame(int localUsersMask, bool bOnlineGame,
if (!NetworkSocketLayer::IsActive())
NetworkSocketLayer::HostGame(port);
const wchar_t *hostName = IQNet::m_player[0].m_gamertag;
unsigned int settings = app.GetGameHostOption(eGameHostOption_All);
NetworkSocketLayer::StartAdvertising(port, hostName, settings, 0, 0, MINECRAFT_NET_VERSION);
if (NetworkSocketLayer::IsActive())
{
// For Dedicated Server, refer to `lan-advertise` in `server.properties`
bool enableLanAdvertising = true;
#ifdef MINECRAFT_SERVER_BUILD
enableLanAdvertising = g_DedicatedServerLanAdvertise;
#endif
if (enableLanAdvertising)
{
const wchar_t* hostName = IQNet::m_player[0].m_gamertag;
unsigned int settings = app.GetGameHostOption(eGameHostOption_All);
NetworkSocketLayer::StartAdvertising(port, hostName, settings, 0, 0, MINECRAFT_NET_VERSION);
}
else
{
NetworkSocketLayer::StopAdvertising();
}
}
//#endif
}
@@ -626,6 +648,7 @@ CPlatformNetworkManagerStub::PlayerFlags::PlayerFlags(INetworkPlayer *pNetworkPl
this->flags = new unsigned char [ count / 8 ];
memset( this->flags, 0, count / 8 );
this->count = count;
this->m_smallId = (pNetworkPlayer && pNetworkPlayer->IsLocal()) ? 256 : (pNetworkPlayer ? (int)pNetworkPlayer->GetSmallId() : -1);
}
CPlatformNetworkManagerStub::PlayerFlags::~PlayerFlags()
{
@@ -663,6 +686,23 @@ void CPlatformNetworkManagerStub::SystemFlagRemovePlayer(INetworkPlayer *pNetwor
}
}
// Clear chunk flags for a system when they disconnect (by smallId). Call even when we don't find the player,
// so we always clear and the smallId can be reused without stale "chunk seen" state.
void CPlatformNetworkManagerStub::SystemFlagRemoveBySmallId(int smallId)
{
if (smallId < 0) return;
for (unsigned int i = 0; i < m_playerFlags.size(); i++)
{
if (m_playerFlags[i]->m_smallId == smallId)
{
delete m_playerFlags[i];
m_playerFlags[i] = m_playerFlags.back();
m_playerFlags.pop_back();
return;
}
}
}
void CPlatformNetworkManagerStub::SystemFlagReset()
{
for( unsigned int i = 0; i < m_playerFlags.size(); i++ )
@@ -99,12 +99,14 @@ private:
INetworkPlayer *m_pNetworkPlayer;
unsigned char *flags;
unsigned int count;
int m_smallId;
PlayerFlags(INetworkPlayer *pNetworkPlayer, unsigned int count);
~PlayerFlags();
};
vector<PlayerFlags *> m_playerFlags;
void SystemFlagAddPlayer(INetworkPlayer *pNetworkPlayer);
void SystemFlagRemovePlayer(INetworkPlayer *pNetworkPlayer);
void SystemFlagRemoveBySmallId(int smallId);
void SystemFlagReset();
public:
virtual void SystemFlagSet(INetworkPlayer *pNetworkPlayer, int index);
@@ -0,0 +1,82 @@
param(
[string]$OutDir,
[string]$ProjectDir
)
Write-Host "Server Postbuild: script started. Output Directory: $OutDir, Project Directory: $ProjectDir"
$directories = @(
"Windows64\GameHDD",
"Common\Media",
"Common\res",
"redist64"
)
foreach ($dir in $directories) {
New-Item -ItemType Directory -Path (Join-Path $OutDir $dir) -Force | Out-Null
}
$folderCopies = @(
@{ Source = "Common\Media"; Dest = "Common\Media" },
@{ Source = "Common\res"; Dest = "Common\res" },
@{ Source = "Common\Postbuilds\Contents\Windows64\redist64"; Dest = "redist64" }
)
foreach ($copy in $folderCopies) {
$src = Join-Path "$ProjectDir\..\Minecraft.Client" $copy.Source
$dst = Join-Path $OutDir $copy.Dest
if (Test-Path $src) {
xcopy /q /y /i /s /e /d "$src" "$dst" 2>$null
}
}
$fileCopies = @(
@{ Source = "Common\Postbuilds\Contents\Windows64\Effects.msscmp"; Dest = "Effects.msscmp" },
@{ Source = "Common\Postbuilds\Contents\Windows64\iggy_w64.dll"; Dest = "iggy_w64.dll" },
@{ Source = "Common\Postbuilds\Contents\Windows64\mss64.dll"; Dest = "mss64.dll" }
)
foreach ($copy in $fileCopies) {
$src = Join-Path "$ProjectDir\..\Minecraft.Client" $copy.Source
$dst = Join-Path $OutDir $copy.Dest
if (Test-Path $src) {
Copy-Item -Path $src -Destination $dst -Force
}
}
$deleteDirs = @(
"Common\Media\Sound",
"Common\Media\Graphics",
"Common\Media\de-DE",
"Common\Media\es-ES",
"Common\Media\fr-FR",
"Common\Media\it-IT",
"Common\Media\ja-JP",
"Common\Media\ko-KR",
"Common\Media\pt-BR",
"Common\Media\pt-PT",
"Common\Media\zh-CHT"
)
foreach ($dir in $deleteDirs) {
$path = Join-Path $OutDir $dir
if (Test-Path $path) {
Remove-Item $path -Recurse -Force -ErrorAction SilentlyContinue
}
}
$delDirs = @(
"Common\Media",
"Common\Media\font"
)
foreach ($dir in $delDirs) {
if ($delDirs -contains $dir) {
$path = Join-Path $OutDir $dir
if (Test-Path $path) {
Get-ChildItem -Path $path -Recurse -Include *.swf, *.txt, *.resx, *.xml, *.loc, *.lang, *.col, *.h, *.xui, *.abc, "Mojang Font_7.ttf", "Mojang Font_11.ttf", MediaDurango.arc, MediaOrbis.arc, MediaPS3.arc, MediaPSVita.arc -File |
Remove-Item -Force -ErrorAction SilentlyContinue
}
}
}
File diff suppressed because it is too large Load Diff
+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)
+3
View File
@@ -40,6 +40,7 @@ typedef struct _NetworkGameInitData
LevelGenerationOptions *levelGen;
DWORD texturePackId;
bool findSeed;
bool dedicatedNoLocalHostPlayer;
unsigned int xzSize;
unsigned char hellScale;
ESavePlatform savePlatform;
@@ -52,6 +53,7 @@ typedef struct _NetworkGameInitData
levelGen = NULL;
texturePackId = 0;
findSeed = false;
dedicatedNoLocalHostPlayer = false;
xzSize = LEVEL_LEGACY_WIDTH;
hellScale = HELL_LEVEL_LEGACY_SCALE;
savePlatform = SAVE_FILE_PLATFORM_LOCAL;
@@ -113,6 +115,7 @@ public:
int maxBuildHeight;
int playerIdleTimeout;
bool forceGameType;
int m_spawnProtectionRadius;
private:
// 4J Added
+143 -1
View File
@@ -14,12 +14,37 @@
#include "..\Minecraft.World\net.minecraft.world.item.h"
#include "..\Minecraft.World\SharedConstants.h"
#include "Settings.h"
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
#include "../Minecraft.Server/ServerLogManager.h"
#include "../Minecraft.Server/Access/Access.h"
#include "../Minecraft.World/Socket.h"
#include <Common/Network/NetworkSocketLayer.h>
#endif
// #ifdef __PS3__
// #include "PS3\Network\NetworkPlayerSony.h"
// #endif
Random *PendingConnection::random = new Random();
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
namespace
{
static unsigned char GetPendingConnectionSmallId(Connection *connection)
{
if (connection != nullptr)
{
Socket *socket = connection->getSocket();
if (socket != nullptr)
{
return socket->getSmallId();
}
}
return 0;
}
}
#endif
PendingConnection::PendingConnection(MinecraftServer *server, Socket *socket, const wstring& id)
{
// 4J - added initialisers
@@ -159,12 +184,123 @@ void PendingConnection::handleLogin(shared_ptr<LoginPacket> packet)
bool sentDisconnect = false;
bool nameTaken = false;
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
{
std::string connectionIp = "";
int connectionPort = 0;
if (!connection || !connection->getSocket()) {
disconnect(DisconnectPacket::eDisconnect_EndOfStream);
return;
}
unsigned char smallId = connection->getSocket()->getSmallId();
if (smallId == 0) {
disconnect(DisconnectPacket::eDisconnect_EndOfStream);
return;
}
if (!ServerRuntime::ServerLogManager::TryGetConnectionRemoteIp(smallId, &connectionIp))
{
SOCKET sock = NetworkSocketLayer::GetSocketForSmallId(smallId);
if (sock != INVALID_SOCKET)
{
sockaddr_in addr;
int addrLen = sizeof(addr);
if (getpeername(sock, (sockaddr*)&addr, &addrLen) == 0)
{
char ipBuf[64] = {};
if (inet_ntop(AF_INET, &addr.sin_addr, ipBuf, sizeof(ipBuf)))
{
connectionIp = ipBuf;
connectionPort = (int)ntohs(addr.sin_port);
}
}
}
if (connectionIp.empty()) {
disconnect(DisconnectPacket::eDisconnect_EndOfStream);
return;
}
}
else {
SOCKET sock = NetworkSocketLayer::GetSocketForSmallId(smallId);
if (sock != INVALID_SOCKET)
{
sockaddr_in addr;
int addrLen = sizeof(addr);
if (getpeername(sock, (sockaddr*)&addr, &addrLen) == 0)
connectionPort = (int)ntohs(addr.sin_port);
}
}
}
#endif
// Use the same Xuid choice as handleAcceptedLogin (offline first, online fallback).
PlayerUID loginXuid = packet->m_offlineXuid;
if (loginXuid == INVALID_XUID) loginXuid = packet->m_onlineXuid;
bool duplicateXuid = false;
if (loginXuid != INVALID_XUID && server->getPlayers()->getPlayer(loginXuid) != nullptr)
{
duplicateXuid = true;
}
else if (packet->m_onlineXuid != INVALID_XUID &&
packet->m_onlineXuid != loginXuid &&
server->getPlayers()->getPlayer(packet->m_onlineXuid) != nullptr)
{
duplicateXuid = true;
}
bool bannedXuid = false;
if (packet->m_offlineXuid != INVALID_XUID)
{
bannedXuid = server->getPlayers()->isXuidBanned(packet->m_offlineXuid);
}
if (!bannedXuid && packet->m_onlineXuid != INVALID_XUID && packet->m_onlineXuid != packet->m_offlineXuid)
{
bannedXuid = server->getPlayers()->isXuidBanned(packet->m_onlineXuid);
}
bool whitelistSatisfied = true;
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (ServerRuntime::Access::IsWhitelistEnabled())
{
whitelistSatisfied = false;
if (loginXuid != INVALID_XUID)
{
whitelistSatisfied = ServerRuntime::Access::IsPlayerWhitelisted(loginXuid);
}
if (!whitelistSatisfied && packet->m_onlineXuid != INVALID_XUID && packet->m_onlineXuid != loginXuid)
{
whitelistSatisfied = ServerRuntime::Access::IsPlayerWhitelisted(packet->m_onlineXuid);
}
}
#endif
if( sentDisconnect )
{
// Do nothing
}
else if( server->getPlayers()->isXuidBanned( packet->m_onlineXuid ) )
else if (bannedXuid)
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
ServerRuntime::ServerLogManager::OnRejectedPlayerLogin(GetPendingConnectionSmallId(connection), name, ServerRuntime::ServerLogManager::eLoginRejectReason_BannedXuid);
#endif
disconnect(DisconnectPacket::eDisconnect_Banned);
}
else if (!whitelistSatisfied)
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
ServerRuntime::ServerLogManager::OnRejectedPlayerLogin(GetPendingConnectionSmallId(connection), name, ServerRuntime::ServerLogManager::eLoginRejectReason_NotWhitelisted);
#endif
disconnect(DisconnectPacket::eDisconnect_Banned);
}
else if (duplicateXuid)
{
// Reject the incoming connection — a player with this UID is already
// on the server. Allowing duplicates causes invisible players and
// other undefined behaviour.
app.DebugPrintf("LOGIN: Rejecting duplicate xuid for name: %ls\n", name.c_str());
disconnect(DisconnectPacket::eDisconnect_Banned);
}
else
@@ -182,6 +318,9 @@ void PendingConnection::handleLogin(shared_ptr<LoginPacket> packet)
if (nameTaken)
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
ServerRuntime::ServerLogManager::OnRejectedPlayerLogin(GetPendingConnectionSmallId(connection), name, ServerRuntime::ServerLogManager::eLoginRejectReason_DuplicateName);
#endif
app.DebugPrintf("Rejecting duplicate name: %ls\n", name.c_str());
// to do: replace this with a dedicated duplicate name
@@ -237,6 +376,9 @@ void PendingConnection::handleAcceptedLogin(shared_ptr<LoginPacket> packet)
shared_ptr<ServerPlayer> playerEntity = server->getPlayers()->getPlayerForLogin(this, name, playerXuid,packet->m_onlineXuid);
if (playerEntity != NULL)
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
ServerRuntime::ServerLogManager::OnAcceptedPlayerLogin(GetPendingConnectionSmallId(connection), name);
#endif
server->getPlayers()->placeNewPlayer(connection, playerEntity, packet);
connection = NULL; // We've moved responsibility for this over to the new PlayerConnection, NULL so we don't delete our reference to it here in our dtor
}
+81 -13
View File
@@ -32,6 +32,22 @@
// 4J Added
#include "..\Minecraft.World\net.minecraft.world.item.crafting.h"
#include "Options.h"
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
#include "..\Minecraft.Server\ServerLogManager.h"
#endif
namespace
{
// Anti-cheat thresholds. Keep server-side checks authoritative even in host mode.
// Base max squared displacement allowed per move packet before speed flags trigger.
const double kMoveBaseAllowanceSq = 100.0;
// Extra squared displacement allowance derived from current server-side velocity.
const double kMoveVelocityAllowanceScale = 100.0;
// Max squared distance for interact/attack when the target is visible (normal reach).
const double kInteractReachSq = 6.0 * 6.0;
// Stricter max squared distance used when LOS is blocked to reduce wall-hit abuse.
const double kInteractBlockedReachSq = 3.0 * 3.0;
}
Random PlayerConnection::random;
@@ -64,6 +80,13 @@ PlayerConnection::PlayerConnection(MinecraftServer *server, Connection *connecti
m_offlineXUID = INVALID_XUID;
m_onlineXUID = INVALID_XUID;
m_bHasClientTickedOnce = false;
m_logSmallId = 0;
// Cache the first valid transport smallId because disconnect teardown can clear it before the server logger runs.
if (this->connection != NULL && this->connection->getSocket() != NULL)
{
m_logSmallId = this->connection->getSocket()->getSmallId();
}
setShowOnMaps(app.GetGameHostOption(eGameHostOption_Gamertags)!=0?true:false);
}
@@ -74,6 +97,17 @@ PlayerConnection::~PlayerConnection()
DeleteCriticalSection(&done_cs);
}
unsigned char PlayerConnection::getLogSmallId()
{
// Fall back to the live socket only while the cached value is still empty.
if (m_logSmallId == 0 && connection != NULL && connection->getSocket() != NULL)
{
m_logSmallId = connection->getSocket()->getSmallId();
}
return m_logSmallId;
}
void PlayerConnection::tick()
{
if( done ) return;
@@ -116,6 +150,14 @@ void PlayerConnection::disconnect(DisconnectPacket::eDisconnectReason reason)
return;
}
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
ServerRuntime::ServerLogManager::OnPlayerDisconnected(
getLogSmallId(),
(player != NULL) ? player->name : std::wstring(),
reason,
true);
#endif
app.DebugPrintf("PlayerConnection disconect reason: %d\n", reason );
player->disconnect();
@@ -280,6 +322,20 @@ void PlayerConnection::handleMovePlayer(shared_ptr<MovePlayerPacket> packet)
}
*/
// Anti-cheat: reject movement packets that exceed server-authoritative bounds.
double velocitySq = player->xd * player->xd + player->yd * player->yd + player->zd * player->zd;
double maxAllowedSq = kMoveBaseAllowanceSq + (velocitySq * kMoveVelocityAllowanceScale);
if (player->isAllowedToFly() || player->gameMode->isCreative())
{
// Creative / flight-allowed players can move farther legitimately per tick.
maxAllowedSq *= 1.5;
}
if (dist > maxAllowedSq)
{
disconnect(DisconnectPacket::eDisconnect_MovedTooQuickly);
return;
}
float r = 1 / 16.0f;
bool oldOk = level->getCubes(player, player->bb->copy()->shrink(r, r, r))->empty();
@@ -306,8 +362,8 @@ void PlayerConnection::handleMovePlayer(shared_ptr<MovePlayerPacket> packet)
xDist = xt - player->x;
yDist = yt - player->y;
// 4J-PB - line below will always be true!
if (yDist > -0.5 || yDist < 0.5)
// Clamp tiny Y drift noise to reduce false positives.
if (yDist > -0.5 && yDist < 0.5)
{
yDist = 0;
}
@@ -428,7 +484,8 @@ void PlayerConnection::handlePlayerAction(shared_ptr<PlayerActionPacket> packet)
if (packet->action == PlayerActionPacket::START_DESTROY_BLOCK)
{
if (true) player->gameMode->startDestroyBlock(x, y, z, packet->face); // 4J - condition was !server->isUnderSpawnProtection(level, x, y, z, player) (from Java 1.6.4) but putting back to old behaviour
// Anti-cheat: validate spawn protection on the server for mining start.
if (!server->isUnderSpawnProtection(level, x, y, z, player)) player->gameMode->startDestroyBlock(x, y, z, packet->face);
else player->connection->send( shared_ptr<TileUpdatePacket>( new TileUpdatePacket(x, y, z, level) ) );
}
@@ -456,8 +513,6 @@ void PlayerConnection::handleUseItem(shared_ptr<UseItemPacket> packet)
int face = packet->getFace();
player->resetLastActionTime();
// 4J Stu - We don't have ops, so just use the levels setting
bool canEditSpawn = level->canEditSpawn; // = level->dimension->id != 0 || server->players->isOp(player->name);
if (packet->getFace() == 255)
{
if (item == NULL) return;
@@ -467,7 +522,8 @@ void PlayerConnection::handleUseItem(shared_ptr<UseItemPacket> packet)
{
if (synched && player->distanceToSqr(x + 0.5, y + 0.5, z + 0.5) < 8 * 8)
{
if (true) // 4J - condition was !server->isUnderSpawnProtection(level, x, y, z, player) (from java 1.6.4) but putting back to old behaviour
// Anti-cheat: block placement/use must pass server-side spawn protection.
if (!server->isUnderSpawnProtection(level, x, y, z, player))
{
player->gameMode->useItemOn(player, level, item, x, y, z, face, packet->getClickX(), packet->getClickY(), packet->getClickZ());
}
@@ -536,7 +592,18 @@ void PlayerConnection::handleUseItem(shared_ptr<UseItemPacket> packet)
void PlayerConnection::onDisconnect(DisconnectPacket::eDisconnectReason reason, void *reasonObjects)
{
EnterCriticalSection(&done_cs);
if( done ) return;
if( done )
{
LeaveCriticalSection(&done_cs);
return;
}
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
ServerRuntime::ServerLogManager::OnPlayerDisconnected(
getLogSmallId(),
(player != NULL) ? player->name : std::wstring(),
reason,
false);
#endif
// logger.info(player.name + " lost connection: " + reason);
// 4J-PB - removed, since it needs to be localised in the language the client is in
//server->players->broadcastAll( shared_ptr<ChatPacket>( new ChatPacket(L"§e" + player->name + L" left the game.") ) );
@@ -754,12 +821,13 @@ void PlayerConnection::handleInteract(shared_ptr<InteractPacket> packet)
// even though the ray is blocked.
if (target != NULL) // && player->canSee(target) && player->distanceToSqr(target) < 6 * 6)
{
//boole canSee = player->canSee(target);
//double maxDist = 6 * 6;
//if (!canSee)
//{
// maxDist = 3 * 3;
//}
// Anti-cheat: enforce reach and LOS on the server to reject forged hits.
bool canSeeTarget = player->canSee(target);
double maxDistSq = canSeeTarget ? kInteractReachSq : kInteractBlockedReachSq;
if (player->distanceToSqr(target) > maxDistSq)
{
return;
}
//if (player->distanceToSqr(target) < maxDist)
//{
+5
View File
@@ -37,6 +37,7 @@ private:
int dropSpamTickCount;
bool m_bHasClientTickedOnce;
unsigned char m_logSmallId;
public:
PlayerConnection(MinecraftServer *server, Connection *connection, shared_ptr<ServerPlayer> player);
@@ -45,6 +46,10 @@ public:
void disconnect(DisconnectPacket::eDisconnectReason reason);
private:
/**
* Returns the stable network smallId used by dedicated-server logging and refreshes it from the live socket when possible
*/
unsigned char getLogSmallId();
double xLastOk, yLastOk, zLastOk;
bool synched;
+11
View File
@@ -31,6 +31,10 @@
#include "Common\Network\Sony\NetworkPlayerSony.h"
#endif
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
#include "..\Minecraft.Server\Access\Access.h"
#endif
// 4J - this class is fairly substantially altered as there didn't seem any point in porting code for banning, whitelisting, ops etc.
PlayerList::PlayerList(MinecraftServer *server)
@@ -1626,6 +1630,13 @@ bool PlayerList::isXuidBanned(PlayerUID xuid)
}
}
#if defined(MINECRAFT_SERVER_BUILD)
if (!banned)
{
banned = ServerRuntime::Access::IsPlayerBanned(xuid);
}
#endif
return banned;
}
+14 -13
View File
@@ -183,24 +183,25 @@ void ServerPlayerGameMode::stopDestroyBlock(int x, int y, int z)
{
Tile *tile = Tile::tiles[t];
// MGH - removed checking for the destroy progress here, it has already been checked on the client before it sent the packet.
// fixes issues with this failing to destroy because of packets bunching up
// float destroyProgress = tile->getDestroyProgress(player, player->level, x, y, z) * (ticksSpentDestroying + 1);
// if (destroyProgress >= .7f || bIgnoreDestroyProgress)
// Anti-cheat: re-check destroy progress on the server for STOP_DESTROY.
int ticksSpentDestroying = gameTicks - destroyProgressStart;
float destroyProgress = tile->getDestroyProgress(player, player->level, x, y, z) * (ticksSpentDestroying + 1);
if (destroyProgress >= 1.0f)
{
isDestroyingBlock = false;
level->destroyTileProgress(player->entityId, x, y, z, -1);
destroyBlock(x, y, z);
}
// else if (!hasDelayedDestroy)
// {
// isDestroyingBlock = false;
// hasDelayedDestroy = true;
// delayedDestroyX = x;
// delayedDestroyY = y;
// delayedDestroyZ = z;
// delayedTickStart = destroyProgressStart;
// }
else if (!hasDelayedDestroy)
{
// Keep server-authoritative mining while allowing legit latency to finish via delayed tick progression.
isDestroyingBlock = false;
hasDelayedDestroy = true;
delayedDestroyX = x;
delayedDestroyY = y;
delayedDestroyZ = z;
delayedTickStart = destroyProgressStart;
}
}
}
}
+460
View File
@@ -0,0 +1,460 @@
#include "stdafx.h"
#include "Access.h"
#include "../Common/StringUtils.h"
#include "../ServerLogger.h"
#include <memory>
#include <mutex>
namespace ServerRuntime
{
namespace Access
{
namespace
{
/**
* **Access State**
*
* These features are used extensively from various parts of the code, so safe read/write handling is implemented
* Stores the published BAN manager snapshot plus a writer gate for clone-and-publish updates
* 公開中のBanManagerスナップショットと更新直列化用ロックを保持する
*/
struct AccessState
{
std::mutex stateLock;
std::mutex writeLock;
std::shared_ptr<BanManager> banManager;
std::shared_ptr<WhitelistManager> whitelistManager;
bool whitelistEnabled;
};
AccessState g_accessState;
/**
* Copies the currently published manager pointer so readers can work without holding the publish mutex
* 公開中のBanManager共有ポインタを複製取得する
*/
static std::shared_ptr<BanManager> GetBanManagerSnapshot()
{
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
return g_accessState.banManager;
}
/**
* Replaces the shared manager pointer with a fully prepared snapshot in one short critical section
* 準備完了したBanManagerスナップショットを短いロックで公開する
*/
static void PublishBanManagerSnapshot(const std::shared_ptr<BanManager> &banManager)
{
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
g_accessState.banManager = banManager;
}
static std::shared_ptr<WhitelistManager> GetWhitelistManagerSnapshot()
{
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
return g_accessState.whitelistManager;
}
static void PublishWhitelistManagerSnapshot(const std::shared_ptr<WhitelistManager> &whitelistManager)
{
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
g_accessState.whitelistManager = whitelistManager;
}
}
std::string FormatXuid(PlayerUID xuid)
{
if (xuid == INVALID_XUID)
{
return "";
}
char buffer[32] = {};
sprintf_s(buffer, sizeof(buffer), "0x%016llx", (unsigned long long)xuid);
return buffer;
}
bool TryParseXuid(const std::string &text, PlayerUID *outXuid)
{
if (outXuid == nullptr)
{
return false;
}
unsigned long long parsed = 0;
if (!StringUtils::TryParseUnsignedLongLong(text, &parsed) || parsed == 0ULL)
{
return false;
}
*outXuid = (PlayerUID)parsed;
return true;
}
bool Initialize(const std::string &baseDirectory, bool whitelistEnabled)
{
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
// Build the replacement manager privately so readers keep using the last published snapshot during disk I/O.
std::shared_ptr<BanManager> banManager = std::make_shared<BanManager>(baseDirectory);
std::shared_ptr<WhitelistManager> whitelistManager = std::make_shared<WhitelistManager>(baseDirectory);
if (!banManager->EnsureBanFilesExist())
{
LogError("access", "failed to ensure dedicated server ban files exist");
return false;
}
if (!whitelistManager->EnsureWhitelistFileExists())
{
LogError("access", "failed to ensure dedicated server whitelist file exists");
return false;
}
if (!banManager->Reload())
{
LogError("access", "failed to load dedicated server ban files");
return false;
}
if (!whitelistManager->Reload())
{
LogError("access", "failed to load dedicated server whitelist file");
return false;
}
std::vector<BannedPlayerEntry> playerEntries;
std::vector<BannedIpEntry> ipEntries;
std::vector<WhitelistedPlayerEntry> whitelistEntries;
banManager->SnapshotBannedPlayers(&playerEntries);
banManager->SnapshotBannedIps(&ipEntries);
whitelistManager->SnapshotWhitelistedPlayers(&whitelistEntries);
PublishBanManagerSnapshot(banManager);
PublishWhitelistManagerSnapshot(whitelistManager);
{
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
g_accessState.whitelistEnabled = whitelistEnabled;
}
LogInfof(
"access",
"loaded %u player bans, %u ip bans, and %u whitelist entries (whitelist=%s)",
(unsigned)playerEntries.size(),
(unsigned)ipEntries.size(),
(unsigned)whitelistEntries.size(),
whitelistEnabled ? "enabled" : "disabled");
return true;
}
void Shutdown()
{
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
PublishWhitelistManagerSnapshot(std::shared_ptr<WhitelistManager>());
PublishWhitelistManagerSnapshot(std::shared_ptr<WhitelistManager>());
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
g_accessState.whitelistEnabled = false;
}
bool Reload()
{
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
std::shared_ptr<BanManager> current = GetBanManagerSnapshot();
std::shared_ptr<WhitelistManager> currentWhitelist = GetWhitelistManagerSnapshot();
if (current == nullptr || currentWhitelist == nullptr)
{
return false;
}
std::shared_ptr<BanManager> banManager = std::make_shared<BanManager>(*current);
std::shared_ptr<WhitelistManager> whitelistManager = std::make_shared<WhitelistManager>(*currentWhitelist);
if (!banManager->EnsureBanFilesExist())
{
return false;
}
if (!whitelistManager->EnsureWhitelistFileExists())
{
return false;
}
if (!banManager->Reload())
{
return false;
}
if (!whitelistManager->Reload())
{
return false;
}
PublishBanManagerSnapshot(banManager);
PublishWhitelistManagerSnapshot(whitelistManager);
return true;
}
bool ReloadWhitelist()
{
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
const auto current = GetWhitelistManagerSnapshot();
if (current == nullptr)
{
return false;
}
auto whitelistManager = std::make_shared<WhitelistManager>(*current);
if (!whitelistManager->EnsureWhitelistFileExists())
{
return false;
}
if (!whitelistManager->Reload())
{
return false;
}
PublishWhitelistManagerSnapshot(whitelistManager);
return true;
}
bool IsInitialized()
{
return GetBanManagerSnapshot() != nullptr && GetWhitelistManagerSnapshot() != nullptr;
}
bool IsWhitelistEnabled()
{
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
return g_accessState.whitelistEnabled;
}
void SetWhitelistEnabled(bool enabled)
{
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
g_accessState.whitelistEnabled = enabled;
}
bool IsPlayerBanned(PlayerUID xuid)
{
const std::string formatted = FormatXuid(xuid);
if (formatted.empty())
{
return false;
}
std::shared_ptr<BanManager> banManager = GetBanManagerSnapshot();
return (banManager != nullptr) ? banManager->IsPlayerBannedByXuid(formatted) : false;
}
bool IsIpBanned(const std::string &ip)
{
std::shared_ptr<BanManager> banManager = GetBanManagerSnapshot();
return (banManager != nullptr) ? banManager->IsIpBanned(ip) : false;
}
bool IsPlayerWhitelisted(PlayerUID xuid)
{
const std::string formatted = FormatXuid(xuid);
if (formatted.empty())
{
return false;
}
std::shared_ptr<WhitelistManager> whitelistManager = GetWhitelistManagerSnapshot();
return (whitelistManager != nullptr) ? whitelistManager->IsPlayerWhitelistedByXuid(formatted) : false;
}
bool AddPlayerBan(PlayerUID xuid, const std::string &name, const BanMetadata &metadata)
{
const std::string formatted = FormatXuid(xuid);
if (formatted.empty())
{
return false;
}
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
std::shared_ptr<BanManager> current = GetBanManagerSnapshot();
if (current == nullptr)
{
return false;
}
std::shared_ptr<BanManager> banManager = std::make_shared<BanManager>(*current);
BannedPlayerEntry entry;
entry.xuid = formatted;
entry.name = name;
entry.metadata = metadata;
if (!banManager->AddPlayerBan(entry))
{
return false;
}
PublishBanManagerSnapshot(banManager);
return true;
}
bool AddIpBan(const std::string &ip, const BanMetadata &metadata)
{
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
std::shared_ptr<BanManager> current = GetBanManagerSnapshot();
if (current == nullptr)
{
return false;
}
std::shared_ptr<BanManager> banManager = std::make_shared<BanManager>(*current);
BannedIpEntry entry;
entry.ip = ip;
entry.metadata = metadata;
if (!banManager->AddIpBan(entry))
{
return false;
}
PublishBanManagerSnapshot(banManager);
return true;
}
bool RemovePlayerBan(PlayerUID xuid)
{
const std::string formatted = FormatXuid(xuid);
if (formatted.empty())
{
return false;
}
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
std::shared_ptr<BanManager> current = GetBanManagerSnapshot();
if (current == nullptr)
{
return false;
}
std::shared_ptr<BanManager> banManager = std::make_shared<BanManager>(*current);
if (!banManager->RemovePlayerBanByXuid(formatted))
{
return false;
}
PublishBanManagerSnapshot(banManager);
return true;
}
bool RemoveIpBan(const std::string &ip)
{
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
std::shared_ptr<BanManager> current = GetBanManagerSnapshot();
if (current == nullptr)
{
return false;
}
std::shared_ptr<BanManager> banManager = std::make_shared<BanManager>(*current);
if (!banManager->RemoveIpBan(ip))
{
return false;
}
PublishBanManagerSnapshot(banManager);
return true;
}
bool AddWhitelistedPlayer(PlayerUID xuid, const std::string &name, const WhitelistMetadata &metadata)
{
const auto formatted = FormatXuid(xuid);
if (formatted.empty())
{
return false;
}
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
const auto current = GetWhitelistManagerSnapshot();
if (current == nullptr)
{
return false;
}
auto whitelistManager = std::make_shared<WhitelistManager>(*current);
const WhitelistedPlayerEntry entry = { formatted, name, metadata };
if (!whitelistManager->AddPlayer(entry))
{
return false;
}
PublishWhitelistManagerSnapshot(whitelistManager);
return true;
}
bool RemoveWhitelistedPlayer(PlayerUID xuid)
{
const auto formatted = FormatXuid(xuid);
if (formatted.empty())
{
return false;
}
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
const auto current = GetWhitelistManagerSnapshot();
if (current == nullptr)
{
return false;
}
auto whitelistManager = std::make_shared<WhitelistManager>(*current);
if (!whitelistManager->RemovePlayerByXuid(formatted))
{
return false;
}
PublishWhitelistManagerSnapshot(whitelistManager);
return true;
}
bool SnapshotBannedPlayers(std::vector<BannedPlayerEntry> *outEntries)
{
if (outEntries == nullptr)
{
return false;
}
std::shared_ptr<BanManager> banManager = GetBanManagerSnapshot();
if (banManager == nullptr)
{
outEntries->clear();
return false;
}
return banManager->SnapshotBannedPlayers(outEntries);
}
bool SnapshotBannedIps(std::vector<BannedIpEntry> *outEntries)
{
if (outEntries == nullptr)
{
return false;
}
std::shared_ptr<BanManager> banManager = GetBanManagerSnapshot();
if (banManager == nullptr)
{
outEntries->clear();
return false;
}
return banManager->SnapshotBannedIps(outEntries);
}
bool SnapshotWhitelistedPlayers(std::vector<WhitelistedPlayerEntry> *outEntries)
{
if (outEntries == nullptr)
{
return false;
}
const auto whitelistManager = GetWhitelistManagerSnapshot();
if (whitelistManager == nullptr)
{
outEntries->clear();
return false;
}
return whitelistManager->SnapshotWhitelistedPlayers(outEntries);
}
}
}
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include "BanManager.h"
#include "WhitelistManager.h"
namespace ServerRuntime
{
/**
* A frontend that will be general-purpose, assuming the implementation of whitelists and ops in the future.
*/
namespace Access
{
bool Initialize(const std::string &baseDirectory = ".", bool whitelistEnabled = false);
void Shutdown();
bool Reload();
bool ReloadWhitelist();
bool IsInitialized();
bool IsWhitelistEnabled();
void SetWhitelistEnabled(bool enabled);
bool IsPlayerBanned(PlayerUID xuid);
bool IsIpBanned(const std::string &ip);
bool IsPlayerWhitelisted(PlayerUID xuid);
bool AddPlayerBan(PlayerUID xuid, const std::string &name, const BanMetadata &metadata);
bool AddIpBan(const std::string &ip, const BanMetadata &metadata);
bool RemovePlayerBan(PlayerUID xuid);
bool RemoveIpBan(const std::string &ip);
bool AddWhitelistedPlayer(PlayerUID xuid, const std::string &name, const WhitelistMetadata &metadata);
bool RemoveWhitelistedPlayer(PlayerUID xuid);
/**
* Copies the current cached player bans for inspection or command output
* 現在のプレイヤーBAN一覧を複製取得
*/
bool SnapshotBannedPlayers(std::vector<BannedPlayerEntry> *outEntries);
/**
* Copies the current cached IP bans for inspection or command output
* 現在のIP BAN一覧を複製取得
*/
bool SnapshotBannedIps(std::vector<BannedIpEntry> *outEntries);
bool SnapshotWhitelistedPlayers(std::vector<WhitelistedPlayerEntry> *outEntries);
std::string FormatXuid(PlayerUID xuid);
bool TryParseXuid(const std::string &text, PlayerUID *outXuid);
}
}
+682
View File
@@ -0,0 +1,682 @@
#include "stdafx.h"
#include "BanManager.h"
#include "../Common/AccessStorageUtils.h"
#include "../Common/FileUtils.h"
#include "../Common/NetworkUtils.h"
#include "../Common/StringUtils.h"
#include "../ServerLogger.h"
#include "../vendor/rapidjson/document.h"
#include "../vendor/rapidjson/error/en.h"
#include "../vendor/rapidjson/prettywriter.h"
#include "../vendor/rapidjson/stringbuffer.h"
#include <algorithm>
#include <stdio.h>
namespace ServerRuntime
{
namespace Access
{
namespace
{
static const char *kBannedPlayersFileName = "banned-players.json";
static const char *kBannedIpsFileName = "banned-ips.json";
static bool TryParseUtcTimestamp(const std::string &text, unsigned long long *outFileTime)
{
if (outFileTime == nullptr)
{
return false;
}
std::string trimmed = StringUtils::TrimAscii(text);
if (trimmed.empty())
{
return false;
}
unsigned year = 0;
unsigned month = 0;
unsigned day = 0;
unsigned hour = 0;
unsigned minute = 0;
unsigned second = 0;
if (sscanf_s(trimmed.c_str(), "%4u-%2u-%2uT%2u:%2u:%2uZ", &year, &month, &day, &hour, &minute, &second) != 6)
{
return false;
}
SYSTEMTIME utc = {};
utc.wYear = (WORD)year;
utc.wMonth = (WORD)month;
utc.wDay = (WORD)day;
utc.wHour = (WORD)hour;
utc.wMinute = (WORD)minute;
utc.wSecond = (WORD)second;
FILETIME fileTime = {};
if (!SystemTimeToFileTime(&utc, &fileTime))
{
return false;
}
ULARGE_INTEGER value = {};
value.LowPart = fileTime.dwLowDateTime;
value.HighPart = fileTime.dwHighDateTime;
*outFileTime = value.QuadPart;
return true;
}
static bool IsMetadataExpired(const BanMetadata &metadata, unsigned long long nowFileTime)
{
if (metadata.expires.empty())
{
return false;
}
unsigned long long expiresFileTime = 0;
if (!TryParseUtcTimestamp(metadata.expires, &expiresFileTime))
{
// Keep malformed metadata active instead of silently unbanning a player or address.
return false;
}
return expiresFileTime <= nowFileTime;
}
}
BanManager::BanManager(const std::string &baseDirectory)
: m_baseDirectory(baseDirectory.empty() ? "." : baseDirectory)
{
}
bool BanManager::EnsureBanFilesExist() const
{
const std::string playersPath = GetBannedPlayersFilePath();
const std::string ipsPath = GetBannedIpsFilePath();
const bool playersOk = AccessStorageUtils::EnsureJsonListFileExists(playersPath);
const bool ipsOk = AccessStorageUtils::EnsureJsonListFileExists(ipsPath);
if (!playersOk)
{
LogErrorf("access", "failed to create %s", playersPath.c_str());
}
if (!ipsOk)
{
LogErrorf("access", "failed to create %s", ipsPath.c_str());
}
return playersOk && ipsOk;
}
bool BanManager::Reload()
{
std::vector<BannedPlayerEntry> players;
std::vector<BannedIpEntry> ips;
if (!LoadPlayers(&players))
{
return false;
}
if (!LoadIps(&ips))
{
return false;
}
m_bannedPlayers.swap(players);
m_bannedIps.swap(ips);
return true;
}
bool BanManager::Save() const
{
std::vector<BannedPlayerEntry> players;
std::vector<BannedIpEntry> ips;
return SnapshotBannedPlayers(&players) &&
SnapshotBannedIps(&ips) &&
SavePlayers(players) &&
SaveIps(ips);
}
bool BanManager::LoadPlayers(std::vector<BannedPlayerEntry> *outEntries) const
{
if (outEntries == nullptr)
{
return false;
}
outEntries->clear();
std::string text;
const std::string path = GetBannedPlayersFilePath();
if (!FileUtils::ReadTextFile(path, &text))
{
LogErrorf("access", "failed to read %s", path.c_str());
return false;
}
if (text.empty())
{
text = "[]";
}
// Strip an optional UTF-8 BOM because some editors prepend it when rewriting JSON files.
const std::string stripped = StringUtils::StripUtf8Bom(text);
rapidjson::Document root;
const rapidjson::ParseResult parseResult = root.Parse(stripped.c_str());
if (!parseResult)
{
LogErrorf(
"access",
"failed to parse %s: %s (offset %zu)",
path.c_str(),
rapidjson::GetParseError_En(parseResult.Code()),
parseResult.Offset());
return false;
}
if (!root.IsArray())
{
LogErrorf("access", "failed to parse %s: root json value is not an array", path.c_str());
return false;
}
const unsigned long long nowFileTime = FileUtils::GetCurrentUtcFileTime();
for (const auto &object : root.GetArray())
{
if (!object.IsObject())
{
LogWarnf("access", "skipping banned player entry that is not an object in %s", path.c_str());
continue;
}
std::string rawXuid;
if (!AccessStorageUtils::TryGetStringField(object, "xuid", &rawXuid))
{
LogWarnf("access", "skipping banned player entry without xuid in %s", path.c_str());
continue;
}
BannedPlayerEntry entry;
entry.xuid = AccessStorageUtils::NormalizeXuid(rawXuid);
if (entry.xuid.empty())
{
LogWarnf("access", "skipping banned player entry with empty xuid in %s", path.c_str());
continue;
}
AccessStorageUtils::TryGetStringField(object, "name", &entry.name);
AccessStorageUtils::TryGetStringField(object, "created", &entry.metadata.created);
AccessStorageUtils::TryGetStringField(object, "source", &entry.metadata.source);
AccessStorageUtils::TryGetStringField(object, "expires", &entry.metadata.expires);
AccessStorageUtils::TryGetStringField(object, "reason", &entry.metadata.reason);
NormalizeMetadata(&entry.metadata);
// Ignore entries that already expired before reload so the in-memory cache starts from the active set.
if (IsMetadataExpired(entry.metadata, nowFileTime))
{
continue;
}
outEntries->push_back(entry);
}
return true;
}
bool BanManager::LoadIps(std::vector<BannedIpEntry> *outEntries) const
{
if (outEntries == nullptr)
{
return false;
}
outEntries->clear();
std::string text;
const std::string path = GetBannedIpsFilePath();
if (!FileUtils::ReadTextFile(path, &text))
{
LogErrorf("access", "failed to read %s", path.c_str());
return false;
}
if (text.empty())
{
text = "[]";
}
// Strip an optional UTF-8 BOM because some editors prepend it when rewriting JSON files.
const std::string stripped = StringUtils::StripUtf8Bom(text);
rapidjson::Document root;
const rapidjson::ParseResult parseResult = root.Parse(stripped.c_str());
if (!parseResult)
{
LogErrorf(
"access",
"failed to parse %s: %s (offset %zu)",
path.c_str(),
rapidjson::GetParseError_En(parseResult.Code()),
parseResult.Offset());
return false;
}
if (!root.IsArray())
{
LogErrorf("access", "failed to parse %s: root json value is not an array", path.c_str());
return false;
}
const unsigned long long nowFileTime = FileUtils::GetCurrentUtcFileTime();
for (const auto &object : root.GetArray())
{
if (!object.IsObject())
{
LogWarnf("access", "skipping banned ip entry that is not an object in %s", path.c_str());
continue;
}
std::string rawIp;
if (!AccessStorageUtils::TryGetStringField(object, "ip", &rawIp))
{
LogWarnf("access", "skipping banned ip entry without ip in %s", path.c_str());
continue;
}
BannedIpEntry entry;
entry.ip = NetworkUtils::NormalizeIpToken(rawIp);
if (entry.ip.empty())
{
LogWarnf("access", "skipping banned ip entry with empty ip in %s", path.c_str());
continue;
}
AccessStorageUtils::TryGetStringField(object, "created", &entry.metadata.created);
AccessStorageUtils::TryGetStringField(object, "source", &entry.metadata.source);
AccessStorageUtils::TryGetStringField(object, "expires", &entry.metadata.expires);
AccessStorageUtils::TryGetStringField(object, "reason", &entry.metadata.reason);
NormalizeMetadata(&entry.metadata);
// Ignore entries that already expired before reload so the in-memory cache starts from the active set.
if (IsMetadataExpired(entry.metadata, nowFileTime))
{
continue;
}
outEntries->push_back(entry);
}
return true;
}
bool BanManager::SavePlayers(const std::vector<BannedPlayerEntry> &entries) const
{
rapidjson::Document root(rapidjson::kArrayType);
auto &allocator = root.GetAllocator();
for (const auto &entry : entries)
{
const std::string normalizedXuid = AccessStorageUtils::NormalizeXuid(entry.xuid);
rapidjson::Value object(rapidjson::kObjectType);
object.AddMember(
"xuid", rapidjson::Value(normalizedXuid.c_str(), static_cast<rapidjson::SizeType>(normalizedXuid.size()), allocator), allocator);
object.AddMember(
"name", rapidjson::Value(entry.name.c_str(), static_cast<rapidjson::SizeType>(entry.name.size()), allocator), allocator);
object.AddMember(
"created",
rapidjson::Value(entry.metadata.created.c_str(), static_cast<rapidjson::SizeType>(entry.metadata.created.size()), allocator),
allocator);
object.AddMember(
"source",
rapidjson::Value(entry.metadata.source.c_str(), static_cast<rapidjson::SizeType>(entry.metadata.source.size()), allocator),
allocator);
object.AddMember(
"expires",
rapidjson::Value(entry.metadata.expires.c_str(), static_cast<rapidjson::SizeType>(entry.metadata.expires.size()), allocator),
allocator);
object.AddMember(
"reason",
rapidjson::Value(entry.metadata.reason.c_str(), static_cast<rapidjson::SizeType>(entry.metadata.reason.size()), allocator),
allocator);
root.PushBack(object, allocator);
}
rapidjson::StringBuffer buffer;
rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
writer.SetIndent(' ', 2);
root.Accept(writer);
const std::string path = GetBannedPlayersFilePath();
const std::string json = root.Empty() ? std::string("[]\n") : (std::string(buffer.GetString(), buffer.GetSize()) + "\n");
if (!FileUtils::WriteTextFileAtomic(path, json))
{
LogErrorf("access", "failed to write %s", path.c_str());
return false;
}
return true;
}
bool BanManager::SaveIps(const std::vector<BannedIpEntry> &entries) const
{
rapidjson::Document root(rapidjson::kArrayType);
auto &allocator = root.GetAllocator();
for (const auto &entry : entries)
{
const std::string normalizedIp = NetworkUtils::NormalizeIpToken(entry.ip);
rapidjson::Value object(rapidjson::kObjectType);
object.AddMember(
"ip", rapidjson::Value(normalizedIp.c_str(), static_cast<rapidjson::SizeType>(normalizedIp.size()), allocator), allocator);
object.AddMember(
"created",
rapidjson::Value(entry.metadata.created.c_str(), static_cast<rapidjson::SizeType>(entry.metadata.created.size()), allocator),
allocator);
object.AddMember(
"source",
rapidjson::Value(entry.metadata.source.c_str(), static_cast<rapidjson::SizeType>(entry.metadata.source.size()), allocator),
allocator);
object.AddMember(
"expires",
rapidjson::Value(entry.metadata.expires.c_str(), static_cast<rapidjson::SizeType>(entry.metadata.expires.size()), allocator),
allocator);
object.AddMember(
"reason",
rapidjson::Value(entry.metadata.reason.c_str(), static_cast<rapidjson::SizeType>(entry.metadata.reason.size()), allocator),
allocator);
root.PushBack(object, allocator);
}
rapidjson::StringBuffer buffer;
rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
writer.SetIndent(' ', 2);
root.Accept(writer);
const std::string path = GetBannedIpsFilePath();
const std::string json = root.Empty() ? std::string("[]\n") : (std::string(buffer.GetString(), buffer.GetSize()) + "\n");
if (!FileUtils::WriteTextFileAtomic(path, json))
{
LogErrorf("access", "failed to write %s", path.c_str());
return false;
}
return true;
}
const std::vector<BannedPlayerEntry> &BanManager::GetBannedPlayers() const
{
return m_bannedPlayers;
}
const std::vector<BannedIpEntry> &BanManager::GetBannedIps() const
{
return m_bannedIps;
}
bool BanManager::SnapshotBannedPlayers(std::vector<BannedPlayerEntry> *outEntries) const
{
if (outEntries == nullptr)
{
return false;
}
outEntries->clear();
outEntries->reserve(m_bannedPlayers.size());
const unsigned long long nowFileTime = FileUtils::GetCurrentUtcFileTime();
for (const auto &entry : m_bannedPlayers)
{
if (!IsMetadataExpired(entry.metadata, nowFileTime))
{
outEntries->push_back(entry);
}
}
return true;
}
bool BanManager::SnapshotBannedIps(std::vector<BannedIpEntry> *outEntries) const
{
if (outEntries == nullptr)
{
return false;
}
outEntries->clear();
outEntries->reserve(m_bannedIps.size());
const unsigned long long nowFileTime = FileUtils::GetCurrentUtcFileTime();
for (const auto &entry : m_bannedIps)
{
if (!IsMetadataExpired(entry.metadata, nowFileTime))
{
outEntries->push_back(entry);
}
}
return true;
}
bool BanManager::IsPlayerBannedByXuid(const std::string &xuid) const
{
const std::string normalized = AccessStorageUtils::NormalizeXuid(xuid);
if (normalized.empty())
{
return false;
}
const unsigned long long nowFileTime = FileUtils::GetCurrentUtcFileTime();
return std::any_of(
m_bannedPlayers.begin(),
m_bannedPlayers.end(),
[&normalized, nowFileTime](const BannedPlayerEntry &entry)
{
return entry.xuid == normalized && !IsMetadataExpired(entry.metadata, nowFileTime);
});
}
bool BanManager::IsIpBanned(const std::string &ip) const
{
const std::string normalized = NetworkUtils::NormalizeIpToken(ip);
if (normalized.empty())
{
return false;
}
const unsigned long long nowFileTime = FileUtils::GetCurrentUtcFileTime();
return std::any_of(
m_bannedIps.begin(),
m_bannedIps.end(),
[&normalized, nowFileTime](const BannedIpEntry &entry)
{
return entry.ip == normalized && !IsMetadataExpired(entry.metadata, nowFileTime);
});
}
bool BanManager::AddPlayerBan(const BannedPlayerEntry &entry)
{
std::vector<BannedPlayerEntry> updatedEntries;
if (!SnapshotBannedPlayers(&updatedEntries))
{
return false;
}
BannedPlayerEntry normalized = entry;
normalized.xuid = AccessStorageUtils::NormalizeXuid(normalized.xuid);
NormalizeMetadata(&normalized.metadata);
if (normalized.xuid.empty())
{
return false;
}
const auto existing = std::find_if(
updatedEntries.begin(),
updatedEntries.end(),
[&normalized](const BannedPlayerEntry &candidate)
{
return candidate.xuid == normalized.xuid;
});
if (existing != updatedEntries.end())
{
// Update the existing entry in place so the stored list remains unique by canonical XUID.
*existing = normalized;
if (!SavePlayers(updatedEntries))
{
return false;
}
m_bannedPlayers.swap(updatedEntries);
return true;
}
updatedEntries.push_back(normalized);
if (!SavePlayers(updatedEntries))
{
return false;
}
m_bannedPlayers.swap(updatedEntries);
return true;
}
bool BanManager::AddIpBan(const BannedIpEntry &entry)
{
std::vector<BannedIpEntry> updatedEntries;
if (!SnapshotBannedIps(&updatedEntries))
{
return false;
}
BannedIpEntry normalized = entry;
normalized.ip = NetworkUtils::NormalizeIpToken(normalized.ip);
NormalizeMetadata(&normalized.metadata);
if (normalized.ip.empty())
{
return false;
}
const auto existing = std::find_if(
updatedEntries.begin(),
updatedEntries.end(),
[&normalized](const BannedIpEntry &candidate)
{
return candidate.ip == normalized.ip;
});
if (existing != updatedEntries.end())
{
// Update the existing entry in place so the stored list remains unique by normalized IP.
*existing = normalized;
if (!SaveIps(updatedEntries))
{
return false;
}
m_bannedIps.swap(updatedEntries);
return true;
}
updatedEntries.push_back(normalized);
if (!SaveIps(updatedEntries))
{
return false;
}
m_bannedIps.swap(updatedEntries);
return true;
}
bool BanManager::RemovePlayerBanByXuid(const std::string &xuid)
{
const std::string normalized = AccessStorageUtils::NormalizeXuid(xuid);
if (normalized.empty())
{
return false;
}
std::vector<BannedPlayerEntry> updatedEntries;
if (!SnapshotBannedPlayers(&updatedEntries))
{
return false;
}
size_t oldSize = updatedEntries.size();
updatedEntries.erase(
std::remove_if(
updatedEntries.begin(),
updatedEntries.end(),
[&normalized](const BannedPlayerEntry &entry) { return entry.xuid == normalized; }),
updatedEntries.end());
if (updatedEntries.size() == oldSize)
{
return false;
}
if (!SavePlayers(updatedEntries))
{
return false;
}
m_bannedPlayers.swap(updatedEntries);
return true;
}
bool BanManager::RemoveIpBan(const std::string &ip)
{
const std::string normalized = NetworkUtils::NormalizeIpToken(ip);
if (normalized.empty())
{
return false;
}
std::vector<BannedIpEntry> updatedEntries;
if (!SnapshotBannedIps(&updatedEntries))
{
return false;
}
size_t oldSize = updatedEntries.size();
updatedEntries.erase(
std::remove_if(
updatedEntries.begin(),
updatedEntries.end(),
[&normalized](const BannedIpEntry &entry) { return entry.ip == normalized; }),
updatedEntries.end());
if (updatedEntries.size() == oldSize)
{
return false;
}
if (!SaveIps(updatedEntries))
{
return false;
}
m_bannedIps.swap(updatedEntries);
return true;
}
std::string BanManager::GetBannedPlayersFilePath() const
{
return BuildPath(kBannedPlayersFileName);
}
std::string BanManager::GetBannedIpsFilePath() const
{
return BuildPath(kBannedIpsFileName);
}
BanMetadata BanManager::BuildDefaultMetadata(const char *source)
{
BanMetadata metadata;
metadata.created = StringUtils::GetCurrentUtcTimestampIso8601();
metadata.source = (source != nullptr) ? source : "Server";
metadata.expires = "";
metadata.reason = "";
return metadata;
}
void BanManager::NormalizeMetadata(BanMetadata *metadata)
{
if (metadata == nullptr)
{
return;
}
metadata->created = StringUtils::TrimAscii(metadata->created);
metadata->source = StringUtils::TrimAscii(metadata->source);
metadata->expires = StringUtils::TrimAscii(metadata->expires);
metadata->reason = StringUtils::TrimAscii(metadata->reason);
}
std::string BanManager::BuildPath(const char *fileName) const
{
return AccessStorageUtils::BuildPathFromBaseDirectory(m_baseDirectory, fileName);
}
}
}
+106
View File
@@ -0,0 +1,106 @@
#pragma once
#include <string>
#include <vector>
namespace ServerRuntime
{
namespace Access
{
/**
* Information shared with player bans and IP bans
* プレイヤーBANとIP BANで共有する情報
*/
struct BanMetadata
{
std::string created;
std::string source;
std::string expires;
std::string reason;
};
struct BannedPlayerEntry
{
std::string xuid;
std::string name;
BanMetadata metadata;
};
struct BannedIpEntry
{
std::string ip;
BanMetadata metadata;
};
/**
* Dedicated server BAN file manager.
*
* Files:
* - banned-players.json
* - banned-ips.json
*
* This class only handles storage/caching.
* Connection-time hooks are wired separately.
*/
class BanManager
{
public:
/**
* **Create Ban Manager**
*
* Binds the manager to the directory that stores the dedicated-server access files
* Dedicated Server のアクセスファイル配置先を設定する
*/
explicit BanManager(const std::string &baseDirectory = ".");
/**
* Creates empty JSON array files when the dedicated server starts without persisted access data
* BANファイルが無い初回起動時に空JSONを用意する
*/
bool EnsureBanFilesExist() const;
bool Reload();
bool Save() const;
bool LoadPlayers(std::vector<BannedPlayerEntry> *outEntries) const;
bool LoadIps(std::vector<BannedIpEntry> *outEntries) const;
bool SavePlayers(const std::vector<BannedPlayerEntry> &entries) const;
bool SaveIps(const std::vector<BannedIpEntry> &entries) const;
const std::vector<BannedPlayerEntry> &GetBannedPlayers() const;
const std::vector<BannedIpEntry> &GetBannedIps() const;
/**
* Copies only currently active player BAN entries so expired metadata does not leak into command output
* 期限切れを除いた有効なプレイヤーBAN一覧を複製取得する
*/
bool SnapshotBannedPlayers(std::vector<BannedPlayerEntry> *outEntries) const;
/**
* Copies only currently active IP BAN entries so expired metadata does not leak into command output
* 期限切れを除いた有効なIP BAN一覧を複製取得する
*/
bool SnapshotBannedIps(std::vector<BannedIpEntry> *outEntries) const;
bool IsPlayerBannedByXuid(const std::string &xuid) const;
bool IsIpBanned(const std::string &ip) const;
bool AddPlayerBan(const BannedPlayerEntry &entry);
bool AddIpBan(const BannedIpEntry &entry);
bool RemovePlayerBanByXuid(const std::string &xuid);
bool RemoveIpBan(const std::string &ip);
std::string GetBannedPlayersFilePath() const;
std::string GetBannedIpsFilePath() const;
static BanMetadata BuildDefaultMetadata(const char *source = "Server");
private:
static void NormalizeMetadata(BanMetadata *metadata);
std::string BuildPath(const char *fileName) const;
private:
std::string m_baseDirectory;
std::vector<BannedPlayerEntry> m_bannedPlayers;
std::vector<BannedIpEntry> m_bannedIps;
};
}
}
@@ -0,0 +1,318 @@
#include "stdafx.h"
#include "WhitelistManager.h"
#include "../Common/AccessStorageUtils.h"
#include "../Common/FileUtils.h"
#include "../Common/StringUtils.h"
#include "../ServerLogger.h"
#include "../vendor/rapidjson/document.h"
#include "../vendor/rapidjson/error/en.h"
#include "../vendor/rapidjson/prettywriter.h"
#include "../vendor/rapidjson/stringbuffer.h"
#include <algorithm>
namespace ServerRuntime
{
namespace Access
{
namespace
{
static const char *kWhitelistFileName = "whitelist.json";
}
WhitelistManager::WhitelistManager(const std::string &baseDirectory)
: m_baseDirectory(baseDirectory.empty() ? "." : baseDirectory)
{
}
bool WhitelistManager::EnsureWhitelistFileExists() const
{
const std::string path = GetWhitelistFilePath();
if (!AccessStorageUtils::EnsureJsonListFileExists(path))
{
LogErrorf("access", "failed to create %s", path.c_str());
return false;
}
return true;
}
bool WhitelistManager::Reload()
{
std::vector<WhitelistedPlayerEntry> players;
if (!LoadPlayers(&players))
{
return false;
}
m_whitelistedPlayers.swap(players);
return true;
}
bool WhitelistManager::Save() const
{
std::vector<WhitelistedPlayerEntry> players;
return SnapshotWhitelistedPlayers(&players) && SavePlayers(players);
}
bool WhitelistManager::LoadPlayers(std::vector<WhitelistedPlayerEntry> *outEntries) const
{
if (outEntries == nullptr)
{
return false;
}
outEntries->clear();
std::string text;
const std::string path = GetWhitelistFilePath();
if (!FileUtils::ReadTextFile(path, &text))
{
LogErrorf("access", "failed to read %s", path.c_str());
return false;
}
if (text.empty())
{
text = "[]";
}
const std::string stripped = StringUtils::StripUtf8Bom(text);
rapidjson::Document root;
const rapidjson::ParseResult parseResult = root.Parse(stripped.c_str());
if (!parseResult)
{
LogErrorf(
"access",
"failed to parse %s: %s (offset %zu)",
path.c_str(),
rapidjson::GetParseError_En(parseResult.Code()),
parseResult.Offset());
return false;
}
if (!root.IsArray())
{
LogErrorf("access", "failed to parse %s: root json value is not an array", path.c_str());
return false;
}
for (const auto &object : root.GetArray())
{
if (!object.IsObject())
{
LogWarnf("access", "skipping whitelist entry that is not an object in %s", path.c_str());
continue;
}
std::string rawXuid;
if (!AccessStorageUtils::TryGetStringField(object, "xuid", &rawXuid))
{
LogWarnf("access", "skipping whitelist entry without xuid in %s", path.c_str());
continue;
}
WhitelistedPlayerEntry entry;
entry.xuid = AccessStorageUtils::NormalizeXuid(rawXuid);
if (entry.xuid.empty())
{
LogWarnf("access", "skipping whitelist entry with empty xuid in %s", path.c_str());
continue;
}
AccessStorageUtils::TryGetStringField(object, "name", &entry.name);
AccessStorageUtils::TryGetStringField(object, "created", &entry.metadata.created);
AccessStorageUtils::TryGetStringField(object, "source", &entry.metadata.source);
NormalizeMetadata(&entry.metadata);
outEntries->push_back(entry);
}
return true;
}
bool WhitelistManager::SavePlayers(const std::vector<WhitelistedPlayerEntry> &entries) const
{
rapidjson::Document root(rapidjson::kArrayType);
auto &allocator = root.GetAllocator();
for (const auto &entry : entries)
{
const std::string normalizedXuid = AccessStorageUtils::NormalizeXuid(entry.xuid);
rapidjson::Value object(rapidjson::kObjectType);
object.AddMember(
"xuid", rapidjson::Value(normalizedXuid.c_str(), static_cast<rapidjson::SizeType>(normalizedXuid.size()), allocator), allocator);
object.AddMember(
"name", rapidjson::Value(entry.name.c_str(), static_cast<rapidjson::SizeType>(entry.name.size()), allocator), allocator);
object.AddMember(
"created",
rapidjson::Value(entry.metadata.created.c_str(), static_cast<rapidjson::SizeType>(entry.metadata.created.size()), allocator),
allocator);
object.AddMember(
"source",
rapidjson::Value(entry.metadata.source.c_str(), static_cast<rapidjson::SizeType>(entry.metadata.source.size()), allocator),
allocator);
root.PushBack(object, allocator);
}
rapidjson::StringBuffer buffer;
rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
writer.SetIndent(' ', 2);
root.Accept(writer);
const std::string path = GetWhitelistFilePath();
const std::string json = root.Empty() ? std::string("[]\n") : (std::string(buffer.GetString(), buffer.GetSize()) + "\n");
if (!FileUtils::WriteTextFileAtomic(path, json))
{
LogErrorf("access", "failed to write %s", path.c_str());
return false;
}
return true;
}
const std::vector<WhitelistedPlayerEntry> &WhitelistManager::GetWhitelistedPlayers() const
{
return m_whitelistedPlayers;
}
bool WhitelistManager::SnapshotWhitelistedPlayers(std::vector<WhitelistedPlayerEntry> *outEntries) const
{
if (outEntries == nullptr)
{
return false;
}
*outEntries = m_whitelistedPlayers;
return true;
}
bool WhitelistManager::IsPlayerWhitelistedByXuid(const std::string &xuid) const
{
const auto normalized = AccessStorageUtils::NormalizeXuid(xuid);
if (normalized.empty())
{
return false;
}
return std::any_of(
m_whitelistedPlayers.begin(),
m_whitelistedPlayers.end(),
[&normalized](const WhitelistedPlayerEntry &entry)
{
return entry.xuid == normalized;
});
}
bool WhitelistManager::AddPlayer(const WhitelistedPlayerEntry &entry)
{
std::vector<WhitelistedPlayerEntry> updatedEntries;
if (!SnapshotWhitelistedPlayers(&updatedEntries))
{
return false;
}
auto normalized = entry;
normalized.xuid = AccessStorageUtils::NormalizeXuid(normalized.xuid);
NormalizeMetadata(&normalized.metadata);
if (normalized.xuid.empty())
{
return false;
}
const auto existing = std::find_if(
updatedEntries.begin(),
updatedEntries.end(),
[&normalized](const WhitelistedPlayerEntry &candidate)
{
return candidate.xuid == normalized.xuid;
});
if (existing != updatedEntries.end())
{
*existing = normalized;
if (!SavePlayers(updatedEntries))
{
return false;
}
m_whitelistedPlayers.swap(updatedEntries);
return true;
}
updatedEntries.push_back(normalized);
if (!SavePlayers(updatedEntries))
{
return false;
}
m_whitelistedPlayers.swap(updatedEntries);
return true;
}
bool WhitelistManager::RemovePlayerByXuid(const std::string &xuid)
{
const auto normalized = AccessStorageUtils::NormalizeXuid(xuid);
if (normalized.empty())
{
return false;
}
std::vector<WhitelistedPlayerEntry> updatedEntries;
if (!SnapshotWhitelistedPlayers(&updatedEntries))
{
return false;
}
const auto oldSize = updatedEntries.size();
updatedEntries.erase(
std::remove_if(
updatedEntries.begin(),
updatedEntries.end(),
[&normalized](const WhitelistedPlayerEntry &entry) { return entry.xuid == normalized; }),
updatedEntries.end());
if (updatedEntries.size() == oldSize)
{
return false;
}
if (!SavePlayers(updatedEntries))
{
return false;
}
m_whitelistedPlayers.swap(updatedEntries);
return true;
}
std::string WhitelistManager::GetWhitelistFilePath() const
{
return BuildPath(kWhitelistFileName);
}
WhitelistMetadata WhitelistManager::BuildDefaultMetadata(const char *source)
{
WhitelistMetadata metadata;
metadata.created = StringUtils::GetCurrentUtcTimestampIso8601();
metadata.source = (source != nullptr) ? source : "Server";
return metadata;
}
void WhitelistManager::NormalizeMetadata(WhitelistMetadata *metadata)
{
if (metadata == nullptr)
{
return;
}
metadata->created = StringUtils::TrimAscii(metadata->created);
metadata->source = StringUtils::TrimAscii(metadata->source);
}
std::string WhitelistManager::BuildPath(const char *fileName) const
{
return AccessStorageUtils::BuildPathFromBaseDirectory(m_baseDirectory, fileName);
}
}
}
@@ -0,0 +1,64 @@
#pragma once
#include <string>
#include <vector>
namespace ServerRuntime
{
namespace Access
{
struct WhitelistMetadata
{
std::string created;
std::string source;
};
struct WhitelistedPlayerEntry
{
std::string xuid;
std::string name;
WhitelistMetadata metadata;
};
/**
* whitelist manager
*
* Files:
* - whitelist.json
*
* Stores and normalizes XUID-based allow entries.
*/
class WhitelistManager
{
public:
explicit WhitelistManager(const std::string &baseDirectory = ".");
bool EnsureWhitelistFileExists() const;
bool Reload();
bool Save() const;
bool LoadPlayers(std::vector<WhitelistedPlayerEntry> *outEntries) const;
bool SavePlayers(const std::vector<WhitelistedPlayerEntry> &entries) const;
const std::vector<WhitelistedPlayerEntry> &GetWhitelistedPlayers() const;
bool SnapshotWhitelistedPlayers(std::vector<WhitelistedPlayerEntry> *outEntries) const;
bool IsPlayerWhitelistedByXuid(const std::string &xuid) const;
bool AddPlayer(const WhitelistedPlayerEntry &entry);
bool RemovePlayerByXuid(const std::string &xuid);
std::string GetWhitelistFilePath() const;
static WhitelistMetadata BuildDefaultMetadata(const char *source = "Server");
private:
static void NormalizeMetadata(WhitelistMetadata *metadata);
std::string BuildPath(const char *fileName) const;
private:
std::string m_baseDirectory;
std::vector<WhitelistedPlayerEntry> m_whitelistedPlayers;
};
}
}
@@ -0,0 +1,105 @@
#pragma once
#include "FileUtils.h"
#include "StringUtils.h"
#include "../vendor/rapidjson/document.h"
#include <stdio.h>
namespace ServerRuntime
{
namespace AccessStorageUtils
{
inline bool IsRegularFile(const std::string &path)
{
const std::wstring widePath = StringUtils::Utf8ToWide(path);
if (widePath.empty())
{
return false;
}
const DWORD attributes = GetFileAttributesW(widePath.c_str());
return (attributes != INVALID_FILE_ATTRIBUTES) && ((attributes & FILE_ATTRIBUTE_DIRECTORY) == 0);
}
inline bool EnsureJsonListFileExists(const std::string &path)
{
return IsRegularFile(path) || FileUtils::WriteTextFileAtomic(path, "[]\n");
}
inline bool TryGetStringField(const rapidjson::Value &object, const char *key, std::string *outValue)
{
if (key == nullptr || outValue == nullptr || !object.IsObject())
{
return false;
}
const auto it = object.FindMember(key);
if (it == object.MemberEnd() || !it->value.IsString())
{
return false;
}
*outValue = it->value.GetString();
return true;
}
inline std::string NormalizeXuid(const std::string &xuid)
{
const std::string trimmed = StringUtils::TrimAscii(xuid);
if (trimmed.empty())
{
return "";
}
unsigned long long numericXuid = 0;
if (StringUtils::TryParseUnsignedLongLong(trimmed, &numericXuid))
{
if (numericXuid == 0ULL)
{
return "";
}
char buffer[32] = {};
sprintf_s(buffer, sizeof(buffer), "0x%016llx", numericXuid);
return buffer;
}
return StringUtils::ToLowerAscii(trimmed);
}
inline std::string BuildPathFromBaseDirectory(const std::string &baseDirectory, const char *fileName)
{
if (fileName == nullptr || fileName[0] == 0)
{
return "";
}
const std::wstring wideFileName = StringUtils::Utf8ToWide(fileName);
if (wideFileName.empty())
{
return "";
}
if (baseDirectory.empty() || baseDirectory == ".")
{
return StringUtils::WideToUtf8(wideFileName);
}
const std::wstring wideBaseDirectory = StringUtils::Utf8ToWide(baseDirectory);
if (wideBaseDirectory.empty())
{
return StringUtils::WideToUtf8(wideFileName);
}
const wchar_t last = wideBaseDirectory[wideBaseDirectory.size() - 1];
if (last == L'\\' || last == L'/')
{
return StringUtils::WideToUtf8(wideBaseDirectory + wideFileName);
}
return StringUtils::WideToUtf8(wideBaseDirectory + L"\\" + wideFileName);
}
}
}
+146
View File
@@ -0,0 +1,146 @@
#include "stdafx.h"
#include "FileUtils.h"
#include "StringUtils.h"
#include <io.h>
#include <stdio.h>
namespace ServerRuntime
{
namespace FileUtils
{
namespace
{
static std::wstring ToWidePath(const std::string &filePath)
{
return StringUtils::Utf8ToWide(filePath);
}
}
unsigned long long GetCurrentUtcFileTime()
{
FILETIME now = {};
GetSystemTimeAsFileTime(&now);
ULARGE_INTEGER value = {};
value.LowPart = now.dwLowDateTime;
value.HighPart = now.dwHighDateTime;
return value.QuadPart;
}
bool ReadTextFile(const std::string &filePath, std::string *outText)
{
if (outText == nullptr)
{
return false;
}
outText->clear();
const std::wstring widePath = ToWidePath(filePath);
if (widePath.empty())
{
return false;
}
FILE *inFile = nullptr;
if (_wfopen_s(&inFile, widePath.c_str(), L"rb") != 0 || inFile == nullptr)
{
return false;
}
if (fseek(inFile, 0, SEEK_END) != 0)
{
fclose(inFile);
return false;
}
long fileSize = ftell(inFile);
if (fileSize < 0)
{
fclose(inFile);
return false;
}
if (fseek(inFile, 0, SEEK_SET) != 0)
{
fclose(inFile);
return false;
}
if (fileSize == 0)
{
fclose(inFile);
return true;
}
outText->resize((size_t)fileSize);
size_t bytesRead = fread(&(*outText)[0], 1, (size_t)fileSize, inFile);
fclose(inFile);
if (bytesRead != (size_t)fileSize)
{
outText->clear();
return false;
}
return true;
}
bool WriteTextFileAtomic(const std::string &filePath, const std::string &text)
{
const std::wstring widePath = ToWidePath(filePath);
if (widePath.empty())
{
return false;
}
const std::wstring tmpPath = widePath + L".tmp";
FILE *outFile = nullptr;
if (_wfopen_s(&outFile, tmpPath.c_str(), L"wb") != 0 || outFile == nullptr)
{
return false;
}
if (!text.empty())
{
size_t bytesWritten = fwrite(text.data(), 1, text.size(), outFile);
if (bytesWritten != text.size())
{
fclose(outFile);
DeleteFileW(tmpPath.c_str());
return false;
}
}
if (fflush(outFile) != 0 || _commit(_fileno(outFile)) != 0)
{
fclose(outFile);
DeleteFileW(tmpPath.c_str());
return false;
}
fclose(outFile);
DWORD attrs = GetFileAttributesW(widePath.c_str());
if (attrs != INVALID_FILE_ATTRIBUTES && ((attrs & FILE_ATTRIBUTE_DIRECTORY) == 0))
{
// Replace the destination without deleting the last known-good file first.
if (ReplaceFileW(widePath.c_str(), tmpPath.c_str(), nullptr, REPLACEFILE_IGNORE_MERGE_ERRORS, nullptr, nullptr))
{
return true;
}
}
if (MoveFileExW(tmpPath.c_str(), widePath.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
{
return true;
}
// Keep the temp file on failure so the original file remains recoverable and the caller can inspect the write result.
return false;
}
}
}
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include <string>
namespace ServerRuntime
{
namespace FileUtils
{
/**
* Reads the full UTF-8 path target into memory without interpreting JSON or line endings
* UTF-8パスのテキストファイル全体をそのまま読み込む
*/
bool ReadTextFile(const std::string &filePath, std::string *outText);
/**
* Writes text through a same-directory temporary file and publishes it with a single replacement step
* 同一ディレクトリの一時ファイル経由で安全に書き換える
*/
bool WriteTextFileAtomic(const std::string &filePath, const std::string &text);
/**
* Returns the current UTC timestamp encoded in Windows FILETIME units for expiry comparisons
* 期限判定用に現在UTC時刻をWindows FILETIME単位で返す
*/
unsigned long long GetCurrentUtcFileTime();
}
}
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include "StringUtils.h"
#include <WS2tcpip.h>
namespace ServerRuntime
{
namespace NetworkUtils
{
inline std::string NormalizeIpToken(const std::string &ip)
{
return StringUtils::ToLowerAscii(StringUtils::TrimAscii(ip));
}
inline bool IsIpLiteral(const std::string &text)
{
const std::string trimmed = StringUtils::TrimAscii(text);
if (trimmed.empty())
{
return false;
}
IN_ADDR ipv4 = {};
IN6_ADDR ipv6 = {};
return InetPtonA(AF_INET, trimmed.c_str(), &ipv4) == 1 || InetPtonA(AF_INET6, trimmed.c_str(), &ipv6) == 1;
}
}
}
+212
View File
@@ -0,0 +1,212 @@
#include "stdafx.h"
#include "StringUtils.h"
#include <cctype>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
namespace ServerRuntime
{
namespace StringUtils
{
std::string WideToUtf8(const std::wstring &value)
{
if (value.empty())
{
return std::string();
}
int charCount = WideCharToMultiByte(CP_UTF8, 0, value.c_str(), (int)value.length(), NULL, 0, NULL, NULL);
if (charCount <= 0)
{
return std::string();
}
std::string utf8;
utf8.resize(charCount);
WideCharToMultiByte(CP_UTF8, 0, value.c_str(), (int)value.length(), &utf8[0], charCount, NULL, NULL);
return utf8;
}
std::wstring Utf8ToWide(const char *value)
{
if (value == NULL || value[0] == 0)
{
return std::wstring();
}
int wideCount = MultiByteToWideChar(CP_UTF8, 0, value, -1, NULL, 0);
if (wideCount <= 0)
{
// Fall back to the current ANSI code page so legacy non-UTF-8 inputs remain readable.
wideCount = MultiByteToWideChar(CP_ACP, 0, value, -1, NULL, 0);
if (wideCount <= 0)
{
return std::wstring();
}
std::wstring wide;
wide.resize(wideCount - 1);
MultiByteToWideChar(CP_ACP, 0, value, -1, &wide[0], wideCount);
return wide;
}
std::wstring wide;
wide.resize(wideCount - 1);
MultiByteToWideChar(CP_UTF8, 0, value, -1, &wide[0], wideCount);
return wide;
}
std::wstring Utf8ToWide(const std::string &value)
{
return Utf8ToWide(value.c_str());
}
std::string StripUtf8Bom(const std::string &value)
{
if (value.size() >= 3 &&
(unsigned char)value[0] == 0xEF &&
(unsigned char)value[1] == 0xBB &&
(unsigned char)value[2] == 0xBF)
{
return value.substr(3);
}
return value;
}
std::string TrimAscii(const std::string &value)
{
size_t start = 0;
while (start < value.length() && std::isspace((unsigned char)value[start]))
{
++start;
}
size_t end = value.length();
while (end > start && std::isspace((unsigned char)value[end - 1]))
{
--end;
}
return value.substr(start, end - start);
}
std::string ToLowerAscii(const std::string &value)
{
std::string lowered = value;
for (size_t i = 0; i < lowered.length(); ++i)
{
lowered[i] = (char)std::tolower((unsigned char)lowered[i]);
}
return lowered;
}
std::string JoinTokens(const std::vector<std::string> &tokens, size_t startIndex, const char *separator)
{
if (startIndex >= tokens.size())
{
return std::string();
}
const auto joinSeparator = std::string((separator != nullptr) ? separator : " ");
size_t totalLength = 0;
for (size_t i = startIndex; i < tokens.size(); ++i)
{
totalLength += tokens[i].size();
}
totalLength += (tokens.size() - startIndex - 1) * joinSeparator.size();
std::string joined;
joined.reserve(totalLength);
for (size_t i = startIndex; i < tokens.size(); ++i)
{
if (!joined.empty())
{
joined += joinSeparator;
}
joined += tokens[i];
}
return joined;
}
bool StartsWithIgnoreCase(const std::string &value, const std::string &prefix)
{
if (prefix.size() > value.size())
{
return false;
}
for (size_t i = 0; i < prefix.size(); ++i)
{
unsigned char a = (unsigned char)value[i];
unsigned char b = (unsigned char)prefix[i];
if (std::tolower(a) != std::tolower(b))
{
return false;
}
}
return true;
}
bool TryParseUnsignedLongLong(const std::string &value, unsigned long long *outValue)
{
if (outValue == nullptr)
{
return false;
}
const std::string trimmed = TrimAscii(value);
if (trimmed.empty())
{
return false;
}
errno = 0;
char *end = nullptr;
const unsigned long long parsed = _strtoui64(trimmed.c_str(), &end, 0);
if (end == trimmed.c_str() || errno != 0)
{
return false;
}
while (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')
{
++end;
}
if (*end != 0)
{
return false;
}
*outValue = parsed;
return true;
}
std::string GetCurrentUtcTimestampIso8601()
{
SYSTEMTIME utc = {};
GetSystemTime(&utc);
char created[64] = {};
sprintf_s(
created,
sizeof(created),
"%04u-%02u-%02uT%02u:%02u:%02uZ",
(unsigned)utc.wYear,
(unsigned)utc.wMonth,
(unsigned)utc.wDay,
(unsigned)utc.wHour,
(unsigned)utc.wMinute,
(unsigned)utc.wSecond);
return created;
}
}
}
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <string>
#include <vector>
namespace ServerRuntime
{
namespace StringUtils
{
std::string WideToUtf8(const std::wstring &value);
std::wstring Utf8ToWide(const char *value);
std::wstring Utf8ToWide(const std::string &value);
std::string StripUtf8Bom(const std::string &value);
std::string TrimAscii(const std::string &value);
std::string ToLowerAscii(const std::string &value);
std::string JoinTokens(const std::vector<std::string> &tokens, size_t startIndex = 0, const char *separator = " ");
bool StartsWithIgnoreCase(const std::string &value, const std::string &prefix);
bool TryParseUnsignedLongLong(const std::string &value, unsigned long long *outValue);
std::string GetCurrentUtcTimestampIso8601();
}
}
+44
View File
@@ -0,0 +1,44 @@
#include "stdafx.h"
#include "ServerCli.h"
#include "ServerCliEngine.h"
#include "ServerCliInput.h"
namespace ServerRuntime
{
ServerCli::ServerCli()
: m_engine(new ServerCliEngine())
, m_input(new ServerCliInput())
{
}
ServerCli::~ServerCli()
{
Stop();
}
void ServerCli::Start()
{
if (m_input && m_engine)
{
m_input->Start(m_engine.get());
}
}
void ServerCli::Stop()
{
if (m_input)
{
m_input->Stop();
}
}
void ServerCli::Poll()
{
if (m_engine)
{
m_engine->Poll();
}
}
}
+50
View File
@@ -0,0 +1,50 @@
#pragma once
#include <memory>
namespace ServerRuntime
{
class ServerCliEngine;
class ServerCliInput;
/**
* **Server CLI facade**
*
* Owns the command engine and input component, and exposes a small lifecycle API.
* CLI 全体の開始・停止・更新をまとめる窓口クラス
*/
class ServerCli
{
public:
ServerCli();
~ServerCli();
/**
* **Start console input processing**
*
* Connects input to the engine and starts background reading.
* 入力処理を開始してエンジンに接続
*/
void Start();
/**
* **Stop console input processing**
*
* Stops background input safely and detaches from the engine.
* 入力処理を安全に停止
*/
void Stop();
/**
* **Process queued command lines**
*
* Drains commands collected by input and executes them in the main loop.
* 入力キューのコマンドを実行
*/
void Poll();
private:
std::unique_ptr<ServerCliEngine> m_engine;
std::unique_ptr<ServerCliInput> m_input;
};
}
@@ -0,0 +1,395 @@
#include "stdafx.h"
#include "ServerCliEngine.h"
#include "ServerCliParser.h"
#include "ServerCliRegistry.h"
#include "commands/IServerCliCommand.h"
#include "commands/ban/CliCommandBan.h"
#include "commands/ban-ip/CliCommandBanIp.h"
#include "commands/ban-list/CliCommandBanList.h"
#include "commands/defaultgamemode/CliCommandDefaultGamemode.h"
#include "commands/enchant/CliCommandEnchant.h"
#include "commands/experience/CliCommandExperience.h"
#include "commands/gamemode/CliCommandGamemode.h"
#include "commands/give/CliCommandGive.h"
#include "commands/help/CliCommandHelp.h"
#include "commands/kill/CliCommandKill.h"
#include "commands/list/CliCommandList.h"
#include "commands/pardon/CliCommandPardon.h"
#include "commands/pardon-ip/CliCommandPardonIp.h"
#include "commands/stop/CliCommandStop.h"
#include "commands/time/CliCommandTime.h"
#include "commands/tp/CliCommandTp.h"
#include "commands/weather/CliCommandWeather.h"
#include "commands/whitelist/CliCommandWhitelist.h"
#include "../Common/StringUtils.h"
#include "../ServerShutdown.h"
#include "../ServerLogger.h"
#include "../../Minecraft.Client/MinecraftServer.h"
#include "../../Minecraft.Client/PlayerList.h"
#include "../../Minecraft.Client/ServerPlayer.h"
#include "../../Minecraft.World/CommandDispatcher.h"
#include "../../Minecraft.World/CommandSender.h"
#include "../../Minecraft.World/LevelSettings.h"
#include "../../Minecraft.World/StringHelpers.h"
#include <stdlib.h>
#include <unordered_set>
namespace ServerRuntime
{
/**
* Create an authorized Sender to make the CLI appear as a user.
* The return value can also be used to display logs.
*/
namespace
{
class ServerCliConsoleCommandSender : public CommandSender
{
public:
explicit ServerCliConsoleCommandSender(const ServerCliEngine *engine)
: m_engine(engine)
{
}
void sendMessage(const wstring &message, ChatPacket::EChatPacketMessage type, int customData, const wstring &additionalMessage) override
{
(void)type;
(void)customData;
(void)additionalMessage;
if (m_engine == nullptr)
{
return;
}
m_engine->LogInfo(StringUtils::WideToUtf8(message));
}
bool hasPermission(EGameCommand command) override
{
(void)command;
return true;
}
private:
const ServerCliEngine *m_engine;
};
}
ServerCliEngine::ServerCliEngine()
: m_registry(new ServerCliRegistry())
, m_consoleSender(std::make_shared<ServerCliConsoleCommandSender>(this))
{
RegisterDefaultCommands();
}
ServerCliEngine::~ServerCliEngine()
{
}
void ServerCliEngine::RegisterDefaultCommands()
{
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandHelp()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandStop()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandList()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandBan()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandBanIp()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandPardon()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandPardonIp()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandBanList()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandWhitelist()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandTp()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandTime()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandWeather()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandGive()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandEnchant()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandKill()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandGamemode()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandDefaultGamemode()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandExperience()));
}
void ServerCliEngine::EnqueueCommandLine(const std::string &line)
{
std::lock_guard<std::mutex> lock(m_queueMutex);
m_pendingLines.push(line);
}
void ServerCliEngine::Poll()
{
for (;;)
{
std::string line;
{
// Keep the lock scope minimal: dequeue only, execute outside.
std::lock_guard<std::mutex> lock(m_queueMutex);
if (m_pendingLines.empty())
{
break;
}
line = m_pendingLines.front();
m_pendingLines.pop();
}
ExecuteCommandLine(line);
}
}
bool ServerCliEngine::ExecuteCommandLine(const std::string &line)
{
// Normalize user input before parsing (trim + optional leading slash).
std::wstring wide = trimString(StringUtils::Utf8ToWide(line));
if (wide.empty())
{
return true;
}
std::string normalizedLine = StringUtils::WideToUtf8(wide);
if (!normalizedLine.empty() && normalizedLine[0] == '/')
{
normalizedLine = normalizedLine.substr(1);
}
ServerCliParsedLine parsed = ServerCliParser::Parse(normalizedLine);
if (parsed.tokens.empty())
{
return true;
}
IServerCliCommand *command = m_registry->FindMutable(parsed.tokens[0]);
if (command == NULL)
{
LogWarn("Unknown command: " + parsed.tokens[0]);
return false;
}
return command->Execute(parsed, this);
}
void ServerCliEngine::BuildCompletions(const std::string &line, std::vector<std::string> *out) const
{
if (out == NULL)
{
return;
}
out->clear();
ServerCliCompletionContext context = ServerCliParser::BuildCompletionContext(line);
bool slashPrefixedCommand = false;
std::string commandToken;
if (!context.parsed.tokens.empty())
{
// Completion accepts both "tp" and "/tp" style command heads.
commandToken = context.parsed.tokens[0];
if (!commandToken.empty() && commandToken[0] == '/')
{
commandToken = commandToken.substr(1);
slashPrefixedCommand = true;
}
}
if (context.currentTokenIndex == 0)
{
std::string prefix = context.prefix;
if (!prefix.empty() && prefix[0] == '/')
{
prefix = prefix.substr(1);
slashPrefixedCommand = true;
}
std::string linePrefix = context.linePrefix;
if (slashPrefixedCommand && linePrefix.empty())
{
// Preserve leading slash when user started with "/".
linePrefix = "/";
}
m_registry->SuggestCommandNames(prefix, linePrefix, out);
}
else
{
const IServerCliCommand *command = m_registry->Find(commandToken);
if (command != NULL)
{
command->Complete(context, this, out);
}
}
std::unordered_set<std::string> seen;
std::vector<std::string> unique;
for (size_t i = 0; i < out->size(); ++i)
{
// Remove duplicates while keeping first-seen ordering.
if (seen.insert((*out)[i]).second)
{
unique.push_back((*out)[i]);
}
}
out->swap(unique);
}
void ServerCliEngine::LogInfo(const std::string &message) const
{
LogInfof("console", "%s", message.c_str());
}
void ServerCliEngine::LogWarn(const std::string &message) const
{
LogWarnf("console", "%s", message.c_str());
}
void ServerCliEngine::LogError(const std::string &message) const
{
LogErrorf("console", "%s", message.c_str());
}
void ServerCliEngine::RequestShutdown() const
{
RequestDedicatedServerShutdown();
}
std::vector<std::string> ServerCliEngine::GetOnlinePlayerNamesUtf8() const
{
std::vector<std::string> result;
MinecraftServer *server = MinecraftServer::getInstance();
if (server == NULL)
{
return result;
}
PlayerList *players = server->getPlayers();
if (players == NULL)
{
return result;
}
for (size_t i = 0; i < players->players.size(); ++i)
{
std::shared_ptr<ServerPlayer> player = players->players[i];
if (player != NULL)
{
result.push_back(StringUtils::WideToUtf8(player->getName()));
}
}
return result;
}
std::shared_ptr<ServerPlayer> ServerCliEngine::FindPlayerByNameUtf8(const std::string &name) const
{
MinecraftServer *server = MinecraftServer::getInstance();
if (server == NULL)
{
return nullptr;
}
PlayerList *players = server->getPlayers();
if (players == NULL)
{
return nullptr;
}
std::wstring target = StringUtils::Utf8ToWide(name);
for (size_t i = 0; i < players->players.size(); ++i)
{
std::shared_ptr<ServerPlayer> player = players->players[i];
if (player != NULL && equalsIgnoreCase(player->getName(), target))
{
return player;
}
}
return nullptr;
}
void ServerCliEngine::SuggestPlayers(const std::string &prefix, const std::string &linePrefix, std::vector<std::string> *out) const
{
std::vector<std::string> players = GetOnlinePlayerNamesUtf8();
std::string loweredPrefix = StringUtils::ToLowerAscii(prefix);
for (size_t i = 0; i < players.size(); ++i)
{
std::string loweredName = StringUtils::ToLowerAscii(players[i]);
if (loweredName.compare(0, loweredPrefix.size(), loweredPrefix) == 0)
{
out->push_back(linePrefix + players[i]);
}
}
}
void ServerCliEngine::SuggestGamemodes(const std::string &prefix, const std::string &linePrefix, std::vector<std::string> *out) const
{
static const char *kModes[] = { "survival", "creative", "s", "c", "0", "1" };
std::string loweredPrefix = StringUtils::ToLowerAscii(prefix);
for (size_t i = 0; i < sizeof(kModes) / sizeof(kModes[0]); ++i)
{
std::string candidate = kModes[i];
std::string loweredCandidate = StringUtils::ToLowerAscii(candidate);
if (loweredCandidate.compare(0, loweredPrefix.size(), loweredPrefix) == 0)
{
out->push_back(linePrefix + candidate);
}
}
}
GameType *ServerCliEngine::ParseGamemode(const std::string &token) const
{
std::string lowered = StringUtils::ToLowerAscii(token);
if (lowered == "survival" || lowered == "s" || lowered == "0")
{
return GameType::SURVIVAL;
}
if (lowered == "creative" || lowered == "c" || lowered == "1")
{
return GameType::CREATIVE;
}
char *end = NULL;
long id = strtol(lowered.c_str(), &end, 10);
if (end != NULL && *end == 0)
{
// Numeric fallback supports extended ids handled by level settings.
return LevelSettings::validateGameType((int)id);
}
return NULL;
}
bool ServerCliEngine::DispatchWorldCommand(EGameCommand command, byteArray commandData, const std::shared_ptr<CommandSender> &sender) const
{
MinecraftServer *server = MinecraftServer::getInstance();
if (server == NULL)
{
LogWarn("MinecraftServer instance is not available.");
return false;
}
CommandDispatcher *dispatcher = server->getCommandDispatcher();
if (dispatcher == NULL)
{
LogWarn("Command dispatcher is not available.");
return false;
}
std::shared_ptr<CommandSender> commandSender = sender;
if (commandSender == nullptr)
{
// fall back to console sender if caller did not provide one
commandSender = m_consoleSender;
}
if (commandSender == nullptr)
{
LogWarn("No command sender is available.");
return false;
}
dispatcher->performCommand(commandSender, command, commandData);
return true;
}
const ServerCliRegistry &ServerCliEngine::Registry() const
{
return *m_registry;
}
}
+127
View File
@@ -0,0 +1,127 @@
#pragma once
#include <memory>
#include <mutex>
#include <queue>
#include <string>
#include <vector>
#include "../../Minecraft.World/ArrayWithLength.h"
#include "../../Minecraft.World/CommandsEnum.h"
class GameType;
class ServerPlayer;
class CommandSender;
namespace ServerRuntime
{
class ServerCliRegistry;
/**
* **CLI execution engine**
*
* Handles parsing, command dispatch, completion suggestions, and server-side helpers.
* 解析・実行・補完エンジン
*/
class ServerCliEngine
{
public:
ServerCliEngine();
~ServerCliEngine();
/**
* **Queue one raw command line**
*
* Called by input thread; execution is deferred to `Poll()`.
* 入力行を実行キューに追加
*/
void EnqueueCommandLine(const std::string &line);
/**
* **Execute queued commands**
*
* Drains pending lines and dispatches them in order.
* キュー済みコマンドを順番に実行
*/
void Poll();
/**
* **Execute one command line immediately**
*
* Parses and dispatches a normalized line to a registered command.
* 1行を直接パースしてコマンド実行
*/
bool ExecuteCommandLine(const std::string &line);
/**
* **Build completion candidates for current line**
*
* Produces command or argument suggestions based on parser context.
* 現在入力に対する補完候補を作成
*/
void BuildCompletions(const std::string &line, std::vector<std::string> *out) const;
void LogInfo(const std::string &message) const;
void LogWarn(const std::string &message) const;
void LogError(const std::string &message) const;
void RequestShutdown() const;
/**
* **List connected players as UTF-8 names**
*
* ここら辺は分けてもいいかも
*/
std::vector<std::string> GetOnlinePlayerNamesUtf8() const;
/**
* **Find a player by UTF-8 name**
*/
std::shared_ptr<ServerPlayer> FindPlayerByNameUtf8(const std::string &name) const;
/**
* **Suggest player-name arguments**
*
* Appends matching player candidates using the given completion prefix.
* プレイヤー名の補完候補
*/
void SuggestPlayers(const std::string &prefix, const std::string &linePrefix, std::vector<std::string> *out) const;
/**
* **Suggest gamemode arguments**
*
* Appends standard gamemode aliases (survival/creative/0/1).
* ゲームモードの補完候補
*/
void SuggestGamemodes(const std::string &prefix, const std::string &linePrefix, std::vector<std::string> *out) const;
/**
* **Parse gamemode token**
*
* Supports names, short aliases, and numeric ids.
* 文字列からゲームモードを解決
*/
GameType *ParseGamemode(const std::string &token) const;
/**
* **Dispatch one Minecraft.World game command**
*
* Uses `Minecraft.World::CommandDispatcher` for actual execution.
* When `sender` is null, an internal console command sender is used.
*
* Minecraft.Worldのコマンドを実行するためのディスパッチャーのラッパー
* 内部でsenderがnullの場合はコンソールコマンド送信者を使用
*/
bool DispatchWorldCommand(EGameCommand command, byteArray commandData, const std::shared_ptr<CommandSender> &sender = nullptr) const;
const ServerCliRegistry &Registry() const;
private:
void RegisterDefaultCommands();
private:
mutable std::mutex m_queueMutex;
std::queue<std::string> m_pendingLines;
std::unique_ptr<ServerCliRegistry> m_registry;
std::shared_ptr<CommandSender> m_consoleSender;
};
}
+285
View File
@@ -0,0 +1,285 @@
#include "stdafx.h"
#include "ServerCliInput.h"
#include "ServerCliEngine.h"
#include "../ServerLogger.h"
#include "../vendor/linenoise/linenoise.h"
#include <ctype.h>
#include <stdlib.h>
namespace
{
bool UseStreamInputMode()
{
const char *mode = getenv("SERVER_CLI_INPUT_MODE");
if (mode != NULL)
{
return _stricmp(mode, "stream") == 0
|| _stricmp(mode, "stdin") == 0;
}
return false;
}
int WaitForStdinReadable(HANDLE stdinHandle, DWORD waitMs)
{
if (stdinHandle == NULL || stdinHandle == INVALID_HANDLE_VALUE)
{
return -1;
}
DWORD fileType = GetFileType(stdinHandle);
if (fileType == FILE_TYPE_PIPE)
{
DWORD available = 0;
if (!PeekNamedPipe(stdinHandle, NULL, 0, NULL, &available, NULL))
{
return -1;
}
return available > 0 ? 1 : 0;
}
if (fileType == FILE_TYPE_CHAR)
{
// console/pty char handles are often not waitable across Wine+Docker.
return 1;
}
DWORD waitResult = WaitForSingleObject(stdinHandle, waitMs);
if (waitResult == WAIT_OBJECT_0)
{
return 1;
}
if (waitResult == WAIT_TIMEOUT)
{
return 0;
}
return -1;
}
}
namespace ServerRuntime
{
// C-style completion callback bridge requires a static instance pointer.
ServerCliInput *ServerCliInput::s_instance = NULL;
ServerCliInput::ServerCliInput()
: m_running(false)
, m_engine(NULL)
{
}
ServerCliInput::~ServerCliInput()
{
Stop();
}
void ServerCliInput::Start(ServerCliEngine *engine)
{
if (engine == NULL || m_running.exchange(true))
{
return;
}
m_engine = engine;
s_instance = this;
linenoiseResetStop();
linenoiseHistorySetMaxLen(128);
linenoiseSetCompletionCallback(&ServerCliInput::CompletionThunk);
m_inputThread = std::thread(&ServerCliInput::RunInputLoop, this);
LogInfo("console", "CLI input thread started.");
}
void ServerCliInput::Stop()
{
if (!m_running.exchange(false))
{
return;
}
// Ask linenoise to break out first, then join thread safely.
linenoiseRequestStop();
if (m_inputThread.joinable())
{
CancelSynchronousIo((HANDLE)m_inputThread.native_handle());
m_inputThread.join();
}
linenoiseSetCompletionCallback(NULL);
if (s_instance == this)
{
s_instance = NULL;
}
m_engine = NULL;
LogInfo("console", "CLI input thread stopped.");
}
bool ServerCliInput::IsRunning() const
{
return m_running.load();
}
void ServerCliInput::RunInputLoop()
{
if (UseStreamInputMode())
{
LogInfo("console", "CLI input mode: stream(file stdin)");
RunStreamInputLoop();
return;
}
RunLinenoiseLoop();
}
/**
* use linenoise for interactive console input, with line editing and history support
*/
void ServerCliInput::RunLinenoiseLoop()
{
while (m_running)
{
char *line = linenoise("server> ");
if (line == NULL)
{
// NULL is expected on stop request (or Ctrl+C inside linenoise).
if (!m_running)
{
break;
}
Sleep(10);
continue;
}
EnqueueLine(line);
linenoiseFree(line);
}
}
/**
* use file-based stdin reading instead of linenoise when requested or when stdin is not a console/pty (e.g. piped input or non-interactive docker)
*/
void ServerCliInput::RunStreamInputLoop()
{
HANDLE stdinHandle = GetStdHandle(STD_INPUT_HANDLE);
if (stdinHandle == NULL || stdinHandle == INVALID_HANDLE_VALUE)
{
LogWarn("console", "stream input mode requested but STDIN handle is unavailable; falling back to linenoise.");
RunLinenoiseLoop();
return;
}
std::string line;
bool skipNextLf = false;
printf("server> ");
fflush(stdout);
while (m_running)
{
int readable = WaitForStdinReadable(stdinHandle, 50);
if (readable <= 0)
{
Sleep(10);
continue;
}
char ch = 0;
DWORD bytesRead = 0;
if (!ReadFile(stdinHandle, &ch, 1, &bytesRead, NULL) || bytesRead == 0)
{
Sleep(10);
continue;
}
if (skipNextLf && ch == '\n')
{
skipNextLf = false;
continue;
}
if (ch == '\r' || ch == '\n')
{
if (ch == '\r')
{
skipNextLf = true;
}
else
{
skipNextLf = false;
}
if (!line.empty())
{
EnqueueLine(line.c_str());
line.clear();
}
printf("server> ");
fflush(stdout);
continue;
}
skipNextLf = false;
if ((unsigned char)ch == 3)
{
continue;
}
if ((unsigned char)ch == 8 || (unsigned char)ch == 127)
{
if (!line.empty())
{
line.resize(line.size() - 1);
}
continue;
}
if (isprint((unsigned char)ch) && line.size() < 4096)
{
line.push_back(ch);
}
}
}
void ServerCliInput::EnqueueLine(const char *line)
{
if (line == NULL || line[0] == 0 || m_engine == NULL)
{
return;
}
// Keep local history and forward command for main-thread execution.
linenoiseHistoryAdd(line);
m_engine->EnqueueCommandLine(line);
}
void ServerCliInput::CompletionThunk(const char *line, linenoiseCompletions *completions)
{
// Static thunk forwards callback into instance state.
if (s_instance != NULL)
{
s_instance->BuildCompletions(line, completions);
}
}
void ServerCliInput::BuildCompletions(const char *line, linenoiseCompletions *completions)
{
if (line == NULL || completions == NULL || m_engine == NULL)
{
return;
}
std::vector<std::string> suggestions;
m_engine->BuildCompletions(line, &suggestions);
for (size_t i = 0; i < suggestions.size(); ++i)
{
linenoiseAddCompletion(completions, suggestions[i].c_str());
}
}
}
+63
View File
@@ -0,0 +1,63 @@
#pragma once
#include <atomic>
#include <thread>
struct linenoiseCompletions;
namespace ServerRuntime
{
class ServerCliEngine;
/**
* **CLI input worker**
*
* Owns the interactive input thread and bridges linenoise callbacks to the engine.
* 入力スレッドと補完コールバックを管理するクラス
*/
class ServerCliInput
{
public:
ServerCliInput();
~ServerCliInput();
/**
* **Start input loop**
*
* Binds to an engine and starts reading user input from the console.
* エンジンに接続して入力ループを開始
*/
void Start(ServerCliEngine *engine);
/**
* **Stop input loop**
*
* Requests stop and joins the input thread.
* 停止要求を出して入力スレッドを終了
*/
void Stop();
/**
* **Check running state**
*
* Returns true while the input thread is active.
* 入力処理が動作中かどうか
*/
bool IsRunning() const;
private:
void RunInputLoop();
void RunLinenoiseLoop();
void RunStreamInputLoop();
void EnqueueLine(const char *line);
static void CompletionThunk(const char *line, linenoiseCompletions *completions);
void BuildCompletions(const char *line, linenoiseCompletions *completions);
private:
std::atomic<bool> m_running;
std::thread m_inputThread;
ServerCliEngine *m_engine;
static ServerCliInput *s_instance;
};
}
@@ -0,0 +1,116 @@
#include "stdafx.h"
#include "ServerCliParser.h"
namespace ServerRuntime
{
static void TokenizeLine(const std::string &line, std::vector<std::string> *tokens, bool *trailingSpace)
{
std::string current;
bool inQuotes = false;
bool escaped = false;
tokens->clear();
*trailingSpace = false;
for (size_t i = 0; i < line.size(); ++i)
{
char ch = line[i];
if (escaped)
{
// Keep escaped character literally (e.g. \" or \ ).
current.push_back(ch);
escaped = false;
continue;
}
if (ch == '\\')
{
escaped = true;
continue;
}
if (ch == '"')
{
// Double quotes group spaces into one token.
inQuotes = !inQuotes;
continue;
}
if (!inQuotes && (ch == ' ' || ch == '\t'))
{
if (!current.empty())
{
tokens->push_back(current);
current.clear();
}
continue;
}
current.push_back(ch);
}
if (!current.empty())
{
tokens->push_back(current);
}
if (!line.empty())
{
char tail = line[line.size() - 1];
// Trailing space means completion targets the next token slot.
*trailingSpace = (!inQuotes && (tail == ' ' || tail == '\t'));
}
}
ServerCliParsedLine ServerCliParser::Parse(const std::string &line)
{
ServerCliParsedLine parsed;
parsed.raw = line;
TokenizeLine(line, &parsed.tokens, &parsed.trailingSpace);
return parsed;
}
ServerCliCompletionContext ServerCliParser::BuildCompletionContext(const std::string &line)
{
ServerCliCompletionContext context;
context.parsed = Parse(line);
if (context.parsed.tokens.empty())
{
context.currentTokenIndex = 0;
context.prefix.clear();
context.linePrefix.clear();
return context;
}
if (context.parsed.trailingSpace)
{
// Cursor is after a separator, so complete a new token.
context.currentTokenIndex = context.parsed.tokens.size();
context.prefix.clear();
}
else
{
// Cursor is inside current token, so complete by its prefix.
context.currentTokenIndex = context.parsed.tokens.size() - 1;
context.prefix = context.parsed.tokens.back();
}
for (size_t i = 0; i < context.currentTokenIndex; ++i)
{
// linePrefix is the immutable left side reused by completion output.
if (!context.linePrefix.empty())
{
context.linePrefix.push_back(' ');
}
context.linePrefix += context.parsed.tokens[i];
}
if (!context.linePrefix.empty())
{
context.linePrefix.push_back(' ');
}
return context;
}
}
@@ -0,0 +1,63 @@
#pragma once
#include <string>
#include <vector>
namespace ServerRuntime
{
/**
* **Parsed command line**
*/
struct ServerCliParsedLine
{
std::string raw;
std::vector<std::string> tokens;
bool trailingSpace;
ServerCliParsedLine()
: trailingSpace(false)
{
}
};
/**
* **Completion context for one input line**
*
* Indicates current token index, token prefix, and the fixed line prefix.
*/
struct ServerCliCompletionContext
{
ServerCliParsedLine parsed;
size_t currentTokenIndex;
std::string prefix;
std::string linePrefix;
ServerCliCompletionContext()
: currentTokenIndex(0)
{
}
};
/**
* **CLI parser helpers**
*
* Converts raw input text into tokenized data used by execution and completion.
*/
class ServerCliParser
{
public:
/**
* **Tokenize one command line**
*
* Supports quoted segments and escaped characters.
*/
static ServerCliParsedLine Parse(const std::string &line);
/**
* **Build completion metadata**
*
* Determines active token position and reusable prefix parts.
*/
static ServerCliCompletionContext BuildCompletionContext(const std::string &line);
};
}
@@ -0,0 +1,99 @@
#include "stdafx.h"
#include "ServerCliRegistry.h"
#include "commands/IServerCliCommand.h"
#include "../Common/StringUtils.h"
namespace ServerRuntime
{
bool ServerCliRegistry::Register(std::unique_ptr<IServerCliCommand> command)
{
if (!command)
{
return false;
}
IServerCliCommand *raw = command.get();
std::string baseName = StringUtils::ToLowerAscii(raw->Name());
// Reject empty/duplicate primary command names.
if (baseName.empty() || m_lookup.find(baseName) != m_lookup.end())
{
return false;
}
std::vector<std::string> aliases = raw->Aliases();
std::vector<std::string> normalizedAliases;
normalizedAliases.reserve(aliases.size());
for (size_t i = 0; i < aliases.size(); ++i)
{
std::string alias = StringUtils::ToLowerAscii(aliases[i]);
// Alias must also be unique across all names and aliases.
if (alias.empty() || m_lookup.find(alias) != m_lookup.end())
{
return false;
}
normalizedAliases.push_back(alias);
}
m_lookup[baseName] = raw;
for (size_t i = 0; i < normalizedAliases.size(); ++i)
{
m_lookup[normalizedAliases[i]] = raw;
}
// Command objects are owned here; lookup stores non-owning pointers.
m_commands.push_back(std::move(command));
return true;
}
const IServerCliCommand *ServerCliRegistry::Find(const std::string &name) const
{
std::string key = StringUtils::ToLowerAscii(name);
auto it = m_lookup.find(key);
if (it == m_lookup.end())
{
return NULL;
}
return it->second;
}
IServerCliCommand *ServerCliRegistry::FindMutable(const std::string &name)
{
std::string key = StringUtils::ToLowerAscii(name);
auto it = m_lookup.find(key);
if (it == m_lookup.end())
{
return NULL;
}
return it->second;
}
void ServerCliRegistry::SuggestCommandNames(const std::string &prefix, const std::string &linePrefix, std::vector<std::string> *out) const
{
for (size_t i = 0; i < m_commands.size(); ++i)
{
const IServerCliCommand *command = m_commands[i].get();
std::string name = command->Name();
if (StringUtils::StartsWithIgnoreCase(name, prefix))
{
out->push_back(linePrefix + name);
}
// Include aliases so users can discover shorthand commands.
std::vector<std::string> aliases = command->Aliases();
for (size_t aliasIndex = 0; aliasIndex < aliases.size(); ++aliasIndex)
{
if (StringUtils::StartsWithIgnoreCase(aliases[aliasIndex], prefix))
{
out->push_back(linePrefix + aliases[aliasIndex]);
}
}
}
}
const std::vector<std::unique_ptr<IServerCliCommand>> &ServerCliRegistry::Commands() const
{
return m_commands;
}
}
@@ -0,0 +1,58 @@
#pragma once
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
namespace ServerRuntime
{
class IServerCliCommand;
/**
* **CLI command registry**
*/
class ServerCliRegistry
{
public:
/**
* **Register a command object**
*
* Validates name/aliases and adds lookup entries.
* コマンドの追加
*/
bool Register(std::unique_ptr<IServerCliCommand> command);
/**
* **Find command by name or alias (const)**
*
* Returns null when no match exists.
*/
const IServerCliCommand *Find(const std::string &name) const;
/**
* **Find mutable command by name or alias**
*
* Used by runtime dispatch path.
*/
IServerCliCommand *FindMutable(const std::string &name);
/**
* **Suggest top-level command names**
*
* Adds matching command names and aliases to the output list.
*/
void SuggestCommandNames(const std::string &prefix, const std::string &linePrefix, std::vector<std::string> *out) const;
/**
* **Get registered command list**
*
* Intended for help output and inspection.
*/
const std::vector<std::unique_ptr<IServerCliCommand>> &Commands() const;
private:
std::vector<std::unique_ptr<IServerCliCommand>> m_commands;
std::unordered_map<std::string, IServerCliCommand *> m_lookup;
};
}
@@ -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;
}
}
}
@@ -0,0 +1,49 @@
#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() {}
/** Primary command name */
virtual const char *Name() const = 0;
/** Optional aliases */
virtual std::vector<std::string> Aliases() const { return std::vector<std::string>(); }
/** 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)engine;
(void)out;
}
};
}
@@ -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);
}
}
}
@@ -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;
};
}
@@ -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;
}
}
@@ -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;
};
}
@@ -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);
}
}
}
@@ -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;
};
}
@@ -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
{
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);
}
}
}
@@ -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;
};
}
@@ -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
{
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);
}
}
}
@@ -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;
};
}
@@ -0,0 +1,192 @@
#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
{
const char *kExperienceUsage = "xp <amount>[L] [player]";
const char *kExperienceUsageWithPlayer = "xp <amount>[L] <player>";
struct ExperienceAmount
{
int amount;
bool levels;
bool take;
};
static bool TryParseExperienceAmount(const std::string &token, ExperienceAmount *outValue)
{
if (outValue == nullptr || token.empty())
{
return false;
}
ExperienceAmount parsed;
parsed.amount = 0;
parsed.levels = false;
parsed.take = false;
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
{
std::vector<std::string> aliases;
aliases.push_back("experience");
return aliases;
}
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;
amount.amount = 0;
amount.levels = false;
amount.take = false;
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);
}
}
}
@@ -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;
};
}
@@ -0,0 +1,111 @@
#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
{
const char *kGamemodeUsage = "gamemode <survival|creative|0|1> [player]";
const char *kGamemodeUsageWithPlayer = "gamemode <survival|creative|0|1> <player>";
}
const char *CliCommandGamemode::Name() const
{
return "gamemode";
}
std::vector<std::string> CliCommandGamemode::Aliases() const
{
std::vector<std::string> aliases;
aliases.push_back("gm");
return aliases;
}
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);
}
}
}
@@ -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;
};
}
@@ -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
{
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);
}
}
}
@@ -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;
};
}
@@ -0,0 +1,49 @@
#include "stdafx.h"
#include "CliCommandHelp.h"
#include "../../ServerCliEngine.h"
#include "../../ServerCliRegistry.h"
#include "../../ServerCliParser.h"
namespace ServerRuntime
{
const char *CliCommandHelp::Name() const
{
return "help";
}
std::vector<std::string> CliCommandHelp::Aliases() const
{
std::vector<std::string> aliases;
aliases.push_back("?");
return aliases;
}
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;
}
}
@@ -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);
};
}
@@ -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
{
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);
}
}
}
@@ -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;
};
}
@@ -0,0 +1,50 @@
#include "stdafx.h"
#include "CliCommandList.h"
#include "../../ServerCliEngine.h"
#include "../../../Common/StringUtils.h"
#include "../../../../Minecraft.Client/MinecraftServer.h"
#include "../../../../Minecraft.Client/PlayerList.h"
#include "../../ServerCliParser.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;
}
}
@@ -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);
};
}
@@ -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);
}
}
}
}
@@ -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;
};
}
@@ -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);
}
}
@@ -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;
};
}
@@ -0,0 +1,33 @@
#include "stdafx.h"
#include "CliCommandStop.h"
#include "../../ServerCliEngine.h"
#include "../../ServerCliParser.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;
}
}
@@ -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);
};
}
@@ -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
{
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);
}
}
}
@@ -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;
};
}
@@ -0,0 +1,84 @@
#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
{
const char *kTpUsage = "tp <player> <target>";
}
const char *CliCommandTp::Name() const
{
return "tp";
}
std::vector<std::string> CliCommandTp::Aliases() const
{
std::vector<std::string> aliases;
aliases.push_back("teleport");
return aliases;
}
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);
}
}
}
@@ -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;
};
}
@@ -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
{
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);
}
}
@@ -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;
};
}
@@ -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);
}
}
}
}
@@ -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;
};
}
+308
View File
@@ -0,0 +1,308 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{FF2DDBF5-8A27-417A-A01C-B98A25C59760}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>MinecraftServer</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v110</PlatformToolset>
<CharacterSet>MultiByte</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v110</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>MultiByte</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
<Import Project="$(VCTargetsPath)\BuildCustomizations\masm.props" />
</ImportGroup>
<ImportGroup Label="Shared" />
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<OutDir>$(SolutionDir)$(Platform)\Minecraft.Server\$(Configuration)\</OutDir>
<IntDir>$(SolutionDir)$(Platform)\Minecraft.Server\$(Configuration)\obj\MinecraftServer\</IntDir>
<TargetName>Minecraft.Server</TargetName>
<LocalDebuggerWorkingDirectory>$(OutDir)</LocalDebuggerWorkingDirectory>
<LocalDebuggerCommandArguments>-port 25565 -bind 0.0.0.0 -name DedicatedServer</LocalDebuggerCommandArguments>
<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<OutDir>$(SolutionDir)$(Platform)\Server\</OutDir>
<IntDir>$(Platform)\Server\</IntDir>
<TargetName>Minecraft.Server</TargetName>
<LocalDebuggerWorkingDirectory>$(OutDir)</LocalDebuggerWorkingDirectory>
<LocalDebuggerCommandArguments>-port 25565 -bind 0.0.0.0 -name DedicatedServer</LocalDebuggerCommandArguments>
<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
<IncludePath>..\Minecraft.World\x64headers;..\Minecraft.Client\Windows64\Sentient\Include;$(IncludePath)</IncludePath>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderOutputFile>Server\$(ProjectName).pch</PrecompiledHeaderOutputFile>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<ExceptionHandling>Sync</ExceptionHandling>
<RuntimeTypeInfo>true</RuntimeTypeInfo>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<PreprocessorDefinitions>_LARGE_WORLDS;_DEBUG_MENUS_ENABLED;_DEBUG;_CRT_NON_CONFORMING_SWPRINTFS;_CRT_SECURE_NO_WARNINGS;_WINDOWS64;MINECRAFT_SERVER_BUILD;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>..\Minecraft.Client;..\Minecraft.Client\Windows64\Iggy\include;..\Minecraft.Client\Xbox\Sentient\Include;..\Minecraft.World\x64headers;..\include;$(ProjectDir)Windows64;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<MASM>
<UseSafeExceptionHandlers>false</UseSafeExceptionHandlers>
</MASM>
<ResourceCompile>
<PreprocessorDefinitions>_WINDOWS64;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>..\Minecraft.Client;..\Minecraft.Client\Xbox;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ResourceCompile>
<Link>
<SubSystem>Console</SubSystem>
<EntryPointSymbol>mainCRTStartup</EntryPointSymbol>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>d3d11.lib;..\Minecraft.Client\x64_Server\Minecraft.Client.lib;..\Minecraft.World\x64_$(Configuration)\Minecraft.World.lib;XInput9_1_0.lib;Windows64\Iggy\lib\iggy_w64.lib;;XInput9_1_0.lib;Windows64\Miles\Lib\mss64.lib;wsock32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
<PostBuildEvent>
<Command>powershell -ExecutionPolicy Bypass -File "$(ProjectDir)Windows64\postbuild_server.ps1" -OutDir "$(OutDir)." -ProjectRoot "$(ProjectDir).." -Configuration "$(Configuration)"</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderOutputFile>Server\$(ProjectName).pch</PrecompiledHeaderOutputFile>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<ExceptionHandling>Sync</ExceptionHandling>
<RuntimeTypeInfo>true</RuntimeTypeInfo>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<PreprocessorDefinitions>MINECRAFT_SERVER_BUILD;_LARGE_WORLDS;_DEBUG_MENUS_ENABLED;_CRT_NON_CONFORMING_SWPRINTFS;_CRT_SECURE_NO_WARNINGS;_WINDOWS64;MINECRAFT_SERVER_BUILD;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>..\Minecraft.Client;..\Minecraft.Client\Windows64\Iggy\include;..\Minecraft.Client\Xbox\Sentient\Include;..\Minecraft.World\x64headers;..\include;..\Minecraft.Client\Windows64;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<MASM>
<UseSafeExceptionHandlers>false</UseSafeExceptionHandlers>
</MASM>
<ResourceCompile>
<PreprocessorDefinitions>_WINDOWS64;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>..\Minecraft.Client;..\Minecraft.Client\Xbox;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ResourceCompile>
<Link>
<SubSystem>Console</SubSystem>
<EntryPointSymbol>mainCRTStartup</EntryPointSymbol>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>d3d11.lib;..\Minecraft.Client\x64_Server\Minecraft.Client.lib;..\Minecraft.World\x64_$(Configuration)\Minecraft.World.lib;XInput9_1_0.lib;Windows64\Iggy\lib\iggy_w64.lib;;XInput9_1_0.lib;Windows64\Miles\Lib\mss64.lib;wsock32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
<PostBuildEvent>
<Command>powershell -ExecutionPolicy Bypass -File "../Minecraft.Client/Common/Postbuilds/Server-postbuild.ps1" -OutDir "$(OutDir)/" -ProjectDir "$(ProjectDir)/"</Command>
<Message>Run Server postbuild script</Message>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="Access\Access.cpp" />
<ClCompile Include="Access\BanManager.cpp" />
<ClCompile Include="Access\WhitelistManager.cpp" />
<ClCompile Include="Common\FileUtils.cpp" />
<ClCompile Include="Common\StringUtils.cpp" />
<ClCompile Include="Console\commands\ban-ip\CliCommandBanIp.cpp" />
<ClCompile Include="Console\commands\ban-list\CliCommandBanList.cpp" />
<ClCompile Include="Console\commands\ban\CliCommandBan.cpp" />
<ClCompile Include="Console\commands\defaultgamemode\CliCommandDefaultGamemode.cpp" />
<ClCompile Include="Console\commands\enchant\CliCommandEnchant.cpp" />
<ClCompile Include="Console\commands\experience\CliCommandExperience.cpp" />
<ClCompile Include="Console\commands\gamemode\CliCommandGamemode.cpp" />
<ClCompile Include="Console\commands\give\CliCommandGive.cpp" />
<ClCompile Include="Console\commands\help\CliCommandHelp.cpp" />
<ClCompile Include="Console\commands\kill\CliCommandKill.cpp" />
<ClCompile Include="Console\commands\list\CliCommandList.cpp" />
<ClCompile Include="Console\commands\pardon-ip\CliCommandPardonIp.cpp" />
<ClCompile Include="Console\commands\pardon\CliCommandPardon.cpp" />
<ClCompile Include="Console\commands\stop\CliCommandStop.cpp" />
<ClCompile Include="Console\commands\time\CliCommandTime.cpp" />
<ClCompile Include="Console\commands\tp\CliCommandTp.cpp" />
<ClCompile Include="Console\commands\weather\CliCommandWeather.cpp" />
<ClCompile Include="Console\commands\whitelist\CliCommandWhitelist.cpp" />
<ClCompile Include="Console\ServerCli.cpp" />
<ClCompile Include="Console\ServerCliEngine.cpp" />
<ClCompile Include="Console\ServerCliInput.cpp" />
<ClCompile Include="Console\ServerCliParser.cpp" />
<ClCompile Include="Console\ServerCliRegistry.cpp" />
<ClCompile Include="ServerLogger.cpp" />
<ClCompile Include="ServerLogManager.cpp" />
<ClCompile Include="ServerProperties.cpp" />
<ClCompile Include="stdafx.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="vendor\linenoise\linenoise.c">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">false</ExcludedFromBuild>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="Windows64\ServerMain.cpp" />
<ClCompile Include="WorldManager.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\bck\ServerLogger.h" />
<ClInclude Include="..\bck\ServerLogManager.h" />
<ClInclude Include="..\bck\ServerProperties.h" />
<ClInclude Include="..\bck\ServerShutdown.h" />
<ClInclude Include="..\bck\WorldManager.h" />
<ClInclude Include="Access\Access.h" />
<ClInclude Include="Access\BanManager.h" />
<ClInclude Include="Access\WhitelistManager.h" />
<ClInclude Include="Console\ServerCli.h" />
<ClInclude Include="Console\ServerCliInput.h" />
<ClInclude Include="Console\commands\ban\CliCommandBan.h" />
<ClInclude Include="Console\commands\ban-ip\CliCommandBanIp.h" />
<ClInclude Include="Console\commands\ban-list\CliCommandBanList.h" />
<ClInclude Include="Console\commands\defaultgamemode\CliCommandDefaultGamemode.h" />
<ClInclude Include="Console\commands\enchant\CliCommandEnchant.h" />
<ClInclude Include="Console\commands\experience\CliCommandExperience.h" />
<ClInclude Include="Console\commands\gamemode\CliCommandGamemode.h" />
<ClInclude Include="Console\commands\give\CliCommandGive.h" />
<ClInclude Include="Console\commands\help\CliCommandHelp.h" />
<ClInclude Include="Console\commands\CommandParsing.h" />
<ClInclude Include="Console\commands\kill\CliCommandKill.h" />
<ClInclude Include="Console\commands\list\CliCommandList.h" />
<ClInclude Include="Console\commands\pardon\CliCommandPardon.h" />
<ClInclude Include="Console\commands\pardon-ip\CliCommandPardonIp.h" />
<ClInclude Include="Console\commands\stop\CliCommandStop.h" />
<ClInclude Include="Console\commands\time\CliCommandTime.h" />
<ClInclude Include="Console\commands\tp\CliCommandTp.h" />
<ClInclude Include="Console\commands\weather\CliCommandWeather.h" />
<ClInclude Include="Console\commands\whitelist\CliCommandWhitelist.h" />
<ClInclude Include="Console\commands\IServerCliCommand.h" />
<ClInclude Include="Console\ServerCliEngine.h" />
<ClInclude Include="Console\ServerCliParser.h" />
<ClInclude Include="Console\ServerCliRegistry.h" />
<ClInclude Include="Common\FileUtils.h" />
<ClInclude Include="Common\AccessStorageUtils.h" />
<ClInclude Include="Common\NetworkUtils.h" />
<ClInclude Include="Common\StringUtils.h" />
<ClInclude Include="ServerLogger.h" />
<ClInclude Include="ServerLogManager.h" />
<ClInclude Include="ServerProperties.h" />
<ClInclude Include="vendor\linenoise\linenoise.h">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">false</ExcludedFromBuild>
</ClInclude>
<ClInclude Include="vendor\rapidjson\allocators.h" />
<ClInclude Include="vendor\rapidjson\cursorstreamwrapper.h" />
<ClInclude Include="vendor\rapidjson\document.h" />
<ClInclude Include="vendor\rapidjson\encodedstream.h" />
<ClInclude Include="vendor\rapidjson\encodings.h" />
<ClInclude Include="vendor\rapidjson\error\en.h" />
<ClInclude Include="vendor\rapidjson\error\error.h" />
<ClInclude Include="vendor\rapidjson\filereadstream.h" />
<ClInclude Include="vendor\rapidjson\filewritestream.h" />
<ClInclude Include="vendor\rapidjson\fwd.h" />
<ClInclude Include="vendor\rapidjson\internal\biginteger.h" />
<ClInclude Include="vendor\rapidjson\internal\clzll.h" />
<ClInclude Include="vendor\rapidjson\internal\diyfp.h" />
<ClInclude Include="vendor\rapidjson\internal\dtoa.h" />
<ClInclude Include="vendor\rapidjson\internal\ieee754.h" />
<ClInclude Include="vendor\rapidjson\internal\itoa.h" />
<ClInclude Include="vendor\rapidjson\internal\meta.h" />
<ClInclude Include="vendor\rapidjson\internal\pow10.h" />
<ClInclude Include="vendor\rapidjson\internal\regex.h" />
<ClInclude Include="vendor\rapidjson\internal\stack.h" />
<ClInclude Include="vendor\rapidjson\internal\strfunc.h" />
<ClInclude Include="vendor\rapidjson\internal\strtod.h" />
<ClInclude Include="vendor\rapidjson\internal\swap.h" />
<ClInclude Include="vendor\rapidjson\istreamwrapper.h" />
<ClInclude Include="vendor\rapidjson\memorybuffer.h" />
<ClInclude Include="vendor\rapidjson\memorystream.h" />
<ClInclude Include="vendor\rapidjson\msinttypes\inttypes.h" />
<ClInclude Include="vendor\rapidjson\msinttypes\stdint.h" />
<ClInclude Include="vendor\rapidjson\ostreamwrapper.h" />
<ClInclude Include="vendor\rapidjson\pointer.h" />
<ClInclude Include="vendor\rapidjson\prettywriter.h" />
<ClInclude Include="vendor\rapidjson\rapidjson.h" />
<ClInclude Include="vendor\rapidjson\reader.h" />
<ClInclude Include="vendor\rapidjson\schema.h" />
<ClInclude Include="vendor\rapidjson\stream.h" />
<ClInclude Include="vendor\rapidjson\stringbuffer.h" />
<ClInclude Include="vendor\rapidjson\uri.h" />
<ClInclude Include="vendor\rapidjson\writer.h" />
<ClInclude Include="Windows64\4JLibs\inc\4J_Input.h">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClInclude>
<ClInclude Include="Windows64\4JLibs\inc\4J_Profile.h">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClInclude>
<ClInclude Include="Windows64\4JLibs\inc\4J_Render.h">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClInclude>
<ClInclude Include="Windows64\4JLibs\inc\4J_Storage.h">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClInclude>
<ClInclude Include="WorldManager.h" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Minecraft.World\Minecraft.World.vcxproj">
<Project>{F046C3CE-9749-4823-B32B-D9CC10B1A2C8}</Project>
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="docs\DEVELOPMENT.en.md" />
<None Include="docs\DEVELOPMENT.ja.md" />
<None Include="vendor\linenoise\LICENSE" />
</ItemGroup>
<ItemGroup>
<Library Include="Windows64\4JLibs\libs\4J_Input.lib">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
</Library>
<Library Include="Windows64\4JLibs\libs\4J_Input_d.lib">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</Library>
<Library Include="Windows64\4JLibs\libs\4J_Profile.lib" />
<Library Include="Windows64\4JLibs\libs\4J_Render_PC.lib">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
</Library>
<Library Include="Windows64\4JLibs\libs\4J_Render_PC_d.lib">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</Library>
<Library Include="Windows64\4JLibs\libs\4J_Storage.lib">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
</Library>
<Library Include="Windows64\4JLibs\libs\4J_Storage_d.lib">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</Library>
<Library Include="Windows64\Iggy\lib\iggyexpruntime_w64.lib" />
<Library Include="Windows64\Iggy\lib\iggyperfmon_w64.lib" />
<Library Include="Windows64\Iggy\lib\iggy_w64.lib" />
<Library Include="Windows64\Miles\lib\mss64.lib" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
<Import Project="$(VCTargetsPath)\BuildCustomizations\masm.targets" />
</ImportGroup>
</Project>
@@ -0,0 +1,529 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hpp;hxx;hm;inl;inc;xsd</Extensions>
</Filter>
<Filter Include="Access">
<UniqueIdentifier>{51610b47-4ac2-4dde-87e2-aceebf26989c}</UniqueIdentifier>
</Filter>
<Filter Include="Common">
<UniqueIdentifier>{c148b1e2-1f21-4aa0-b174-cc8c24c8951a}</UniqueIdentifier>
</Filter>
<Filter Include="Console">
<UniqueIdentifier>{24c18f03-9685-42cc-8e58-e4b12e4c174c}</UniqueIdentifier>
</Filter>
<Filter Include="Console\commands">
<UniqueIdentifier>{029a5d10-ef33-4a25-9bcd-9f39944dd5f1}</UniqueIdentifier>
</Filter>
<Filter Include="Console\commands\ban">
<UniqueIdentifier>{5a30b59a-62d3-4f1e-b967-b77db2cec47b}</UniqueIdentifier>
</Filter>
<Filter Include="Console\commands\ban-ip">
<UniqueIdentifier>{01b98446-8ff2-495d-bc87-32b2681a7df3}</UniqueIdentifier>
</Filter>
<Filter Include="Console\commands\ban-list">
<UniqueIdentifier>{89569093-5317-40c0-be12-a19e4a3249a8}</UniqueIdentifier>
</Filter>
<Filter Include="Console\commands\defaultgamemode">
<UniqueIdentifier>{e313f52b-43b9-4dda-9559-af5a245d73b2}</UniqueIdentifier>
</Filter>
<Filter Include="Console\commands\enchant">
<UniqueIdentifier>{a6863c9c-ed24-4571-8004-e8ed4cd900a7}</UniqueIdentifier>
</Filter>
<Filter Include="Console\commands\experience">
<UniqueIdentifier>{af0f8212-558d-4182-821e-dc5c6af8765d}</UniqueIdentifier>
</Filter>
<Filter Include="Console\commands\gamemode">
<UniqueIdentifier>{ea6bea52-2793-4618-86b5-c737d1c0b873}</UniqueIdentifier>
</Filter>
<Filter Include="Console\commands\give">
<UniqueIdentifier>{a6301a38-475d-47f2-9450-37a3c135b826}</UniqueIdentifier>
</Filter>
<Filter Include="Console\commands\help">
<UniqueIdentifier>{bb281a8e-a612-459b-91c3-6f3bf8ede342}</UniqueIdentifier>
</Filter>
<Filter Include="Console\commands\kill">
<UniqueIdentifier>{efa1f6f1-ab08-4256-a8ab-1ad7eea0d10a}</UniqueIdentifier>
</Filter>
<Filter Include="Console\commands\list">
<UniqueIdentifier>{83df2dae-2a78-476c-b708-05659e444219}</UniqueIdentifier>
</Filter>
<Filter Include="Console\commands\pardon">
<UniqueIdentifier>{ae702713-2488-411d-8141-fd5d5a348ffa}</UniqueIdentifier>
</Filter>
<Filter Include="Console\commands\pardon-ip">
<UniqueIdentifier>{5f7f200c-d8a9-4b30-98a7-4f71e4d000f9}</UniqueIdentifier>
</Filter>
<Filter Include="Console\commands\stop">
<UniqueIdentifier>{81a36a77-c03d-459f-a951-dc00a7d760d7}</UniqueIdentifier>
</Filter>
<Filter Include="Console\commands\time">
<UniqueIdentifier>{eb779138-4cc1-4ee4-b212-337d56e109ee}</UniqueIdentifier>
</Filter>
<Filter Include="Console\commands\tp">
<UniqueIdentifier>{7bdf9604-e397-4e03-bbf1-2fda3cd851f9}</UniqueIdentifier>
</Filter>
<Filter Include="Console\commands\weather">
<UniqueIdentifier>{dbda2ebe-0841-4b02-b6a0-9f77f0dd88ea}</UniqueIdentifier>
</Filter>
<Filter Include="Console\commands\whitelist">
<UniqueIdentifier>{ec72e562-e436-4e4d-afd1-96662ff66149}</UniqueIdentifier>
</Filter>
<Filter Include="docs">
<UniqueIdentifier>{6b18e686-df1f-467d-a533-75dca2ac6f68}</UniqueIdentifier>
</Filter>
<Filter Include="vendor">
<UniqueIdentifier>{59579052-3978-492b-acb4-c31a0bd63bcd}</UniqueIdentifier>
</Filter>
<Filter Include="vendor\linenoise">
<UniqueIdentifier>{4d7261d2-3fae-4386-bbdd-8d4dad156f59}</UniqueIdentifier>
</Filter>
<Filter Include="Windows64">
<UniqueIdentifier>{08e4175f-35b8-4f37-9335-5f460a62ba2c}</UniqueIdentifier>
</Filter>
<Filter Include="vendor\rapidjson">
<UniqueIdentifier>{227f9fab-7527-4e66-bb0a-77634e3c1c63}</UniqueIdentifier>
</Filter>
<Filter Include="vendor\rapidjson\error">
<UniqueIdentifier>{fafde418-a3b2-4986-82a8-46fb199d0bd5}</UniqueIdentifier>
</Filter>
<Filter Include="vendor\rapidjson\internal">
<UniqueIdentifier>{3d43b9b8-1c52-4d65-b93d-d16b8d610800}</UniqueIdentifier>
</Filter>
<Filter Include="vendor\rapidjson\msinttypes">
<UniqueIdentifier>{a7c5b6eb-46cc-4cc0-ac7b-88a2def415fb}</UniqueIdentifier>
</Filter>
<Filter Include="Windows64\4JLibs">
<UniqueIdentifier>{4a22e8a3-ce95-4389-9199-0c728fe25f8e}</UniqueIdentifier>
</Filter>
<Filter Include="Windows64\4JLibs\inc">
<UniqueIdentifier>{bd464b7d-b9ee-471a-ba0a-66c8cd67daa8}</UniqueIdentifier>
</Filter>
<Filter Include="Windows64\4JLibs\libs">
<UniqueIdentifier>{99a0ecc1-cdc6-4077-9988-88a17add709b}</UniqueIdentifier>
</Filter>
<Filter Include="Windows64\4JLibs\Media">
<UniqueIdentifier>{4ca625a8-aae3-4121-a9e3-61a71490928f}</UniqueIdentifier>
</Filter>
<Filter Include="Windows64\Iggy">
<UniqueIdentifier>{b4403f41-77f4-49c6-8948-8ec7afd744c7}</UniqueIdentifier>
</Filter>
<Filter Include="Windows64\Iggy\lib">
<UniqueIdentifier>{ad11c0f5-63b0-49cb-a426-dd5c2e93b0e3}</UniqueIdentifier>
</Filter>
<Filter Include="Windows64\Miles Sound System">
<UniqueIdentifier>{a0a5cd4d-bc9b-450f-a2c1-c67e7739e52c}</UniqueIdentifier>
</Filter>
<Filter Include="Windows64\Miles Sound System\dll">
<UniqueIdentifier>{fd6ceca9-2e6d-4b2d-b6aa-8c103049a8e7}</UniqueIdentifier>
</Filter>
<Filter Include="Windows64\Miles Sound System\dll\Debug">
<UniqueIdentifier>{005fdd55-6b11-40bc-bc7a-59afa39d5782}</UniqueIdentifier>
</Filter>
<Filter Include="Windows64\Miles Sound System\dll\Release">
<UniqueIdentifier>{6976df84-e36b-4d48-b2f8-35d2ac4a9495}</UniqueIdentifier>
</Filter>
<Filter Include="Windows64\Miles Sound System\lib">
<UniqueIdentifier>{19759ef7-84d7-4625-8cf5-65ce2b44c8e1}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="Access\Access.cpp">
<Filter>Access</Filter>
</ClCompile>
<ClCompile Include="Access\BanManager.cpp">
<Filter>Access</Filter>
</ClCompile>
<ClCompile Include="Access\WhitelistManager.cpp">
<Filter>Access</Filter>
</ClCompile>
<ClCompile Include="Common\FileUtils.cpp">
<Filter>Common</Filter>
</ClCompile>
<ClCompile Include="Common\StringUtils.cpp">
<Filter>Common</Filter>
</ClCompile>
<ClCompile Include="Console\ServerCli.cpp">
<Filter>Console</Filter>
</ClCompile>
<ClCompile Include="Console\ServerCliEngine.cpp">
<Filter>Console</Filter>
</ClCompile>
<ClCompile Include="Console\ServerCliInput.cpp">
<Filter>Console</Filter>
</ClCompile>
<ClCompile Include="Console\ServerCliParser.cpp">
<Filter>Console</Filter>
</ClCompile>
<ClCompile Include="Console\ServerCliRegistry.cpp">
<Filter>Console</Filter>
</ClCompile>
<ClCompile Include="Console\commands\ban\CliCommandBan.cpp">
<Filter>Console\commands\ban</Filter>
</ClCompile>
<ClCompile Include="Console\commands\ban-ip\CliCommandBanIp.cpp">
<Filter>Console\commands\ban-ip</Filter>
</ClCompile>
<ClCompile Include="Console\commands\ban-list\CliCommandBanList.cpp">
<Filter>Console\commands\ban-list</Filter>
</ClCompile>
<ClCompile Include="Console\commands\defaultgamemode\CliCommandDefaultGamemode.cpp">
<Filter>Console\commands\defaultgamemode</Filter>
</ClCompile>
<ClCompile Include="Console\commands\enchant\CliCommandEnchant.cpp">
<Filter>Console\commands\enchant</Filter>
</ClCompile>
<ClCompile Include="Console\commands\experience\CliCommandExperience.cpp">
<Filter>Console\commands\experience</Filter>
</ClCompile>
<ClCompile Include="Console\commands\gamemode\CliCommandGamemode.cpp">
<Filter>Console\commands\gamemode</Filter>
</ClCompile>
<ClCompile Include="Console\commands\give\CliCommandGive.cpp">
<Filter>Console\commands\give</Filter>
</ClCompile>
<ClCompile Include="Console\commands\help\CliCommandHelp.cpp">
<Filter>Console\commands\help</Filter>
</ClCompile>
<ClCompile Include="Console\commands\kill\CliCommandKill.cpp">
<Filter>Console\commands\kill</Filter>
</ClCompile>
<ClCompile Include="Console\commands\list\CliCommandList.cpp">
<Filter>Console\commands\list</Filter>
</ClCompile>
<ClCompile Include="Console\commands\whitelist\CliCommandWhitelist.cpp">
<Filter>Console\commands\whitelist</Filter>
</ClCompile>
<ClCompile Include="Console\commands\weather\CliCommandWeather.cpp">
<Filter>Console\commands\weather</Filter>
</ClCompile>
<ClCompile Include="Console\commands\tp\CliCommandTp.cpp">
<Filter>Console\commands\tp</Filter>
</ClCompile>
<ClCompile Include="Console\commands\time\CliCommandTime.cpp">
<Filter>Console\commands\time</Filter>
</ClCompile>
<ClCompile Include="Console\commands\stop\CliCommandStop.cpp">
<Filter>Console\commands\stop</Filter>
</ClCompile>
<ClCompile Include="Console\commands\pardon-ip\CliCommandPardonIp.cpp">
<Filter>Console\commands\pardon-ip</Filter>
</ClCompile>
<ClCompile Include="Console\commands\pardon\CliCommandPardon.cpp">
<Filter>Console\commands\pardon</Filter>
</ClCompile>
<ClCompile Include="vendor\linenoise\linenoise.c">
<Filter>vendor\linenoise</Filter>
</ClCompile>
<ClCompile Include="Windows64\ServerMain.cpp">
<Filter>Windows64</Filter>
</ClCompile>
<ClCompile Include="stdafx.cpp" />
<ClCompile Include="ServerLogger.cpp" />
<ClCompile Include="ServerLogManager.cpp" />
<ClCompile Include="ServerProperties.cpp" />
<ClCompile Include="WorldManager.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="Common\AccessStorageUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Common\FileUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Common\NetworkUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Common\StringUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Console\commands\ban\CliCommandBan.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Console\commands\ban-ip\CliCommandBanIp.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Console\commands\ban-list\CliCommandBanList.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Console\commands\CommandParsing.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Console\commands\defaultgamemode\CliCommandDefaultGamemode.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Console\commands\enchant\CliCommandEnchant.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Console\commands\experience\CliCommandExperience.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Console\commands\gamemode\CliCommandGamemode.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Console\commands\give\CliCommandGive.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Console\commands\help\CliCommandHelp.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Console\commands\IServerCliCommand.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Console\commands\kill\CliCommandKill.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Console\commands\list\CliCommandList.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Console\commands\pardon\CliCommandPardon.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Console\commands\pardon-ip\CliCommandPardonIp.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Console\commands\stop\CliCommandStop.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Console\commands\time\CliCommandTime.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Console\commands\tp\CliCommandTp.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Console\commands\weather\CliCommandWeather.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Console\commands\whitelist\CliCommandWhitelist.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Console\ServerCli.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Console\ServerCliEngine.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Console\ServerCliInput.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Console\ServerCliParser.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Console\ServerCliRegistry.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="vendor\linenoise\linenoise.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="ServerLogger.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="ServerLogManager.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="ServerProperties.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="WorldManager.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Access\Access.h">
<Filter>Access</Filter>
</ClInclude>
<ClInclude Include="Access\BanManager.h">
<Filter>Access</Filter>
</ClInclude>
<ClInclude Include="Access\WhitelistManager.h">
<Filter>Access</Filter>
</ClInclude>
<ClInclude Include="..\bck\ServerLogger.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\bck\ServerLogManager.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\bck\ServerProperties.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\bck\ServerShutdown.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\bck\WorldManager.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\allocators.h">
<Filter>vendor\rapidjson</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\cursorstreamwrapper.h">
<Filter>vendor\rapidjson</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\document.h">
<Filter>vendor\rapidjson</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\encodedstream.h">
<Filter>vendor\rapidjson</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\encodings.h">
<Filter>vendor\rapidjson</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\filereadstream.h">
<Filter>vendor\rapidjson</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\filewritestream.h">
<Filter>vendor\rapidjson</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\fwd.h">
<Filter>vendor\rapidjson</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\istreamwrapper.h">
<Filter>vendor\rapidjson</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\memorybuffer.h">
<Filter>vendor\rapidjson</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\memorystream.h">
<Filter>vendor\rapidjson</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\ostreamwrapper.h">
<Filter>vendor\rapidjson</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\pointer.h">
<Filter>vendor\rapidjson</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\prettywriter.h">
<Filter>vendor\rapidjson</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\rapidjson.h">
<Filter>vendor\rapidjson</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\reader.h">
<Filter>vendor\rapidjson</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\schema.h">
<Filter>vendor\rapidjson</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\stream.h">
<Filter>vendor\rapidjson</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\stringbuffer.h">
<Filter>vendor\rapidjson</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\uri.h">
<Filter>vendor\rapidjson</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\writer.h">
<Filter>vendor\rapidjson</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\error\en.h">
<Filter>vendor\rapidjson\error</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\error\error.h">
<Filter>vendor\rapidjson\error</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\internal\biginteger.h">
<Filter>vendor\rapidjson\internal</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\internal\clzll.h">
<Filter>vendor\rapidjson\internal</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\internal\diyfp.h">
<Filter>vendor\rapidjson\internal</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\internal\dtoa.h">
<Filter>vendor\rapidjson\internal</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\internal\ieee754.h">
<Filter>vendor\rapidjson\internal</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\internal\itoa.h">
<Filter>vendor\rapidjson\internal</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\internal\meta.h">
<Filter>vendor\rapidjson\internal</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\internal\pow10.h">
<Filter>vendor\rapidjson\internal</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\internal\regex.h">
<Filter>vendor\rapidjson\internal</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\internal\stack.h">
<Filter>vendor\rapidjson\internal</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\internal\strfunc.h">
<Filter>vendor\rapidjson\internal</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\internal\strtod.h">
<Filter>vendor\rapidjson\internal</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\internal\swap.h">
<Filter>vendor\rapidjson\internal</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\msinttypes\inttypes.h">
<Filter>vendor\rapidjson\msinttypes</Filter>
</ClInclude>
<ClInclude Include="vendor\rapidjson\msinttypes\stdint.h">
<Filter>vendor\rapidjson\msinttypes</Filter>
</ClInclude>
<ClInclude Include="Windows64\4JLibs\inc\4J_Input.h">
<Filter>Windows64\4JLibs\inc</Filter>
</ClInclude>
<ClInclude Include="Windows64\4JLibs\inc\4J_Profile.h">
<Filter>Windows64\4JLibs\inc</Filter>
</ClInclude>
<ClInclude Include="Windows64\4JLibs\inc\4J_Render.h">
<Filter>Windows64\4JLibs\inc</Filter>
</ClInclude>
<ClInclude Include="Windows64\4JLibs\inc\4J_Storage.h">
<Filter>Windows64\4JLibs\inc</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<None Include="docs\DEVELOPMENT.en.md">
<Filter>docs</Filter>
</None>
<None Include="docs\DEVELOPMENT.ja.md">
<Filter>docs</Filter>
</None>
<None Include="vendor\linenoise\LICENSE">
<Filter>vendor\linenoise</Filter>
</None>
</ItemGroup>
<ItemGroup>
<Library Include="Windows64\4JLibs\libs\4J_Input.lib">
<Filter>Windows64\4JLibs\libs</Filter>
</Library>
<Library Include="Windows64\4JLibs\libs\4J_Input_d.lib">
<Filter>Windows64\4JLibs\libs</Filter>
</Library>
<Library Include="Windows64\4JLibs\libs\4J_Profile.lib">
<Filter>Windows64\4JLibs\libs</Filter>
</Library>
<Library Include="Windows64\4JLibs\libs\4J_Render_PC.lib">
<Filter>Windows64\4JLibs\libs</Filter>
</Library>
<Library Include="Windows64\4JLibs\libs\4J_Render_PC_d.lib">
<Filter>Windows64\4JLibs\libs</Filter>
</Library>
<Library Include="Windows64\4JLibs\libs\4J_Storage.lib">
<Filter>Windows64\4JLibs\libs</Filter>
</Library>
<Library Include="Windows64\4JLibs\libs\4J_Storage_d.lib">
<Filter>Windows64\4JLibs\libs</Filter>
</Library>
<Library Include="Windows64\Iggy\lib\iggy_w64.lib">
<Filter>Windows64\Iggy\lib</Filter>
</Library>
<Library Include="Windows64\Iggy\lib\iggyexpruntime_w64.lib">
<Filter>Windows64\Iggy\lib</Filter>
</Library>
<Library Include="Windows64\Iggy\lib\iggyperfmon_w64.lib">
<Filter>Windows64\Iggy\lib</Filter>
</Library>
<Library Include="Windows64\Miles\lib\mss64.lib">
<Filter>Windows64\Miles Sound System\lib</Filter>
</Library>
</ItemGroup>
</Project>
+401
View File
@@ -0,0 +1,401 @@
#include "stdafx.h"
#include "ServerLogManager.h"
#include "Common/StringUtils.h"
#include "ServerLogger.h"
#include <array>
#include <mutex>
namespace ServerRuntime
{
namespace ServerLogManager
{
namespace
{
/**
* **!! This information is managed solely for logging purposes, but it is questionable from a liability perspective, so it will eventually need to be separated !!**
*
* Tracks the remote IP and accepted player name associated with one `smallId`
* 1つのsmallIdに紐づく接続IPとプレイヤー名を保持する
*/
struct ConnectionLogEntry
{
std::string remoteIp;
std::string playerName;
};
/**
* Owns the shared connection cache used by hook points running on different threads
* 複数スレッドのhookから共有される接続キャッシュを保持する
*/
struct ServerLogState
{
std::mutex stateLock;
std::array<ConnectionLogEntry, 256> entries;
};
ServerLogState g_serverLogState;
static bool IsDedicatedServerLoggingEnabled()
{
// PIEBOT: Minecraft.Server only compiles if the build config is set to Server
return true;
}
static void ResetConnectionLogEntry(ConnectionLogEntry *entry)
{
if (entry == NULL)
{
return;
}
entry->remoteIp.clear();
entry->playerName.clear();
}
static std::string NormalizeRemoteIp(const char *ip)
{
if (ip == NULL || ip[0] == 0)
{
return std::string("unknown");
}
return std::string(ip);
}
static std::string NormalizePlayerName(const std::wstring &playerName)
{
std::string playerNameUtf8 = StringUtils::WideToUtf8(playerName);
if (playerNameUtf8.empty())
{
playerNameUtf8 = "<unknown>";
}
return playerNameUtf8;
}
// Default to the main app channel when the caller does not provide a source tag.
static const char *NormalizeClientLogSource(const char *source)
{
if (source == NULL || source[0] == 0)
{
return "app";
}
return source;
}
static void EmitClientDebugLogLine(const char *source, const std::string &line)
{
if (line.empty())
{
return;
}
LogDebugf("client", "[%s] %s", NormalizeClientLogSource(source), line.c_str());
}
// Split one debug payload into individual lines so each line becomes a prompt-safe server log entry.
static void ForwardClientDebugMessage(const char *source, const char *message)
{
if (message == NULL || message[0] == 0)
{
return;
}
const char *cursor = message;
while (*cursor != 0)
{
const char *lineStart = cursor;
while (*cursor != 0 && *cursor != '\r' && *cursor != '\n')
{
++cursor;
}
// Split multi-line client debug output into prompt-safe server log entries.
if (cursor > lineStart)
{
EmitClientDebugLogLine(source, std::string(lineStart, (size_t)(cursor - lineStart)));
}
while (*cursor == '\r' || *cursor == '\n')
{
++cursor;
}
}
}
// Share the same formatting path for app, user, and legacy debug-spew forwards.
static void ForwardFormattedClientDebugLogV(const char *source, const char *format, va_list args)
{
if (!IsDedicatedServerLoggingEnabled() || format == NULL || format[0] == 0)
{
return;
}
char messageBuffer[2048] = {};
vsnprintf_s(messageBuffer, sizeof(messageBuffer), _TRUNCATE, format, args);
ForwardClientDebugMessage(source, messageBuffer);
}
static const char *TcpRejectReasonToString(ETcpRejectReason reason)
{
switch (reason)
{
case eTcpRejectReason_BannedIp: return "banned-ip";
case eTcpRejectReason_GameNotReady: return "game-not-ready";
case eTcpRejectReason_ServerFull: return "server-full";
default: return "unknown";
}
}
static const char *LoginRejectReasonToString(ELoginRejectReason reason)
{
switch (reason)
{
case eLoginRejectReason_BannedXuid: return "banned-xuid";
case eLoginRejectReason_NotWhitelisted: return "not-whitelisted";
case eLoginRejectReason_DuplicateXuid: return "duplicate-xuid";
case eLoginRejectReason_DuplicateName: return "duplicate-name";
default: return "unknown";
}
}
static const char *DisconnectReasonToString(DisconnectPacket::eDisconnectReason reason)
{
switch (reason)
{
case DisconnectPacket::eDisconnect_None: return "none";
case DisconnectPacket::eDisconnect_Quitting: return "quitting";
case DisconnectPacket::eDisconnect_Closed: return "closed";
case DisconnectPacket::eDisconnect_LoginTooLong: return "login-too-long";
case DisconnectPacket::eDisconnect_IllegalStance: return "illegal-stance";
case DisconnectPacket::eDisconnect_IllegalPosition: return "illegal-position";
case DisconnectPacket::eDisconnect_MovedTooQuickly: return "moved-too-quickly";
case DisconnectPacket::eDisconnect_NoFlying: return "no-flying";
case DisconnectPacket::eDisconnect_Kicked: return "kicked";
case DisconnectPacket::eDisconnect_TimeOut: return "timeout";
case DisconnectPacket::eDisconnect_Overflow: return "overflow";
case DisconnectPacket::eDisconnect_EndOfStream: return "end-of-stream";
case DisconnectPacket::eDisconnect_ServerFull: return "server-full";
case DisconnectPacket::eDisconnect_OutdatedServer: return "outdated-server";
case DisconnectPacket::eDisconnect_OutdatedClient: return "outdated-client";
case DisconnectPacket::eDisconnect_UnexpectedPacket: return "unexpected-packet";
case DisconnectPacket::eDisconnect_ConnectionCreationFailed: return "connection-creation-failed";
case DisconnectPacket::eDisconnect_NoMultiplayerPrivilegesHost: return "no-multiplayer-privileges-host";
case DisconnectPacket::eDisconnect_NoMultiplayerPrivilegesJoin: return "no-multiplayer-privileges-join";
case DisconnectPacket::eDisconnect_NoUGC_AllLocal: return "no-ugc-all-local";
case DisconnectPacket::eDisconnect_NoUGC_Single_Local: return "no-ugc-single-local";
case DisconnectPacket::eDisconnect_ContentRestricted_AllLocal: return "content-restricted-all-local";
case DisconnectPacket::eDisconnect_ContentRestricted_Single_Local: return "content-restricted-single-local";
case DisconnectPacket::eDisconnect_NoUGC_Remote: return "no-ugc-remote";
case DisconnectPacket::eDisconnect_NoFriendsInGame: return "no-friends-in-game";
case DisconnectPacket::eDisconnect_Banned: return "banned";
case DisconnectPacket::eDisconnect_NotFriendsWithHost: return "not-friends-with-host";
case DisconnectPacket::eDisconnect_NATMismatch: return "nat-mismatch";
default: return "unknown";
}
}
}
// Only forward client-side debug output while the process is running as the dedicated server.
bool ShouldForwardClientDebugLogs()
{
return IsDedicatedServerLoggingEnabled();
}
void ForwardClientAppDebugLogV(const char *format, va_list args)
{
ForwardFormattedClientDebugLogV("app", format, args);
}
void ForwardClientUserDebugLogV(int user, const char *format, va_list args)
{
char source[32] = {};
_snprintf_s(source, sizeof(source), _TRUNCATE, "app:user=%d", user);
ForwardFormattedClientDebugLogV(source, format, args);
}
void ForwardClientDebugSpewLogV(const char *format, va_list args)
{
ForwardFormattedClientDebugLogV("debug-spew", format, args);
}
// Clear every cached connection slot during startup so stale metadata never leaks into future logs.
void Initialize()
{
std::lock_guard<std::mutex> stateLock(g_serverLogState.stateLock);
for (size_t index = 0; index < g_serverLogState.entries.size(); ++index)
{
ResetConnectionLogEntry(&g_serverLogState.entries[index]);
}
}
// Reuse Initialize as the shutdown cleanup path because both operations wipe the cache.
void Shutdown()
{
Initialize();
}
// Log the raw socket arrival before a smallId is assigned so early rejects still have an IP in the logs.
void OnIncomingTcpConnection(const char *ip)
{
if (!IsDedicatedServerLoggingEnabled())
{
return;
}
const std::string remoteIp = NormalizeRemoteIp(ip);
LogInfof("network", "incoming tcp connection from %s", remoteIp.c_str());
}
// TCP rejects happen before connection state is cached, so log directly from the supplied remote IP.
void OnRejectedTcpConnection(const char *ip, ETcpRejectReason reason)
{
if (!IsDedicatedServerLoggingEnabled())
{
return;
}
const std::string remoteIp = NormalizeRemoteIp(ip);
LogWarnf("network", "rejected tcp connection from %s: reason=%s", remoteIp.c_str(), TcpRejectReasonToString(reason));
}
// Cache the accepted remote IP immediately so later login and disconnect logs can reuse it.
void OnAcceptedTcpConnection(unsigned char smallId, const char *ip)
{
if (!IsDedicatedServerLoggingEnabled())
{
return;
}
const std::string remoteIp = NormalizeRemoteIp(ip);
{
std::lock_guard<std::mutex> stateLock(g_serverLogState.stateLock);
ConnectionLogEntry &entry = g_serverLogState.entries[smallId];
ResetConnectionLogEntry(&entry);
entry.remoteIp = remoteIp;
}
LogInfof("network", "accepted tcp connection from %s as smallId=%u", remoteIp.c_str(), (unsigned)smallId);
}
// Once login succeeds, bind the resolved player name onto the cached transport entry.
void OnAcceptedPlayerLogin(unsigned char smallId, const std::wstring &playerName)
{
if (!IsDedicatedServerLoggingEnabled())
{
return;
}
const std::string playerNameUtf8 = NormalizePlayerName(playerName);
std::string remoteIp("unknown");
{
std::lock_guard<std::mutex> stateLock(g_serverLogState.stateLock);
ConnectionLogEntry &entry = g_serverLogState.entries[smallId];
entry.playerName = playerNameUtf8;
if (!entry.remoteIp.empty())
{
remoteIp = entry.remoteIp;
}
}
LogInfof("network", "accepted player login: name=\"%s\" ip=%s smallId=%u", playerNameUtf8.c_str(), remoteIp.c_str(), (unsigned)smallId);
}
// Read the cached IP for the rejection log, then clear the slot because the player never fully joined.
void OnRejectedPlayerLogin(unsigned char smallId, const std::wstring &playerName, ELoginRejectReason reason)
{
if (!IsDedicatedServerLoggingEnabled())
{
return;
}
const std::string playerNameUtf8 = NormalizePlayerName(playerName);
std::string remoteIp("unknown");
{
std::lock_guard<std::mutex> stateLock(g_serverLogState.stateLock);
ConnectionLogEntry &entry = g_serverLogState.entries[smallId];
if (!entry.remoteIp.empty())
{
remoteIp = entry.remoteIp;
}
ResetConnectionLogEntry(&entry);
}
LogWarnf("network", "rejected login from %s: name=\"%s\" reason=%s", remoteIp.c_str(), playerNameUtf8.c_str(), LoginRejectReasonToString(reason));
}
// Disconnect logging is the final consumer of cached metadata, so it also clears the slot afterward.
void OnPlayerDisconnected(
unsigned char smallId,
const std::wstring &playerName,
DisconnectPacket::eDisconnectReason reason,
bool initiatedByServer)
{
if (!IsDedicatedServerLoggingEnabled())
{
return;
}
std::string playerNameUtf8 = NormalizePlayerName(playerName);
std::string remoteIp("unknown");
{
// Copy state under lock and emit the log after unlocking so CLI output never blocks connection bookkeeping.
std::lock_guard<std::mutex> stateLock(g_serverLogState.stateLock);
ConnectionLogEntry &entry = g_serverLogState.entries[smallId];
if (!entry.remoteIp.empty())
{
remoteIp = entry.remoteIp;
}
if (playerNameUtf8 == "<unknown>" && !entry.playerName.empty())
{
playerNameUtf8 = entry.playerName;
}
ResetConnectionLogEntry(&entry);
}
LogInfof(
"network",
"%s: name=\"%s\" ip=%s smallId=%u reason=%s",
initiatedByServer ? "disconnecting player" : "player disconnected",
playerNameUtf8.c_str(),
remoteIp.c_str(),
(unsigned)smallId,
DisconnectReasonToString(reason));
}
/**
* For logging purposes, the responsibility is technically misplaced, but the IP is cached in `LogManager`.
* Those cached values are then used to retrieve the player's IP.
*
* Eventually, this should be implemented in a separate class or on the `Minecraft.Client` side instead.
*/
bool TryGetConnectionRemoteIp(unsigned char smallId, std::string *outIp)
{
if (!IsDedicatedServerLoggingEnabled() || outIp == NULL)
{
return false;
}
std::lock_guard<std::mutex> stateLock(g_serverLogState.stateLock);
const ConnectionLogEntry &entry = g_serverLogState.entries[smallId];
if (entry.remoteIp.empty() || entry.remoteIp == "unknown")
{
return false;
}
*outIp = entry.remoteIp;
return true;
}
// Provide explicit cache cleanup for paths that terminate without going through disconnect logging.
void ClearConnection(unsigned char smallId)
{
std::lock_guard<std::mutex> stateLock(g_serverLogState.stateLock);
ResetConnectionLogEntry(&g_serverLogState.entries[smallId]);
}
}
}
+127
View File
@@ -0,0 +1,127 @@
#pragma once
#include <string>
#include <stdarg.h>
#include "../Minecraft.World/DisconnectPacket.h"
namespace ServerRuntime
{
namespace ServerLogManager
{
/**
* Identifies why the dedicated server rejected a TCP connection before login completed
* ログイン完了前にTCP接続を拒否した理由
*/
enum ETcpRejectReason
{
eTcpRejectReason_BannedIp = 0,
eTcpRejectReason_GameNotReady,
eTcpRejectReason_ServerFull
};
/**
* Identifies why the dedicated server rejected a player during login validation
* ログイン検証中にプレイヤーを拒否した理由
*/
enum ELoginRejectReason
{
eLoginRejectReason_BannedXuid = 0,
eLoginRejectReason_NotWhitelisted,
eLoginRejectReason_DuplicateXuid,
eLoginRejectReason_DuplicateName
};
/**
* Returns `true` when client-side debug logs should be redirected into the dedicated server logger
* dedicated server時にclient側デバッグログを転送すかどうか
*/
bool ShouldForwardClientDebugLogs();
/**
* Formats and forwards `CMinecraftApp::DebugPrintf` output through the dedicated server logger
* CMinecraftApp::DebugPrintf の出力を専用サーバーロガーへ転送
*/
void ForwardClientAppDebugLogV(const char *format, va_list args);
/**
* Formats and forwards `CMinecraftApp::DebugPrintf(int user, ...)` output through the dedicated server logger
* CMinecraftApp::DebugPrintf(int user, ...) の出力を専用サーバーロガーへ転送
*/
void ForwardClientUserDebugLogV(int user, const char *format, va_list args);
/**
* Formats and forwards legacy `DebugSpew` output through the dedicated server logger
* 従来の DebugSpew 出力を専用サーバーロガーへ転送
*/
void ForwardClientDebugSpewLogV(const char *format, va_list args);
/**
* Clears cached connection metadata before the dedicated server starts accepting players
* 接続ログ管理用のキャッシュを初期化
*/
void Initialize();
/**
* Releases cached connection metadata after the dedicated server stops
* 接続ログ管理用のキャッシュを停止時に破棄
*/
void Shutdown();
/**
* **Log Incoming TCP Connection**
*
* Emits a named log for a raw TCP accept before smallId assignment finishes
* smallId割り当て前のTCP接続を記録
*/
void OnIncomingTcpConnection(const char *ip);
/**
* Emits a named log for a TCP connection rejected before login starts
* ログイン開始前に拒否したTCP接続を記録
*/
void OnRejectedTcpConnection(const char *ip, ETcpRejectReason reason);
/**
* Stores the remote IP for the assigned smallId and logs the accepted transport connection
* 割り当て済みsmallIdに対接続IPを保存して記録
*/
void OnAcceptedTcpConnection(unsigned char smallId, const char *ip);
/**
* Associates a player name with the connection and emits the accepted login log
* 接続にプレイヤー名を関連付けてログイン成功を記録
*/
void OnAcceptedPlayerLogin(unsigned char smallId, const std::wstring &playerName);
/**
* Emits a named login rejection log and clears cached metadata for that smallId
* ログイン拒否を記録し対象smallIdのキャッシュを破棄
*/
void OnRejectedPlayerLogin(unsigned char smallId, const std::wstring &playerName, ELoginRejectReason reason);
/**
* Emits a named disconnect log using cached connection metadata and then clears that entry
* 接続キャッシュを使って切断ログを出しその後で破棄
*/
void OnPlayerDisconnected(
unsigned char smallId,
const std::wstring &playerName,
DisconnectPacket::eDisconnectReason reason,
bool initiatedByServer);
/**
* Reads the cached remote IP for a live smallId without consuming the entry
* Eventually, this should be implemented in a separate class or on the `Minecraft.Client` side instead.
*
* 指定smallIdの接続IPをキャッシュから参照する
*/
bool TryGetConnectionRemoteIp(unsigned char smallId, std::string *outIp);
/**
* Removes any remembered IP or player name for the specified smallId
* 指定smallIdに紐づく接続キャッシュを消去
*/
void ClearConnection(unsigned char smallId);
}
}
+258
View File
@@ -0,0 +1,258 @@
#include "stdafx.h"
#include "ServerLogger.h"
#include "Common//StringUtils.h"
#include "vendor//linenoise//linenoise.h"
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
namespace ServerRuntime
{
static volatile LONG g_minLogLevel = (LONG)eServerLogLevel_Info;
static const char *NormalizeCategory(const char *category)
{
if (category == NULL || category[0] == 0)
{
return "server";
}
return category;
}
static const char *LogLevelToString(EServerLogLevel level)
{
switch (level)
{
case eServerLogLevel_Debug:
return "DEBUG";
case eServerLogLevel_Info:
return "INFO";
case eServerLogLevel_Warn:
return "WARN";
case eServerLogLevel_Error:
return "ERROR";
default:
return "INFO";
}
}
static WORD LogLevelToColor(EServerLogLevel level)
{
switch (level)
{
case eServerLogLevel_Debug:
return FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY;
case eServerLogLevel_Warn:
return FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY;
case eServerLogLevel_Error:
return FOREGROUND_RED | FOREGROUND_INTENSITY;
case eServerLogLevel_Info:
default:
return FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY;
}
}
static void BuildTimestamp(char *buffer, size_t bufferSize)
{
if (buffer == NULL || bufferSize == 0)
{
return;
}
SYSTEMTIME localTime;
GetLocalTime(&localTime);
sprintf_s(
buffer,
bufferSize,
"%04u-%02u-%02u %02u:%02u:%02u.%03u",
(unsigned)localTime.wYear,
(unsigned)localTime.wMonth,
(unsigned)localTime.wDay,
(unsigned)localTime.wHour,
(unsigned)localTime.wMinute,
(unsigned)localTime.wSecond,
(unsigned)localTime.wMilliseconds);
}
static bool ShouldLog(EServerLogLevel level)
{
return ((LONG)level >= g_minLogLevel);
}
static void WriteLogLine(EServerLogLevel level, const char *category, const char *message)
{
if (!ShouldLog(level))
{
return;
}
linenoiseExternalWriteBegin();
const char *safeCategory = NormalizeCategory(category);
const char *safeMessage = (message != NULL) ? message : "";
char timestamp[32] = {};
BuildTimestamp(timestamp, sizeof(timestamp));
HANDLE stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_SCREEN_BUFFER_INFO originalInfo;
bool hasColorConsole = false;
if (stdoutHandle != INVALID_HANDLE_VALUE && stdoutHandle != NULL)
{
if (GetConsoleScreenBufferInfo(stdoutHandle, &originalInfo))
{
hasColorConsole = true;
SetConsoleTextAttribute(stdoutHandle, LogLevelToColor(level));
}
}
printf(
"[%s][%s][%s] %s\n",
timestamp,
LogLevelToString(level),
safeCategory,
safeMessage);
fflush(stdout);
if (hasColorConsole)
{
SetConsoleTextAttribute(stdoutHandle, originalInfo.wAttributes);
}
linenoiseExternalWriteEnd();
}
static void WriteLogLineV(EServerLogLevel level, const char *category, const char *format, va_list args)
{
char messageBuffer[2048] = {};
if (format == NULL)
{
WriteLogLine(level, category, "");
return;
}
vsnprintf_s(messageBuffer, sizeof(messageBuffer), _TRUNCATE, format, args);
WriteLogLine(level, category, messageBuffer);
}
bool TryParseServerLogLevel(const char *value, EServerLogLevel *outLevel)
{
if (value == NULL || outLevel == NULL)
{
return false;
}
if (_stricmp(value, "debug") == 0)
{
*outLevel = eServerLogLevel_Debug;
return true;
}
if (_stricmp(value, "info") == 0)
{
*outLevel = eServerLogLevel_Info;
return true;
}
if (_stricmp(value, "warn") == 0 || _stricmp(value, "warning") == 0)
{
*outLevel = eServerLogLevel_Warn;
return true;
}
if (_stricmp(value, "error") == 0)
{
*outLevel = eServerLogLevel_Error;
return true;
}
return false;
}
void SetServerLogLevel(EServerLogLevel level)
{
if (level < eServerLogLevel_Debug)
{
level = eServerLogLevel_Debug;
}
else if (level > eServerLogLevel_Error)
{
level = eServerLogLevel_Error;
}
g_minLogLevel = (LONG)level;
}
EServerLogLevel GetServerLogLevel()
{
return (EServerLogLevel)g_minLogLevel;
}
void LogDebug(const char *category, const char *message)
{
WriteLogLine(eServerLogLevel_Debug, category, message);
}
void LogInfo(const char *category, const char *message)
{
WriteLogLine(eServerLogLevel_Info, category, message);
}
void LogWarn(const char *category, const char *message)
{
WriteLogLine(eServerLogLevel_Warn, category, message);
}
void LogError(const char *category, const char *message)
{
WriteLogLine(eServerLogLevel_Error, category, message);
}
void LogDebugf(const char *category, const char *format, ...)
{
va_list args;
va_start(args, format);
WriteLogLineV(eServerLogLevel_Debug, category, format, args);
va_end(args);
}
void LogInfof(const char *category, const char *format, ...)
{
va_list args;
va_start(args, format);
WriteLogLineV(eServerLogLevel_Info, category, format, args);
va_end(args);
}
void LogWarnf(const char *category, const char *format, ...)
{
va_list args;
va_start(args, format);
WriteLogLineV(eServerLogLevel_Warn, category, format, args);
va_end(args);
}
void LogErrorf(const char *category, const char *format, ...)
{
va_list args;
va_start(args, format);
WriteLogLineV(eServerLogLevel_Error, category, format, args);
va_end(args);
}
void LogStartupStep(const char *message)
{
LogInfo("startup", message);
}
void LogWorldIO(const char *message)
{
LogInfo("world-io", message);
}
void LogWorldName(const char *prefix, const std::wstring &name)
{
std::string utf8 = StringUtils::WideToUtf8(name);
LogInfof("world-io", "%s: %s", (prefix != NULL) ? prefix : "name", utf8.c_str());
}
}
+44
View File
@@ -0,0 +1,44 @@
#pragma once
#include <string>
namespace ServerRuntime
{
enum EServerLogLevel
{
eServerLogLevel_Debug = 0,
eServerLogLevel_Info = 1,
eServerLogLevel_Warn = 2,
eServerLogLevel_Error = 3
};
/**
* **Parse Log Level String**
*
* Converts a string value into log level (`debug`/`info`/`warn`/`error`)
* ログレベル文字列の変換処理
*
* @param value Source string
* @param outLevel Output location for parsed level
* @return `true` when conversion succeeds
*/
bool TryParseServerLogLevel(const char *value, EServerLogLevel *outLevel);
void SetServerLogLevel(EServerLogLevel level);
EServerLogLevel GetServerLogLevel();
void LogDebug(const char *category, const char *message);
void LogInfo(const char *category, const char *message);
void LogWarn(const char *category, const char *message);
void LogError(const char *category, const char *message);
/** Emit formatted log output with the specified level and category */
void LogDebugf(const char *category, const char *format, ...);
void LogInfof(const char *category, const char *format, ...);
void LogWarnf(const char *category, const char *format, ...);
void LogErrorf(const char *category, const char *format, ...);
void LogStartupStep(const char *message);
void LogWorldIO(const char *message);
void LogWorldName(const char *prefix, const std::wstring &name);
}
+930
View File
@@ -0,0 +1,930 @@
#include "stdafx.h"
#include "ServerProperties.h"
#include "ServerLogger.h"
#include "Common//StringUtils.h"
#include "Common//FileUtils.h"
#include "..//Minecraft.World//ChunkSource.h"
#include <cctype>
#include <map>
#include <stdio.h>
#include <stdlib.h>
#include <unordered_map>
namespace ServerRuntime
{
using StringUtils::ToLowerAscii;
using StringUtils::TrimAscii;
using StringUtils::StripUtf8Bom;
using StringUtils::Utf8ToWide;
using StringUtils::WideToUtf8;
struct ServerPropertyDefault
{
const char *key;
const char *value;
};
static const char *kServerPropertiesPath = "server.properties";
static const size_t kMaxSaveIdLength = 31;
static const int kDefaultServerPort = 25565;
static const int kDefaultMaxPlayers = 16;
static const int kMaxDedicatedPlayers = 256;
static const int kDefaultAutosaveIntervalSeconds = 60;
static const char *kLanAdvertisePropertyKey = "lan-advertise";
static const ServerPropertyDefault kServerPropertyDefaults[] =
{
{ "allow-flight", "true" },
{ "allow-nether", "true" },
{ "autosave-interval", "60" },
{ "bedrock-fog", "true" },
{ "bonus-chest", "false" },
{ "difficulty", "1" },
{ "disable-saving", "false" },
{ "do-daylight-cycle", "true" },
{ "do-mob-loot", "true" },
{ "do-mob-spawning", "true" },
{ "do-tile-drops", "true" },
{ "fire-spreads", "true" },
{ "friends-of-friends", "false" },
{ "gamemode", "0" },
{ "gamertags", "true" },
{ "generate-structures", "true" },
{ "host-can-be-invisible", "true" },
{ "host-can-change-hunger", "true" },
{ "host-can-fly", "true" },
{ "keep-inventory", "false" },
{ "level-id", "world" },
{ "level-name", "world" },
{ "level-seed", "" },
{ "level-type", "default" },
{ "world-size", "classic" },
{ "spawn-protection", "0" },
{ "log-level", "info" },
{ "max-build-height", "256" },
{ "max-players", "16" },
{ "mob-griefing", "true" },
{ "motd", "A Minecraft Server" },
{ "natural-regeneration", "true" },
{ "pvp", "true" },
{ "server-ip", "0.0.0.0" },
{ "server-name", "DedicatedServer" },
{ "server-port", "25565" },
{ "white-list", "false" },
{ "lan-advertise", "false" },
{ "spawn-animals", "true" },
{ "spawn-monsters", "true" },
{ "spawn-npcs", "true" },
{ "tnt", "true" },
{ "trust-players", "true" }
};
static std::string BoolToString(bool value)
{
return value ? "true" : "false";
}
static std::string IntToString(int value)
{
char buffer[32] = {};
sprintf_s(buffer, sizeof(buffer), "%d", value);
return std::string(buffer);
}
static std::string Int64ToString(__int64 value)
{
char buffer[64] = {};
_i64toa_s(value, buffer, sizeof(buffer), 10);
return std::string(buffer);
}
static int ClampInt(int value, int minValue, int maxValue)
{
if (value < minValue)
{
return minValue;
}
if (value > maxValue)
{
return maxValue;
}
return value;
}
static bool TryParseBool(const std::string &value, bool *outValue)
{
if (outValue == NULL)
{
return false;
}
std::string lowered = ToLowerAscii(TrimAscii(value));
if (lowered == "true" || lowered == "1" || lowered == "yes" || lowered == "on")
{
*outValue = true;
return true;
}
if (lowered == "false" || lowered == "0" || lowered == "no" || lowered == "off")
{
*outValue = false;
return true;
}
return false;
}
static bool TryParseInt(const std::string &value, int *outValue)
{
if (outValue == NULL)
{
return false;
}
std::string trimmed = TrimAscii(value);
if (trimmed.empty())
{
return false;
}
char *end = NULL;
long parsed = strtol(trimmed.c_str(), &end, 10);
if (end == trimmed.c_str() || *end != 0)
{
return false;
}
*outValue = (int)parsed;
return true;
}
static bool TryParseInt64(const std::string &value, __int64 *outValue)
{
if (outValue == NULL)
{
return false;
}
std::string trimmed = TrimAscii(value);
if (trimmed.empty())
{
return false;
}
char *end = NULL;
__int64 parsed = _strtoi64(trimmed.c_str(), &end, 10);
if (end == trimmed.c_str() || *end != 0)
{
return false;
}
*outValue = parsed;
return true;
}
static std::string LogLevelToPropertyValue(EServerLogLevel level)
{
switch (level)
{
case eServerLogLevel_Debug:
return "debug";
case eServerLogLevel_Warn:
return "warn";
case eServerLogLevel_Error:
return "error";
case eServerLogLevel_Info:
default:
return "info";
}
}
/**
* **Normalize Save ID**
*
* Normalizes an arbitrary string into a safe save destination ID
* Conversion rules:
* - Lowercase alphabetic characters
* - Keep only `[a-z0-9_.-]`
* - Replace spaces and unsupported characters with `_`
* - Fallback to `world` when empty
* - Enforce max length to match storage constraints
* 保存先IDの正規化処理
*/
static std::string NormalizeSaveId(const std::string &source)
{
std::string out;
out.reserve(source.length());
// Normalize into a character set that is safe for storage save IDs
// Replace invalid characters with '_' and fold letter case to reduce collisions
for (size_t i = 0; i < source.length(); ++i)
{
unsigned char ch = (unsigned char)source[i];
if (ch >= 'A' && ch <= 'Z')
{
ch = (unsigned char)(ch - 'A' + 'a');
}
const bool alnum = (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9');
const bool passthrough = (ch == '_') || (ch == '-') || (ch == '.');
if (alnum || passthrough)
{
out.push_back((char)ch);
}
else if (std::isspace(ch))
{
out.push_back('_');
}
else if (ch < 0x80)
{
out.push_back('_');
}
}
if (out.empty())
{
out = "world";
}
// Add a prefix when needed to avoid awkward leading characters
if (!((out[0] >= 'a' && out[0] <= 'z') || (out[0] >= '0' && out[0] <= '9')))
{
out = std::string("w_") + out;
}
// Clamp length to the 4J-side filename buffer constraint
if (out.length() > kMaxSaveIdLength)
{
out.resize(kMaxSaveIdLength);
}
return out;
}
static void ApplyDefaultServerProperties(std::unordered_map<std::string, std::string> *properties)
{
if (properties == NULL)
{
return;
}
const size_t defaultCount = sizeof(kServerPropertyDefaults) / sizeof(kServerPropertyDefaults[0]);
for (size_t i = 0; i < defaultCount; ++i)
{
(*properties)[kServerPropertyDefaults[i].key] = kServerPropertyDefaults[i].value;
}
}
/**
* **Parse server.properties Text**
*
* Extracts key/value pairs from `server.properties` format text
* - Ignores lines starting with `#` or `!` as comments
* - Accepts `=` or `:` as separators
* - Skips invalid lines and continues
* server.propertiesのパース処理
*/
static bool ReadServerPropertiesFile(const char *filePath, std::unordered_map<std::string, std::string> *properties, int *outParsedCount)
{
if (properties == NULL)
{
return false;
}
std::string text;
if (filePath == NULL || !FileUtils::ReadTextFile(filePath, &text))
{
return false;
}
text = StripUtf8Bom(text);
int parsedCount = 0;
for (size_t start = 0; start <= text.length();)
{
size_t end = text.find_first_of("\r\n", start);
size_t nextStart = text.length() + 1;
if (end != std::string::npos)
{
nextStart = end + 1;
if (text[end] == '\r' && nextStart < text.length() && text[nextStart] == '\n')
{
++nextStart;
}
}
std::string line;
if (end == std::string::npos)
{
line = text.substr(start);
}
else
{
line = text.substr(start, end - start);
}
std::string trimmedLine = TrimAscii(line);
if (trimmedLine.empty())
{
start = nextStart;
continue;
}
if (trimmedLine[0] == '#' || trimmedLine[0] == '!')
{
start = nextStart;
continue;
}
size_t eqPos = trimmedLine.find('=');
size_t colonPos = trimmedLine.find(':');
size_t sepPos = std::string::npos;
if (eqPos == std::string::npos)
{
sepPos = colonPos;
}
else if (colonPos == std::string::npos)
{
sepPos = eqPos;
}
else
{
sepPos = (eqPos < colonPos) ? eqPos : colonPos;
}
if (sepPos == std::string::npos)
{
start = nextStart;
continue;
}
std::string key = TrimAscii(trimmedLine.substr(0, sepPos));
if (key.empty())
{
start = nextStart;
continue;
}
std::string value = TrimAscii(trimmedLine.substr(sepPos + 1));
(*properties)[key] = value;
++parsedCount;
start = nextStart;
}
if (outParsedCount != NULL)
{
*outParsedCount = parsedCount;
}
return true;
}
/**
* **Write server.properties Text**
*
* Writes key/value data back as `server.properties`
* Sorts keys before writing to keep output order stable
* server.propertiesの書き戻し処理
*/
static bool WriteServerPropertiesFile(const char *filePath, const std::unordered_map<std::string, std::string> &properties)
{
if (filePath == NULL)
{
return false;
}
std::string text;
text += "# Minecraft server properties\n";
text += "# Auto-generated and normalized when missing\n";
std::map<std::string, std::string> sortedProperties(properties.begin(), properties.end());
for (std::map<std::string, std::string>::const_iterator it = sortedProperties.begin(); it != sortedProperties.end(); ++it)
{
text += it->first;
text += "=";
text += it->second;
text += "\n";
}
return FileUtils::WriteTextFileAtomic(filePath, text);
}
static bool ReadNormalizedBoolProperty(
std::unordered_map<std::string, std::string> *properties,
const char *key,
bool defaultValue,
bool *shouldWrite)
{
std::string raw = TrimAscii((*properties)[key]);
bool value = defaultValue;
if (!TryParseBool(raw, &value))
{
value = defaultValue;
}
std::string normalized = BoolToString(value);
if (raw != normalized)
{
(*properties)[key] = normalized;
if (shouldWrite != NULL)
{
*shouldWrite = true;
}
}
return value;
}
static int ReadNormalizedIntProperty(
std::unordered_map<std::string, std::string> *properties,
const char *key,
int defaultValue,
int minValue,
int maxValue,
bool *shouldWrite)
{
std::string raw = TrimAscii((*properties)[key]);
int value = defaultValue;
if (!TryParseInt(raw, &value))
{
value = defaultValue;
}
value = ClampInt(value, minValue, maxValue);
std::string normalized = IntToString(value);
if (raw != normalized)
{
(*properties)[key] = normalized;
if (shouldWrite != NULL)
{
*shouldWrite = true;
}
}
return value;
}
static std::string ReadNormalizedStringProperty(
std::unordered_map<std::string, std::string> *properties,
const char *key,
const std::string &defaultValue,
size_t maxLength,
bool *shouldWrite)
{
std::string value = TrimAscii((*properties)[key]);
if (value.empty())
{
value = defaultValue;
}
if (maxLength > 0 && value.length() > maxLength)
{
value.resize(maxLength);
}
if (value != (*properties)[key])
{
(*properties)[key] = value;
if (shouldWrite != NULL)
{
*shouldWrite = true;
}
}
return value;
}
static bool ReadNormalizedOptionalInt64Property(
std::unordered_map<std::string, std::string> *properties,
const char *key,
__int64 *outValue,
bool *shouldWrite)
{
std::string raw = TrimAscii((*properties)[key]);
if (raw.empty())
{
if ((*properties)[key] != "")
{
(*properties)[key] = "";
if (shouldWrite != NULL)
{
*shouldWrite = true;
}
}
return false;
}
__int64 parsed = 0;
if (!TryParseInt64(raw, &parsed))
{
(*properties)[key] = "";
if (shouldWrite != NULL)
{
*shouldWrite = true;
}
return false;
}
std::string normalized = Int64ToString(parsed);
if (raw != normalized)
{
(*properties)[key] = normalized;
if (shouldWrite != NULL)
{
*shouldWrite = true;
}
}
if (outValue != NULL)
{
*outValue = parsed;
}
return true;
}
static EServerLogLevel ReadNormalizedLogLevelProperty(
std::unordered_map<std::string, std::string> *properties,
const char *key,
EServerLogLevel defaultValue,
bool *shouldWrite)
{
std::string raw = TrimAscii((*properties)[key]);
EServerLogLevel value = defaultValue;
if (!TryParseServerLogLevel(raw.c_str(), &value))
{
value = defaultValue;
}
std::string normalized = LogLevelToPropertyValue(value);
if (raw != normalized)
{
(*properties)[key] = normalized;
if (shouldWrite != NULL)
{
*shouldWrite = true;
}
}
return value;
}
static std::string ReadNormalizedLevelTypeProperty(
std::unordered_map<std::string, std::string> *properties,
const char *key,
bool *outIsFlat,
bool *shouldWrite)
{
std::string raw = TrimAscii((*properties)[key]);
std::string lowered = ToLowerAscii(raw);
bool isFlat = false;
std::string normalized = "default";
if (lowered == "flat" || lowered == "superflat" || lowered == "1")
{
isFlat = true;
normalized = "flat";
}
else if (lowered == "default" || lowered == "normal" || lowered == "0")
{
isFlat = false;
normalized = "default";
}
if (raw != normalized)
{
(*properties)[key] = normalized;
if (shouldWrite != NULL)
{
*shouldWrite = true;
}
}
if (outIsFlat != NULL)
{
*outIsFlat = isFlat;
}
return normalized;
}
static std::string WorldSizeToPropertyValue(int worldSize)
{
switch (worldSize)
{
case e_worldSize_Small:
return "small";
case e_worldSize_Medium:
return "medium";
case e_worldSize_Large:
return "large";
case e_worldSize_Classic:
default:
return "classic";
}
}
static int WorldSizeToXzChunks(int worldSize)
{
switch (worldSize)
{
case e_worldSize_Small:
return LEVEL_WIDTH_SMALL;
case e_worldSize_Medium:
return LEVEL_WIDTH_MEDIUM;
case e_worldSize_Large:
return LEVEL_WIDTH_LARGE;
case e_worldSize_Classic:
default:
return LEVEL_WIDTH_CLASSIC;
}
}
static int WorldSizeToHellScale(int worldSize)
{
switch (worldSize)
{
case e_worldSize_Small:
return HELL_LEVEL_SCALE_SMALL;
case e_worldSize_Medium:
return HELL_LEVEL_SCALE_MEDIUM;
case e_worldSize_Large:
return HELL_LEVEL_SCALE_LARGE;
case e_worldSize_Classic:
default:
return HELL_LEVEL_SCALE_CLASSIC;
}
}
static bool TryParseWorldSize(const std::string &lowered, int *outWorldSize)
{
if (outWorldSize == NULL)
{
return false;
}
if (lowered == "classic" || lowered == "54" || lowered == "1")
{
*outWorldSize = e_worldSize_Classic;
return true;
}
if (lowered == "small" || lowered == "64" || lowered == "2")
{
*outWorldSize = e_worldSize_Small;
return true;
}
if (lowered == "medium" || lowered == "192" || lowered == "3")
{
*outWorldSize = e_worldSize_Medium;
return true;
}
if (lowered == "large" || lowered == "320" || lowered == "4")
{
*outWorldSize = e_worldSize_Large;
return true;
}
return false;
}
static int ReadNormalizedWorldSizeProperty(
std::unordered_map<std::string, std::string> *properties,
const char *key,
int defaultWorldSize,
int *outXzChunks,
int *outHellScale,
bool *shouldWrite)
{
std::string raw = TrimAscii((*properties)[key]);
std::string lowered = ToLowerAscii(raw);
int worldSize = defaultWorldSize;
if (!raw.empty())
{
int parsedWorldSize = defaultWorldSize;
if (TryParseWorldSize(lowered, &parsedWorldSize))
{
worldSize = parsedWorldSize;
}
}
std::string normalized = WorldSizeToPropertyValue(worldSize);
if (raw != normalized)
{
(*properties)[key] = normalized;
if (shouldWrite != NULL)
{
*shouldWrite = true;
}
}
if (outXzChunks != NULL)
{
*outXzChunks = WorldSizeToXzChunks(worldSize);
}
if (outHellScale != NULL)
{
*outHellScale = WorldSizeToHellScale(worldSize);
}
return worldSize;
}
/**
* **Load Effective Server Properties Config**
*
* Loads effective world settings, repairs missing or invalid values, and returns normalized config
* - Creates defaults when file is missing
* - Fills required keys when absent
* - Normalizes `level-id` to a safe format
* - Auto-saves when any fix is applied
* 実効設定の読み込みと補正処理
*/
ServerPropertiesConfig LoadServerPropertiesConfig()
{
ServerPropertiesConfig config;
std::unordered_map<std::string, std::string> defaults;
std::unordered_map<std::string, std::string> loaded;
ApplyDefaultServerProperties(&defaults);
int parsedCount = 0;
bool readSuccess = ReadServerPropertiesFile(kServerPropertiesPath, &loaded, &parsedCount);
std::unordered_map<std::string, std::string> merged = defaults;
bool shouldWrite = false;
if (!readSuccess)
{
LogWorldIO("server.properties not found or unreadable; creating defaults");
shouldWrite = true;
}
else
{
if (parsedCount == 0)
{
LogWorldIO("server.properties has no properties; applying defaults");
shouldWrite = true;
}
const size_t defaultCount = sizeof(kServerPropertyDefaults) / sizeof(kServerPropertyDefaults[0]);
for (size_t i = 0; i < defaultCount; ++i)
{
if (loaded.find(kServerPropertyDefaults[i].key) == loaded.end())
{
shouldWrite = true;
break;
}
}
}
for (std::unordered_map<std::string, std::string>::const_iterator it = loaded.begin(); it != loaded.end(); ++it)
{
// Merge loaded values over defaults and keep unknown keys whenever possible
merged[it->first] = it->second;
}
std::string worldName = TrimAscii(merged["level-name"]);
if (worldName.empty())
{
worldName = "world";
shouldWrite = true;
}
std::string worldSaveId = TrimAscii(merged["level-id"]);
if (worldSaveId.empty())
{
// If level-id is missing, derive it from level-name to lock save destination
worldSaveId = NormalizeSaveId(worldName);
shouldWrite = true;
}
else
{
// Normalize existing level-id as well to avoid future inconsistencies
std::string normalized = NormalizeSaveId(worldSaveId);
if (normalized != worldSaveId)
{
worldSaveId = normalized;
shouldWrite = true;
}
}
merged["level-name"] = worldName;
merged["level-id"] = worldSaveId;
config.worldName = Utf8ToWide(worldName.c_str());
config.worldSaveId = worldSaveId;
config.serverPort = ReadNormalizedIntProperty(&merged, "server-port", kDefaultServerPort, 1, 65535, &shouldWrite);
config.serverIp = ReadNormalizedStringProperty(&merged, "server-ip", "0.0.0.0", 255, &shouldWrite);
config.lanAdvertise = ReadNormalizedBoolProperty(&merged, kLanAdvertisePropertyKey, false, &shouldWrite);
config.whiteListEnabled = ReadNormalizedBoolProperty(&merged, "white-list", false, &shouldWrite);
config.serverName = ReadNormalizedStringProperty(&merged, "server-name", "DedicatedServer", 16, &shouldWrite);
config.maxPlayers = ReadNormalizedIntProperty(&merged, "max-players", kDefaultMaxPlayers, 1, kMaxDedicatedPlayers, &shouldWrite);
config.seed = 0;
config.hasSeed = ReadNormalizedOptionalInt64Property(&merged, "level-seed", &config.seed, &shouldWrite);
config.logLevel = ReadNormalizedLogLevelProperty(&merged, "log-level", eServerLogLevel_Info, &shouldWrite);
config.autosaveIntervalSeconds = ReadNormalizedIntProperty(&merged, "autosave-interval", kDefaultAutosaveIntervalSeconds, 5, 3600, &shouldWrite);
config.difficulty = ReadNormalizedIntProperty(&merged, "difficulty", 1, 0, 3, &shouldWrite);
config.gameMode = ReadNormalizedIntProperty(&merged, "gamemode", 0, 0, 1, &shouldWrite);
config.worldSize = ReadNormalizedWorldSizeProperty(
&merged,
"world-size",
e_worldSize_Classic,
&config.worldSizeChunks,
&config.worldHellScale,
&shouldWrite);
config.levelType = ReadNormalizedLevelTypeProperty(&merged, "level-type", &config.levelTypeFlat, &shouldWrite);
config.spawnProtectionRadius = ReadNormalizedIntProperty(&merged, "spawn-protection", 0, 0, 256, &shouldWrite);
config.generateStructures = ReadNormalizedBoolProperty(&merged, "generate-structures", true, &shouldWrite);
config.bonusChest = ReadNormalizedBoolProperty(&merged, "bonus-chest", false, &shouldWrite);
config.pvp = ReadNormalizedBoolProperty(&merged, "pvp", true, &shouldWrite);
config.trustPlayers = ReadNormalizedBoolProperty(&merged, "trust-players", true, &shouldWrite);
config.fireSpreads = ReadNormalizedBoolProperty(&merged, "fire-spreads", true, &shouldWrite);
config.tnt = ReadNormalizedBoolProperty(&merged, "tnt", true, &shouldWrite);
config.spawnAnimals = ReadNormalizedBoolProperty(&merged, "spawn-animals", true, &shouldWrite);
config.spawnNpcs = ReadNormalizedBoolProperty(&merged, "spawn-npcs", true, &shouldWrite);
config.spawnMonsters = ReadNormalizedBoolProperty(&merged, "spawn-monsters", true, &shouldWrite);
config.allowFlight = ReadNormalizedBoolProperty(&merged, "allow-flight", true, &shouldWrite);
config.allowNether = ReadNormalizedBoolProperty(&merged, "allow-nether", true, &shouldWrite);
config.friendsOfFriends = ReadNormalizedBoolProperty(&merged, "friends-of-friends", false, &shouldWrite);
config.gamertags = ReadNormalizedBoolProperty(&merged, "gamertags", true, &shouldWrite);
config.bedrockFog = ReadNormalizedBoolProperty(&merged, "bedrock-fog", true, &shouldWrite);
config.hostCanFly = ReadNormalizedBoolProperty(&merged, "host-can-fly", true, &shouldWrite);
config.hostCanChangeHunger = ReadNormalizedBoolProperty(&merged, "host-can-change-hunger", true, &shouldWrite);
config.hostCanBeInvisible = ReadNormalizedBoolProperty(&merged, "host-can-be-invisible", true, &shouldWrite);
config.disableSaving = ReadNormalizedBoolProperty(&merged, "disable-saving", false, &shouldWrite);
config.mobGriefing = ReadNormalizedBoolProperty(&merged, "mob-griefing", true, &shouldWrite);
config.keepInventory = ReadNormalizedBoolProperty(&merged, "keep-inventory", false, &shouldWrite);
config.doMobSpawning = ReadNormalizedBoolProperty(&merged, "do-mob-spawning", true, &shouldWrite);
config.doMobLoot = ReadNormalizedBoolProperty(&merged, "do-mob-loot", true, &shouldWrite);
config.doTileDrops = ReadNormalizedBoolProperty(&merged, "do-tile-drops", true, &shouldWrite);
config.naturalRegeneration = ReadNormalizedBoolProperty(&merged, "natural-regeneration", true, &shouldWrite);
config.doDaylightCycle = ReadNormalizedBoolProperty(&merged, "do-daylight-cycle", true, &shouldWrite);
config.maxBuildHeight = ReadNormalizedIntProperty(&merged, "max-build-height", 256, 64, 256, &shouldWrite);
config.motd = ReadNormalizedStringProperty(&merged, "motd", "A Minecraft Server", 255, &shouldWrite);
if (shouldWrite)
{
if (WriteServerPropertiesFile(kServerPropertiesPath, merged))
{
LogWorldIO("wrote server.properties");
}
else
{
LogWorldIO("failed to write server.properties");
}
}
return config;
}
/**
* **Save World Identity While Preserving Other Keys**
*
* Saves world identity fields while preserving as many other settings as possible
* - Reads existing file and merges including unknown keys
* - Updates `level-name`, `level-id`, and `white-list` before writing back
* ワールド識別情報の保存処理
*/
bool SaveServerPropertiesConfig(const ServerPropertiesConfig &config)
{
std::unordered_map<std::string, std::string> merged;
ApplyDefaultServerProperties(&merged);
std::unordered_map<std::string, std::string> loaded;
int parsedCount = 0;
if (ReadServerPropertiesFile(kServerPropertiesPath, &loaded, &parsedCount))
{
for (std::unordered_map<std::string, std::string>::const_iterator it = loaded.begin(); it != loaded.end(); ++it)
{
// Keep existing content so keys untouched by caller are not dropped
merged[it->first] = it->second;
}
}
std::string worldName = TrimAscii(WideToUtf8(config.worldName));
if (worldName.empty())
{
worldName = "world"; // Default world name
}
std::string worldSaveId = TrimAscii(config.worldSaveId);
if (worldSaveId.empty())
{
worldSaveId = NormalizeSaveId(worldName);
}
else
{
worldSaveId = NormalizeSaveId(worldSaveId);
}
merged["level-name"] = worldName;
merged["level-id"] = worldSaveId;
merged["white-list"] = BoolToString(config.whiteListEnabled);
return WriteServerPropertiesFile(kServerPropertiesPath, merged);
}
}
+105
View File
@@ -0,0 +1,105 @@
#pragma once
#include <string>
#include "ServerLogger.h"
namespace ServerRuntime
{
/**
* `server.properties`
*/
struct ServerPropertiesConfig
{
/** world name `level-name` */
std::wstring worldName;
/** world save id `level-id` */
std::string worldSaveId;
/** `server-port` */
int serverPort;
/** `server-ip` */
std::string serverIp;
/** `lan-advertise` */
bool lanAdvertise;
/** `white-list` */
bool whiteListEnabled;
/** `server-name` (max 16 chars at runtime) */
std::string serverName;
/** `max-players` */
int maxPlayers;
/** `level-seed` is explicitly set */
bool hasSeed;
/** `level-seed` */
__int64 seed;
/** `log-level` */
EServerLogLevel logLevel;
/** `autosave-interval` (seconds) */
int autosaveIntervalSeconds;
/** host options / game settings */
int difficulty;
int gameMode;
/** `world-size` preset (`classic` / `small` / `medium` / `large`) */
int worldSize;
/** Overworld chunk width derived from `world-size` */
int worldSizeChunks;
/** Nether scale derived from `world-size` */
int worldHellScale;
bool levelTypeFlat;
/** `spawn-protection` radius in blocks (0 disables protection) */
int spawnProtectionRadius;
bool generateStructures;
bool bonusChest;
bool pvp;
bool trustPlayers;
bool fireSpreads;
bool tnt;
bool spawnAnimals;
bool spawnNpcs;
bool spawnMonsters;
bool allowFlight;
bool allowNether;
bool friendsOfFriends;
bool gamertags;
bool bedrockFog;
bool hostCanFly;
bool hostCanChangeHunger;
bool hostCanBeInvisible;
bool disableSaving;
bool mobGriefing;
bool keepInventory;
bool doMobSpawning;
bool doMobLoot;
bool doTileDrops;
bool naturalRegeneration;
bool doDaylightCycle;
/** other MinecraftServer runtime settings */
int maxBuildHeight;
std::string levelType;
std::string motd;
};
/**
* server.properties loader
*
* - ファイル欠損時はデフォルト値で新規作成
* - 必須キー不足時は補完して再保存
* - `level-id` は保存先として安全な形式へ正規化
*
* @return `WorldManager` が利用するワールド設定
*/
ServerPropertiesConfig LoadServerPropertiesConfig();
/**
* server.properties saver
*
* - `level-name` と `level-id` を更新
* - `white-list` を更新
* - それ以外の既存キーは極力保持
*
* @param config 保存するワールド識別情報と永続化対象設定
* @return 書き込み成功時 `true`
*/
bool SaveServerPropertiesConfig(const ServerPropertiesConfig &config);
}
+6
View File
@@ -0,0 +1,6 @@
#pragma once
namespace ServerRuntime
{
void RequestDedicatedServerShutdown();
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
+730
View File
@@ -0,0 +1,730 @@
#include "stdafx.h"
#include "Common/App_Defines.h"
#include "Common/Network/GameNetworkManager.h"
#include "Input.h"
#include "Minecraft.h"
#include "MinecraftServer.h"
#include "../Access/Access.h"
#include "../Common/StringUtils.h"
#include "../ServerLogger.h"
#include "../ServerLogManager.h"
#include "../ServerProperties.h"
#include "../ServerShutdown.h"
#include "../WorldManager.h"
#include "../Console/ServerCli.h"
#include "Tesselator.h"
#include "Windows64/4JLibs/inc/4J_Render.h"
#include "Windows64/GameConfig/Minecraft.spa.h"
#include "Common/Network/NetworkSocketLayer.h"
#include "Windows64/Windows64_UIController.h"
#include "Common/UI/UI.h"
#include "../../Minecraft.World/AABB.h"
#include "../../Minecraft.World/Vec3.h"
#include "../../Minecraft.World/IntCache.h"
#include "../../Minecraft.World/ChunkSource.h"
#include "../../Minecraft.World/TilePos.h"
#include "../../Minecraft.World/compression.h"
#include "../../Minecraft.World/OldChunkStorage.h"
#include "../../Minecraft.World/ConsoleSaveFileOriginal.h"
#include "../../Minecraft.World/net.minecraft.world.level.tile.h"
#include "../../Minecraft.World/Random.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atomic>
extern ATOM MyRegisterClass(HINSTANCE hInstance);
extern BOOL InitInstance(HINSTANCE hInstance, int nCmdShow);
extern HRESULT InitDevice();
extern void CleanupDevice();
extern void DefineActions(void);
extern HWND g_hWnd;
extern int g_iScreenWidth;
extern int g_iScreenHeight;
extern char g_Win64Username[17];
extern wchar_t g_Win64UsernameW[17];
extern ID3D11Device* g_pd3dDevice;
extern ID3D11DeviceContext* g_pImmediateContext;
extern IDXGISwapChain* g_pSwapChain;
extern ID3D11RenderTargetView* g_pRenderTargetView;
extern ID3D11DepthStencilView* g_pDepthStencilView;
extern DWORD dwProfileSettingsA[];
static const int kProfileValueCount = 5;
static const int kProfileSettingCount = 4;
struct DedicatedServerConfig
{
int port;
char bindIP[256];
char name[17];
int maxPlayers;
int worldSize;
int worldSizeChunks;
int worldHellScale;
__int64 seed;
ServerRuntime::EServerLogLevel logLevel;
bool hasSeed;
bool showHelp;
};
static std::atomic<bool> g_shutdownRequested(false);
static const DWORD kDefaultAutosaveIntervalMs = 60 * 1000;
static const int kServerActionPad = 0;
static bool IsShutdownRequested()
{
return g_shutdownRequested.load();
}
namespace ServerRuntime
{
void RequestDedicatedServerShutdown()
{
g_shutdownRequested.store(true);
}
}
/**
* Calls Access::Shutdown automatically once dedicated access control was initialized successfully
* アクセス制御初期化後のShutdownを自動化する
*/
class AccessShutdownGuard
{
public:
AccessShutdownGuard()
: m_active(false)
{
}
void Activate()
{
m_active = true;
}
~AccessShutdownGuard()
{
if (m_active)
{
ServerRuntime::Access::Shutdown();
}
}
private:
bool m_active;
};
static BOOL WINAPI ConsoleCtrlHandlerProc(DWORD ctrlType)
{
switch (ctrlType)
{
case CTRL_C_EVENT:
case CTRL_BREAK_EVENT:
case CTRL_CLOSE_EVENT:
case CTRL_SHUTDOWN_EVENT:
ServerRuntime::RequestDedicatedServerShutdown();
return TRUE;
default:
return FALSE;
}
}
/**
* **Wait For Server Stopped Signal**
*
* Thread entry used during shutdown to wait until the network layer reports server stop completion
* 停止通知待機用の終了スレッド処理
*/
static int WaitForServerStoppedThreadProc(void *)
{
if (g_NetworkManager.ServerStoppedValid())
{
g_NetworkManager.ServerStoppedWait();
}
return 0;
}
static void PrintUsage()
{
ServerRuntime::LogInfo("usage", "Minecraft.Server.exe [options]");
ServerRuntime::LogInfo("usage", " -port <1-65535> Listen TCP port (default: server.properties:server-port)");
ServerRuntime::LogInfo("usage", " -ip <addr> Bind address (default: server.properties:server-ip)");
ServerRuntime::LogInfo("usage", " -bind <addr> Alias of -ip");
ServerRuntime::LogInfo("usage", " -name <name> Host display name (max 16 chars, default: server.properties:server-name)");
ServerRuntime::LogInfo("usage", " -maxplayers <1-8> Public slots (default: server.properties:max-players)");
ServerRuntime::LogInfo("usage", " -seed <int64> World seed (overrides server.properties:level-seed)");
ServerRuntime::LogInfo("usage", " -loglevel <level> debug|info|warn|error (default: server.properties:log-level)");
ServerRuntime::LogInfo("usage", " -help Show this help");
}
using ServerRuntime::LoadServerPropertiesConfig;
using ServerRuntime::LogError;
using ServerRuntime::LogErrorf;
using ServerRuntime::LogInfof;
using ServerRuntime::LogDebugf;
using ServerRuntime::LogStartupStep;
using ServerRuntime::LogWarn;
using ServerRuntime::LogWorldIO;
using ServerRuntime::SaveServerPropertiesConfig;
using ServerRuntime::SetServerLogLevel;
using ServerRuntime::ServerPropertiesConfig;
using ServerRuntime::TryParseServerLogLevel;
using ServerRuntime::StringUtils::WideToUtf8;
using ServerRuntime::BootstrapWorldForServer;
using ServerRuntime::eWorldBootstrap_CreatedNew;
using ServerRuntime::eWorldBootstrap_Failed;
using ServerRuntime::eWorldBootstrap_Loaded;
using ServerRuntime::WaitForWorldActionIdle;
using ServerRuntime::WorldBootstrapResult;
static bool ParseIntArg(const char *value, int *outValue)
{
if (value == NULL || *value == 0)
return false;
char *end = NULL;
long parsed = strtol(value, &end, 10);
if (end == value || *end != 0)
return false;
*outValue = (int)parsed;
return true;
}
static bool ParseInt64Arg(const char *value, __int64 *outValue)
{
if (value == NULL || *value == 0)
return false;
char *end = NULL;
__int64 parsed = _strtoi64(value, &end, 10);
if (end == value || *end != 0)
return false;
*outValue = parsed;
return true;
}
static bool ParseCommandLine(int argc, char **argv, DedicatedServerConfig *config)
{
for (int i = 1; i < argc; ++i)
{
const char *arg = argv[i];
if (_stricmp(arg, "-help") == 0 || _stricmp(arg, "--help") == 0 || _stricmp(arg, "-h") == 0)
{
config->showHelp = true;
return true;
}
else if ((_stricmp(arg, "-port") == 0) && (i + 1 < argc))
{
int port = 0;
if (!ParseIntArg(argv[++i], &port) || port <= 0 || port > 65535)
{
LogError("startup", "Invalid -port value.");
return false;
}
config->port = port;
}
else if ((_stricmp(arg, "-ip") == 0 || _stricmp(arg, "-bind") == 0) && (i + 1 < argc))
{
strncpy_s(config->bindIP, sizeof(config->bindIP), argv[++i], _TRUNCATE);
}
else if ((_stricmp(arg, "-name") == 0) && (i + 1 < argc))
{
strncpy_s(config->name, sizeof(config->name), argv[++i], _TRUNCATE);
}
else if ((_stricmp(arg, "-maxplayers") == 0) && (i + 1 < argc))
{
int maxPlayers = 0;
if (!ParseIntArg(argv[++i], &maxPlayers) || maxPlayers <= 0 || maxPlayers > MINECRAFT_NET_MAX_PLAYERS)
{
LogError("startup", "Invalid -maxplayers value.");
return false;
}
config->maxPlayers = maxPlayers;
}
else if ((_stricmp(arg, "-seed") == 0) && (i + 1 < argc))
{
if (!ParseInt64Arg(argv[++i], &config->seed))
{
LogError("startup", "Invalid -seed value.");
return false;
}
config->hasSeed = true;
}
else if ((_stricmp(arg, "-loglevel") == 0) && (i + 1 < argc))
{
if (!TryParseServerLogLevel(argv[++i], &config->logLevel))
{
LogError("startup", "Invalid -loglevel value. Use debug/info/warn/error.");
return false;
}
}
else
{
LogErrorf("startup", "Unknown or incomplete argument: %s", arg);
return false;
}
}
return true;
}
static void SetExeWorkingDirectory()
{
char exePath[MAX_PATH] = {};
GetModuleFileNameA(NULL, exePath, MAX_PATH);
char *slash = strrchr(exePath, '\\');
if (slash != NULL)
{
*(slash + 1) = 0;
SetCurrentDirectoryA(exePath);
}
}
static void ApplyServerPropertiesToDedicatedConfig(const ServerPropertiesConfig &serverProperties, DedicatedServerConfig *config)
{
if (config == NULL)
{
return;
}
config->port = serverProperties.serverPort;
strncpy_s(
config->bindIP,
sizeof(config->bindIP),
serverProperties.serverIp.empty() ? "0.0.0.0" : serverProperties.serverIp.c_str(),
_TRUNCATE);
strncpy_s(
config->name,
sizeof(config->name),
serverProperties.serverName.empty() ? "DedicatedServer" : serverProperties.serverName.c_str(),
_TRUNCATE);
config->maxPlayers = serverProperties.maxPlayers;
config->worldSize = serverProperties.worldSize;
config->worldSizeChunks = serverProperties.worldSizeChunks;
config->worldHellScale = serverProperties.worldHellScale;
config->logLevel = serverProperties.logLevel;
config->hasSeed = serverProperties.hasSeed;
config->seed = serverProperties.seed;
}
/**
* **Tick Core Async Subsystems**
*
* Advances core subsystems for one frame to keep async processing alive
* Call continuously even inside wait loops to avoid stalling storage/profile/network work
* 非同期進行維持のためのティック処理
*/
static void TickCoreSystems()
{
g_NetworkManager.DoWork();
ProfileManager.Tick();
StorageManager.Tick();
//ConsoleSaveFileOriginal::flushPendingBackgroundSave();
}
/**
* **Handle Queued XUI Server Action Once**
*
* Processes queued XUI/server action once
* XUIアクションの単発処理
*/
static void HandleXuiActions()
{
app.HandleXuiActions();
}
/**
* Dedicated Server Entory Point
*
* 主な責務:
* - プロセス/描画/ネットワークの初期化
* - `WorldManager` によるワールドロードまたは新規作成
* - メインループと定期オートセーブ実行
* - 終了時の最終保存と各サブシステムの安全停止
*/
int main(int argc, char **argv)
{
DedicatedServerConfig config;
config.port = NETWORK_LAN_DEFAULT_PORT;
strncpy_s(config.bindIP, sizeof(config.bindIP), "0.0.0.0", _TRUNCATE);
strncpy_s(config.name, sizeof(config.name), "DedicatedServer", _TRUNCATE);
config.maxPlayers = MINECRAFT_NET_MAX_PLAYERS;
config.worldSize = e_worldSize_Classic;
config.worldSizeChunks = LEVEL_WIDTH_CLASSIC;
config.worldHellScale = HELL_LEVEL_SCALE_CLASSIC;
config.seed = 0;
config.logLevel = ServerRuntime::eServerLogLevel_Info;
config.hasSeed = false;
config.showHelp = false;
SetConsoleCtrlHandler(ConsoleCtrlHandlerProc, TRUE);
SetExeWorkingDirectory();
// Load base settings from server.properties, then override with CLI values when provided
ServerPropertiesConfig serverProperties = LoadServerPropertiesConfig();
ApplyServerPropertiesToDedicatedConfig(serverProperties, &config);
if (!ParseCommandLine(argc, argv, &config))
{
PrintUsage();
return 1;
}
if (config.showHelp)
{
PrintUsage();
return 0;
}
SetServerLogLevel(config.logLevel);
LogStartupStep("initializing process state");
AccessShutdownGuard accessShutdownGuard;
g_iScreenWidth = 1280;
g_iScreenHeight = 720;
strncpy_s(g_Win64Username, sizeof(g_Win64Username), config.name, _TRUNCATE);
MultiByteToWideChar(CP_ACP, 0, g_Win64Username, -1, g_Win64UsernameW, 17);
g_MultiplayerHost = true;
g_MultiplayerJoin = false;
g_MultiplayerPort = config.port;
strncpy_s(g_MultiplayerIP, sizeof(g_MultiplayerIP), config.bindIP, _TRUNCATE);
g_DedicatedServerPort = config.port;
strncpy_s(g_DedicatedServerBindIP, sizeof(g_DedicatedServerBindIP), config.bindIP, _TRUNCATE);
g_DedicatedServerLanAdvertise = serverProperties.lanAdvertise;
LogStartupStep("initializing server log manager");
ServerRuntime::ServerLogManager::Initialize();
LogStartupStep("initializing dedicated access control");
if (!ServerRuntime::Access::Initialize(".", serverProperties.whiteListEnabled))
{
LogError("startup", "Failed to initialize dedicated server access control.");
return 2;
}
accessShutdownGuard.Activate();
LogInfof("startup", "LAN advertise: %s", serverProperties.lanAdvertise ? "enabled" : "disabled");
LogInfof("startup", "Whitelist: %s", serverProperties.whiteListEnabled ? "enabled" : "disabled");
LogInfof("startup", "Spawn protection radius: %d", serverProperties.spawnProtectionRadius);
#ifdef _LARGE_WORLDS
LogInfof(
"startup",
"World size preset: %d (xz=%d, hell-scale=%d)",
config.worldSize,
config.worldSizeChunks,
config.worldHellScale);
#endif
LogStartupStep("registering hidden window class");
HINSTANCE hInstance = GetModuleHandle(NULL);
MyRegisterClass(hInstance);
LogStartupStep("creating hidden window");
if (!InitInstance(hInstance, SW_HIDE))
{
LogError("startup", "Failed to create window instance.");
return 2;
}
ShowWindow(g_hWnd, SW_HIDE);
LogStartupStep("initializing graphics device wrappers");
if (FAILED(InitDevice()))
{
LogError("startup", "Failed to initialize D3D device.");
CleanupDevice();
return 2;
}
LogStartupStep("loading media/string tables");
app.loadMediaArchive();
RenderManager.Initialise(g_pd3dDevice, g_pSwapChain);
app.loadStringTable();
ui.init(g_pd3dDevice, g_pImmediateContext, g_pRenderTargetView, g_pDepthStencilView, g_iScreenWidth, g_iScreenHeight);
InputManager.Initialise(1, 3, MINECRAFT_ACTION_MAX, ACTION_MAX_MENU);
DefineActions();
InputManager.SetJoypadMapVal(0, 0);
InputManager.SetKeyRepeatRate(0.3f, 0.2f);
ProfileManager.Initialise(
TITLEID_MINECRAFT,
app.m_dwOfferID,
PROFILE_VERSION_10,
kProfileValueCount,
kProfileSettingCount,
dwProfileSettingsA,
app.GAME_DEFINED_PROFILE_DATA_BYTES * XUSER_MAX_COUNT,
&app.uiGameDefinedDataChangedBitmask);
ProfileManager.SetDefaultOptionsCallback(&CConsoleMinecraftApp::DefaultOptionsCallback, (LPVOID)&app);
ProfileManager.SetDebugFullOverride(true);
LogStartupStep("initializing network manager");
g_NetworkManager.Initialise();
for (int i = 0; i < MINECRAFT_NET_MAX_PLAYERS; ++i)
{
IQNet::m_player[i].m_smallId = (BYTE)i;
IQNet::m_player[i].m_isRemote = false;
IQNet::m_player[i].m_isHostPlayer = (i == 0);
swprintf_s(IQNet::m_player[i].m_gamertag, 32, L"Player%d", i);
}
wcscpy_s(IQNet::m_player[0].m_gamertag, 32, g_Win64UsernameW);
NetworkSocketLayer::Initialize();
Tesselator::CreateNewThreadStorage(1024 * 1024);
AABB::CreateNewThreadStorage();
Vec3::CreateNewThreadStorage();
IntCache::CreateNewThreadStorage();
Compression::CreateNewThreadStorage();
OldChunkStorage::CreateNewThreadStorage();
Level::enableLightingCache();
Tile::CreateNewThreadStorage();
LogStartupStep("creating Minecraft singleton");
Minecraft::main();
Minecraft *minecraft = Minecraft::GetInstance();
if (minecraft == NULL)
{
LogError("startup", "Minecraft initialization failed.");
CleanupDevice();
return 3;
}
app.InitGameSettings();
MinecraftServer::resetFlags();
app.SetTutorialMode(false);
app.SetCorruptSaveDeleted(false);
app.SetGameHostOption(eGameHostOption_Difficulty, serverProperties.difficulty);
app.SetGameHostOption(eGameHostOption_FriendsOfFriends, serverProperties.friendsOfFriends ? 1 : 0);
app.SetGameHostOption(eGameHostOption_Gamertags, serverProperties.gamertags ? 1 : 0);
app.SetGameHostOption(eGameHostOption_BedrockFog, serverProperties.bedrockFog ? 1 : 0);
app.SetGameHostOption(eGameHostOption_GameType, serverProperties.gameMode);
app.SetGameHostOption(eGameHostOption_LevelType, serverProperties.levelTypeFlat ? 1 : 0);
app.SetGameHostOption(eGameHostOption_Structures, serverProperties.generateStructures ? 1 : 0);
app.SetGameHostOption(eGameHostOption_BonusChest, serverProperties.bonusChest ? 1 : 0);
app.SetGameHostOption(eGameHostOption_PvP, serverProperties.pvp ? 1 : 0);
app.SetGameHostOption(eGameHostOption_TrustPlayers, serverProperties.trustPlayers ? 1 : 0);
app.SetGameHostOption(eGameHostOption_FireSpreads, serverProperties.fireSpreads ? 1 : 0);
app.SetGameHostOption(eGameHostOption_TNT, serverProperties.tnt ? 1 : 0);
app.SetGameHostOption(
eGameHostOption_CheatsEnabled,
(serverProperties.hostCanFly || serverProperties.hostCanChangeHunger || serverProperties.hostCanBeInvisible) ? 1 : 0);
app.SetGameHostOption(eGameHostOption_HostCanFly, serverProperties.hostCanFly ? 1 : 0);
app.SetGameHostOption(eGameHostOption_HostCanChangeHunger, serverProperties.hostCanChangeHunger ? 1 : 0);
app.SetGameHostOption(eGameHostOption_HostCanBeInvisible, serverProperties.hostCanBeInvisible ? 1 : 0);
app.SetGameHostOption(eGameHostOption_DisableSaving, serverProperties.disableSaving ? 1 : 0);
app.SetGameHostOption(eGameHostOption_MobGriefing, serverProperties.mobGriefing ? 1 : 0);
app.SetGameHostOption(eGameHostOption_KeepInventory, serverProperties.keepInventory ? 1 : 0);
app.SetGameHostOption(eGameHostOption_DoMobSpawning, serverProperties.doMobSpawning ? 1 : 0);
app.SetGameHostOption(eGameHostOption_DoMobLoot, serverProperties.doMobLoot ? 1 : 0);
app.SetGameHostOption(eGameHostOption_DoTileDrops, serverProperties.doTileDrops ? 1 : 0);
app.SetGameHostOption(eGameHostOption_NaturalRegeneration, serverProperties.naturalRegeneration ? 1 : 0);
app.SetGameHostOption(eGameHostOption_DoDaylightCycle, serverProperties.doDaylightCycle ? 1 : 0);
#ifdef _LARGE_WORLDS
app.SetGameHostOption(eGameHostOption_WorldSize, serverProperties.worldSize);
// Apply desired target size for loading existing worlds.
// Expansion happens only when target size is larger than current level size.
app.SetGameNewWorldSize(serverProperties.worldSizeChunks, true);
app.SetGameNewHellScale(serverProperties.worldHellScale);
#endif
StorageManager.SetSaveDisabled(serverProperties.disableSaving);
// Read world name and fixed save-id from server.properties
// Delegate load-vs-create decision to WorldManager
std::wstring targetWorldName = serverProperties.worldName;
if (targetWorldName.empty())
{
targetWorldName = L"world"; // Default world name
}
WorldBootstrapResult worldBootstrap = BootstrapWorldForServer(serverProperties, kServerActionPad, &TickCoreSystems);
if (worldBootstrap.status == eWorldBootstrap_Loaded)
{
const std::string &loadedSaveFilename = worldBootstrap.resolvedSaveId;
if (!loadedSaveFilename.empty() && _stricmp(loadedSaveFilename.c_str(), serverProperties.worldSaveId.c_str()) != 0)
{
// Persist the actually loaded save-id back to config
// Keep lookup keys aligned for next startup
LogWorldIO("updating level-id to loaded save filename");
serverProperties.worldSaveId = loadedSaveFilename;
if (!SaveServerPropertiesConfig(serverProperties))
{
LogWorldIO("failed to persist updated level-id");
}
}
}
else if (worldBootstrap.status == eWorldBootstrap_Failed)
{
LogErrorf("world-io", "Failed to load configured world \"%s\".", WideToUtf8(targetWorldName).c_str());
NetworkSocketLayer::Shutdown();
g_NetworkManager.Terminate();
CleanupDevice();
return 4;
}
NetworkGameInitData *param = new NetworkGameInitData();
if (config.hasSeed)
{
param->seed = config.seed;
}
else
{
param->seed = (new Random())->nextLong();
}
#ifdef _LARGE_WORLDS
param->xzSize = (unsigned int)config.worldSizeChunks;
param->hellScale = (unsigned char)config.worldHellScale;
#endif
param->saveData = worldBootstrap.saveData;
param->settings = app.GetGameHostOption(eGameHostOption_All);
param->dedicatedNoLocalHostPlayer = true;
LogStartupStep("starting hosted network game thread");
g_NetworkManager.HostGame(0, true, false, (unsigned char)config.maxPlayers, 0);
g_NetworkManager.FakeLocalPlayerJoined();
C4JThread *startThread = new C4JThread(&CGameNetworkManager::RunNetworkGameThreadProc, (LPVOID)param, "RunNetworkGame");
startThread->Run();
while (startThread->isRunning() && !IsShutdownRequested())
{
TickCoreSystems();
Sleep(10);
}
startThread->WaitForCompletion(INFINITE);
int startupResult = startThread->GetExitCode();
delete startThread;
if (startupResult != 0)
{
LogErrorf("startup", "Failed to start dedicated server (code %d).", startupResult);
NetworkSocketLayer::Shutdown();
g_NetworkManager.Terminate();
CleanupDevice();
return 4;
}
LogStartupStep("server startup complete");
LogInfof("startup", "Dedicated server listening on %s:%d", g_MultiplayerIP, g_MultiplayerPort);
if (worldBootstrap.status == eWorldBootstrap_CreatedNew && !IsShutdownRequested()/* && !app.m_bShutdown*/)
{
// Windows64 suppresses saveToDisc right after new world creation
// Dedicated Server explicitly runs the initial save here
LogWorldIO("requesting initial save for newly created world");
WaitForWorldActionIdle(kServerActionPad, 5000, &TickCoreSystems, &HandleXuiActions);
app.SetXuiServerAction(kServerActionPad, eXuiServerAction_AutoSaveGame);
if (!WaitForWorldActionIdle(kServerActionPad, 30000, &TickCoreSystems, &HandleXuiActions))
{
LogWorldIO("initial save timed out");
LogWarn("world-io", "Timed out waiting for initial save action to finish.");
}
else
{
LogWorldIO("initial save completed");
}
}
DWORD autosaveIntervalMs = kDefaultAutosaveIntervalMs;
if (serverProperties.autosaveIntervalSeconds > 0)
{
autosaveIntervalMs = (DWORD)(serverProperties.autosaveIntervalSeconds * 1000);
}
DWORD nextAutosaveTick = GetTickCount() + autosaveIntervalMs;
bool autosaveRequested = false;
ServerRuntime::ServerCli serverCli;
serverCli.Start();
while (!IsShutdownRequested() /*&& !app.m_bShutdown*/)
{
TickCoreSystems();
HandleXuiActions();
serverCli.Poll();
if (IsShutdownRequested()/* || app.m_bShutdown*/)
{
break;
}
if (autosaveRequested && app.GetXuiServerAction(kServerActionPad) == eXuiServerAction_Idle/* && !ConsoleSaveFileOriginal::hasPendingBackgroundSave()*/)
{
LogWorldIO("autosave completed");
autosaveRequested = false;
}
if (MinecraftServer::serverHalted())
{
break;
}
DWORD now = GetTickCount();
if ((LONG)(now - nextAutosaveTick) >= 0)
{
if (app.GetXuiServerAction(kServerActionPad) == eXuiServerAction_Idle/* && !ConsoleSaveFileOriginal::hasPendingBackgroundSave()*/)
{
LogWorldIO("requesting autosave");
app.SetXuiServerAction(kServerActionPad, eXuiServerAction_AutoSaveGame);
autosaveRequested = true;
}
nextAutosaveTick = now + autosaveIntervalMs;
}
Sleep(10);
}
serverCli.Stop();
/*app.m_bShutdown = true;*/
LogInfof("shutdown", "Dedicated server stopped");
MinecraftServer *server = MinecraftServer::getInstance();
if (server != NULL/* && !ConsoleSaveFileOriginal::hasPendingBackgroundSave()*/)
{
server->setSaveOnExit(true);
LogWorldIO("requesting save before shutdown");
LogWorldIO("using saveOnExit for shutdown");
}
/*if (ConsoleSaveFileOriginal::hasPendingBackgroundSave())
{
LogWorldIO("Waiting for autosave to complete...");
}*/
MinecraftServer::HaltServer();
if (g_NetworkManager.ServerStoppedValid())
{
C4JThread waitThread(&WaitForServerStoppedThreadProc, NULL, "WaitServerStopped");
waitThread.Run();
while (waitThread.isRunning())
{
TickCoreSystems();
Sleep(10);
}
waitThread.WaitForCompletion(INFINITE);
}
/*while (ConsoleSaveFileOriginal::hasPendingBackgroundSave())
{
TickCoreSystems();
Sleep(10);
}*/
LogInfof("shutdown", "Cleaning up and exiting.");
NetworkSocketLayer::Shutdown();
LogDebugf("shutdown", "Network layer shutdown complete.");
g_NetworkManager.Terminate();
LogDebugf("shutdown", "Network manager terminated.");
ServerRuntime::ServerLogManager::Shutdown();
CleanupDevice();
return 0;
}
+641
View File
@@ -0,0 +1,641 @@
#include "stdafx.h"
#include "WorldManager.h"
#include "Minecraft.h"
#include "MinecraftServer.h"
#include "ServerLogger.h"
#include "Common//StringUtils.h"
#include <stdio.h>
#include <string.h>
namespace ServerRuntime
{
using StringUtils::Utf8ToWide;
using StringUtils::WideToUtf8;
enum EWorldSaveLoadResult
{
eWorldSaveLoad_Loaded,
eWorldSaveLoad_NotFound,
eWorldSaveLoad_Failed
};
struct SaveInfoQueryContext
{
bool done;
bool success;
SAVE_DETAILS *details;
SaveInfoQueryContext()
: done(false)
, success(false)
, details(NULL)
{
}
};
struct SaveDataLoadContext
{
bool done;
bool isCorrupt;
bool isOwner;
SaveDataLoadContext()
: done(false)
, isCorrupt(true)
, isOwner(false)
{
}
};
/**
* **Apply Save ID To StorageManager**
*
* Applies the configured save destination ID (`level-id`) to `StorageManager`
* - Re-applies the same ID at startup and before save to avoid destination drift
* - Ignores empty values as invalid
* - For some reason, a date-based world file occasionally appears after a debug build, but the cause is unknown.
* 保存先IDの適用処理
*
* @param saveFilename Normalized save destination ID
*/
static void SetStorageSaveUniqueFilename(const std::string &saveFilename)
{
if (saveFilename.empty())
{
return;
}
char filenameBuffer[64] = {};
strncpy_s(filenameBuffer, sizeof(filenameBuffer), saveFilename.c_str(), _TRUNCATE);
StorageManager.SetSaveUniqueFilename(filenameBuffer);
}
static void LogSaveFilename(const char *prefix, const std::string &saveFilename)
{
LogInfof("world-io", "%s: %s", (prefix != NULL) ? prefix : "save-filename", saveFilename.c_str());
}
/**
* Verifies a directory exists and creates it when missing
* - Treats an existing non-directory path as failure
* - Returns whether the directory had to be created
* ディレクトリ存在保証の補助処理
*/
static bool EnsureDirectoryExists(const std::wstring &directoryPath, bool *outCreated)
{
if (outCreated != NULL)
{
*outCreated = false;
}
if (directoryPath.empty())
{
return false;
}
DWORD attrs = GetFileAttributesW(directoryPath.c_str());
if (attrs != INVALID_FILE_ATTRIBUTES)
{
if ((attrs & FILE_ATTRIBUTE_DIRECTORY) != 0)
{
return true;
}
LogErrorf("world-io", "path exists but is not a directory: %s", WideToUtf8(directoryPath).c_str());
return false;
}
if (CreateDirectoryW(directoryPath.c_str(), NULL))
{
if (outCreated != NULL)
{
*outCreated = true;
}
return true;
}
DWORD error = GetLastError();
if (error == ERROR_ALREADY_EXISTS)
{
attrs = GetFileAttributesW(directoryPath.c_str());
if (attrs != INVALID_FILE_ATTRIBUTES && ((attrs & FILE_ATTRIBUTE_DIRECTORY) != 0))
{
return true;
}
}
LogErrorf(
"world-io",
"failed to create directory %s (error=%lu)",
WideToUtf8(directoryPath).c_str(),
(unsigned long)error);
return false;
}
/**
* Prepares the save root used by the Windows64 storage layout
* - Creates `Windows64` first because `CreateDirectoryW` is not recursive
* - Creates `Windows64\\GameHDD` when missing before world bootstrap starts
* Windows64用保存先ディレクトリの存在保証
*/
static bool EnsureGameHddRootExists()
{
bool windows64Created = false;
if (!EnsureDirectoryExists(L"Windows64", &windows64Created))
{
return false;
}
bool gameHddCreated = false;
if (!EnsureDirectoryExists(L"Windows64\\GameHDD", &gameHddCreated))
{
return false;
}
if (windows64Created || gameHddCreated)
{
LogWorldIO("created missing Windows64\\GameHDD storage directories");
}
return true;
}
static void LogEnumeratedSaveInfo(int index, const SAVE_INFO &saveInfo)
{
std::wstring title = Utf8ToWide(saveInfo.UTF8SaveTitle);
std::wstring filename = Utf8ToWide(saveInfo.UTF8SaveFilename);
std::string titleUtf8 = WideToUtf8(title);
std::string filenameUtf8 = WideToUtf8(filename);
char logLine[512] = {};
sprintf_s(
logLine,
sizeof(logLine),
"save[%d] title=\"%s\" filename=\"%s\"",
index,
titleUtf8.c_str(),
filenameUtf8.c_str());
LogDebug("world-io", logLine);
}
/**
* **Save List Callback**
*
* Captures async save-list results into `SaveInfoQueryContext` and marks completion for the waiter
* セーブ一覧取得の完了通知
*/
static int GetSavesInfoCallbackProc(LPVOID lpParam, SAVE_DETAILS *pSaveDetails, const bool bRes)
{
SaveInfoQueryContext *context = (SaveInfoQueryContext *)lpParam;
if (context != NULL)
{
context->details = pSaveDetails;
context->success = bRes;
context->done = true;
}
return 0;
}
/**
* **Save Data Load Callback**
*
* Writes load results such as corruption status into `SaveDataLoadContext`
* セーブ読み込み結果の反映
*/
static int LoadSaveDataCallbackProc(LPVOID lpParam, const bool bIsCorrupt, const bool bIsOwner)
{
SaveDataLoadContext *context = (SaveDataLoadContext *)lpParam;
if (context != NULL)
{
context->isCorrupt = bIsCorrupt;
context->isOwner = bIsOwner;
context->done = true;
}
return 0;
}
/**
* **Wait For Save List Completion**
*
* Waits until save-list retrieval completes
* - Prefers callback completion as the primary signal
* - Also falls back to polling because some environments populate `ReturnSavesInfo()` before callback
* セーブ一覧待機の補助処理
*
* @return `true` when completion is detected
*/
static bool WaitForSaveInfoResult(SaveInfoQueryContext *context, DWORD timeoutMs, WorldManagerTickProc tickProc)
{
DWORD start = GetTickCount();
while ((GetTickCount() - start) < timeoutMs)
{
if (context->done)
{
return true;
}
if (context->details == NULL)
{
// Some implementations fill ReturnSavesInfo before the callback
// Keep polling as a fallback instead of relying only on callback completion
SAVE_DETAILS *details = StorageManager.ReturnSavesInfo();
if (details != NULL)
{
context->details = details;
context->success = true;
context->done = true;
return true;
}
}
if (tickProc != NULL)
{
tickProc();
}
Sleep(10);
}
return context->done;
}
/**
* **Wait For Save Data Load Completion**
*
* Waits for the save-data load callback to complete
* セーブ本体の読み込み待機
*
* @return `true` when callback is reached, `false` on timeout
*/
static bool WaitForSaveLoadResult(SaveDataLoadContext *context, DWORD timeoutMs, WorldManagerTickProc tickProc)
{
DWORD start = GetTickCount();
while ((GetTickCount() - start) < timeoutMs)
{
if (context->done)
{
return true;
}
if (tickProc != NULL)
{
tickProc();
}
Sleep(10);
}
return context->done;
}
/**
* **Match SAVE_INFO By World Name**
*
* Compares both save title and save filename against the target world name
* ワールド名一致判定
*/
static bool SaveInfoMatchesWorldName(const SAVE_INFO &saveInfo, const std::wstring &targetWorldName)
{
if (targetWorldName.empty())
{
return false;
}
std::wstring saveTitle = Utf8ToWide(saveInfo.UTF8SaveTitle);
std::wstring saveFilename = Utf8ToWide(saveInfo.UTF8SaveFilename);
if (!saveTitle.empty() && (_wcsicmp(saveTitle.c_str(), targetWorldName.c_str()) == 0))
{
return true;
}
if (!saveFilename.empty() && (_wcsicmp(saveFilename.c_str(), targetWorldName.c_str()) == 0))
{
return true;
}
return false;
}
/**
* **Match SAVE_INFO By Save Filename**
*
* Checks whether `SAVE_INFO` matches by save destination ID (`UTF8SaveFilename`)
* 保存先ID一致判定
*/
static bool SaveInfoMatchesSaveFilename(const SAVE_INFO &saveInfo, const std::string &targetSaveFilename)
{
if (targetSaveFilename.empty() || saveInfo.UTF8SaveFilename[0] == 0)
{
return false;
}
return (_stricmp(saveInfo.UTF8SaveFilename, targetSaveFilename.c_str()) == 0);
}
/**
* **Apply World Identity To Storage**
*
* Applies world identity (`level-name` + `level-id`) to storage
* - Always sets both display name and ID to avoid partial configuration
* - Helps prevent unintended new save destinations across environment differences
* 保存先と表示名の同期処理
*/
static void ApplyWorldStorageTarget(const std::wstring &worldName, const std::string &saveId)
{
// Set both title (display name) and save ID (actual folder name) explicitly
// Setting only one side can create unexpected new save targets in some environments
StorageManager.SetSaveTitle(worldName.c_str());
SetStorageSaveUniqueFilename(saveId);
}
/**
* **Prepare World Save Data For Startup**
*
* Searches for a save matching the target world and extracts startup payload when found
* Match priority:
* 1. Exact match by `level-id` (`UTF8SaveFilename`)
* 2. Fallback match by `level-name` against title or filename
* ワールド一致セーブの探索処理
*
* @return
* - `eWorldSaveLoad_Loaded`: Existing save loaded successfully
* - `eWorldSaveLoad_NotFound`: No matching save found
* - `eWorldSaveLoad_Failed`: API failure, corruption, or invalid data
*/
static EWorldSaveLoadResult PrepareWorldSaveData(
const std::wstring &targetWorldName,
const std::string &targetSaveFilename,
int actionPad,
WorldManagerTickProc tickProc,
LoadSaveDataThreadParam **outSaveData,
std::string *outResolvedSaveFilename)
{
if (outSaveData == NULL)
{
return eWorldSaveLoad_Failed;
}
*outSaveData = NULL;
if (outResolvedSaveFilename != NULL)
{
outResolvedSaveFilename->clear();
}
LogWorldIO("enumerating saves for configured world");
StorageManager.ClearSavesInfo();
SaveInfoQueryContext infoContext;
int infoState = StorageManager.GetSavesInfo(actionPad, &GetSavesInfoCallbackProc, &infoContext, "save");
if (infoState == C4JStorage::ESaveGame_Idle)
{
infoContext.done = true;
infoContext.success = true;
infoContext.details = StorageManager.ReturnSavesInfo();
}
else if (infoState != C4JStorage::ESaveGame_GetSavesInfo)
{
LogWorldIO("GetSavesInfo failed to start");
return eWorldSaveLoad_Failed;
}
if (!WaitForSaveInfoResult(&infoContext, 10000, tickProc))
{
LogWorldIO("timed out waiting for save list");
return eWorldSaveLoad_Failed;
}
if (infoContext.details == NULL)
{
infoContext.details = StorageManager.ReturnSavesInfo();
}
if (infoContext.details == NULL)
{
LogWorldIO("failed to retrieve save list");
return eWorldSaveLoad_Failed;
}
int matchedIndex = -1;
if (!targetSaveFilename.empty())
{
// 1) If save ID is provided, search by it first
// This is the most stable way to reuse the same world target
for (int i = 0; i < infoContext.details->iSaveC; ++i)
{
LogEnumeratedSaveInfo(i, infoContext.details->SaveInfoA[i]);
if (SaveInfoMatchesSaveFilename(infoContext.details->SaveInfoA[i], targetSaveFilename))
{
matchedIndex = i;
break;
}
}
}
if (matchedIndex < 0 && targetSaveFilename.empty())
{
for (int i = 0; i < infoContext.details->iSaveC; ++i)
{
LogEnumeratedSaveInfo(i, infoContext.details->SaveInfoA[i]);
}
}
for (int i = 0; i < infoContext.details->iSaveC; ++i)
{
// 2) If no save matched by ID, try compatibility fallback
// Match worldName against save title or save filename
if (matchedIndex >= 0)
{
break;
}
if (SaveInfoMatchesWorldName(infoContext.details->SaveInfoA[i], targetWorldName))
{
matchedIndex = i;
break;
}
}
if (matchedIndex < 0)
{
LogWorldIO("no save matched configured world name");
return eWorldSaveLoad_NotFound;
}
std::wstring matchedTitle = Utf8ToWide(infoContext.details->SaveInfoA[matchedIndex].UTF8SaveTitle);
if (matchedTitle.empty())
{
matchedTitle = targetWorldName;
}
LogWorldName("matched save title", matchedTitle);
SAVE_INFO *matchedSaveInfo = &infoContext.details->SaveInfoA[matchedIndex];
std::wstring matchedFilename = Utf8ToWide(matchedSaveInfo->UTF8SaveFilename);
if (!matchedFilename.empty())
{
LogWorldName("matched save filename", matchedFilename);
}
ApplyWorldStorageTarget(targetWorldName, targetSaveFilename);
std::string resolvedSaveFilename;
if (matchedSaveInfo->UTF8SaveFilename[0] != 0)
{
// Prefer the save ID that was actually matched, then keep using it for future saves
resolvedSaveFilename = matchedSaveInfo->UTF8SaveFilename;
SetStorageSaveUniqueFilename(resolvedSaveFilename);
}
else if (!targetSaveFilename.empty())
{
resolvedSaveFilename = targetSaveFilename;
}
if (outResolvedSaveFilename != NULL)
{
*outResolvedSaveFilename = resolvedSaveFilename;
}
SaveDataLoadContext loadContext;
int loadState = StorageManager.LoadSaveData(matchedSaveInfo, &LoadSaveDataCallbackProc, &loadContext);
if (loadState != C4JStorage::ESaveGame_Load && loadState != C4JStorage::ESaveGame_Idle)
{
LogWorldIO("LoadSaveData failed to start");
return eWorldSaveLoad_Failed;
}
if (loadState == C4JStorage::ESaveGame_Load)
{
if (!WaitForSaveLoadResult(&loadContext, 15000, tickProc))
{
LogWorldIO("timed out waiting for save data load");
return eWorldSaveLoad_Failed;
}
if (loadContext.isCorrupt)
{
LogWorldIO("target save is corrupt; aborting load");
return eWorldSaveLoad_Failed;
}
}
unsigned int saveSize = StorageManager.GetSaveSize();
if (saveSize == 0)
{
// Treat zero-byte payload as failure even when load API reports success
LogWorldIO("loaded save has zero size");
return eWorldSaveLoad_Failed;
}
byteArray loadedSaveData(saveSize, false);
unsigned int loadedSize = saveSize;
StorageManager.GetSaveData(loadedSaveData.data, &loadedSize);
if (loadedSize == 0)
{
LogWorldIO("failed to copy loaded save data from storage manager");
return eWorldSaveLoad_Failed;
}
*outSaveData = new LoadSaveDataThreadParam(loadedSaveData.data, loadedSize, matchedTitle);
LogWorldIO("prepared save data payload for server startup");
return eWorldSaveLoad_Loaded;
}
/**
* **Bootstrap World State For Server Startup**
*
* Determines final world startup state
* - Returns loaded save data when an existing save is found
* - Prepares a new world context when not found
* - Returns `Failed` when startup should be aborted
* サーバー起動時のワールド確定処理
*/
WorldBootstrapResult BootstrapWorldForServer(
const ServerPropertiesConfig &config,
int actionPad,
WorldManagerTickProc tickProc)
{
WorldBootstrapResult result;
if (!EnsureGameHddRootExists())
{
LogWorldIO("failed to prepare Windows64\\GameHDD storage root");
return result;
}
std::wstring targetWorldName = config.worldName;
std::string targetSaveFilename = config.worldSaveId;
if (targetWorldName.empty())
{
targetWorldName = L"world";
}
LogWorldName("configured level-name", targetWorldName);
if (!targetSaveFilename.empty())
{
LogSaveFilename("configured level-id", targetSaveFilename);
}
ApplyWorldStorageTarget(targetWorldName, targetSaveFilename);
std::string loadedSaveFilename;
EWorldSaveLoadResult worldLoadResult = PrepareWorldSaveData(
targetWorldName,
targetSaveFilename,
actionPad,
tickProc,
&result.saveData,
&loadedSaveFilename);
if (worldLoadResult == eWorldSaveLoad_Loaded)
{
result.status = eWorldBootstrap_Loaded;
result.resolvedSaveId = loadedSaveFilename;
LogStartupStep("loading configured world from save data");
}
else if (worldLoadResult == eWorldSaveLoad_NotFound)
{
// Create a new context only when no matching save exists
// Fix saveId here so the next startup writes to the same location
result.status = eWorldBootstrap_CreatedNew;
result.resolvedSaveId = targetSaveFilename;
LogStartupStep("configured world not found; creating new world");
LogWorldIO("creating new world save context");
StorageManager.ResetSaveData();
ApplyWorldStorageTarget(targetWorldName, targetSaveFilename);
}
else
{
result.status = eWorldBootstrap_Failed;
}
return result;
}
/**
* **Wait Until Server XUI Action Is Idle**
*
* Keeps tick/handle running during save action so async processing does not stall
* XUIアクション待機中の進行維持処理
*/
bool WaitForWorldActionIdle(
int actionPad,
DWORD timeoutMs,
WorldManagerTickProc tickProc,
WorldManagerHandleActionsProc handleActionsProc)
{
DWORD start = GetTickCount();
while (app.GetXuiServerAction(actionPad) != eXuiServerAction_Idle && !MinecraftServer::serverHalted())
{
// Keep network and storage progressing while waiting
// If this stops, save action itself may stall and time out
if (tickProc != NULL)
{
tickProc();
}
if (handleActionsProc != NULL)
{
handleActionsProc();
}
if ((GetTickCount() - start) >= timeoutMs)
{
return false;
}
Sleep(10);
}
return (app.GetXuiServerAction(actionPad) == eXuiServerAction_Idle);
}
}
+92
View File
@@ -0,0 +1,92 @@
#pragma once
#include <string>
#include <windows.h>
#include "ServerProperties.h"
struct _LoadSaveDataThreadParam;
typedef struct _LoadSaveDataThreadParam LoadSaveDataThreadParam;
namespace ServerRuntime
{
/** Tick callback used while waiting on async storage/network work */
typedef void (*WorldManagerTickProc)();
/** Optional action handler used while waiting for server actions */
typedef void (*WorldManagerHandleActionsProc)();
/**
* **World Bootstrap Status**
*
* Result type for world startup preparation, either loading an existing world or creating a new one
* ワールド起動準備の結果種別
*/
enum EWorldBootstrapStatus
{
/** Found and loaded an existing world */
eWorldBootstrap_Loaded,
/** No matching save was found, created a new world context */
eWorldBootstrap_CreatedNew,
/** Bootstrap failed and server startup should be aborted */
eWorldBootstrap_Failed
};
/**
* **World Bootstrap Result**
*
* Output payload returned by world startup preparation
* ワールド起動準備の出力データ
*/
struct WorldBootstrapResult
{
/** Bootstrap status */
EWorldBootstrapStatus status;
/** Save data used for server initialization, `NULL` when creating a new world */
LoadSaveDataThreadParam *saveData;
/** Save ID that was actually selected */
std::string resolvedSaveId;
WorldBootstrapResult()
: status(eWorldBootstrap_Failed)
, saveData(NULL)
{
}
};
/**
* **Bootstrap Target World For Server Startup**
*
* Resolves whether the target world should be loaded from an existing save or created as new
* - Applies `level-name` and `level-id` from `server.properties`
* - Loads when a matching save exists
* - Creates a new world context only when no save matches
* サーバー起動時のロードか新規作成かを確定する
*
* @param config Normalized `server.properties` values
* @param actionPad padId used by async storage APIs
* @param tickProc Tick callback run while waiting for async completion
* @return Bootstrap result including whether save data was loaded
*/
WorldBootstrapResult BootstrapWorldForServer(
const ServerPropertiesConfig &config,
int actionPad,
WorldManagerTickProc tickProc);
/**
* **Wait Until Server Action Returns To Idle**
*
* Waits until server action state reaches `Idle`
* サーバーアクションの待機処理
*
* @param actionPad padId to monitor
* @param timeoutMs Timeout in milliseconds
* @param tickProc Tick callback run inside the wait loop
* @param handleActionsProc Optional action handler callback
* @return `true` when `Idle` is reached before timeout
*/
bool WaitForWorldActionIdle(
int actionPad,
DWORD timeoutMs,
WorldManagerTickProc tickProc,
WorldManagerHandleActionsProc handleActionsProc);
}
+284
View File
@@ -0,0 +1,284 @@
# Minecraft.Server Developer Guide (English)
This document is for contributors who are new to `Minecraft.Server` and need a practical map for adding or modifying features safely.
## 1. What This Server Does
`Minecraft.Server` is the dedicated-server executable entry for this codebase.
Core responsibilities:
- Switch the process working directory to the executable folder before relative file I/O
- Load, normalize, and repair `server.properties`
- Initialize dedicated runtime systems, connection logging, and access control
- Load or create the target world and keep `level-id` aligned with the actual save destination
- Run the dedicated main loop (network tick, XUI actions, autosave, CLI input)
- Maintain operator-facing access files such as `banned-players.json` and `banned-ips.json`
- Perform an initial save for newly created worlds and then shut down safely
## 2. Important Files
### Startup and Runtime
- `Windows64/ServerMain.cpp`
- `PrintUsage()` and `ParseCommandLine()`
- `SetExeWorkingDirectory()`
- Runtime setup and shutdown flow
- Initial save path for newly created worlds
- Main loop, autosave scheduler, and CLI polling
### World Selection and Save Load
- `WorldManager.h`
- `WorldManager.cpp`
- Finds matching save by `level-id` first, then world-name fallback
- Applies storage title + save ID consistently
- Wait helpers for async storage/server action completion
### Server Properties
- `ServerProperties.h`
- `ServerProperties.cpp`
- Default values and normalization ranges
- Parse/repair/write `server.properties`
- Exposes `ServerPropertiesConfig`
- `SaveServerPropertiesConfig()` rewrites `level-name`, `level-id`, and `white-list`
### Access Control, Ban, and Whitelist Storage
- `Access/Access.h`
- `Access/Access.cpp`
- Process-wide access-control facade
- Published snapshot model used by console commands and login checks
- `Access/BanManager.h`
- `Access/BanManager.cpp`
- Reads/writes `banned-players.json` and `banned-ips.json`
- Normalizes identifiers and filters expired entries from snapshots
- `Access/WhitelistManager.h`
- `Access/WhitelistManager.cpp`
- Reads/writes `whitelist.json`
- Normalizes XUID-based whitelist entries used by login validation and CLI commands
### Logging and Connection Audit
- `ServerLogger.h`
- `ServerLogger.cpp`
- Log level parsing
- Colored/timestamped console logs
- General categories such as `startup`, `world-io`, `console`, `access`, `network`, and `shutdown`
- `ServerLogManager.h`
- `ServerLogManager.cpp`
- Accepted/rejected TCP connection logs
- Login/disconnect audit logs
- Remote-IP cache used by `ban-ip <player>`
### Console Command System
- `Console/ServerCli.cpp` (facade)
- `Console/ServerCliInput.cpp` (linenoise input thread + completion bridge)
- `Console/ServerCliParser.cpp` (tokenization, quoted args, completion context)
- `Console/ServerCliEngine.cpp` (dispatch, completion, helpers)
- `Console/ServerCliRegistry.cpp` (command registration + lookup)
- `Console/commands/*` (individual commands)
## 3. End-to-End Startup Flow
Main flow in `Windows64/ServerMain.cpp`:
1. `SetExeWorkingDirectory()` switches the current directory to the executable folder.
2. Load and normalize `server.properties` via `LoadServerPropertiesConfig()`.
3. Copy config into `DedicatedServerConfig`, then apply CLI overrides (`-port`, `-ip`/`-bind`, `-name`, `-maxplayers`, `-seed`, `-loglevel`, `-help`/`--help`/`-h`).
4. Initialize process state, `ServerLogManager`, and `Access::Initialize(".")`.
5. Initialize window/device/profile/network/thread-local systems.
6. Set host/game options from `ServerPropertiesConfig`.
7. Bootstrap world with `BootstrapWorldForServer(...)`.
8. If world bootstrap resolves a different normalized save ID, persist it with `SaveServerPropertiesConfig()`.
9. Start hosted game thread (`RunNetworkGameThreadProc`).
10. If a brand-new world was created, explicitly request one initial save.
11. Enter the main loop:
- `TickCoreSystems()`
- `HandleXuiActions()`
- `serverCli.Poll()`
- autosave scheduling
12. On shutdown:
- stop CLI input
- request save-on-exit / halt server
- wait for network shutdown completion
- terminate log, access, network, and device systems
## 4. Current Operator Surface
### 4.1 Launch Arguments
- `-port <1-65535>`
- `-ip <addr>` or `-bind <addr>`
- `-name <name>` (runtime max 16 chars)
- `-maxplayers <1-8>`
- `-seed <int64>`
- `-loglevel <debug|info|warn|error>`
- `-help`, `--help`, `-h`
Notes:
- CLI overrides affect only the current process.
- The only values currently written back by the server are `level-name` and `level-id`, and that happens when world bootstrap resolves identity changes.
### 4.2 Built-in Console Commands
- `help` / `?`
- `stop`
- `list`
- `ban <player> [reason ...]`
- currently requires the target player to be online
- `ban-ip <address|player> [reason ...]`
- accepts a literal IPv4/IPv6 address or an online player's current remote IP
- `pardon <player>`
- `pardon-ip <address>`
- only accepts a literal address
- `banlist`
- `tp <player> <target>` / `teleport`
- `gamemode <survival|creative|0|1> [player]` / `gm`
CLI behavior notes:
- Command parsing accepts both `cmd` and `/cmd`.
- Quoted arguments are supported by `ServerCliParser`.
- Completion is implemented per command via `Complete(...)`.
### 4.3 Files Written Next to the Executable
- `server.properties`
- `banned-players.json`
- `banned-ips.json`
This follows from `SetExeWorkingDirectory()`, so these files are resolved relative to `Minecraft.Server.exe`, not the shell directory you launched from.
## 5. Common Development Tasks
### 5.1 Add a New CLI Command
Use this pattern when adding commands like `/kick`, `/time`, etc.
1. Add files under `Console/commands/`
- `CliCommandYourCommand.h`
- `CliCommandYourCommand.cpp`
2. Implement `IServerCliCommand`
- `Name()`, `Usage()`, `Description()`, `Execute(...)`
- optional: `Aliases()` and `Complete(...)`
3. Register the command in `ServerCliEngine::RegisterDefaultCommands()`.
4. Add source/header to build definitions:
- `CMakeLists.txt` (`MINECRAFT_SERVER_SOURCES`)
- `Minecraft.Server/Minecraft.Server.vcxproj` (`<ClCompile>` / `<ClInclude>`)
5. Manual verify:
- command appears in `help`
- command executes correctly
- completion works for both `cmd` and `/cmd`
- quoted arguments behave as expected
Implementation references:
- `CliCommandHelp.cpp` for a simple no-arg command
- `CliCommandTp.cpp` for multi-arg + completion + runtime checks
- `CliCommandGamemode.cpp` for argument parsing and aliases
- `CliCommandBanIp.cpp` for access-backed behavior with connection metadata
### 5.2 Add or Change a `server.properties` Key
1. Add/update the field in `ServerPropertiesConfig` (`ServerProperties.h`).
2. Add a default entry to `kServerPropertyDefaults` (`ServerProperties.cpp`).
3. Load and normalize the value in `LoadServerPropertiesConfig()`.
- Use existing helpers for bool/int/string/int64/log level/level type.
4. If this value should be written back, update `SaveServerPropertiesConfig()`.
- Note: today that function intentionally only persists world identity.
5. Apply it to runtime where needed:
- `ApplyServerPropertiesToDedicatedConfig(...)`
- host options in `ServerMain.cpp` (`app.SetGameHostOption(...)`)
- `PrintUsage()` / `ParseCommandLine()` if the key also gets a CLI override
6. Manual verify:
- missing key regeneration
- invalid value normalization
- clamped ranges still make sense
- runtime behavior reflects the new value
Normalization details worth remembering:
- `level-id` is normalized to a safe save ID and length-limited.
- `server-name` is capped to 16 runtime chars.
- `max-players` is clamped to `1..8`.
- `autosave-interval` is clamped to `5..3600`.
- `level-type` normalizes to `default` or `flat`.
### 5.3 Change Ban / Access Behavior
Primary code lives in `Access/Access.cpp`, `Access/BanManager.cpp`, and `ServerLogManager.cpp`.
When changing this area:
- Keep `BanManager` responsible for storage/caching, not live-network policy.
- Keep the clone-and-publish snapshot pattern in `Access.cpp` so readers never block on disk I/O.
- Remember that `ban-ip <player>` depends on `ServerLogManager::TryGetConnectionRemoteIp(...)`.
- Keep expired entries out of `SnapshotBannedPlayers()` / `SnapshotBannedIps()` output.
- Verify:
- clean boot creates empty ban files when missing
- `ban`, `ban-ip`, `pardon`, `pardon-ip`, and `banlist` still work
- online bans disconnect live targets immediately
- manual edits still reload safely if you later add or extend reload paths
### 5.4 Change World Load/Create Behavior
Primary code is in `WorldManager.cpp`.
Current matching policy:
1. Match by `level-id` (`UTF8SaveFilename`) first.
2. Fall back to world-name match on title/file name.
When changing this logic:
- Keep `ApplyWorldStorageTarget(...)` usage consistent (title + save ID together).
- Preserve periodic ticking in wait loops (`tickProc`) to avoid async deadlocks.
- Keep timeout/error logs specific enough for diagnosis.
- Verify:
- existing world is reused correctly
- no accidental new save directory creation
- shutdown save still succeeds
- newly created worlds still get the explicit initial save from `ServerMain.cpp`
### 5.5 Add Logging for New Feature Work
Use `ServerLogger` helpers:
- `LogDebug`, `LogInfo`, `LogWarn`, `LogError`
- formatted variants `LogDebugf`, `LogInfof`, etc.
Use `ServerLogManager` when the event is specifically part of the transport/login/disconnect lifecycle.
Recommended categories:
- `startup` for init/shutdown lifecycle
- `world-io` for save/world operations
- `console` for CLI command handling
- `access` for ban/access control state
- `network` for connection/login audit
## 6. Build and Run
From repository root:
```powershell
cmake -S . -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Debug --target MinecraftServer
cd .\build\Debug
.\Minecraft.Server.exe -port 25565 -bind 0.0.0.0 -maxplayers 8 -name DedicatedServer
```
Notes:
- The process switches its working directory to the executable directory at startup.
- `server.properties`, `banned-players.json`, and `banned-ips.json` are therefore read/written next to the executable.
- For Visual Studio workflow, see root `COMPILE.md`.
## 7. Safety Checklist Before Commit
- the server starts without crash when `server.properties` is missing or sparse
- missing access files are recreated on a clean boot
- existing world loads by expected `level-id`
- new world creation still performs the explicit initial save
- CLI input and completion remain responsive
- `banlist` output stays sane after adding/removing bans
- no busy-wait path removed from async wait loops
- both CMake and `.vcxproj` include newly added source files
## 8. Quick Troubleshooting
- Unknown command:
- check `RegisterDefaultCommands()` and build-file entries
- `server.properties` or ban files seem to load from the wrong folder:
- remember `SetExeWorkingDirectory()` moves the working directory to the executable folder
- Autosave or shutdown save timing out:
- confirm wait loops still call `TickCoreSystems()` and `HandleXuiActions()` where required
- World not reused on restart:
- inspect `level-id` normalization and matching logic in `WorldManager.cpp`
- `ban-ip <player>` cannot resolve an address:
- confirm the player is currently online and `ServerLogManager` has a cached remote IP for that connection
- Settings not applied:
- confirm the value is loaded into `ServerPropertiesConfig`, optionally copied into `DedicatedServerConfig`, and then applied in `ServerMain.cpp`
+286
View File
@@ -0,0 +1,286 @@
# Minecraft.Server 開発ガイド (日本語)
この文書は、`Minecraft.Server` に新しく入る開発者が、安全に機能追加や改修を行うための実践的な地図として使うことを想定しています
## 1. このサーバーが担うこと
`Minecraft.Server` は、このコードベースにおける専用サーバー実行ファイルのエントリーポイントです
主な責務:
- 相対パスのファイル I/O を行う前に、カレントディレクトリを実行ファイルのあるフォルダへ切り替える
- `server.properties` を読み込み、正規化し、不足や不正値を補完する
- 専用サーバー向けランタイム、接続ログ、アクセス制御を初期化する
- 対象ワールドをロードまたは新規作成し、実際のセーブ先と `level-id` を整合させる
- 専用サーバーのメインループを回す (network tick, XUI actions, autosave, CLI input)
- `banned-players.json` や `banned-ips.json` など運用向けファイルを維持する
- 新規ワールドの初回保存を実行し、その後安全にシャットダウンする
## 2. 重要ファイル
### 起動とランタイム
- `Windows64/ServerMain.cpp`
- `PrintUsage()` と `ParseCommandLine()`
- `SetExeWorkingDirectory()`
- 起動/終了フロー
- 新規ワールド初回保存の経路
- メインループ、オートセーブ、CLI ポーリング
### ワールド選択とセーブ読込
- `WorldManager.h`
- `WorldManager.cpp`
- `level-id` 優先、その後 world 名フォールバックでセーブ探索
- storage title と save ID を常にセットで適用
- 非同期 storage/server action 完了待ちの helper を提供
### サーバー設定
- `ServerProperties.h`
- `ServerProperties.cpp`
- 既定値と正規化レンジ
- `server.properties` の読込/補修/書込
- `ServerPropertiesConfig` の提供
- `SaveServerPropertiesConfig()` は `level-name` / `level-id` / `white-list` を書き換える
### アクセス制御と BAN / Whitelist 永続化
- `Access/Access.h`
- `Access/Access.cpp`
- プロセス全体で使うアクセス制御 facade
- コンソールコマンドとログイン判定から参照される公開スナップショット管理
- `Access/BanManager.h`
- `Access/BanManager.cpp`
- `banned-players.json` と `banned-ips.json` の読込/書込
- 識別子の正規化と、期限切れエントリを除いた snapshot 出力
- `Access/WhitelistManager.h`
- `Access/WhitelistManager.cpp`
- `whitelist.json` の読込/書込
- ログイン判定と CLI で使う XUID whitelist の正規化管理
### ログと接続監査
- `ServerLogger.h`
- `ServerLogger.cpp`
- ログレベル解釈
- 色付き/タイムスタンプ付きコンソールログ
- `startup`, `world-io`, `console`, `access`, `network`, `shutdown` などのカテゴリ
- `ServerLogManager.h`
- `ServerLogManager.cpp`
- TCP 接続 accept/reject ログ
- ログイン/切断の監査ログ
- `ban-ip <player>` が使う remote IP キャッシュ
### コンソールコマンドシステム
- `Console/ServerCli.cpp` (facade)
- `Console/ServerCliInput.cpp` (linenoise 入力スレッド + completion bridge)
- `Console/ServerCliParser.cpp` (トークン分解、クォート、補完コンテキスト)
- `Console/ServerCliEngine.cpp` (実行ディスパッチ、補完、共通ヘルパー)
- `Console/ServerCliRegistry.cpp` (登録と名前解決)
- `Console/commands/*` (各コマンド実装)
## 3. 起動フロー全体
`Windows64/ServerMain.cpp` の主な流れ:
1. `SetExeWorkingDirectory()` でカレントディレクトリを実行ファイルのフォルダへ切り替える
2. `LoadServerPropertiesConfig()` で `server.properties` を読み込み、正規化する
3. `DedicatedServerConfig` へ反映したあと、CLI 引数で上書きする (`-port`, `-ip`/`-bind`, `-name`, `-maxplayers`, `-seed`, `-loglevel`, `-help`/`--help`/`-h`)
4. プロセス状態、`ServerLogManager`、`Access::Initialize(".")` を初期化する
5. window/device/profile/network/thread-local 系を初期化する
6. `ServerPropertiesConfig` をゲームホスト設定へ反映する
7. `BootstrapWorldForServer(...)` でワールドを決定する
8. 読み込まれたセーブ ID が正規化後に変わった場合は、`SaveServerPropertiesConfig()` で書き戻す
9. `RunNetworkGameThreadProc` でホストゲームスレッドを起動する
10. 新規ワールドが作成された場合は、専用サーバー側で明示的に初回保存を要求する
11. メインループに入る:
- `TickCoreSystems()`
- `HandleXuiActions()`
- `serverCli.Poll()`
- オートセーブスケジュール
12. 終了時:
- CLI 入力を停止
- save-on-exit を要求してサーバー停止
- ネットワーク停止完了を待機
- ログ/アクセス制御/ネットワーク/デバイスを終了
## 4. 現在の運用インターフェース
### 4.1 起動引数
- `-port <1-65535>`
- `-ip <addr>` または `-bind <addr>`
- `-name <name>` (実行時上限 16 文字)
- `-maxplayers <1-8>`
- `-seed <int64>`
- `-loglevel <debug|info|warn|error>`
- `-help`, `--help`, `-h`
補足:
- CLI による上書きは、その起動中のプロセスにだけ効きます
- 現在サーバーが書き戻す値は `level-name` と `level-id` だけで、ワールド解決時に識別情報が変わった場合に限られます
### 4.2 組み込みコンソールコマンド
- `help` / `?`
- `stop`
- `list`
- `ban <player> [reason ...]`
- 現状では対象プレイヤーがオンラインである必要があります
- `ban-ip <address|player> [reason ...]`
- リテラル IPv4/IPv6 か、オンラインプレイヤーの現在 IP を対象にできます
- `pardon <player>`
- `pardon-ip <address>`
- リテラルアドレスのみ受け付けます
- `banlist`
- `tp <player> <target>` / `teleport`
- `gamemode <survival|creative|0|1> [player]` / `gm`
CLI 挙動の補足:
- `cmd` と `/cmd` の両方を受け付けます
- `ServerCliParser` により引用符付き引数を扱えます
- 補完は各コマンドの `Complete(...)` で実装します
### 4.3 実行ファイル横に書かれるファイル
- `server.properties`
- `banned-players.json`
- `banned-ips.json`
これは `SetExeWorkingDirectory()` による挙動ですつまり、これらのファイルはシェル上の起動場所ではなく `Minecraft.Server.exe` 基準で解決されます
## 5. よくある開発作業
### 5.1 CLI コマンドを追加する
`/kick` や `/time` のようなコマンド追加時の基本手順:
1. `Console/commands/` にファイルを追加
- `CliCommandYourCommand.h`
- `CliCommandYourCommand.cpp`
2. `IServerCliCommand` を実装
- `Name()`, `Usage()`, `Description()`, `Execute(...)`
- 必要なら `Aliases()` と `Complete(...)`
3. `ServerCliEngine::RegisterDefaultCommands()` に登録する
4. ビルド定義に追加する
- `CMakeLists.txt` (`MINECRAFT_SERVER_SOURCES`)
- `Minecraft.Server/Minecraft.Server.vcxproj` (`<ClCompile>` / `<ClInclude>`)
5. 手動確認
- `help` に表示される
- 実行結果が期待通り
- 補完が `cmd` と `/cmd` の両方で動く
- 引用符付き引数が期待通り処理される
参考実装:
- `CliCommandHelp.cpp` (単純コマンド)
- `CliCommandTp.cpp` (複数引数 + 補完 + 実行時チェック)
- `CliCommandGamemode.cpp` (引数解釈 + エイリアス)
- `CliCommandBanIp.cpp` (接続メタデータを使うアクセス制御系コマンド)
### 5.2 `server.properties` キーを追加/変更する
1. `ServerProperties.h` の `ServerPropertiesConfig` にフィールドを追加/更新する
2. `ServerProperties.cpp` の `kServerPropertyDefaults` に既定値を追加する
3. `LoadServerPropertiesConfig()` で読み込みと正規化を実装する
- bool/int/string/int64/log level/level type 用の既存 helper を使う
4. 書き戻し対象にしたいなら `SaveServerPropertiesConfig()` を更新する
- ただし現状この関数は、意図的にワールド識別情報だけを永続化します
5. 実行時反映箇所を更新する:
- `ApplyServerPropertiesToDedicatedConfig(...)`
- `ServerMain.cpp` の `app.SetGameHostOption(...)`
- CLI 上書きも持たせるなら `PrintUsage()` / `ParseCommandLine()`
6. 手動確認:
- 欠損キーの自動補完
- 不正値の正規化
- clamp 範囲が妥当か
- 実行時挙動に反映されるか
> 覚えておくと良い正規化ポイント
- `level-id` は安全な save ID に正規化され、長さ制限も掛かる
- `server-name` は実行時 16 文字まで
- `max-players` は `1..8` に clamp される(あとで増やす必要あり)
- `autosave-interval` は `5..3600` に clamp される
- `level-type` は `default` または `flat` に正規化される
### 5.3 BAN / アクセス制御挙動を変更する
主な実装は `Access/Access.cpp`, `Access/BanManager.cpp`, `ServerLogManager.cpp` にあります
変更時の注意:
- `BanManager` は storage/caching に責務を寄せ、 live-network policy を持ち込みすぎない
- `Access.cpp` の clone-and-publish スナップショット方式を保ち、読取側がディスク I/O で止まらないようにする
- `ban-ip <player>` は `ServerLogManager::TryGetConnectionRemoteIp(...)` に依存することを忘れない
- `SnapshotBannedPlayers()` / `SnapshotBannedIps()` には期限切れエントリを混ぜない
- 確認項目:
- 欠損時に空の BAN ファイルが初回起動で生成される
- `ban`, `ban-ip`, `pardon`, `pardon-ip`, `banlist` が動く
- オンライン対象の BAN が即時切断まで到達する
- 将来 reload 経路を増やしても手動編集が安全に再読込できる
### 5.4 ワールドロード/新規作成ロジックを変更する
主な実装は `WorldManager.cpp` にあります
現在の探索ポリシー:
1. `level-id` (`UTF8SaveFilename`) 完全一致を優先
2. 失敗時に world 名一致へフォールバック
変更時の注意:
- `ApplyWorldStorageTarget(...)` で title と save ID を常にセットで扱う
- 待機ループで `tickProc` を回し続ける
- これを止めると非同期進行が止まり、タイムアウトしやすくなる
- タイムアウトや失敗ログは具体的に残す
- 確認項目
- 既存ワールドを正しく再利用できるか
- 意図しない新規セーブ先が増えていないか
- 終了時保存が成功するか
- 新規ワールド時の明示的初回保存が `ServerMain.cpp` から維持されているか
### 5.5 ログを追加する
`ServerLogger` の API を利用:
- `LogDebug`, `LogInfo`, `LogWarn`, `LogError`
- フォーマット付きは `LogDebugf`, `LogInfof` など
transport/login/disconnect ライフサイクルに属するイベントなら `ServerLogManager` 側を使います
推奨カテゴリ:
- `startup`: 起動ライフサイクル
- `shutdown`: 停止ライフサイクル
- `world-io`: ワールド/保存処理
- `console`: CLI コマンド処理
- `access`: BAN/アクセス制御状態
- `network`: 接続/ログイン監査
## 6. ビルドと実行
リポジトリルートで実行:
```powershell
cmake -S . -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Debug --target MinecraftServer
cd .\build\Debug
.\Minecraft.Server.exe -port 25565 -bind 0.0.0.0 -name DedicatedServer
```
補足:
- プロセスは起動時にカレントディレクトリを実行ファイルの場所へ切り替えます
- `server.properties`, `banned-players.json`, `banned-ips.json` はそのため実行ファイル横に読み書きされます
- Visual Studio ワークフローはルートの `COMPILE.md` を参照してください
## 7. 変更前チェックリスト
- `server.properties` が欠損または疎でもクラッシュせず起動できる
- 欠損したアクセス制御ファイルがクリーンブート時に再生成される
- 既存ワールドが期待した `level-id` でロードされる
- 新規ワールド作成時の明示的初回保存が維持される
- CLI 入力と補完が引き続き応答する
- `banlist` 出力が BAN 追加/解除後も破綻しない
- 非同期待機ループから `TickCoreSystems()` など busy-wait 防止用ティックを消していない
- 新規追加したソースが CMake と `.vcxproj` の両方に入っている
## 8. クイックトラブルシュート
- コマンドが認識されない:
- `RegisterDefaultCommands()` とビルド定義を確認する
- `server.properties` や BAN ファイルの読込先が想定と違う:
- `SetExeWorkingDirectory()` により実行ファイルのフォルダへ移動していることを確認する
- オートセーブ/終了時保存がタイムアウトする:
- 待機ループ内で `TickCoreSystems()` と `HandleXuiActions()` を回しているか確認する
- 再起動時に同じワールドを使わない:
- `level-id` の正規化と `WorldManager.cpp` の一致判定を確認する
- `ban-ip <player>` で IP を解決できない:
- 対象プレイヤーがオンラインで、`ServerLogManager` に接続 IP がキャッシュされているか確認する
- 設定変更が効かない:
- 値が `ServerPropertiesConfig` にロードされ、必要なら `DedicatedServerConfig` にコピーされ、その後 `ServerMain.cpp` で反映されているか確認する
+8
View File
@@ -0,0 +1,8 @@
// stdafx.cpp : source file that includes just the standard includes
// Minecraft.Server.pch will be the pre-compiled header
// stdafx.obj will contain the pre-compiled type information
#include "stdafx.h"
// TODO: reference any additional headers you need in STDAFX.H
// and not in this file

Some files were not shown because too many files have changed in this diff Show More