aboutsummaryrefslogtreecommitdiff
path: root/Minecraft.Server/WorldManager.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'Minecraft.Server/WorldManager.cpp')
-rw-r--r--Minecraft.Server/WorldManager.cpp641
1 files changed, 641 insertions, 0 deletions
diff --git a/Minecraft.Server/WorldManager.cpp b/Minecraft.Server/WorldManager.cpp
new file mode 100644
index 00000000..b9ec3dd9
--- /dev/null
+++ b/Minecraft.Server/WorldManager.cpp
@@ -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);
+}
+}
+