diff --git a/WeaveLoader.Launcher/Program.cs b/WeaveLoader.Launcher/Program.cs index 77d70cf..f6445af 100644 --- a/WeaveLoader.Launcher/Program.cs +++ b/WeaveLoader.Launcher/Program.cs @@ -1,11 +1,14 @@ using System.Diagnostics; using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text.Json; namespace WeaveLoader.Launcher; class Program { private const string RuntimeDllName = "WeaveLoaderRuntime.dll"; + private const string MetadataFileName = "metadata.json"; [STAThread] static int Main(string[] args) @@ -63,8 +66,24 @@ class Program Console.WriteLine($"Saved game path to {configFile}"); } + string metadataPath = Path.Combine(metadataDir, MetadataFileName); + string offsetsPath = Path.Combine(metadataDir, "offsets.json"); + + if (File.Exists(metadataPath)) + { + if (TryReadMetadataSha(metadataPath, out string expectedSha) && + TryGetFileSha256(config.GameExePath, out string actualSha) && + !string.Equals(expectedSha, actualSha, StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine("[WARN] metadata.json does not match game executable. Removing stale metadata."); + SafeDelete(metadataPath); + SafeDelete(mappingPath); + SafeDelete(offsetsPath); + } + } + bool mappingMissing = !File.Exists(mappingPath); - bool offsetsMissing = !File.Exists(Path.Combine(metadataDir, "offsets.json")); + bool offsetsMissing = !File.Exists(offsetsPath); if (mappingMissing || offsetsMissing) { @@ -72,7 +91,6 @@ class Program Directory.CreateDirectory(metadataDir); string pdbPath = Path.ChangeExtension(config.GameExePath, ".pdb") ?? ""; - string offsetsPath = Path.Combine(metadataDir, "offsets.json"); if (!File.Exists(pdbDumpExe)) { Console.WriteLine($"[WARN] pdbdump.exe not found at {pdbDumpExe} (mapping.json will not be generated)"); @@ -152,4 +170,54 @@ class Program return 1; } } + + private static bool TryReadMetadataSha(string metadataPath, out string sha) + { + sha = ""; + try + { + using var stream = File.OpenRead(metadataPath); + using var doc = JsonDocument.Parse(stream); + if (!doc.RootElement.TryGetProperty("gameExe", out var gameExe)) + return false; + if (!gameExe.TryGetProperty("sha256", out var shaProp)) + return false; + sha = shaProp.GetString() ?? ""; + return !string.IsNullOrWhiteSpace(sha); + } + catch + { + return false; + } + } + + private static bool TryGetFileSha256(string path, out string sha) + { + sha = ""; + try + { + using var stream = File.OpenRead(path); + using var sha256 = SHA256.Create(); + byte[] hash = sha256.ComputeHash(stream); + sha = Convert.ToHexString(hash).ToLowerInvariant(); + return true; + } + catch + { + return false; + } + } + + private static void SafeDelete(string path) + { + try + { + if (File.Exists(path)) + File.Delete(path); + } + catch (Exception ex) + { + Console.WriteLine($"[WARN] Failed to delete {path}: {ex.Message}"); + } + } } diff --git a/WeaveLoaderRuntime/CMakeLists.txt b/WeaveLoaderRuntime/CMakeLists.txt index c0c8aa8..a0e1f35 100644 --- a/WeaveLoaderRuntime/CMakeLists.txt +++ b/WeaveLoaderRuntime/CMakeLists.txt @@ -113,6 +113,7 @@ target_include_directories(WeaveLoaderRuntime PRIVATE target_link_libraries(WeaveLoaderRuntime PRIVATE minhook raw_pdb + bcrypt ) if(EXISTS "${NETHOST_LIB}") @@ -154,6 +155,7 @@ target_link_libraries(pdbdump PRIVATE raw_pdb Dbghelp OleAut32 + bcrypt ) target_include_directories(pdbdump PRIVATE diff --git a/WeaveLoaderRuntime/src/GameHooks.cpp b/WeaveLoaderRuntime/src/GameHooks.cpp index e4cb096..29b5f0e 100644 --- a/WeaveLoaderRuntime/src/GameHooks.cpp +++ b/WeaveLoaderRuntime/src/GameHooks.cpp @@ -21,6 +21,8 @@ #include #include #include +#include +#include #include #include #include @@ -116,6 +118,7 @@ namespace GameHooks static std::string s_modsPath; static std::unordered_map s_modAssetRoots; static bool s_modAssetsIndexed = false; + static std::atomic s_modAssetsIndexing{false}; static std::mutex s_modAssetsMutex; // Verified from compiled Player::inventory accesses in this game build. static constexpr ptrdiff_t kPlayerInventoryOffset = 0x340; @@ -392,6 +395,11 @@ namespace GameHooks return out; } + static bool IsAsyncModAssetsEnabled() + { + return true; + } + static void BuildModAssetIndexLocked() { s_modAssetRoots.clear(); @@ -445,8 +453,31 @@ namespace GameHooks FindClose(h); } + static void StartModAssetIndexAsync() + { + if (s_modsPath.empty()) + return; + if (s_modAssetsIndexed || s_modAssetsIndexing.load()) + return; + s_modAssetsIndexing = true; + std::thread([]() + { + { + std::lock_guard guard(s_modAssetsMutex); + BuildModAssetIndexLocked(); + } + s_modAssetsIndexing = false; + LogUtil::Log("[WeaveLoader] ModAssets: async index complete (%zu namespaces)", s_modAssetRoots.size()); + }).detach(); + } + static void EnsureModAssetIndex() { + if (IsAsyncModAssetsEnabled()) + { + StartModAssetIndexAsync(); + return; + } if (s_modAssetsIndexed) return; std::lock_guard guard(s_modAssetsMutex); @@ -494,7 +525,19 @@ namespace GameHooks if (nsKey.empty()) return false; - EnsureModAssetIndex(); + if (IsAsyncModAssetsEnabled()) + { + if (!s_modAssetsIndexed) + { + StartModAssetIndexAsync(); + if (!s_modAssetsIndexed) + return false; + } + } + else + { + EnsureModAssetIndex(); + } auto it = s_modAssetRoots.find(nsKey); if (it == s_modAssetRoots.end()) return false; @@ -1294,10 +1337,14 @@ namespace GameHooks bool __fastcall Hooked_LevelSetTileAndData(void* thisPtr, int x, int y, int z, int tile, int data, int updateFlags) { + const int oldBlockId = s_levelGetTile ? s_levelGetTile(thisPtr, x, y, z) : -1; const bool result = Original_LevelSetTileAndData ? Original_LevelSetTileAndData(thisPtr, x, y, z, tile, data, updateFlags) : false; + if (result && s_levelGetTile) + WorldIdRemap::MarkChunkDirtyByBlockUpdate(x, z, oldBlockId, tile); + if (result && tile > 0) DispatchManagedBlockById(tile, thisPtr, x, y, z, 0, 0); @@ -2540,6 +2587,7 @@ namespace GameHooks void __fastcall Hooked_MinecraftTick(void* thisPtr, bool bFirst, bool bUpdateTextures) { + ModAtlas::PollAsyncBuild(); CullSpawnedEntitiesBelowWorld(); Original_MinecraftTick(thisPtr, bFirst, bUpdateTextures); CullSpawnedEntitiesBelowWorld(); @@ -2586,6 +2634,8 @@ namespace GameHooks s_modAssetsIndexed = false; s_modAssetRoots.clear(); } + if (IsAsyncModAssetsEnabled()) + StartModAssetIndexAsync(); NativeExports::SetModsPath(modsPath); ModAtlas::SetBasePaths(modsPath, gameResPath); ModAtlas::EnsureAtlasesBuilt(); diff --git a/WeaveLoaderRuntime/src/ModAtlas.cpp b/WeaveLoaderRuntime/src/ModAtlas.cpp index e7c2785..4a453e8 100644 --- a/WeaveLoaderRuntime/src/ModAtlas.cpp +++ b/WeaveLoaderRuntime/src/ModAtlas.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -11,6 +12,7 @@ #include #include #include +#include #define STB_IMAGE_IMPLEMENTATION #define STB_IMAGE_WRITE_IMPLEMENTATION @@ -59,7 +61,10 @@ namespace ModAtlas static thread_local bool s_hasPendingPage = false; static thread_local int s_pendingAtlasType = -1; static thread_local int s_pendingPage = 0; - static thread_local bool s_buildInProgress = false; + static std::atomic s_buildInProgress{false}; + static std::atomic s_asyncBuildQueued{false}; + static std::atomic s_asyncBuildComplete{false}; + static bool s_createFileHookInstalled = false; static bool s_applyMergedToBufferedImage = false; static constexpr int kTerrainGridCols = 16; @@ -85,6 +90,11 @@ namespace ModAtlas return s_maxPixels; } + static std::string BuildAtlasesInternal(const std::string& modsPath, + const std::string& terrainBasePath, + const std::string& itemsBasePath); + static void RefreshMergedPaths(); + static bool IsReservedTerrainCell(int row, int col) { if (row < 0 || col < 0 || col >= kTerrainGridCols) @@ -95,6 +105,31 @@ namespace ModAtlas return row < reserveRows; } + static bool IsAsyncAtlasEnabled() + { + return true; + } + + static void StartAsyncBuild(const std::string& terrainPath, const std::string& itemsPath) + { + if (s_asyncBuildQueued.load()) + return; + s_asyncBuildQueued = true; + s_buildInProgress = true; + LogUtil::Log("[WeaveLoader] ModAtlas: starting async build"); + + const std::string modsPath = s_modsPath; + std::thread([modsPath, terrainPath, itemsPath]() + { + BuildAtlasesInternal(modsPath, terrainPath, itemsPath); + s_lastTerrainBasePath = terrainPath; + s_lastItemsBasePath = itemsPath; + s_buildInProgress = false; + s_asyncBuildComplete = true; + LogUtil::Log("[WeaveLoader] ModAtlas: async build complete"); + }).detach(); + } + // CreateFileW hook: redirect game file opens to merged atlases static std::wstring s_mergedTerrainW; static std::wstring s_mergedItemsW; @@ -818,12 +853,17 @@ namespace ModAtlas { s_modsPath = modsPath; s_gameResPath = gameResPath; + s_asyncBuildQueued = false; + s_asyncBuildComplete = false; + s_buildInProgress = false; } void SetOverrideAtlasPath(int atlasType, const std::string& path) { if (atlasType == 0) s_overrideTerrainPath = path; else if (atlasType == 1) s_overrideItemsPath = path; + s_asyncBuildQueued = false; + s_asyncBuildComplete = false; } static void UpdateLastBaseDims(int atlasType, int w, int h) @@ -1065,11 +1105,27 @@ namespace ModAtlas bool EnsureAtlasesBuilt() { - if (s_buildInProgress) + if (s_buildInProgress.load()) return s_hasModTextures; if (s_modsPath.empty() || s_gameResPath.empty()) return false; + if (IsAsyncAtlasEnabled() && !s_applyMergedToBufferedImage) + { + if (s_asyncBuildComplete.load()) + return s_hasModTextures; + + std::string terrainPath = !s_overrideTerrainPath.empty() + ? s_overrideTerrainPath + : (s_gameResPath + "\\terrain.png"); + std::string itemsPath = !s_overrideItemsPath.empty() + ? s_overrideItemsPath + : (s_gameResPath + "\\items.png"); + + StartAsyncBuild(terrainPath, itemsPath); + return s_hasModTextures; + } + s_buildInProgress = true; // Pre-scan for mods so we can avoid nuking a valid atlas when a pack @@ -1168,19 +1224,37 @@ namespace ModAtlas CreateDirectoryA(s_virtualAtlasDir.c_str(), nullptr); } + void PollAsyncBuild() + { + if (!IsAsyncAtlasEnabled()) + return; + if (!s_asyncBuildComplete.exchange(false)) + return; + + RefreshMergedPaths(); + InstallCreateFileHook(s_gameResPath); + LogUtil::Log("[WeaveLoader] ModAtlas: async build applied"); + } + std::string GetMergedTerrainPath() { + if (s_buildInProgress.load()) + return ""; return s_hasTerrainPage0Mods ? GetMergedPagePath(0, 0) : ""; } std::string GetMergedItemsPath() { + if (s_buildInProgress.load()) + return ""; // Never replace vanilla items page 0 unless explicitly needed. return s_hasItemsPage0Mods ? GetMergedPagePath(1, 0) : ""; } std::string GetMergedPagePath(int atlasType, int page) { + if (s_buildInProgress.load()) + return ""; if (s_mergedDir.empty() || page < 0) return ""; if (atlasType == 0) { @@ -1193,6 +1267,8 @@ namespace ModAtlas std::string GetMergedMipmapPath(int atlasType, int mipLevel) { + if (s_buildInProgress.load()) + return ""; if (s_mergedDir.empty() || mipLevel <= 1) return ""; const char* stem = (atlasType == 0) ? "terrain" : "items"; @@ -1204,6 +1280,8 @@ namespace ModAtlas std::string GetVirtualPagePath(int atlasType, int page) { + if (s_buildInProgress.load()) + return ""; if (s_virtualAtlasDir.empty() || page < 0) return ""; if (atlasType == 0) return BuildVirtualPageOutputPath(s_virtualAtlasDir, "terrain", page); @@ -1222,7 +1300,7 @@ namespace ModAtlas const std::vector& GetBlockEntries() { return s_blockEntries; } const std::vector& GetItemEntries() { return s_itemEntries; } - bool HasModTextures() { return s_hasModTextures; } + bool HasModTextures() { return s_buildInProgress.load() ? false : s_hasModTextures; } void NoteIconAtlasType(void* iconPtr, int atlasType) { @@ -1320,12 +1398,26 @@ namespace ModAtlas return true; } + static void RefreshMergedPaths() + { + const std::string mergedTerrain = GetMergedTerrainPath(); + const std::string mergedItems = GetMergedItemsPath(); + if (!mergedTerrain.empty()) + s_mergedTerrainW = std::wstring(mergedTerrain.begin(), mergedTerrain.end()); + else + s_mergedTerrainW.clear(); + if (!mergedItems.empty()) + s_mergedItemsW = std::wstring(mergedItems.begin(), mergedItems.end()); + else + s_mergedItemsW.clear(); + } + static HANDLE WINAPI Hooked_CreateFileW( LPCWSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile) { - if (s_buildInProgress) + if (s_buildInProgress.load()) { return s_originalCreateFileW(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); @@ -1372,11 +1464,13 @@ namespace ModAtlas bool InstallCreateFileHook(const std::string& gameResPath) { - if (!s_hasModTextures) return false; + if (s_createFileHookInstalled) + return true; + if (!s_hasModTextures && !s_buildInProgress.load()) return false; std::string mergedTerrain = GetMergedTerrainPath(); std::string mergedItems = GetMergedItemsPath(); - if (mergedTerrain.empty() && mergedItems.empty()) + if (mergedTerrain.empty() && mergedItems.empty() && !s_buildInProgress.load()) return false; if (!mergedTerrain.empty()) @@ -1404,6 +1498,7 @@ namespace ModAtlas return false; } + s_createFileHookInstalled = true; LogUtil::Log("[WeaveLoader] ModAtlas: CreateFileW hook installed (terrain=%s, items=%s)", mergedTerrain.c_str(), mergedItems.c_str()); return true; @@ -1420,6 +1515,7 @@ namespace ModAtlas } s_mergedTerrainW.clear(); s_mergedItemsW.clear(); + s_createFileHookInstalled = false; LogUtil::Log("[WeaveLoader] ModAtlas: CreateFileW hook removed"); } @@ -1436,6 +1532,8 @@ namespace ModAtlas void CreateModIcons(void* textureMap) { + if (s_buildInProgress.load()) + return; if (!s_hasModTextures || !s_simpleIconCtor || !textureMap) return; if (!s_operatorNew) { LogUtil::Log("[WeaveLoader] ModAtlas: operator new not resolved, skipping icon creation"); return; } @@ -1487,6 +1585,8 @@ namespace ModAtlas void FixupModIcons() { + if (s_buildInProgress.load()) + return; if (s_modIcons.empty() || !s_originalRegisterIcon) return; // After Minecraft::init, vanilla icons have field_0x48 properly set. @@ -1537,6 +1637,8 @@ namespace ModAtlas void* LookupModIcon(const std::wstring& name) { + if (s_buildInProgress.load()) + return nullptr; auto it = s_modIcons.find(name); if (it != s_modIcons.end()) return it->second; @@ -1545,6 +1647,8 @@ namespace ModAtlas bool TryGetIconRoute(void* iconPtr, int& outAtlasType, int& outPage) { + if (s_buildInProgress.load()) + return false; auto it = s_iconRoutes.find(iconPtr); if (it == s_iconRoutes.end()) return false; @@ -1555,6 +1659,13 @@ namespace ModAtlas void NotifyIconSampled(void* iconPtr) { + if (s_buildInProgress.load()) + { + s_hasPendingPage = false; + s_pendingAtlasType = -1; + s_pendingPage = 0; + return; + } auto it = s_iconRoutes.find(iconPtr); if (it != s_iconRoutes.end()) { diff --git a/WeaveLoaderRuntime/src/ModAtlas.h b/WeaveLoaderRuntime/src/ModAtlas.h index 53088fc..d1472c2 100644 --- a/WeaveLoaderRuntime/src/ModAtlas.h +++ b/WeaveLoaderRuntime/src/ModAtlas.h @@ -39,6 +39,7 @@ namespace ModAtlas /// Call before textures->stitch(). Returns path to generated dir, or empty if none. std::string BuildAtlases(const std::string& modsPath, const std::string& gameResPath); void SetVirtualAtlasDirectory(const std::string& dir); + void PollAsyncBuild(); /// Get path to merged terrain.png (if built) std::string GetMergedTerrainPath(); diff --git a/WeaveLoaderRuntime/src/WorldIdRemap.cpp b/WeaveLoaderRuntime/src/WorldIdRemap.cpp index 61aa2b5..713bee6 100644 --- a/WeaveLoaderRuntime/src/WorldIdRemap.cpp +++ b/WeaveLoaderRuntime/src/WorldIdRemap.cpp @@ -7,6 +7,7 @@ #include "PdbParser.h" #include +#include #include #include #include @@ -15,6 +16,7 @@ #include #include #include +#include #include namespace @@ -32,6 +34,7 @@ namespace constexpr unsigned char kTagCompoundId = 10; constexpr int kChunkWidth = 16; constexpr int kChunkMaxY = 256; + constexpr int kBlockModStart = 174; using TagNewTag_fn = void* (__fastcall *)(unsigned char type, const std::wstring& name); using LevelChunkGetTile_fn = int (__fastcall *)(void* thisPtr, int x, int y, int z); @@ -70,6 +73,8 @@ namespace int s_chunkSaveLogCount = 0; int s_chunkIoLogCount = 0; std::mutex s_chunkMetaMutex; + std::mutex s_dirtyChunksMutex; + std::unordered_set s_dirtyChunks; struct ChunkMeta { @@ -78,6 +83,33 @@ namespace int z = 0; }; std::unordered_map s_chunkMetaByPtr; + + uint64_t MakeChunkKey(int x, int z) + { + return (static_cast(static_cast(x)) << 32) | + static_cast(z); + } + + void MarkChunkDirtyInternal(int x, int z) + { + const uint64_t key = MakeChunkKey(x, z); + std::lock_guard lock(s_dirtyChunksMutex); + s_dirtyChunks.insert(key); + } + + bool IsChunkDirtyInternal(int x, int z) + { + const uint64_t key = MakeChunkKey(x, z); + std::lock_guard lock(s_dirtyChunksMutex); + return s_dirtyChunks.find(key) != s_dirtyChunks.end(); + } + + void ClearChunkDirtyInternal(int x, int z) + { + const uint64_t key = MakeChunkKey(x, z); + std::lock_guard lock(s_dirtyChunksMutex); + s_dirtyChunks.erase(key); + } std::unordered_map> s_loadedNamespaceByChunk; std::unordered_map> s_chunkNamespaceCache; @@ -821,6 +853,19 @@ namespace WorldIdRemap #endif } + void MarkChunkDirtyByBlockUpdate(int x, int z, int oldBlockId, int newBlockId) + { + if (oldBlockId == newBlockId) + return; + if (oldBlockId < 0 && newBlockId < 0) + return; + const bool oldIsMod = (oldBlockId >= kBlockModStart && oldBlockId <= 255); + const bool newIsMod = (newBlockId >= kBlockModStart && newBlockId <= 255); + if (!oldIsMod && !newIsMod) + return; + MarkChunkDirtyInternal(x >> 4, z >> 4); + } + void SaveChunkBlockNamespaces(void* chunkStoragePtr, void* levelChunkPtr) { if (!chunkStoragePtr || !levelChunkPtr || !s_levelChunkGetTile) @@ -845,6 +890,9 @@ namespace WorldIdRemap const std::string cacheKey = MakeChunkCacheKey(storage, meta.x, meta.z); std::unordered_map loadedEntries; const bool hadLoadedMap = TryGetLoadedChunkNamespaces(levelChunkPtr, &loadedEntries) && !loadedEntries.empty(); + const bool isDirty = IsChunkDirtyInternal(meta.x, meta.z); + if (!isDirty && !hadLoadedMap) + return; std::unordered_map nextEntries; const int missingBlockFallback = IdRegistry::Instance().GetMissingFallback(IdRegistry::Type::Block); @@ -900,13 +948,18 @@ namespace WorldIdRemap } if (nextEntries.empty() && !hadLoadedMap) + { + if (isDirty) + ClearChunkDirtyInternal(meta.x, meta.z); return; + } if (!WriteChunkNamespaceMap(storage->saveFile, path, nextEntries)) return; { std::lock_guard lock(s_chunkMetaMutex); s_chunkNamespaceCache[cacheKey] = nextEntries; } + ClearChunkDirtyInternal(meta.x, meta.z); if (s_chunkSaveLogCount < 64) { diff --git a/WeaveLoaderRuntime/src/WorldIdRemap.h b/WeaveLoaderRuntime/src/WorldIdRemap.h index c87732a..e30398d 100644 --- a/WeaveLoaderRuntime/src/WorldIdRemap.h +++ b/WeaveLoaderRuntime/src/WorldIdRemap.h @@ -11,6 +11,7 @@ namespace WorldIdRemap void EnsureMissingPlaceholders(); int RemapChunkBlockIds(void* chunkStoragePtr, void* levelChunkPtr, int chunkX, int chunkZ); void SaveChunkBlockNamespaces(void* chunkStoragePtr, void* levelChunkPtr); + void MarkChunkDirtyByBlockUpdate(int x, int z, int oldBlockId, int newBlockId); void TagModdedItemInstance(void* itemInstancePtr, void* compoundTagPtr); void RemapItemInstanceFromTag(void* itemInstancePtr, void* compoundTagPtr); } diff --git a/WeaveLoaderRuntime/src/dllmain.cpp b/WeaveLoaderRuntime/src/dllmain.cpp index ab05783..9f076c4 100644 --- a/WeaveLoaderRuntime/src/dllmain.cpp +++ b/WeaveLoaderRuntime/src/dllmain.cpp @@ -1,6 +1,11 @@ #include +#include #include +#include +#include +#include #include +#include #include "LogUtil.h" #include "CrashHandler.h" #include "PdbParser.h" @@ -23,6 +28,185 @@ static std::string GetDllDirectory(HMODULE hModule) return ".\\"; } +static bool ReadFileToString(const std::string& path, std::string& out) +{ + std::ifstream in(path, std::ios::in | std::ios::binary); + if (!in.is_open()) + return false; + std::ostringstream ss; + ss << in.rdbuf(); + out = ss.str(); + return true; +} + +static bool ComputeSha256File(const char* path, std::string& outHex) +{ + if (!path || !*path) + return false; + HANDLE hFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (hFile == INVALID_HANDLE_VALUE) + return false; + + BCRYPT_ALG_HANDLE hAlg = nullptr; + BCRYPT_HASH_HANDLE hHash = nullptr; + DWORD hashObjectSize = 0; + DWORD hashSize = 0; + DWORD cbData = 0; + std::vector hashObject; + std::vector hashBytes; + + if (BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_SHA256_ALGORITHM, nullptr, 0) != 0) + { + CloseHandle(hFile); + return false; + } + + if (BCryptGetProperty(hAlg, BCRYPT_OBJECT_LENGTH, reinterpret_cast(&hashObjectSize), + sizeof(DWORD), &cbData, 0) != 0 || + BCryptGetProperty(hAlg, BCRYPT_HASH_LENGTH, reinterpret_cast(&hashSize), + sizeof(DWORD), &cbData, 0) != 0) + { + BCryptCloseAlgorithmProvider(hAlg, 0); + CloseHandle(hFile); + return false; + } + + hashObject.resize(hashObjectSize); + hashBytes.resize(hashSize); + if (BCryptCreateHash(hAlg, &hHash, hashObject.data(), hashObjectSize, nullptr, 0, 0) != 0) + { + BCryptCloseAlgorithmProvider(hAlg, 0); + CloseHandle(hFile); + return false; + } + + unsigned char buffer[1 << 16]; + DWORD bytesRead = 0; + while (ReadFile(hFile, buffer, sizeof(buffer), &bytesRead, nullptr) && bytesRead > 0) + { + if (BCryptHashData(hHash, buffer, bytesRead, 0) != 0) + { + BCryptDestroyHash(hHash); + BCryptCloseAlgorithmProvider(hAlg, 0); + CloseHandle(hFile); + return false; + } + } + + if (BCryptFinishHash(hHash, hashBytes.data(), hashSize, 0) != 0) + { + BCryptDestroyHash(hHash); + BCryptCloseAlgorithmProvider(hAlg, 0); + CloseHandle(hFile); + return false; + } + + BCryptDestroyHash(hHash); + BCryptCloseAlgorithmProvider(hAlg, 0); + CloseHandle(hFile); + + std::ostringstream oss; + oss.setf(std::ios::hex, std::ios::basefield); + oss.fill('0'); + for (unsigned char b : hashBytes) + oss << std::setw(2) << static_cast(b); + outHex = oss.str(); + return true; +} + +static bool ExtractGameExeSha256(const std::string& json, std::string& outSha) +{ + const std::string gameExeKey = "\"gameExe\""; + const std::string shaKey = "\"sha256\""; + size_t gamePos = json.find(gameExeKey); + if (gamePos == std::string::npos) + return false; + size_t shaPos = json.find(shaKey, gamePos); + if (shaPos == std::string::npos) + return false; + size_t colon = json.find(':', shaPos); + if (colon == std::string::npos) + return false; + size_t quote1 = json.find('"', colon + 1); + if (quote1 == std::string::npos) + return false; + size_t quote2 = json.find('"', quote1 + 1); + if (quote2 == std::string::npos || quote2 <= quote1 + 1) + return false; + outSha = json.substr(quote1 + 1, quote2 - quote1 - 1); + return !outSha.empty(); +} + +static std::string ToLowerAscii(std::string value) +{ + for (char& c : value) + c = static_cast(tolower(static_cast(c))); + return value; +} + +static void RemoveStaleMetadata(const std::string& baseDir) +{ + const std::string metaDir = baseDir + "metadata\\"; + const char* files[] = { + "mapping.json", + "offsets.json", + "metadata.json" + }; + for (const char* name : files) + { + std::string path = metaDir + name; + if (GetFileAttributesA(path.c_str()) != INVALID_FILE_ATTRIBUTES) + { + if (DeleteFileA(path.c_str()) == 0) + LogUtil::Log("[WeaveLoader] Warning: failed to delete stale %s", path.c_str()); + else + LogUtil::Log("[WeaveLoader] Deleted stale %s", path.c_str()); + } + } + + const char* rootFiles[] = { + "mapping.json", + "offsets.json" + }; + for (const char* name : rootFiles) + { + std::string path = baseDir + name; + if (GetFileAttributesA(path.c_str()) != INVALID_FILE_ATTRIBUTES) + { + if (DeleteFileA(path.c_str()) == 0) + LogUtil::Log("[WeaveLoader] Warning: failed to delete stale %s", path.c_str()); + else + LogUtil::Log("[WeaveLoader] Deleted stale %s", path.c_str()); + } + } +} + +static void ValidateMetadataForExecutable(const std::string& baseDir, const char* exePath) +{ + const std::string metadataPath = baseDir + "metadata\\metadata.json"; + if (GetFileAttributesA(metadataPath.c_str()) == INVALID_FILE_ATTRIBUTES) + return; + + std::string json; + if (!ReadFileToString(metadataPath, json)) + return; + + std::string expectedSha; + if (!ExtractGameExeSha256(json, expectedSha)) + return; + + std::string actualSha; + if (!ComputeSha256File(exePath, actualSha)) + return; + + if (ToLowerAscii(expectedSha) != ToLowerAscii(actualSha)) + { + LogUtil::Log("[WeaveLoader] Metadata mismatch: game executable hash does not match metadata.json. Removing stale mappings/offsets."); + RemoveStaleMetadata(baseDir); + } +} + DWORD WINAPI InitThread(LPVOID lpParam) { LogUtil::Log("[WeaveLoader] InitThread started (module=%p)", g_hModule); @@ -33,13 +217,6 @@ DWORD WINAPI InitThread(LPVOID lpParam) std::string baseDir = GetDllDirectory(g_hModule); LogUtil::Log("[WeaveLoader] Runtime DLL directory: %s", baseDir.c_str()); - std::string mappingPath = baseDir + "metadata\\mapping.json"; - if (!SymbolRegistry::Instance().LoadFromFile(mappingPath.c_str())) - { - std::string fallbackPath = baseDir + "mapping.json"; - SymbolRegistry::Instance().LoadFromFile(fallbackPath.c_str()); - } - char cwd[MAX_PATH] = {0}; GetCurrentDirectoryA(MAX_PATH, cwd); LogUtil::Log("[WeaveLoader] Game working directory: %s", cwd); @@ -48,6 +225,15 @@ DWORD WINAPI InitThread(LPVOID lpParam) GetModuleFileNameA(nullptr, exePath, MAX_PATH); LogUtil::Log("[WeaveLoader] Host executable: %s", exePath); + ValidateMetadataForExecutable(baseDir, exePath); + + std::string mappingPath = baseDir + "metadata\\mapping.json"; + if (!SymbolRegistry::Instance().LoadFromFile(mappingPath.c_str())) + { + std::string fallbackPath = baseDir + "mapping.json"; + SymbolRegistry::Instance().LoadFromFile(fallbackPath.c_str()); + } + SymbolResolver symbols; if (!symbols.Initialize()) { diff --git a/WeaveLoaderRuntime/tools/pdbdump.cpp b/WeaveLoaderRuntime/tools/pdbdump.cpp index ae9a1ae..116d0c7 100644 --- a/WeaveLoaderRuntime/tools/pdbdump.cpp +++ b/WeaveLoaderRuntime/tools/pdbdump.cpp @@ -1,10 +1,13 @@ #include "PdbParser.h" #include +#include #include #include #include #include +#include #include +#include #include #include #include @@ -16,6 +19,19 @@ namespace #ifndef SymTagData constexpr DWORD SymTagData = 7; #endif + struct FileFingerprint + { + std::string path; + uint64_t size = 0; + uint64_t mtime = 0; + std::string sha256; + bool ok = false; + }; + + uint64_t FileTimeToUint64(const FILETIME& ft) + { + return (static_cast(ft.dwHighDateTime) << 32) | ft.dwLowDateTime; + } std::string Trim(const std::string& s) { size_t start = 0; @@ -57,6 +73,114 @@ namespace return std::string(buffer); } + bool ComputeSha256(HANDLE file, std::string& outHex) + { + if (file == INVALID_HANDLE_VALUE) + return false; + BCRYPT_ALG_HANDLE hAlg = nullptr; + BCRYPT_HASH_HANDLE hHash = nullptr; + DWORD hashObjectSize = 0; + DWORD hashSize = 0; + DWORD cbData = 0; + std::vector hashObject; + std::vector hashBytes; + + if (BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_SHA256_ALGORITHM, nullptr, 0) != 0) + return false; + + if (BCryptGetProperty(hAlg, BCRYPT_OBJECT_LENGTH, reinterpret_cast(&hashObjectSize), + sizeof(DWORD), &cbData, 0) != 0) + { + BCryptCloseAlgorithmProvider(hAlg, 0); + return false; + } + if (BCryptGetProperty(hAlg, BCRYPT_HASH_LENGTH, reinterpret_cast(&hashSize), + sizeof(DWORD), &cbData, 0) != 0) + { + BCryptCloseAlgorithmProvider(hAlg, 0); + return false; + } + + hashObject.resize(hashObjectSize); + hashBytes.resize(hashSize); + + if (BCryptCreateHash(hAlg, &hHash, hashObject.data(), hashObjectSize, nullptr, 0, 0) != 0) + { + BCryptCloseAlgorithmProvider(hAlg, 0); + return false; + } + + if (SetFilePointer(file, 0, nullptr, FILE_BEGIN) == INVALID_SET_FILE_POINTER) + { + BCryptDestroyHash(hHash); + BCryptCloseAlgorithmProvider(hAlg, 0); + return false; + } + + unsigned char buffer[1 << 16]; + DWORD bytesRead = 0; + while (ReadFile(file, buffer, sizeof(buffer), &bytesRead, nullptr) && bytesRead > 0) + { + if (BCryptHashData(hHash, buffer, bytesRead, 0) != 0) + { + BCryptDestroyHash(hHash); + BCryptCloseAlgorithmProvider(hAlg, 0); + return false; + } + } + + if (BCryptFinishHash(hHash, hashBytes.data(), hashSize, 0) != 0) + { + BCryptDestroyHash(hHash); + BCryptCloseAlgorithmProvider(hAlg, 0); + return false; + } + + BCryptDestroyHash(hHash); + BCryptCloseAlgorithmProvider(hAlg, 0); + + std::ostringstream oss; + oss.setf(std::ios::hex, std::ios::basefield); + oss.fill('0'); + for (unsigned char b : hashBytes) + oss << std::setw(2) << static_cast(b); + outHex = oss.str(); + return true; + } + + bool GetFileFingerprint(const char* path, FileFingerprint& out) + { + if (!path || !*path) + return false; + out.path = path; + HANDLE hFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (hFile == INVALID_HANDLE_VALUE) + return false; + + LARGE_INTEGER size{}; + FILETIME ft{}; + if (!GetFileSizeEx(hFile, &size) || !GetFileTime(hFile, nullptr, nullptr, &ft)) + { + CloseHandle(hFile); + return false; + } + + out.size = static_cast(size.QuadPart); + out.mtime = FileTimeToUint64(ft); + out.ok = ComputeSha256(hFile, out.sha256); + CloseHandle(hFile); + return out.ok; + } + + std::string GetDirName(const std::string& path) + { + const size_t pos = path.find_last_of("\\/"); + if (pos == std::string::npos) + return std::string(); + return path.substr(0, pos); + } + struct TypeInfo { std::string raw; @@ -320,6 +444,56 @@ namespace } out << '"'; } + + void WriteFingerprint(std::ostream& out, const char* label, const char* path, const FileFingerprint& fp) + { + out << " \"" << label << "\": {\n"; + out << " \"path\": "; + WriteJsonString(out, path ? path : ""); + out << ",\n"; + out << " \"sha256\": "; + WriteJsonString(out, fp.ok ? fp.sha256 : ""); + out << ",\n"; + out << " \"size\": " << (fp.ok ? fp.size : 0) << ",\n"; + out << " \"mtime\": " << (fp.ok ? fp.mtime : 0) << ",\n"; + out << " \"ok\": " << (fp.ok ? "true" : "false") << "\n"; + out << " }"; + } + + bool WriteMetadataJson(const std::string& outPath, + const char* pdbPath, + const char* exePath, + const char* mappingPath, + const char* offsetsPath) + { + std::ofstream out(outPath, std::ios::out | std::ios::trunc); + if (!out.is_open()) + return false; + + FileFingerprint pdbFp{}; + FileFingerprint exeFp{}; + if (pdbPath) + GetFileFingerprint(pdbPath, pdbFp); + if (exePath) + GetFileFingerprint(exePath, exeFp); + + out << "{\n"; + out << " \"format\": 1,\n"; + out << " \"mappingJson\": "; + WriteJsonString(out, mappingPath ? mappingPath : ""); + out << ",\n"; + out << " \"offsetsJson\": "; + WriteJsonString(out, offsetsPath ? offsetsPath : ""); + out << ",\n"; + WriteFingerprint(out, "pdb", pdbPath, pdbFp); + if (exePath && *exePath) + { + out << ",\n"; + WriteFingerprint(out, "gameExe", exePath, exeFp); + } + out << "\n}\n"; + return true; + } } struct EnumTypesContext @@ -614,5 +788,14 @@ int main(int argc, char** argv) else std::cout << "Failed to write " << offsetsOut << "\n"; } + + const std::string outDir = GetDirName(outPath); + const std::string metadataPath = outDir.empty() + ? std::string("metadata.json") + : (outDir + "\\metadata.json"); + if (WriteMetadataJson(metadataPath, pdbPath, offsetsExe, outPath, offsetsOut)) + std::cout << "Wrote " << metadataPath << "\n"; + else + std::cout << "Failed to write " << metadataPath << "\n"; return 0; }