feat(api/runtime): java-style assets and localization sync

This commit is contained in:
Jacobwasbeast
2026-03-10 14:36:23 -05:00
parent 36094e0ea9
commit 70dbff3fac
38 changed files with 794 additions and 110 deletions

View File

@@ -4,6 +4,7 @@
#include "MainMenuOverlay.h"
#include "ModStrings.h"
#include "ModAtlas.h"
#include "NativeExports.h"
#include "CustomPickaxeRegistry.h"
#include "CustomToolMaterialRegistry.h"
#include "CustomBlockRegistry.h"
@@ -17,7 +18,9 @@
#include <cstring>
#include <cwchar>
#include <cwctype>
#include <cctype>
#include <memory>
#include <mutex>
#include <unordered_map>
#include <cstddef>
#include <vector>
@@ -110,6 +113,10 @@ namespace GameHooks
static InventoryRemoveResource_fn s_inventoryRemoveResource = nullptr;
static void* s_inventoryVtable = nullptr;
static ItemInstanceHurtAndBreak_fn s_itemInstanceHurtAndBreak = nullptr;
static std::string s_modsPath;
static std::unordered_map<std::string, std::string> s_modAssetRoots;
static bool s_modAssetsIndexed = false;
static std::mutex s_modAssetsMutex;
// Verified from compiled Player::inventory accesses in this game build.
static constexpr ptrdiff_t kPlayerInventoryOffset = 0x340;
static constexpr ptrdiff_t kLevelIsClientSideOffset = 0x268;
@@ -334,6 +341,150 @@ namespace GameHooks
return path.compare(pathLen - suffLen, suffLen, suffix) == 0;
}
static std::string ToLowerAscii(const std::string& value)
{
std::string out;
out.reserve(value.size());
for (char ch : value)
out.push_back((char)tolower((unsigned char)ch));
return out;
}
static std::string WStringToLowerAscii(const std::wstring& value)
{
std::string out;
out.reserve(value.size());
for (wchar_t ch : value)
{
if (ch > 0x7F)
return std::string();
out.push_back((char)tolower((unsigned char)ch));
}
return out;
}
static void BuildModAssetIndexLocked()
{
s_modAssetRoots.clear();
s_modAssetsIndexed = true;
if (s_modsPath.empty())
return;
WIN32_FIND_DATAA fd;
std::string search = s_modsPath + "\\*";
HANDLE h = FindFirstFileA(search.c_str(), &fd);
if (h == INVALID_HANDLE_VALUE)
return;
do
{
if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) continue;
if (fd.cFileName[0] == '.') continue;
std::string modFolder = fd.cFileName;
std::string assetsPath = s_modsPath + "\\" + modFolder + "\\assets";
DWORD attr = GetFileAttributesA(assetsPath.c_str());
if (attr == INVALID_FILE_ATTRIBUTES || !(attr & FILE_ATTRIBUTE_DIRECTORY))
continue;
WIN32_FIND_DATAA nsfd;
std::string nsSearch = assetsPath + "\\*";
HANDLE hNs = FindFirstFileA(nsSearch.c_str(), &nsfd);
if (hNs == INVALID_HANDLE_VALUE)
continue;
do
{
if (!(nsfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) continue;
if (nsfd.cFileName[0] == '.') continue;
std::string nsName = ToLowerAscii(nsfd.cFileName);
if (nsName.empty())
continue;
if (s_modAssetRoots.find(nsName) == s_modAssetRoots.end())
{
s_modAssetRoots.emplace(nsName, assetsPath);
}
else
{
LogUtil::Log("[WeaveLoader] ModAssets: duplicate namespace '%s' (folder=%s) ignored",
nsName.c_str(), modFolder.c_str());
}
} while (FindNextFileA(hNs, &nsfd));
FindClose(hNs);
} while (FindNextFileA(h, &fd));
FindClose(h);
}
static void EnsureModAssetIndex()
{
if (s_modAssetsIndexed)
return;
std::lock_guard<std::mutex> guard(s_modAssetsMutex);
if (!s_modAssetsIndexed)
BuildModAssetIndexLocked();
}
static bool FileExistsW(const std::wstring& path)
{
DWORD attr = GetFileAttributesW(path.c_str());
return (attr != INVALID_FILE_ATTRIBUTES) && !(attr & FILE_ATTRIBUTE_DIRECTORY);
}
static bool TryResolveModAssetPath(const std::wstring& requestPath, std::wstring& outPath)
{
if (s_modsPath.empty())
return false;
std::wstring lower = NormalizeLowerPath(requestPath);
if (lower.find(L"://") != std::wstring::npos)
return false;
const std::wstring kAssets = L"/assets/";
size_t assetsPos = lower.find(kAssets);
if (assetsPos == std::wstring::npos)
return false;
size_t nsStart = assetsPos + kAssets.size();
if (nsStart >= lower.size())
return false;
size_t nsEnd = lower.find(L'/', nsStart);
if (nsEnd == std::wstring::npos || nsEnd <= nsStart)
return false;
std::wstring ns = lower.substr(nsStart, nsEnd - nsStart);
if (ns.empty())
return false;
size_t relStart = nsEnd + 1;
if (relStart >= lower.size())
return false;
std::wstring rel = lower.substr(relStart);
std::string nsKey = WStringToLowerAscii(ns);
if (nsKey.empty())
return false;
EnsureModAssetIndex();
auto it = s_modAssetRoots.find(nsKey);
if (it == s_modAssetRoots.end())
return false;
std::wstring rootW(it->second.begin(), it->second.end());
std::wstring relW = ns + L"/" + rel;
for (wchar_t& ch : relW)
{
if (ch == L'/')
ch = L'\\';
}
std::wstring fullPath = rootW + L"\\" + relW;
if (!FileExistsW(fullPath))
return false;
outPath = fullPath;
return true;
}
static int DetectAtlasTypeFromResource(void* resourcePtr)
{
if (!resourcePtr)
@@ -2284,6 +2435,14 @@ namespace GameHooks
}
}
std::wstring modAssetPath;
if (TryResolveModAssetPath(*path, modAssetPath))
{
LogUtil::Log("[WeaveLoader] getResourceAsStream: redirecting %ls -> %ls",
path->c_str(), modAssetPath.c_str());
return Original_GetResourceAsStream(&modAssetPath);
}
return Original_GetResourceAsStream(fileName);
}
@@ -2372,6 +2531,13 @@ namespace GameHooks
}
modsPath = base + "mods";
atlas_done:
s_modsPath = modsPath;
{
std::lock_guard<std::mutex> guard(s_modAssetsMutex);
s_modAssetsIndexed = false;
s_modAssetRoots.clear();
}
NativeExports::SetModsPath(modsPath);
ModAtlas::SetBasePaths(modsPath, gameResPath);
ModAtlas::EnsureAtlasesBuilt();

