feat(runtime): async metadata + chunk save guard

This commit is contained in:
Jacobwasbeast
2026-03-10 21:01:47 -05:00
parent 86cbf225dc
commit 4a48472b99
9 changed files with 671 additions and 16 deletions
+70 -2
View File
@@ -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}");
}
}
}
+2
View File
@@ -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
+51 -1
View File
@@ -21,6 +21,8 @@
#include <cctype>
#include <memory>
#include <mutex>
#include <atomic>
#include <thread>
#include <unordered_map>
#include <cstddef>
#include <vector>
@@ -116,6 +118,7 @@ namespace GameHooks
static std::string s_modsPath;
static std::unordered_map<std::string, std::string> s_modAssetRoots;
static bool s_modAssetsIndexed = false;
static std::atomic<bool> 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<std::mutex> 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<std::mutex> 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();
+117 -6
View File
@@ -3,6 +3,7 @@
#include <Windows.h>
#include <MinHook.h>
#include <algorithm>
#include <atomic>
#include <cwctype>
#include <cstring>
#include <cstdlib>
@@ -11,6 +12,7 @@
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <thread>
#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<bool> s_buildInProgress{false};
static std::atomic<bool> s_asyncBuildQueued{false};
static std::atomic<bool> 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<ModTextureEntry>& GetBlockEntries() { return s_blockEntries; }
const std::vector<ModTextureEntry>& 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())
{
+1
View File
@@ -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();
+53
View File
@@ -7,6 +7,7 @@
#include "PdbParser.h"
#include <Windows.h>
#include <cstdint>
#include <cstddef>
#include <fstream>
#include <iomanip>
@@ -15,6 +16,7 @@
#include <sstream>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
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<uint64_t> s_dirtyChunks;
struct ChunkMeta
{
@@ -78,6 +83,33 @@ namespace
int z = 0;
};
std::unordered_map<void*, ChunkMeta> s_chunkMetaByPtr;
uint64_t MakeChunkKey(int x, int z)
{
return (static_cast<uint64_t>(static_cast<uint32_t>(x)) << 32) |
static_cast<uint32_t>(z);
}
void MarkChunkDirtyInternal(int x, int z)
{
const uint64_t key = MakeChunkKey(x, z);
std::lock_guard<std::mutex> lock(s_dirtyChunksMutex);
s_dirtyChunks.insert(key);
}
bool IsChunkDirtyInternal(int x, int z)
{
const uint64_t key = MakeChunkKey(x, z);
std::lock_guard<std::mutex> 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<std::mutex> lock(s_dirtyChunksMutex);
s_dirtyChunks.erase(key);
}
std::unordered_map<void*, std::unordered_map<int, std::string>> s_loadedNamespaceByChunk;
std::unordered_map<std::string, std::unordered_map<int, std::string>> 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<int, std::string> loadedEntries;
const bool hadLoadedMap = TryGetLoadedChunkNamespaces(levelChunkPtr, &loadedEntries) && !loadedEntries.empty();
const bool isDirty = IsChunkDirtyInternal(meta.x, meta.z);
if (!isDirty && !hadLoadedMap)
return;
std::unordered_map<int, std::string> 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<std::mutex> lock(s_chunkMetaMutex);
s_chunkNamespaceCache[cacheKey] = nextEntries;
}
ClearChunkDirtyInternal(meta.x, meta.z);
if (s_chunkSaveLogCount < 64)
{
+1
View File
@@ -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);
}
+193 -7
View File
@@ -1,6 +1,11 @@
#include <Windows.h>
#include <bcrypt.h>
#include <cstdio>
#include <fstream>
#include <iomanip>
#include <sstream>
#include <string>
#include <vector>
#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<unsigned char> hashObject;
std::vector<unsigned char> hashBytes;
if (BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_SHA256_ALGORITHM, nullptr, 0) != 0)
{
CloseHandle(hFile);
return false;
}
if (BCryptGetProperty(hAlg, BCRYPT_OBJECT_LENGTH, reinterpret_cast<PUCHAR>(&hashObjectSize),
sizeof(DWORD), &cbData, 0) != 0 ||
BCryptGetProperty(hAlg, BCRYPT_HASH_LENGTH, reinterpret_cast<PUCHAR>(&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<int>(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<char>(tolower(static_cast<unsigned char>(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())
{
+183
View File
@@ -1,10 +1,13 @@
#include "PdbParser.h"
#include <Windows.h>
#include <bcrypt.h>
#include <DbgHelp.h>
#include <OleAuto.h>
#include <algorithm>
#include <cctype>
#include <cstdint>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <string>
@@ -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<uint64_t>(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<unsigned char> hashObject;
std::vector<unsigned char> hashBytes;
if (BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_SHA256_ALGORITHM, nullptr, 0) != 0)
return false;
if (BCryptGetProperty(hAlg, BCRYPT_OBJECT_LENGTH, reinterpret_cast<PUCHAR>(&hashObjectSize),
sizeof(DWORD), &cbData, 0) != 0)
{
BCryptCloseAlgorithmProvider(hAlg, 0);
return false;
}
if (BCryptGetProperty(hAlg, BCRYPT_HASH_LENGTH, reinterpret_cast<PUCHAR>(&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<int>(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<uint64_t>(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;
}