add: refactor world loader & add server properties

- Introduced ServerLogger for logging startup steps and world I/O operations.
- Implemented ServerProperties for loading and saving server configuration from `server.properties`.
- Added WorldManager to handle world loading and creation based on server properties.
- Updated ServerMain to integrate server properties loading and world management.
- Enhanced project files to include new source and header files for the server components.
This commit is contained in:
kuwacom 2026-03-05 07:31:49 +09:00
parent eb75a935b3
commit 6d44e40b4e
9 changed files with 1267 additions and 12 deletions

View file

@ -646,6 +646,9 @@
<PrecompiledHeader>NotUsing</PrecompiledHeader> <PrecompiledHeader>NotUsing</PrecompiledHeader>
</ClCompile> </ClCompile>
<ClCompile Include="..\Minecraft.Client\glWrapper.cpp" /> <ClCompile Include="..\Minecraft.Client\glWrapper.cpp" />
<ClCompile Include="ServerLogger.cpp" />
<ClCompile Include="ServerProperties.cpp" />
<ClCompile Include="WorldManager.cpp" />
<ClCompile Include="..\Minecraft.Client\stdafx.cpp"> <ClCompile Include="..\Minecraft.Client\stdafx.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
@ -653,6 +656,11 @@
<ClCompile Include="..\Minecraft.Client\stubs.cpp" /> <ClCompile Include="..\Minecraft.Client\stubs.cpp" />
<ClCompile Include="Windows64\ServerMain.cpp" /> <ClCompile Include="Windows64\ServerMain.cpp" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ClInclude Include="ServerLogger.h" />
<ClInclude Include="ServerProperties.h" />
<ClInclude Include="WorldManager.h" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<MASM Include="..\Minecraft.Client\iob_shim.asm" /> <MASM Include="..\Minecraft.Client\iob_shim.asm" />
</ItemGroup> </ItemGroup>

View file

@ -6,8 +6,28 @@
</Filter> </Filter>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="ServerLogger.cpp">
<Filter>Server</Filter>
</ClCompile>
<ClCompile Include="ServerProperties.cpp">
<Filter>Server</Filter>
</ClCompile>
<ClCompile Include="WorldManager.cpp">
<Filter>Server</Filter>
</ClCompile>
<ClCompile Include="Windows64\ServerMain.cpp"> <ClCompile Include="Windows64\ServerMain.cpp">
<Filter>Server</Filter> <Filter>Server</Filter>
</ClCompile> </ClCompile>
</ItemGroup> </ItemGroup>
<ItemGroup>
<ClInclude Include="ServerLogger.h">
<Filter>Server</Filter>
</ClInclude>
<ClInclude Include="ServerProperties.h">
<Filter>Server</Filter>
</ClInclude>
<ClInclude Include="WorldManager.h">
<Filter>Server</Filter>
</ClInclude>
</ItemGroup>
</Project> </Project>

View file

@ -0,0 +1,74 @@
#include "stdafx.h"
#include "ServerLogger.h"
#include <stdio.h>
namespace ServerRuntime
{
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)
{
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;
}
void LogStartupStep(const char *message)
{
printf("[startup] %s\n", message);
fflush(stdout);
}
void LogWorldIO(const char *message)
{
printf("[world-io] %s\n", message);
fflush(stdout);
}
void LogWorldName(const char *prefix, const std::wstring &name)
{
std::string utf8 = WideToUtf8(name);
printf("[world-io] %s: %s\n", prefix, utf8.c_str());
fflush(stdout);
}
}

View file

@ -0,0 +1,13 @@
#pragma once
#include <string>
namespace ServerRuntime
{
std::string WideToUtf8(const std::wstring &value);
std::wstring Utf8ToWide(const char *value);
void LogStartupStep(const char *message);
void LogWorldIO(const char *message);
void LogWorldName(const char *prefix, const std::wstring &name);
}

View file

