diff --git a/Minecraft.Server/Minecraft.Server.vcxproj b/Minecraft.Server/Minecraft.Server.vcxproj index a32361380..c2f0c96dd 100644 --- a/Minecraft.Server/Minecraft.Server.vcxproj +++ b/Minecraft.Server/Minecraft.Server.vcxproj @@ -646,6 +646,9 @@ NotUsing + + + Create Create @@ -653,6 +656,11 @@ + + + + + diff --git a/Minecraft.Server/Minecraft.Server.vcxproj.filters b/Minecraft.Server/Minecraft.Server.vcxproj.filters index 544f1c502..930213ad9 100644 --- a/Minecraft.Server/Minecraft.Server.vcxproj.filters +++ b/Minecraft.Server/Minecraft.Server.vcxproj.filters @@ -6,8 +6,28 @@ + + Server + + + Server + + + Server + Server + + + Server + + + Server + + + Server + + diff --git a/Minecraft.Server/ServerLogger.cpp b/Minecraft.Server/ServerLogger.cpp new file mode 100644 index 000000000..2d2af3f38 --- /dev/null +++ b/Minecraft.Server/ServerLogger.cpp @@ -0,0 +1,74 @@ +#include "stdafx.h" + +#include "ServerLogger.h" + +#include + +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); +} +} diff --git a/Minecraft.Server/ServerLogger.h b/Minecraft.Server/ServerLogger.h new file mode 100644 index 000000000..90dad6e11 --- /dev/null +++ b/Minecraft.Server/ServerLogger.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +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); +} diff --git a/Minecraft.Server/ServerProperties.cpp b/Minecraft.Server/ServerProperties.cpp new file mode 100644 index 000000000..bd0d44cf6 --- /dev/null +++ b/Minecraft.Server/ServerProperties.cpp @@ -0,0 +1,376 @@ +#include "stdafx.h" + +#include "ServerProperties.h" + +#include "ServerLogger.h" + +#include +#include +#include +#include +#include + +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 *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 *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 &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 sortedProperties(properties.begin(), properties.end()); + for (std::map::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 defaults; + std::unordered_map loaded; + ApplyDefaultServerProperties(&defaults); + + int parsedCount = 0; + bool readSuccess = ReadServerPropertiesFile(kServerPropertiesPath, &loaded, &parsedCount); + std::unordered_map 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::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 merged; + ApplyDefaultServerProperties(&merged); + + std::unordered_map loaded; + int parsedCount = 0; + if (ReadServerPropertiesFile(kServerPropertiesPath, &loaded, &parsedCount)) + { + for (std::unordered_map::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); +} +} diff --git a/Minecraft.Server/ServerProperties.h b/Minecraft.Server/ServerProperties.h new file mode 100644 index 000000000..9c009f1d3 --- /dev/null +++ b/Minecraft.Server/ServerProperties.h @@ -0,0 +1,39 @@ +#pragma once + +#include + +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); +} diff --git a/Minecraft.Server/Windows64/ServerMain.cpp b/Minecraft.Server/Windows64/ServerMain.cpp index 11d9fa671..7e029c038 100644 --- a/Minecraft.Server/Windows64/ServerMain.cpp +++ b/Minecraft.Server/Windows64/ServerMain.cpp @@ -6,6 +6,9 @@ #include "Minecraft.h" #include "MinecraftServer.h" #include "Options.h" +#include "..\ServerLogger.h" +#include "..\ServerProperties.h" +#include "..\WorldManager.h" #include "Tesselator.h" #include "Windows64/4JLibs/inc/4J_Render.h" #include "Windows64/GameConfig/Minecraft.spa.h" @@ -58,6 +61,8 @@ struct DedicatedServerConfig }; static volatile bool g_shutdownRequested = false; +static const DWORD kAutosaveIntervalMs = 60 * 1000; +static const int kServerActionPad = 0; static BOOL WINAPI ConsoleCtrlHandlerProc(DWORD ctrlType) { @@ -75,6 +80,11 @@ static BOOL WINAPI ConsoleCtrlHandlerProc(DWORD ctrlType) } } +/** + * サーバー停止通知が到達するまで待機する終了用スレッド関数 + * + * シャットダウン時にネットワーク層の停止完了を同期するために使う + */ static int WaitForServerStoppedThreadProc(void *) { if (g_NetworkManager.ServerStoppedValid()) @@ -96,11 +106,17 @@ static void PrintUsage() printf(" -help Show this help\n"); } -static void LogStartupStep(const char *message) -{ - printf("[startup] %s\n", message); - fflush(stdout); -} +using ServerRuntime::LoadServerPropertiesConfig; +using ServerRuntime::LogStartupStep; +using ServerRuntime::LogWorldIO; +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) { @@ -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) { DedicatedServerConfig config; @@ -338,12 +384,46 @@ int main(int argc, char **argv) app.SetGameHostOption(eGameHostOption_HostCanChangeHunger, 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(); if (config.hasSeed) { param->seed = config.seed; } - param->saveData = NULL; + param->saveData = worldBootstrap.saveData; param->settings = app.GetGameHostOption(eGameHostOption_All); param->dedicatedNoLocalHostPlayer = true; @@ -356,9 +436,7 @@ int main(int argc, char **argv) while (startThread->isRunning() && !g_shutdownRequested) { - g_NetworkManager.DoWork(); - ProfileManager.Tick(); - StorageManager.Tick(); + TickCoreSystems(); Sleep(10); } @@ -377,23 +455,61 @@ int main(int argc, char **argv) LogStartupStep("server startup complete"); 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) { - g_NetworkManager.DoWork(); - ProfileManager.Tick(); - StorageManager.Tick(); + TickCoreSystems(); app.HandleXuiActions(); + if (autosaveRequested && app.GetXuiServerAction(kServerActionPad) == eXuiServerAction_Idle) + { + LogWorldIO("autosave completed"); + autosaveRequested = false; + } + if (MinecraftServer::serverHalted()) { 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); } 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(); if (g_NetworkManager.ServerStoppedValid()) diff --git a/Minecraft.Server/WorldManager.cpp b/Minecraft.Server/WorldManager.cpp new file mode 100644 index 000000000..73909a360 --- /dev/null +++ b/Minecraft.Server/WorldManager.cpp @@ -0,0 +1,528 @@ +#include "stdafx.h" + +#include "WorldManager.h" + +#include "Minecraft.h" +#include "MinecraftServer.h" +#include "ServerLogger.h" + +#include +#include + +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); +} +} diff --git a/Minecraft.Server/WorldManager.h b/Minecraft.Server/WorldManager.h new file mode 100644 index 000000000..88a482518 --- /dev/null +++ b/Minecraft.Server/WorldManager.h @@ -0,0 +1,81 @@ +#pragma once + +#include +#include + +#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); +}