mirror of
https://github.com/Jacobwasbeast/LegacyWeaveLoader.git
synced 2026-09-23 09:22:34 +00:00
feat(runtime): async metadata + chunk save guard
This commit is contained in:
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user