@ -0,0 +1,376 @@
#include "stdafx.h"
#include "ServerProperties.h"
#include "ServerLogger.h"
#include <cctype>
#include <fstream>
#include <map>
#include <stdio.h>
#include <unordered_map>
namespace ServerRuntime
{
struct ServerPropertyDefault
{
const char *key;
const char *value;
};
static const char *kServerPropertiesPath = "server.properties";
static const size_t kMaxSaveIdLength = 31;
static const ServerPropertyDefault kServerPropertyDefaults[] =
{
{ "level-name", "world" },
{ "level-id", "world" },
{ "level-type", "default" },
{ "gamemode", "0" },
{ "max-build-height", "256" },
{ "spawn-animals", "true" },
{ "spawn-npcs", "true" },
{ "spawn-monsters", "true" },
{ "pvp", "true" },
{ "server-ip", "" },
{ "motd", "A Minecraft Server" }
};
static 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);
}
/**
* IDとして安全な形式に正規化する
*
* :
* -
* - `[a-z0-9_.-]`
* - / `_`
* - `world`
* -
*/
static std::string NormalizeSaveId(const std::string &source)
{
std::string out;
out.reserve(source.length());
// Storage 側の保存先IDとして安全に扱える文字セットへ正規化する
// 不正文字は '_' に落とし、大小は吸収して衝突を減らす
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";
}
// 先頭文字が扱いづらいケースを避けるため、必要に応じて接頭辞を付与する
if (!((out[0] >= 'a' && out[0] <= 'z') || (out[0] >= '0' && out[0] <= '9')))
{
out = std::string("w_") + out;
}
// 4J 側の filename バッファ制約に合わせて長さを制限する
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;
}
}
/**
* `server.properties` key/value
*
* - `#` / `!`
* - `=` `:`
* -
*/
static bool ReadServerPropertiesFile(const char *filePath, std::unordered_map<std::string, std::string> *properties, int *outParsedCount)
{
if (properties == NULL)
{
return false;
}
std::ifstream inFile(filePath, std::ios::in | std::ios::binary);
if (!inFile.is_open())
{
return false;
}
int parsedCount = 0;
std::string line;
while (std::getline(inFile, line))
{
if (!line.empty() && line[line.length() - 1] == '\r')
{
line.erase(line.length() - 1);
}
std::string trimmedLine = TrimAscii(line);
if (trimmedLine.empty())
{
continue;
}
if (trimmedLine[0] == '#' || trimmedLine[0] == '!')
{
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)
{
continue;
}
std::string key = TrimAscii(trimmedLine.substr(0, sepPos));
if (key.empty())
{
continue;
}
std::string value = TrimAscii(trimmedLine.substr(sepPos + 1));
(*properties)[key] = value;
++parsedCount;
}
if (outParsedCount != NULL)
{
*outParsedCount = parsedCount;
}
return true;
}
/**
* key/value `server.properties`
*
*
*/
static bool WriteServerPropertiesFile(const char *filePath, const std::unordered_map<std::string, std::string> &properties)
{
FILE *outFile = fopen(filePath, "wb");
if (outFile == NULL)
{
return false;
}
fprintf(outFile, "# Minecraft server properties\n");
fprintf(outFile, "# Auto-generated 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)
{
fprintf(outFile, "%s=%s\n", it->first.c_str(), it->second.c_str());
}
fclose(outFile);
return true;
}
/**
* /
*
* -
* -
* - `level-id`
* -
*/
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)
{
// 既存値をデフォルトへ上書きマージして、未知キーも可能な限り維持する
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())
{
// level-id が未設定なら level-name から自動生成して保存先を固定する
worldSaveId = NormalizeSaveId(worldName);
shouldWrite = true;
}
else
{
// 既存の level-id も正規化して、将来の不整合を防ぐ
std::string normalized = NormalizeSaveId(worldSaveId);
if (normalized != worldSaveId)
{
worldSaveId = normalized;
shouldWrite = true;
}
}
merged["level-name"] = worldName;
merged["level-id"] = worldSaveId;
if (shouldWrite)
{
if (WriteServerPropertiesFile(kServerPropertiesPath, merged))
{
LogWorldIO("wrote server.properties");
}
else
{
LogWorldIO("failed to write server.properties");
}
}
config.worldName = Utf8ToWide(worldName.c_str());
config.worldSaveId = worldSaveId;
return config;
}
/**
*
*
* -
* - `level-name` / `level-id`
*/
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)
{
// 呼び出し側が触っていないキーを落とさないように、既存内容を保持する
merged[it->first] = it->second;
}
}
std::string worldName = TrimAscii(WideToUtf8(config.worldName));
if (worldName.empty())
{
worldName = "world"; // フォルト名
}
std::string worldSaveId = TrimAscii(config.worldSaveId);
if (worldSaveId.empty())
{
worldSaveId = NormalizeSaveId(worldName);
}
else
{
worldSaveId = NormalizeSaveId(worldSaveId);
}
merged["level-name"] = worldName;
merged["level-id"] = worldSaveId;
return WriteServerPropertiesFile(kServerPropertiesPath, merged);
}
}