View File

@@ -679,6 +679,10 @@ bool HookManager::Install(const SymbolResolver& symbols)
symbols.pServerLevelAddToTickNextTick ? symbols.pServerLevelAddToTickNextTick
: symbols.pLevelAddToTickNextTick,
symbols.pLevelGetTile);
NativeExports::SetLocalizationSymbols(
symbols.pMinecraftApp,
symbols.pGetMinecraftLanguage,
symbols.pGetMinecraftLocale);
if (symbols.pTexturesBindTextureResource)
{

View File

@@ -10,6 +10,7 @@
#include <sstream>
#include <string>
#include <unordered_map>
#include <unordered_set>
#define STB_IMAGE_IMPLEMENTATION
#define STB_IMAGE_WRITE_IMPLEMENTATION
@@ -113,12 +114,64 @@ namespace ModAtlas
return r;
}
static bool HasPngExtension(const std::string& name)
{
if (name.size() < 4) return false;
char a = (char)tolower((unsigned char)name[name.size() - 4]);
char b = (char)tolower((unsigned char)name[name.size() - 3]);
char c = (char)tolower((unsigned char)name[name.size() - 2]);
char d = (char)tolower((unsigned char)name[name.size() - 1]);
return a == '.' && b == 'p' && c == 'n' && d == 'g';
}
static void ScanPngTree(const std::string& dir,
const std::string& baseDir,
const std::string& iconPrefix,
std::vector<std::pair<std::string, std::string>>& out,
std::unordered_set<std::string>& seen)
{
WIN32_FIND_DATAA fd;
std::string search = dir + "\\*";
HANDLE h = FindFirstFileA(search.c_str(), &fd);
if (h == INVALID_HANDLE_VALUE) return;
do
{
if (fd.cFileName[0] == '.') continue;
std::string fullPath = dir + "\\" + fd.cFileName;
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
ScanPngTree(fullPath, baseDir, iconPrefix, out, seen);
continue;
}
if (!HasPngExtension(fd.cFileName)) continue;
if (fullPath.size() <= baseDir.size()) continue;
std::string rel = fullPath.substr(baseDir.size());
if (!rel.empty() && (rel[0] == '\\' || rel[0] == '/'))
rel.erase(0, 1);
for (char& ch : rel)
{
if (ch == '\\') ch = '/';
}
if (rel.size() < 4) continue;
rel.resize(rel.size() - 4);
rel = ToLower(rel);
std::string iconName = iconPrefix + rel;
if (seen.insert(iconName).second)
out.push_back({ iconName, fullPath });
} while (FindNextFileA(h, &fd));
FindClose(h);
}
static void FindModTextures(const std::string& modsPath,
std::vector<std::pair<std::string, std::string>>& blocks,
std::vector<std::pair<std::string, std::string>>& items)
{
blocks.clear();
items.clear();
std::unordered_set<std::string> seenBlocks;
std::unordered_set<std::string> seenItems;
WIN32_FIND_DATAA fd;
std::string search = modsPath + "\\*";
@@ -134,34 +187,31 @@ namespace ModAtlas
std::string assetsPath = modFolder + "\\assets";
if (GetFileAttributesA(assetsPath.c_str()) != INVALID_FILE_ATTRIBUTES)
{
std::string modId = ToLower(fd.cFileName);
size_t pos = modId.find('-');
while (pos != std::string::npos) { modId.erase(pos, 1); pos = modId.find('-'); }
std::string blocksPath = assetsPath + "\\blocks";
std::string itemsPath = assetsPath + "\\items";
auto scanDir = [&](const std::string& dir, std::vector<std::pair<std::string, std::string>>& out, const std::string& prefix)
// Java-style assets: assets/<namespace>/textures/block|item/*.png
WIN32_FIND_DATAA nsfd;
std::string nsSearch = assetsPath + "\\*";
HANDLE hNs = FindFirstFileA(nsSearch.c_str(), &nsfd);
if (hNs != INVALID_HANDLE_VALUE)
{
std::string search2 = dir + "\\*.png";
HANDLE h2 = FindFirstFileA(search2.c_str(), &fd);
if (h2 == INVALID_HANDLE_VALUE) return;
do
{
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) continue;
std::string name = fd.cFileName;
name.resize(name.size() - 4);
std::string iconName = modId + ":" + name;
std::string fullPath = dir + "\\" + fd.cFileName;
out.push_back({ iconName, fullPath });
} while (FindNextFileA(h2, &fd));
FindClose(h2);
};
if (!(nsfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) continue;
if (nsfd.cFileName[0] == '.') continue;
if (GetFileAttributesA(blocksPath.c_str()) == FILE_ATTRIBUTE_DIRECTORY)
scanDir(blocksPath, blocks, "blocks");
if (GetFileAttributesA(itemsPath.c_str()) == FILE_ATTRIBUTE_DIRECTORY)
scanDir(itemsPath, items, "items");
std::string nsFolder = nsfd.cFileName;
std::string nsName = ToLower(nsFolder);
std::string nsPath = assetsPath + "\\" + nsFolder;
std::string texturesPath = nsPath + "\\textures";
std::string blocksPath = texturesPath + "\\block";
std::string itemsPath = texturesPath + "\\item";
if (GetFileAttributesA(blocksPath.c_str()) == FILE_ATTRIBUTE_DIRECTORY)
ScanPngTree(blocksPath, blocksPath, nsName + ":block/", blocks, seenBlocks);
if (GetFileAttributesA(itemsPath.c_str()) == FILE_ATTRIBUTE_DIRECTORY)
ScanPngTree(itemsPath, itemsPath, nsName + ":item/", items, seenItems);
} while (FindNextFileA(hNs, &nsfd));
FindClose(hNs);
}
}
} while (FindNextFileA(h, &fd));
FindClose(h);