View file

@ -0,0 +1,39 @@
#pragma once
#include <string>
namespace ServerRuntime
{
/**
* `server.properties`
*/
struct ServerPropertiesConfig
{
/** world name `level-name` */
std::wstring worldName;
/** world save id `level-id` */
std::string worldSaveId;
};
/**
* server.properties loader
*
* -
* -
* - `level-id`
*
* @return `WorldManager`
*/
ServerPropertiesConfig LoadServerPropertiesConfig();
/**
* server.properties saver
*
* - `level-name` `level-id`
* -
*
* @param config
* @return `true`
*/
bool SaveServerPropertiesConfig(const ServerPropertiesConfig &config);
}

View file

@ -6,6 +6,9 @@
#include "Minecraft.h" #include "Minecraft.h"
#include "MinecraftServer.h" #include "MinecraftServer.h"
#include "Options.h" #include "Options.h"
#include "..\ServerLogger.h"
#include "..\ServerProperties.h"
#include "..\WorldManager.h"
#include "Tesselator.h" #include "Tesselator.h"
#include "Windows64/4JLibs/inc/4J_Render.h" #include "Windows64/4JLibs/inc/4J_Render.h"
#include "Windows64/GameConfig/Minecraft.spa.h" #include "Windows64/GameConfig/Minecraft.spa.h"
@ -58,6 +61,8 @@ struct DedicatedServerConfig
}; };
static volatile bool g_shutdownRequested = false; static volatile bool g_shutdownRequested = false;
static const DWORD kAutosaveIntervalMs = 60 * 1000;
static const int kServerActionPad = 0;
static BOOL WINAPI ConsoleCtrlHandlerProc(DWORD ctrlType) static BOOL WINAPI ConsoleCtrlHandlerProc(DWORD ctrlType)
{ {
@ -75,6 +80,11 @@ static BOOL WINAPI ConsoleCtrlHandlerProc(DWORD ctrlType)
} }
} }
/**
*
*
* 使
*/
static int WaitForServerStoppedThreadProc(void *) static int WaitForServerStoppedThreadProc(void *)
{ {
if (g_NetworkManager.ServerStoppedValid()) if (g_NetworkManager.ServerStoppedValid())
@ -96,11 +106,17 @@ static void PrintUsage()
printf(" -help Show this help\n"); printf(" -help Show this help\n");
} }
static void LogStartupStep(const char *message) using ServerRuntime::LoadServerPropertiesConfig;
{ using ServerRuntime::LogStartupStep;
printf("[startup] %s\n", message); using ServerRuntime::LogWorldIO;
fflush(stdout); using ServerRuntime::SaveServerPropertiesConfig;
} using ServerRuntime::ServerPropertiesConfig;
using ServerRuntime::WideToUtf8;
using ServerRuntime::BootstrapWorldForServer;
using ServerRuntime::eWorldBootstrap_Failed;
using ServerRuntime::eWorldBootstrap_Loaded;
using ServerRuntime::WaitForWorldActionIdle;
using ServerRuntime::WorldBootstrapResult;
static bool ParseIntArg(const char *value, int *outValue) static bool ParseIntArg(const char *value, int *outValue)
{ {
@ -199,6 +215,36 @@ static void SetExeWorkingDirectory()
} }
} }
/**
* 1
*
* //
*
*/
static void TickCoreSystems()
{
g_NetworkManager.DoWork();
ProfileManager.Tick();
StorageManager.Tick();
}
/**
* XUI / 1
*/
static void HandleXuiActions()
{
app.HandleXuiActions();
}
/**
* Dedicated Server Entory Point
*
* :
* - //
* - `WorldManager`
* -
* -
*/
int main(int argc, char **argv) int main(int argc, char **argv)
{ {
DedicatedServerConfig config; DedicatedServerConfig config;
@ -338,12 +384,46 @@ int main(int argc, char **argv)
app.SetGameHostOption(eGameHostOption_HostCanChangeHunger, 1); app.SetGameHostOption(eGameHostOption_HostCanChangeHunger, 1);
app.SetGameHostOption(eGameHostOption_HostCanBeInvisible, 1); app.SetGameHostOption(eGameHostOption_HostCanBeInvisible, 1);
StorageManager.SetSaveDisabled(false);
// server.properties から world 名と固定 save-id を取得し、
// WorldManager にロード/新規作成判定を委譲する
ServerPropertiesConfig serverProperties = LoadServerPropertiesConfig();
std::wstring targetWorldName = serverProperties.worldName;
if (targetWorldName.empty())
{
targetWorldName = L"world"; // デフォ名
}
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)
{
// 実際に読み込まれた save-id を設定ファイルへ戻して、
// 次回起動時の探索キーを揃える
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)
{
printf("Failed to load configured world \"%s\".\n", WideToUtf8(targetWorldName).c_str());
WinsockNetLayer::Shutdown();
g_NetworkManager.Terminate();
CleanupDevice();
return 4;
}
NetworkGameInitData *param = new NetworkGameInitData(); NetworkGameInitData *param = new NetworkGameInitData();
if (config.hasSeed) if (config.hasSeed)
{ {
param->seed = config.seed; param->seed = config.seed;
} }
param->saveData = NULL; param->saveData = worldBootstrap.saveData;
param->settings = app.GetGameHostOption(eGameHostOption_All); param->settings = app.GetGameHostOption(eGameHostOption_All);
param->dedicatedNoLocalHostPlayer = true; param->dedicatedNoLocalHostPlayer = true;
@ -356,9 +436,7 @@ int main(int argc, char **argv)
while (startThread->isRunning() && !g_shutdownRequested) while (startThread->isRunning() && !g_shutdownRequested)
{ {
g_NetworkManager.DoWork(); TickCoreSystems();
ProfileManager.Tick();
StorageManager.Tick();
Sleep(10); Sleep(10);
} }
@ -377,23 +455,61 @@ int main(int argc, char **argv)
LogStartupStep("server startup complete"); LogStartupStep("server startup complete");
printf("Dedicated server listening on %s:%d\n", g_Win64MultiplayerIP, g_Win64MultiplayerPort); printf("Dedicated server listening on %s:%d\n", g_Win64MultiplayerIP, g_Win64MultiplayerPort);
DWORD nextAutosaveTick = GetTickCount() + kAutosaveIntervalMs;
bool autosaveRequested = false;
while (!g_shutdownRequested && !app.m_bShutdown) while (!g_shutdownRequested && !app.m_bShutdown)
{ {
g_NetworkManager.DoWork(); TickCoreSystems();
ProfileManager.Tick();
StorageManager.Tick();
app.HandleXuiActions(); app.HandleXuiActions();
if (autosaveRequested && app.GetXuiServerAction(kServerActionPad) == eXuiServerAction_Idle)
{
LogWorldIO("autosave completed");
autosaveRequested = false;
}
if (MinecraftServer::serverHalted()) if (MinecraftServer::serverHalted())
{ {
break; break;
} }
DWORD now = GetTickCount();
if ((LONG)(now - nextAutosaveTick) >= 0)
{
if (app.GetXuiServerAction(kServerActionPad) == eXuiServerAction_Idle)
{
LogWorldIO("requesting autosave");
app.SetXuiServerAction(kServerActionPad, eXuiServerAction_AutoSaveGame);
autosaveRequested = true;
}
nextAutosaveTick = now + kAutosaveIntervalMs;
}
Sleep(10); Sleep(10);
} }
printf("Stopping dedicated server...\n"); printf("Stopping dedicated server...\n");
MinecraftServer *server = MinecraftServer::getInstance();
if (server != NULL)
{
server->setSaveOnExit(true);
}
LogWorldIO("requesting save before shutdown");
// 終了時保存の前に Idle へ戻して、既存の action と競合しないようにする
WaitForWorldActionIdle(kServerActionPad, 5000, &TickCoreSystems, &HandleXuiActions);
app.SetXuiServerAction(kServerActionPad, eXuiServerAction_SaveGame);
if (!WaitForWorldActionIdle(kServerActionPad, 15000, &TickCoreSystems, &HandleXuiActions))
{
LogWorldIO("shutdown save timed out");
printf("Timed out waiting for shutdown save action to finish.\n");
}
else
{
LogWorldIO("shutdown save completed");
}
MinecraftServer::HaltServer(); MinecraftServer::HaltServer();
if (g_NetworkManager.ServerStoppedValid()) if (g_NetworkManager.ServerStoppedValid())

View file

@ -0,0 +1,528 @@
#include "stdafx.h"
#include "WorldManager.h"
#include "Minecraft.h"
#include "MinecraftServer.h"
#include "ServerLogger.h"
#include <stdio.h>
#include <string.h>
namespace ServerRuntime
{
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)
{
}
};
/**
* `StorageManager` ID`level-id`
*
* - IDを設定し
* -
*
* @param saveFilename 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)
{
printf("[world-io] %s: %s\n", prefix, saveFilename.c_str());
fflush(stdout);
}
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());
LogWorldIO(logLine);
}
/**
* (callback)
*
* `SaveInfoQueryContext`
*/
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;
}
/**
* (callback)
*
* `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;
}
/**
*
*
* - callback
* - callback `ReturnSavesInfo()`
*
*
* @return `true`
*/
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)
{
// 実装/環境によっては callback より先に ReturnSavesInfo が埋まるため、
// callback 完了待ちだけに依存せずポーリングでも救済する
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;
}
/**
* callback
*
* @return callback `true` `false`
*/
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;
}
/**
* `SAVE_INFO`
*
*
*/
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;
}
/**
* ID`UTF8SaveFilename` `SAVE_INFO`
*/
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);
}
/**
* `level-name` + `level-id`
*
* - /IDだけの片設定を避け
* -
*/
static void ApplyWorldStorageTarget(const std::wstring &worldName, const std::string &saveId)
{
// タイトル(表示名)と保存先ID(実体フォルダ名)を明示的に両方設定する
// どちらか片方だけだと環境によって新規保存先が生成されることがある
StorageManager.SetSaveTitle(worldName.c_str());
SetStorageSaveUniqueFilename(saveId);
}
/**
*
*
* :
* 1. `level-id``UTF8SaveFilename`
* 2. `level-name` /
*
* @return
* - `eWorldSaveLoad_Loaded`:
* - `eWorldSaveLoad_NotFound`:
* - `eWorldSaveLoad_Failed`: API失敗//
*/
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) 保存先IDが指定されている場合は最優先で一致検索
// これが最も安定して「同じワールド」を再利用できる(勝手に上書きで新規作成されることがある)
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) 保存先IDで見つからない場合は互換フォールバックとして
// タイトル/ファイル名と worldName の一致を試す
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)
{
// 実際に見つかった保存先IDを優先採用し、今後の保存も同じ先に固定する
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)
{
// 読み込み成功扱いでも実データが0byteなら安全側で失敗扱いにする
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;
}
/**
*
*
* -
* -
* - `Failed`
*/
WorldBootstrapResult BootstrapWorldForServer(
const ServerPropertiesConfig &config,
int actionPad,
WorldManagerTickProc tickProc)
{
WorldBootstrapResult 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)
{
// 一致セーブがない場合のみ新規コンテキストを作る
// この時点で saveId を固定しておくことで、次回起動時に同じ場所へ保存される
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;
}
/**
* XUI `Idle`
*
* tick/handle
*/
bool WaitForWorldActionIdle(
int actionPad,
DWORD timeoutMs,
WorldManagerTickProc tickProc,
WorldManagerHandleActionsProc handleActionsProc)
{
DWORD start = GetTickCount();
while (app.GetXuiServerAction(actionPad) != eXuiServerAction_Idle && !MinecraftServer::serverHalted())
{
// 待機中もネットワーク/ストレージ進行を止めない
// ここを止めると save action 自体が進まずタイムアウトしやすい
if (tickProc != NULL)
{
tickProc();
}
if (handleActionsProc != NULL)
{
handleActionsProc();
}
if ((GetTickCount() - start) >= timeoutMs)
{
return false;
}
Sleep(10);
}
return (app.GetXuiServerAction(actionPad) == eXuiServerAction_Idle);
}
}