View File

@@ -6,7 +6,8 @@
/// <summary>
/// Builds merged terrain.png and items.png atlases from mod assets.
/// Scans mods/*/assets/blocks/*.png and items/*.png, stitches into copies of the
/// Scans Java-style assets under mods/*/assets/<namespace>/textures/block|item/*.png,
/// then stitches into copies of the
/// vanilla atlases stored in mods/ModLoader/generated/. A CreateFileW hook redirects
/// the game's file opens to the merged copies so vanilla files are never modified.
/// </summary>

View File

@@ -26,6 +26,13 @@ namespace
LevelSetTileAndData_fn s_levelSetTileAndData = nullptr;
LevelAddToTickNextTick_fn s_levelAddToTickNextTick = nullptr;
LevelGetTile_fn s_levelGetTile = nullptr;
using GetMinecraftLanguage_fn = unsigned char (__fastcall *)(void* thisPtr, int pad);
void* s_minecraftApp = nullptr;
GetMinecraftLanguage_fn s_getMinecraftLanguage = nullptr;
GetMinecraftLanguage_fn s_getMinecraftLocale = nullptr;
bool s_loggedMissingLanguage = false;
std::string s_modsPath;
}
void NativeExports::SetLevelInteropSymbols(void* hasNeighborSignal, void* setTileAndData, void* addToTickNextTick, void* getTile)
@@ -36,6 +43,52 @@ void NativeExports::SetLevelInteropSymbols(void* hasNeighborSignal, void* setTil
s_levelGetTile = reinterpret_cast<LevelGetTile_fn>(getTile);
}
void NativeExports::SetLocalizationSymbols(void* appPtr, void* getLanguage, void* getLocale)
{
s_minecraftApp = appPtr;
s_getMinecraftLanguage = reinterpret_cast<GetMinecraftLanguage_fn>(getLanguage);
s_getMinecraftLocale = reinterpret_cast<GetMinecraftLanguage_fn>(getLocale);
}
void NativeExports::SetModsPath(const std::string& modsPath)
{
s_modsPath = modsPath;
}
static std::string ResolveDefaultModsPath()
{
HMODULE hMod = nullptr;
std::string modsPath;
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (LPCSTR)&ResolveDefaultModsPath, &hMod) && hMod)
{
char dllPath[MAX_PATH] = { 0 };
if (GetModuleFileNameA(hMod, dllPath, MAX_PATH))
{
std::string dllDir(dllPath);
size_t dllPos = dllDir.find_last_of("\\/");
if (dllPos != std::string::npos)
{
dllDir.resize(dllPos + 1);
modsPath = dllDir + "mods";
return modsPath;
}
}
}
char exePath[MAX_PATH] = { 0 };
if (GetModuleFileNameA(nullptr, exePath, MAX_PATH))
{
std::string exeDir(exePath);
size_t exePos = exeDir.find_last_of("\\/");
if (exePos != std::string::npos)
{
exeDir.resize(exePos + 1);
modsPath = exeDir + "mods";
}
}
return modsPath;
}
static std::wstring Utf8ToWide(const char* utf8)
{
if (!utf8 || !utf8[0]) return std::wstring();
@@ -512,6 +565,34 @@ void native_register_string(int descriptionId, const char* displayName)
ModStrings::Register(descriptionId, wName.c_str());
}
int native_get_minecraft_language()
{
if (!s_minecraftApp || !s_getMinecraftLanguage)
{
if (!s_loggedMissingLanguage)
{
s_loggedMissingLanguage = true;
LogUtil::Log("[WeaveLoader] native_get_minecraft_language: symbols unavailable");
}
return -1;
}
return (int)s_getMinecraftLanguage(s_minecraftApp, 0);
}
int native_get_minecraft_locale()
{
if (!s_minecraftApp || !s_getMinecraftLocale)
return -1;
return (int)s_getMinecraftLocale(s_minecraftApp, 0);
}
const char* native_get_mods_path()
{
if (s_modsPath.empty())
s_modsPath = ResolveDefaultModsPath();
return s_modsPath.c_str();
}
int native_register_entity(
const char* namespacedId,
float width,

View File

@@ -1,8 +1,12 @@
#pragma once
#include <string>
namespace NativeExports
{
void SetLevelInteropSymbols(void* hasNeighborSignal, void* setTileAndData, void* addToTickNextTick, void* getTile);
void SetLocalizationSymbols(void* appPtr, void* getLanguage, void* getLocale);
void SetModsPath(const std::string& modsPath);
}
/// Exported C functions callable from C# via P/Invoke.
@@ -119,6 +123,9 @@ extern "C"
__declspec(dllexport) int native_allocate_description_id();
__declspec(dllexport) void native_register_string(int descriptionId, const char* displayName);
__declspec(dllexport) int native_get_minecraft_language();
__declspec(dllexport) int native_get_minecraft_locale();
__declspec(dllexport) const char* native_get_mods_path();
__declspec(dllexport) int native_register_entity(
const char* namespacedId,

View File

@@ -125,6 +125,8 @@ static const char* SYM_BUFFEREDIMAGE_CTOR_FILE = "??0BufferedImage@@QEAA@AEBV?$b
static const char* SYM_BUFFEREDIMAGE_CTOR_DLC = "??0BufferedImage@@QEAA@PEAVDLCPack@@AEBV?$basic_string@_WU?$char_traits@_W@std@@V?$allocator@_W@2@@std@@_N@Z";
static const char* SYM_TEXTUREMANAGER_CREATETEXTURE = "?createTexture@TextureManager@@QEAAPEAVTexture@@AEBV?$basic_string@_WU?$char_traits@_W@std@@V?$allocator@_W@2@@std@@HHHHHHH_NPEAVBufferedImage@@@Z";
static const char* SYM_TEXTURE_TRANSFERFROMIMAGE = "?transferFromImage@Texture@@QEAAXPEAVBufferedImage@@@Z";
static const char* SYM_GET_MINECRAFT_LANGUAGE = "?GetMinecraftLanguage@CMinecraftApp@@QEAAEH@Z";
static const char* SYM_GET_MINECRAFT_LOCALE = "?GetMinecraftLocale@CMinecraftApp@@QEAAEH@Z";
static const char* SYM_ABSTRACT_TEXPACK_GETIMAGE = "?getImageResource@AbstractTexturePack@@UEAAPEAVBufferedImage@@AEBV?$basic_string@_WU?$char_traits@_W@std@@V?$allocator@_W@2@@std@@_N10@Z";
static const char* SYM_DLC_TEXPACK_GETIMAGE = "?getImageResource@DLCTexturePack@@UEAAPEAVBufferedImage@@AEBV?$basic_string@_WU?$char_traits@_W@std@@V?$allocator@_W@2@@std@@_N10@Z";
static const char* SYM_MINECRAFT_SETLEVEL = "?setLevel@Minecraft@@QEAAXPEAVMultiPlayerLevel@@HV?$shared_ptr@VPlayer@@@std@@_N2@Z";
@@ -330,6 +332,15 @@ bool SymbolResolver::ResolveGameFunctions()
pTextureAtlasLocationBlocks = Resolve(SYM_TEXATLAS_BLOCKS);
pTextureAtlasLocationItems = Resolve(SYM_TEXATLAS_ITEMS);
pTileTiles = Resolve(SYM_TILE_TILES);
pMinecraftApp = Resolve("?app@@3VCMinecraftApp@@A");
if (!pMinecraftApp)
pMinecraftApp = ResolveExactProcName(m_moduleBase, "app");
pGetMinecraftLanguage = Resolve(SYM_GET_MINECRAFT_LANGUAGE);
if (!pGetMinecraftLanguage)
pGetMinecraftLanguage = ResolveExactProcName(m_moduleBase, "CMinecraftApp::GetMinecraftLanguage");
pGetMinecraftLocale = Resolve(SYM_GET_MINECRAFT_LOCALE);
if (!pGetMinecraftLocale)
pGetMinecraftLocale = ResolveExactProcName(m_moduleBase, "CMinecraftApp::GetMinecraftLocale");
pLevelHasNeighborSignal = Resolve(SYM_LEVEL_HASNEIGHBORSIGNAL);
pLevelSetTileAndData = Resolve(SYM_LEVEL_SETTILEANDDATA);
pLevelAddToTickNextTick = Resolve(SYM_LEVEL_ADDTOTICKNEXTTICK);
@@ -462,6 +473,9 @@ bool SymbolResolver::ResolveGameFunctions()
logSym("TextureAtlas::LOCATION_BLOCKS", pTextureAtlasLocationBlocks);
logSym("TextureAtlas::LOCATION_ITEMS", pTextureAtlasLocationItems);
logSym("Tile::tiles", pTileTiles);
logSym("app (CMinecraftApp)", pMinecraftApp);
logSym("CMinecraftApp::GetMinecraftLanguage", pGetMinecraftLanguage);
logSym("CMinecraftApp::GetMinecraftLocale", pGetMinecraftLocale);
logSym("Level::hasNeighborSignal", pLevelHasNeighborSignal);
logSym("Level::setTileAndData", pLevelSetTileAndData);
logSym("Level::addToTickNextTick", pLevelAddToTickNextTick);

View File

@@ -100,6 +100,9 @@ public:
void* pTextureAtlasLocationBlocks = nullptr; // TextureAtlas::LOCATION_BLOCKS
void* pTextureAtlasLocationItems = nullptr; // TextureAtlas::LOCATION_ITEMS
void* pTileTiles = nullptr; // Tile::tiles (Tile*[]) for tile id lookup
void* pMinecraftApp = nullptr; // global CMinecraftApp app
void* pGetMinecraftLanguage = nullptr; // CMinecraftApp::GetMinecraftLanguage(int)
void* pGetMinecraftLocale = nullptr; // CMinecraftApp::GetMinecraftLocale(int)
void* pLevelHasNeighborSignal = nullptr; // Level::hasNeighborSignal(int,int,int)
void* pLevelSetTileAndData = nullptr; // Level::setTileAndData(int,int,int,int,int,int)
void* pLevelAddToTickNextTick = nullptr; // Level::addToTickNextTick(int,int,int,int,int)

View File

@@ -662,7 +662,7 @@ namespace WorldIdRemap
1.0f,
1.0f,
1,
L"weaveloader.api:missing_block",
L"weaveloader.api:block/missing_block",
0.0f,
15,
kMissingBlockDescriptionId);
@@ -676,7 +676,7 @@ namespace WorldIdRemap
missingItemId,
64,
0,
L"weaveloader.api:missing_item",
L"weaveloader.api:item/missing_item",
kMissingItemDescriptionId);
IdRegistry::Instance().SetMissingFallback(IdRegistry::Type::Item, missingItemId);
}