View file

@ -0,0 +1,81 @@
#pragma once
#include <string>
#include <windows.h>
#include "ServerProperties.h"
struct _LoadSaveDataThreadParam;
typedef struct _LoadSaveDataThreadParam LoadSaveDataThreadParam;
namespace ServerRuntime
{
/** 非同期ストレージ/ネットワーク待機中に回すティック関数 */
typedef void (*WorldManagerTickProc)();
/** サーバーアクション待機中に任意で回すアクション処理関数 */
typedef void (*WorldManagerHandleActionsProc)();
/**
* /
*/
enum EWorldBootstrapStatus
{
/** 既存ワールドを発見し、ロードできた */
eWorldBootstrap_Loaded,
/** 一致するセーブが無く、新規ワールド文脈を作成した */
eWorldBootstrap_CreatedNew,
/** 起動準備に失敗し、サーバー起動を中断すべき状態 */
eWorldBootstrap_Failed
};
/**
*
*/
struct WorldBootstrapResult
{
/** 起動準備ステータス */
EWorldBootstrapStatus status;
/** サーバー初期化用のセーブデータ(新規時は `NULL` */
LoadSaveDataThreadParam *saveData;
/** 実際に採用された保存先ID */
std::string resolvedSaveId;
WorldBootstrapResult()
: status(eWorldBootstrap_Failed)
, saveData(NULL)
{
}
};
/**
* /
*
* - `server.properties` `level-name` / `level-id`
* -
* -
*
* @param config `server.properties`
* @param actionPad APIで使うpadId
* @param tickProc
* @return
*/
WorldBootstrapResult BootstrapWorldForServer(
const ServerPropertiesConfig &config,
int actionPad,
WorldManagerTickProc tickProc);
/**
* `Idle`
*
* @param actionPad padId
* @param timeoutMs
* @param tickProc
* @param handleActionsProc
* @return `Idle` `true`
*/
bool WaitForWorldActionIdle(
int actionPad,
DWORD timeoutMs,
WorldManagerTickProc tickProc,
WorldManagerHandleActionsProc handleActionsProc);
}