mirror of
https://github.com/Jacobwasbeast/LegacyWeaveLoader.git
synced 2026-06-30 08:11:30 +00:00
Mod textures, display names, and atlas injection
Mod Atlas (ModAtlas.cpp/h): - Build merged terrain.png and items.png from mod assets (blocks/*.png, items/*.png) - Scan vanilla atlas for empty (fully transparent) cells; place mod textures only there - Install merged atlases over game files before Minecraft::init; restore originals after - Hook loadUVs to create SimpleIcon objects for mod textures - Hook registerIcon to return mod icons when requested by name - FixupModIcons: copy field_0x48 (source-image ptr) from vanilla icons after init Mod Strings (ModStrings.cpp/h): - Store mod display names by description ID - Hook GetString to serve mod names for blocks/items API changes: - BlockProperties/ItemProperties: .Name(displayName), namespaced .Icon() - NativeInterop: displayName params, native_allocate_description_id, native_register_string - Registry.Assets for string registration - Output: mods/LegacyForge.API/, mods/ExampleMod/ (per-mod folders) Mod discovery: - Scan mods/*/ for mod folders; load DLLs from each - LegacyForge.API as mod in mods/LegacyForge.API/ ExampleMod: - Ruby ore block and ruby item with custom textures and names - Assets: blocks/ruby_ore.png, items/ruby.png, lang files - Furnace recipe: ruby_ore -> ruby Runtime: loadUVs, registerIcon, getResourceAsStream, GetString hooks; stb_image for PNG
This commit is contained in:
@@ -20,13 +20,15 @@ public class ExampleMod : IMod
|
||||
.Hardness(3.0f)
|
||||
.Resistance(15f)
|
||||
.Sound(SoundType.Stone)
|
||||
.Icon("ruby_ore")
|
||||
.Icon("examplemod:ruby_ore") // From assets/blocks/ruby_ore.png
|
||||
.Name("Ruby Ore")
|
||||
.InCreativeTab(CreativeTab.BuildingBlocks));
|
||||
|
||||
Ruby = Registry.Item.Register("examplemod:ruby",
|
||||
new ItemProperties()
|
||||
.MaxStackSize(64)
|
||||
.Icon("ruby")
|
||||
.Icon("examplemod:ruby") // From assets/items/ruby.png
|
||||
.Name("Ruby")
|
||||
.InCreativeTab(CreativeTab.Materials));
|
||||
|
||||
Registry.Recipe.AddFurnace("examplemod:ruby_ore", "examplemod:ruby", 1.0f);
|
||||
|
||||
@@ -8,13 +8,20 @@
|
||||
<AssemblyName>ExampleMod</AssemblyName>
|
||||
<Description>Example mod for LegacyForge demonstrating the mod API</Description>
|
||||
<Version>1.0.0</Version>
|
||||
<OutputPath>..\build\mods</OutputPath>
|
||||
<OutputPath>..\build\mods\ExampleMod</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LegacyForge.API\LegacyForge.API.csproj" />
|
||||
<!-- Private=false: do not copy LegacyForge.API into ExampleMod. API lives in mods/LegacyForge.API/ -->
|
||||
<ProjectReference Include="..\LegacyForge.API\LegacyForge.API.csproj">
|
||||
<Private>false</Private>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="assets\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
25
ExampleMod/assets/README.md
Normal file
25
ExampleMod/assets/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# ExampleMod Assets
|
||||
|
||||
## Language files
|
||||
|
||||
Language files live in `assets/lang/` with the format `{locale}.lang` (e.g. `en-GB.lang`, `de-DE.lang`).
|
||||
|
||||
**Current API:** Use `BlockProperties.Name()` and `ItemProperties.Name()` when registering blocks and items. These set the display name shown in-game. The ModLoader hooks into the game's string lookup so your names appear correctly.
|
||||
|
||||
**Future:** Multi-locale support may load from these `.lang` files. Format: `key=value` per line, with `#` for comments.
|
||||
|
||||
## Textures
|
||||
|
||||
Mod textures are supported via the dynamic atlas system. Place PNG files in:
|
||||
|
||||
- **Blocks:** `assets/blocks/{name}.png` → icon `{modid}:{name}` (e.g. `ruby_ore.png` → `examplemod:ruby_ore`)
|
||||
- **Items:** `assets/items/{name}.png` → icon `{modid}:{name}` (e.g. `ruby.png` → `examplemod:ruby`)
|
||||
|
||||
The mod ID is derived from the mod folder name (lowercase, hyphens removed). Use the namespaced icon in `BlockProperties.Icon()` and `ItemProperties.Icon()`:
|
||||
|
||||
```csharp
|
||||
.Icon("examplemod:ruby_ore") // block from assets/blocks/ruby_ore.png
|
||||
.Icon("examplemod:ruby") // item from assets/items/ruby.png
|
||||
```
|
||||
|
||||
Textures must be 16×16 pixels (or any size; they are scaled). For vanilla icons, use names like `gold_ore`, `diamond`, etc.
|
||||
BIN
ExampleMod/assets/blocks/ruby_ore.png
Normal file
BIN
ExampleMod/assets/blocks/ruby_ore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 313 B |
BIN
ExampleMod/assets/items/ruby.png
Normal file
BIN
ExampleMod/assets/items/ruby.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 313 B |
5
ExampleMod/assets/lang/de-DE.lang
Normal file
5
ExampleMod/assets/lang/de-DE.lang
Normal file
@@ -0,0 +1,5 @@
|
||||
# ExampleMod language file (de-DE)
|
||||
# German translations for ExampleMod content.
|
||||
|
||||
block.examplemod.ruby_ore=Rubinerz
|
||||
item.examplemod.ruby=Rubin
|
||||
7
ExampleMod/assets/lang/en-GB.lang
Normal file
7
ExampleMod/assets/lang/en-GB.lang
Normal file
@@ -0,0 +1,7 @@
|
||||
# ExampleMod language file (en-GB)
|
||||
# Display names for blocks and items.
|
||||
# In the current API, use BlockProperties.Name() and ItemProperties.Name() instead.
|
||||
# This file documents the expected format for future multi-locale support.
|
||||
|
||||
block.examplemod.ruby_ore=Ruby Ore
|
||||
item.examplemod.ruby=Ruby
|
||||
27
LegacyForge.API/Assets/AssetRegistry.cs
Normal file
27
LegacyForge.API/Assets/AssetRegistry.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace LegacyForge.API.Assets;
|
||||
|
||||
/// <summary>
|
||||
/// Asset registration for mods. Use for language strings and (future) texture paths.
|
||||
/// Block and item display names are typically set via BlockProperties.Name() and ItemProperties.Name().
|
||||
/// Use this registry for additional strings (e.g. GUI labels, messages).
|
||||
/// </summary>
|
||||
public static class AssetRegistry
|
||||
{
|
||||
/// <summary>
|
||||
/// Register a display string for a custom description ID.
|
||||
/// Use native_allocate_description_id() to get an ID, then wire it to your custom UI/entity.
|
||||
/// </summary>
|
||||
public static void RegisterString(int descriptionId, string displayName)
|
||||
{
|
||||
NativeInterop.native_register_string(descriptionId, displayName ?? "");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocate a new description ID for custom strings.
|
||||
/// IDs are in the mod range (10000+) and are looked up via the GetString hook.
|
||||
/// </summary>
|
||||
public static int AllocateDescriptionId()
|
||||
{
|
||||
return NativeInterop.native_allocate_description_id();
|
||||
}
|
||||
}
|
||||
@@ -47,14 +47,18 @@ public class BlockProperties
|
||||
internal float LightEmissionValue = 0.0f;
|
||||
internal int LightBlockValue = 255;
|
||||
internal CreativeTab CreativeTabValue = CreativeTab.None;
|
||||
internal string? NameValue;
|
||||
|
||||
public BlockProperties Material(MaterialType material) { MaterialValue = material; return this; }
|
||||
public BlockProperties Hardness(float hardness) { HardnessValue = hardness; return this; }
|
||||
public BlockProperties Resistance(float resistance) { ResistanceValue = resistance; return this; }
|
||||
public BlockProperties Sound(SoundType sound) { SoundValue = sound; return this; }
|
||||
/// <summary>Icon name in the terrain atlas. Use namespaced ID for mod textures (e.g. "examplemod:ruby_ore" from assets/blocks/ruby_ore.png), or vanilla names like "stone", "gold_ore".</summary>
|
||||
public BlockProperties Icon(string iconName) { IconValue = iconName; return this; }
|
||||
public BlockProperties LightLevel(float level) { LightEmissionValue = level; return this; }
|
||||
public BlockProperties LightBlocking(int level) { LightBlockValue = level; return this; }
|
||||
public BlockProperties Indestructible() { HardnessValue = -1.0f; ResistanceValue = 6000000f; return this; }
|
||||
public BlockProperties InCreativeTab(CreativeTab tab) { CreativeTabValue = tab; return this; }
|
||||
/// <summary>Display name shown in-game (e.g. "Ruby Ore"). Used for localization.</summary>
|
||||
public BlockProperties Name(string displayName) { NameValue = displayName; return this; }
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@ public static class BlockRegistry
|
||||
(int)properties.SoundValue,
|
||||
properties.IconValue,
|
||||
properties.LightEmissionValue,
|
||||
properties.LightBlockValue);
|
||||
properties.LightBlockValue,
|
||||
properties.NameValue ?? "");
|
||||
|
||||
if (numericId < 0)
|
||||
throw new InvalidOperationException($"Failed to register block '{id}'. No free IDs or invalid parameters.");
|
||||
|
||||
@@ -9,8 +9,10 @@ public class ItemProperties
|
||||
internal int MaxDamageValue = 0;
|
||||
internal string IconValue = "";
|
||||
internal CreativeTab CreativeTabValue = CreativeTab.None;
|
||||
internal string? NameValue;
|
||||
|
||||
public ItemProperties MaxStackSize(int size) { MaxStackSizeValue = size; return this; }
|
||||
/// <summary>Icon name in the items atlas. Use namespaced ID for mod textures (e.g. "examplemod:ruby" from assets/items/ruby.png), or vanilla names like "diamond", "ingotIron".</summary>
|
||||
public ItemProperties Icon(string iconName) { IconValue = iconName; return this; }
|
||||
|
||||
/// <summary>
|
||||
@@ -19,4 +21,6 @@ public class ItemProperties
|
||||
/// </summary>
|
||||
public ItemProperties MaxDamage(int damage) { MaxDamageValue = damage; MaxStackSizeValue = 1; return this; }
|
||||
public ItemProperties InCreativeTab(CreativeTab tab) { CreativeTabValue = tab; return this; }
|
||||
/// <summary>Display name shown in-game (e.g. "Ruby"). Used for localization.</summary>
|
||||
public ItemProperties Name(string displayName) { NameValue = displayName; return this; }
|
||||
}
|
||||
|
||||
@@ -36,7 +36,8 @@ public static class ItemRegistry
|
||||
id.ToString(),
|
||||
properties.MaxStackSizeValue,
|
||||
properties.MaxDamageValue,
|
||||
properties.IconValue);
|
||||
properties.IconValue,
|
||||
properties.NameValue ?? "");
|
||||
|
||||
if (numericId < 0)
|
||||
throw new InvalidOperationException($"Failed to register item '{id}'. No free IDs or invalid parameters.");
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<AssemblyName>LegacyForge.API</AssemblyName>
|
||||
<Description>Mod API for LegacyForge - Minecraft Legacy Edition mod loader</Description>
|
||||
<Version>1.0.0</Version>
|
||||
<OutputPath>..\build</OutputPath>
|
||||
<OutputPath>..\build\mods\LegacyForge.API</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -19,14 +19,22 @@ internal static class NativeInterop
|
||||
int soundType,
|
||||
string iconName,
|
||||
float lightEmission,
|
||||
int lightBlock);
|
||||
int lightBlock,
|
||||
string displayName);
|
||||
|
||||
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
|
||||
internal static extern int native_register_item(
|
||||
string namespacedId,
|
||||
int maxStackSize,
|
||||
int maxDamage,
|
||||
string iconName);
|
||||
string iconName,
|
||||
string displayName);
|
||||
|
||||
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
|
||||
internal static extern int native_allocate_description_id();
|
||||
|
||||
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
|
||||
internal static extern void native_register_string(int descriptionId, string displayName);
|
||||
|
||||
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
|
||||
internal static extern int native_register_entity(
|
||||
|
||||
@@ -2,12 +2,13 @@ using LegacyForge.API.Block;
|
||||
using LegacyForge.API.Item;
|
||||
using LegacyForge.API.Entity;
|
||||
using LegacyForge.API.Recipe;
|
||||
using LegacyForge.API.Assets;
|
||||
|
||||
namespace LegacyForge.API;
|
||||
|
||||
/// <summary>
|
||||
/// Central access point for all LegacyForge registries.
|
||||
/// Use Registry.Block, Registry.Item, Registry.Entity, or Registry.Recipe to register content.
|
||||
/// Use Registry.Block, Registry.Item, Registry.Entity, Registry.Recipe, or Registry.Assets.
|
||||
/// </summary>
|
||||
public static class Registry
|
||||
{
|
||||
@@ -42,4 +43,13 @@ public static class Registry
|
||||
public static void AddFurnace(Identifier input, Identifier output, float xp)
|
||||
=> RecipeRegistry.AddFurnace(input, output, xp);
|
||||
}
|
||||
|
||||
/// <summary>Asset registration for language strings and (future) textures.</summary>
|
||||
public static class Assets
|
||||
{
|
||||
public static void RegisterString(int descriptionId, string displayName)
|
||||
=> AssetRegistry.RegisterString(descriptionId, displayName);
|
||||
public static int AllocateDescriptionId()
|
||||
=> AssetRegistry.AllocateDescriptionId();
|
||||
}
|
||||
}
|
||||
|
||||
14
LegacyForge.Core/LegacyForgeApiMod.cs
Normal file
14
LegacyForge.Core/LegacyForgeApiMod.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using LegacyForge.API;
|
||||
|
||||
namespace LegacyForge.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Built-in mod representing the LegacyForge API. Counts in the mod list and appears
|
||||
/// when mods/LegacyForge.API/ exists. Does not run lifecycle hooks.
|
||||
/// </summary>
|
||||
[Mod("legacyforge.api", Name = "LegacyForge API", Version = "1.0.0", Author = "LegacyForge",
|
||||
Description = "Mod API and shared types")]
|
||||
internal sealed class LegacyForgeApiMod : IMod
|
||||
{
|
||||
public void OnInitialize() { }
|
||||
}
|
||||
@@ -21,28 +21,45 @@ internal static class ModDiscovery
|
||||
return mods;
|
||||
}
|
||||
|
||||
var dllFiles = Directory.GetFiles(modsPath, "*.dll");
|
||||
Logger.Info($"Scanning {modsPath} -- found {dllFiles.Length} DLL(s)");
|
||||
|
||||
foreach (var dllPath in dllFiles)
|
||||
// Count LegacyForge.API as a mod when its folder exists (mods/LegacyForge.API/)
|
||||
var apiFolder = Path.Combine(modsPath, "LegacyForge.API");
|
||||
if (Directory.Exists(apiFolder))
|
||||
{
|
||||
string fileName = Path.GetFileName(dllPath);
|
||||
var apiMod = new LegacyForgeApiMod();
|
||||
var attr = typeof(LegacyForgeApiMod).GetCustomAttribute<ModAttribute>()!;
|
||||
mods.Add(new DiscoveredMod(apiMod, attr, typeof(ModDiscovery).Assembly));
|
||||
Logger.Info($"Discovered mod: {attr.Name} v{attr.Version} by {attr.Author} (mods/LegacyForge.API/)");
|
||||
}
|
||||
|
||||
// Skip the API assembly -- it's a shared dependency, not a mod
|
||||
if (fileName.Equals("LegacyForge.API.dll", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
// Scan each mod folder: mods/ExampleMod/, mods/SomeMod/, etc.
|
||||
// Each subfolder may contain one or more mod DLLs (we skip LegacyForge.API.dll)
|
||||
var modFolders = Directory.GetDirectories(modsPath);
|
||||
foreach (var folder in modFolders)
|
||||
{
|
||||
var folderName = Path.GetFileName(folder);
|
||||
var dllFiles = Directory.GetFiles(folder, "*.dll", SearchOption.TopDirectoryOnly);
|
||||
|
||||
try
|
||||
foreach (var dllPath in dllFiles)
|
||||
{
|
||||
var discovered = LoadModAssembly(dllPath);
|
||||
mods.AddRange(discovered);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"Failed to load mod from {fileName}: {ex.Message}");
|
||||
string fileName = Path.GetFileName(dllPath);
|
||||
|
||||
// Skip the API assembly -- it's in its own folder and counted above
|
||||
if (fileName.Equals("LegacyForge.API.dll", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
var discovered = LoadModAssembly(dllPath);
|
||||
mods.AddRange(discovered);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"Failed to load mod from {fileName}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Info($"Scanning {modsPath} -- found {mods.Count} mod(s) total");
|
||||
return mods;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,15 @@ FetchContent_MakeAvailable(minhook)
|
||||
# Ensure MinHook also uses static release CRT
|
||||
set_target_properties(minhook PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreaded")
|
||||
|
||||
# ── stb_image (PNG load/save for mod atlas) ───────────────────────────
|
||||
set(STB_DIR "${CMAKE_CURRENT_SOURCE_DIR}/third_party/stb")
|
||||
if(NOT EXISTS "${STB_DIR}/stb_image.h")
|
||||
message(WARNING "stb_image.h not found. Run: curl -sL https://raw.githubusercontent.com/nothings/stb/master/stb_image.h -o third_party/stb/stb_image.h")
|
||||
endif()
|
||||
if(NOT EXISTS "${STB_DIR}/stb_image_write.h")
|
||||
message(WARNING "stb_image_write.h not found. Run: curl -sL https://raw.githubusercontent.com/nothings/stb/master/stb_image_write.h -o third_party/stb/stb_image_write.h")
|
||||
endif()
|
||||
|
||||
# ── raw_pdb (fallback PDB parser for Wine/Proton) ────────────────────
|
||||
set(RAWPDB_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||
FetchContent_Declare(
|
||||
@@ -81,10 +90,13 @@ add_library(LegacyForgeRuntime SHARED
|
||||
src/CreativeInventory.cpp
|
||||
src/MainMenuOverlay.cpp
|
||||
src/GameObjectFactory.cpp
|
||||
src/ModStrings.cpp
|
||||
src/ModAtlas.cpp
|
||||
)
|
||||
|
||||
target_include_directories(LegacyForgeRuntime PRIVATE
|
||||
"${NETHOST_INCLUDE_DIR}"
|
||||
"${STB_DIR}"
|
||||
)
|
||||
|
||||
target_link_libraries(LegacyForgeRuntime PRIVATE
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
#include "DotNetHost.h"
|
||||
#include "CreativeInventory.h"
|
||||
#include "MainMenuOverlay.h"
|
||||
#include "ModStrings.h"
|
||||
#include "ModAtlas.h"
|
||||
#include "LogUtil.h"
|
||||
#include <Windows.h>
|
||||
#include <string>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
@@ -16,6 +20,75 @@ namespace GameHooks
|
||||
MainMenuCustomDraw_fn Original_MainMenuCustomDraw = nullptr;
|
||||
Present_fn Original_Present = nullptr;
|
||||
OutputDebugStringA_fn Original_OutputDebugStringA = nullptr;
|
||||
GetString_fn Original_GetString = nullptr;
|
||||
GetResourceAsStream_fn Original_GetResourceAsStream = nullptr;
|
||||
LoadUVs_fn Original_LoadUVs = nullptr;
|
||||
RegisterIcon_fn Original_RegisterIcon = nullptr;
|
||||
|
||||
void __fastcall Hooked_LoadUVs(void* thisPtr)
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] Hooked_LoadUVs: ENTER (textureMap=%p)", thisPtr);
|
||||
if (Original_LoadUVs)
|
||||
Original_LoadUVs(thisPtr);
|
||||
LogUtil::Log("[LegacyForge] Hooked_LoadUVs: original returned, creating mod icons");
|
||||
ModAtlas::CreateModIcons(thisPtr);
|
||||
LogUtil::Log("[LegacyForge] Hooked_LoadUVs: DONE");
|
||||
}
|
||||
|
||||
static int s_registerIconCallCount = 0;
|
||||
|
||||
void* __fastcall Hooked_RegisterIcon(void* thisPtr, const std::wstring& name)
|
||||
{
|
||||
s_registerIconCallCount++;
|
||||
void* modIcon = ModAtlas::LookupModIcon(name);
|
||||
if (modIcon)
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] registerIcon #%d: '%ls' -> MOD ICON %p",
|
||||
s_registerIconCallCount, name.c_str(), modIcon);
|
||||
return modIcon;
|
||||
}
|
||||
void* result = Original_RegisterIcon ? Original_RegisterIcon(thisPtr, name) : nullptr;
|
||||
if (s_registerIconCallCount <= 30 || !result)
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] registerIcon #%d: '%ls' -> vanilla %p",
|
||||
s_registerIconCallCount, name.c_str(), result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void* Hooked_GetResourceAsStream(const void* fileName)
|
||||
{
|
||||
const std::wstring* path = static_cast<const std::wstring*>(fileName);
|
||||
if (ModAtlas::HasModTextures() && Original_GetResourceAsStream && path)
|
||||
{
|
||||
std::string terrainPath = ModAtlas::GetMergedTerrainPath();
|
||||
std::string itemsPath = ModAtlas::GetMergedItemsPath();
|
||||
if (!terrainPath.empty() && path->find(L"terrain.png") != std::wstring::npos)
|
||||
{
|
||||
std::wstring ourPath(terrainPath.begin(), terrainPath.end());
|
||||
LogUtil::Log("[LegacyForge] getResourceAsStream: redirecting terrain.png to merged atlas");
|
||||
return Original_GetResourceAsStream(&ourPath);
|
||||
}
|
||||
if (!itemsPath.empty() && path->find(L"items.png") != std::wstring::npos)
|
||||
{
|
||||
std::wstring ourPath(itemsPath.begin(), itemsPath.end());
|
||||
LogUtil::Log("[LegacyForge] getResourceAsStream: redirecting items.png to merged atlas");
|
||||
return Original_GetResourceAsStream(&ourPath);
|
||||
}
|
||||
}
|
||||
return Original_GetResourceAsStream ? Original_GetResourceAsStream(fileName) : nullptr;
|
||||
}
|
||||
|
||||
const wchar_t* Hooked_GetString(int id)
|
||||
{
|
||||
if (ModStrings::IsModId(id))
|
||||
{
|
||||
const wchar_t* modStr = ModStrings::Get(id);
|
||||
if (modStr)
|
||||
return modStr;
|
||||
}
|
||||
return Original_GetString ? Original_GetString(id) : L"";
|
||||
}
|
||||
|
||||
void Hooked_RunStaticCtors()
|
||||
{
|
||||
@@ -40,8 +113,43 @@ namespace GameHooks
|
||||
|
||||
void __fastcall Hooked_MinecraftInit(void* thisPtr)
|
||||
{
|
||||
char baseDir[MAX_PATH] = { 0 };
|
||||
GetModuleFileNameA(nullptr, baseDir, MAX_PATH);
|
||||
std::string base(baseDir);
|
||||
size_t pos = base.find_last_of("\\/");
|
||||
if (pos != std::string::npos) base.resize(pos + 1);
|
||||
std::string gameResPath = base + "Common\\res\\TitleUpdate\\res";
|
||||
|
||||
HMODULE hMod = nullptr;
|
||||
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (LPCSTR)&Hooked_MinecraftInit, &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);
|
||||
std::string modsPath = dllDir + "mods";
|
||||
ModAtlas::BuildAtlases(modsPath, gameResPath);
|
||||
goto atlas_done;
|
||||
}
|
||||
}
|
||||
}
|
||||
ModAtlas::BuildAtlases(base + "mods", gameResPath);
|
||||
atlas_done:
|
||||
|
||||
// Overwrite the game's atlas PNGs with our merged versions BEFORE init
|
||||
// loads them. The originals are backed up and restored after init.
|
||||
ModAtlas::InstallAtlasFiles(gameResPath);
|
||||
|
||||
Original_MinecraftInit(thisPtr);
|
||||
|
||||
// After init, vanilla icons have their source-image pointer (field_0x48)
|
||||
// fully populated. Copy it to our mod icons so getSourceHeight() works.
|
||||
ModAtlas::FixupModIcons();
|
||||
|
||||
LogUtil::Log("[LegacyForge] Hook: Minecraft::init complete -- calling PostInit");
|
||||
DotNetHost::CallPostInit();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include <Windows.h>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
/// Function pointer typedefs matching the game's actual function signatures.
|
||||
/// x64 MSVC uses __fastcall-like convention (this in rcx, args in rdx/r8/r9).
|
||||
@@ -13,6 +14,10 @@ typedef void (*CreativeStaticCtor_fn)();
|
||||
typedef void (__fastcall *MainMenuCustomDraw_fn)(void* thisPtr, void* region);
|
||||
typedef void (__fastcall *Present_fn)(void* thisPtr);
|
||||
typedef void (WINAPI *OutputDebugStringA_fn)(const char* lpOutputString);
|
||||
typedef const wchar_t* (*GetString_fn)(int);
|
||||
typedef void* (*GetResourceAsStream_fn)(const void* fileName);
|
||||
typedef void (__fastcall *LoadUVs_fn)(void* thisPtr);
|
||||
typedef void* (__fastcall *RegisterIcon_fn)(void* thisPtr, const std::wstring& name);
|
||||
|
||||
namespace GameHooks
|
||||
{
|
||||
@@ -24,6 +29,10 @@ namespace GameHooks
|
||||
extern MainMenuCustomDraw_fn Original_MainMenuCustomDraw;
|
||||
extern Present_fn Original_Present;
|
||||
extern OutputDebugStringA_fn Original_OutputDebugStringA;
|
||||
extern GetString_fn Original_GetString;
|
||||
extern GetResourceAsStream_fn Original_GetResourceAsStream;
|
||||
extern LoadUVs_fn Original_LoadUVs;
|
||||
extern RegisterIcon_fn Original_RegisterIcon;
|
||||
|
||||
void Hooked_RunStaticCtors();
|
||||
void __fastcall Hooked_MinecraftTick(void* thisPtr, bool bFirst, bool bUpdateTextures);
|
||||
@@ -33,4 +42,8 @@ namespace GameHooks
|
||||
void __fastcall Hooked_MainMenuCustomDraw(void* thisPtr, void* region);
|
||||
void __fastcall Hooked_Present(void* thisPtr);
|
||||
void WINAPI Hooked_OutputDebugStringA(const char* lpOutputString);
|
||||
const wchar_t* Hooked_GetString(int id);
|
||||
void* Hooked_GetResourceAsStream(const void* fileName);
|
||||
void __fastcall Hooked_LoadUVs(void* thisPtr);
|
||||
void* __fastcall Hooked_RegisterIcon(void* thisPtr, const std::wstring& name);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ typedef void* (__fastcall *TileSetFloat_fn)(void* thisPtr, float val);
|
||||
typedef void* (__fastcall *TileSetSoundType_fn)(void* thisPtr, const void* soundType);
|
||||
// Tile* Tile::setIconName(const std::wstring&) — protected virtual
|
||||
typedef void* (__fastcall *TileSetIconName_fn)(void* thisPtr, const std::wstring& name);
|
||||
// Tile* Tile::setDescriptionId(unsigned int) — public virtual
|
||||
typedef void* (__fastcall *TileSetDescriptionId_fn)(void* thisPtr, unsigned int id);
|
||||
|
||||
// TileItem::TileItem(int id)
|
||||
typedef void (__fastcall *TileItemCtor_fn)(void* thisPtr, int id);
|
||||
@@ -22,17 +24,21 @@ typedef void (__fastcall *TileItemCtor_fn)(void* thisPtr, int id);
|
||||
typedef void (__fastcall *ItemCtor_fn)(void* thisPtr, int id);
|
||||
// Item* Item::setIconName(const std::wstring&)
|
||||
typedef void* (__fastcall *ItemSetIconName_fn)(void* thisPtr, const std::wstring& name);
|
||||
// Item::getDescriptionId(int) — used to extract the descriptionId field offset
|
||||
typedef unsigned int (__fastcall *ItemGetDescriptionId_fn)(void* thisPtr, int auxData);
|
||||
|
||||
static TileCtor_fn fnTileCtor = nullptr;
|
||||
static TileSetFloat_fn fnSetDestroyTime = nullptr;
|
||||
static TileSetFloat_fn fnSetExplodeable = nullptr;
|
||||
static TileSetSoundType_fn fnSetSoundType = nullptr;
|
||||
static TileSetIconName_fn fnTileSetIconName= nullptr;
|
||||
static TileSetDescriptionId_fn fnTileSetDescriptionId = nullptr;
|
||||
|
||||
static TileItemCtor_fn fnTileItemCtor = nullptr;
|
||||
|
||||
static ItemCtor_fn fnItemCtor = nullptr;
|
||||
static ItemSetIconName_fn fnItemSetIconName= nullptr;
|
||||
static int s_itemDescIdOffset = -1; // offset of descriptionId field in Item, extracted from getDescriptionId
|
||||
|
||||
// Store ADDRESSES of Material*/SoundType* statics so we can dereference lazily
|
||||
// (they're NULL at resolve time because staticCtor hasn't run yet).
|
||||
@@ -76,6 +82,8 @@ bool ResolveSymbols(SymbolResolver& resolver)
|
||||
"?setSoundType@Tile@@MEAAPEAV1@PEBVSoundType@1@@Z");
|
||||
fnTileSetIconName = (TileSetIconName_fn)resolver.Resolve(
|
||||
"?setIconName@Tile@@MEAAPEAV1@AEBV?$basic_string@_WU?$char_traits@_W@std@@V?$allocator@_W@2@@std@@@Z");
|
||||
fnTileSetDescriptionId = (TileSetDescriptionId_fn)resolver.Resolve(
|
||||
"?setDescriptionId@Tile@@UEAAPEAV1@I@Z");
|
||||
|
||||
// TileItem constructor
|
||||
fnTileItemCtor = (TileItemCtor_fn)resolver.Resolve("??0TileItem@@QEAA@H@Z");
|
||||
@@ -86,6 +94,34 @@ bool ResolveSymbols(SymbolResolver& resolver)
|
||||
// Item::setIconName
|
||||
fnItemSetIconName = (ItemSetIconName_fn)resolver.Resolve(
|
||||
"?setIconName@Item@@QEAAPEAV1@AEBV?$basic_string@_WU?$char_traits@_W@std@@V?$allocator@_W@2@@std@@@Z");
|
||||
// Item::setDescriptionId is inlined — extract the field offset from getDescriptionId instead.
|
||||
// getDescriptionId(int) is "mov eax, [rcx+offset]; ret" so we parse the offset from its opcodes.
|
||||
void* fnItemGetDescId = resolver.Resolve("?getDescriptionId@Item@@UEAAIH@Z");
|
||||
if (fnItemGetDescId)
|
||||
{
|
||||
const uint8_t* code = static_cast<const uint8_t*>(fnItemGetDescId);
|
||||
if (code[0] == 0x8B && code[1] == 0x41)
|
||||
{
|
||||
// mov eax, [rcx+disp8] — 8B 41 XX
|
||||
s_itemDescIdOffset = static_cast<int>(code[2]);
|
||||
LogUtil::Log("[LegacyForge] Item descriptionId offset = 0x%X (from getDescriptionId disp8)", s_itemDescIdOffset);
|
||||
}
|
||||
else if (code[0] == 0x8B && code[1] == 0x81)
|
||||
{
|
||||
// mov eax, [rcx+disp32] — 8B 81 XX XX XX XX
|
||||
s_itemDescIdOffset = *reinterpret_cast<const int*>(code + 2);
|
||||
LogUtil::Log("[LegacyForge] Item descriptionId offset = 0x%X (from getDescriptionId disp32)", s_itemDescIdOffset);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] Item::getDescriptionId has unexpected opcode pattern: %02X %02X %02X",
|
||||
code[0], code[1], code[2]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] MISSING: Item::getDescriptionId — cannot set item display names");
|
||||
}
|
||||
|
||||
// Resolve Material* static pointer ADDRESSES (values are NULL until staticCtor runs)
|
||||
auto resolveMat = [&](int idx, const char* sym) {
|
||||
@@ -162,7 +198,7 @@ bool ResolveSymbols(SymbolResolver& resolver)
|
||||
}
|
||||
|
||||
bool CreateTile(int tileId, int materialType, float hardness, float resistance,
|
||||
int soundType, const wchar_t* iconName)
|
||||
int soundType, const wchar_t* iconName, int descriptionId)
|
||||
{
|
||||
if (!s_resolved || !fnTileCtor)
|
||||
{
|
||||
@@ -201,8 +237,13 @@ bool CreateTile(int tileId, int materialType, float hardness, float resistance,
|
||||
fnTileSetIconName(tile, name);
|
||||
}
|
||||
|
||||
LogUtil::Log("[LegacyForge] Created Tile id=%d (material=%d, icon=%ls)", tileId, materialType,
|
||||
iconName ? iconName : L"<none>");
|
||||
if (fnTileSetDescriptionId && descriptionId >= 0)
|
||||
{
|
||||
fnTileSetDescriptionId(tile, static_cast<unsigned int>(descriptionId));
|
||||
}
|
||||
|
||||
LogUtil::Log("[LegacyForge] Created Tile id=%d (material=%d, icon=%ls, descId=%d)", tileId, materialType,
|
||||
iconName ? iconName : L"<none>", descriptionId);
|
||||
|
||||
// Create the corresponding TileItem so the block can appear in inventory.
|
||||
// TileItem(tileId - 256) -> Item::Item(tileId - 256) -> id = tileId, items[tileId] = this
|
||||
@@ -217,7 +258,7 @@ bool CreateTile(int tileId, int materialType, float hardness, float resistance,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CreateItem(int itemId, int maxStackSize, const wchar_t* iconName)
|
||||
bool CreateItem(int itemId, int maxStackSize, const wchar_t* iconName, int descriptionId)
|
||||
{
|
||||
if (!s_resolved || !fnItemCtor)
|
||||
{
|
||||
@@ -239,8 +280,14 @@ bool CreateItem(int itemId, int maxStackSize, const wchar_t* iconName)
|
||||
fnItemSetIconName(item, name);
|
||||
}
|
||||
|
||||
LogUtil::Log("[LegacyForge] Created Item id=%d (ctorParam=%d, icon=%ls)",
|
||||
itemId, ctorParam, iconName ? iconName : L"<none>");
|
||||
if (s_itemDescIdOffset > 0 && descriptionId >= 0)
|
||||
{
|
||||
*reinterpret_cast<unsigned int*>(static_cast<char*>(item) + s_itemDescIdOffset) =
|
||||
static_cast<unsigned int>(descriptionId);
|
||||
}
|
||||
|
||||
LogUtil::Log("[LegacyForge] Created Item id=%d (ctorParam=%d, icon=%ls, descId=%d)",
|
||||
itemId, ctorParam, iconName ? iconName : L"<none>", descriptionId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -11,10 +11,12 @@ namespace GameObjectFactory
|
||||
// and create a matching TileItem in Item::items[].
|
||||
// materialType/soundType map to the C# API enums (see BlockProperties.cs).
|
||||
// iconName is the texture atlas key (e.g. "ruby_ore").
|
||||
// descriptionId: if >= 0, call setDescriptionId on the Tile for localization.
|
||||
bool CreateTile(int tileId, int materialType, float hardness, float resistance,
|
||||
int soundType, const wchar_t* iconName);
|
||||
int soundType, const wchar_t* iconName, int descriptionId = -1);
|
||||
|
||||
// Create an Item game object. itemId is the FINAL id (256 + constructor param).
|
||||
// The Item is registered in Item::items[itemId].
|
||||
bool CreateItem(int itemId, int maxStackSize, const wchar_t* iconName);
|
||||
// descriptionId: if >= 0, call setDescriptionId on the Item for localization.
|
||||
bool CreateItem(int itemId, int maxStackSize, const wchar_t* iconName, int descriptionId = -1);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "HookManager.h"
|
||||
#include "GameHooks.h"
|
||||
#include "ModAtlas.h"
|
||||
#include "SymbolResolver.h"
|
||||
#include "CreativeInventory.h"
|
||||
#include "MainMenuOverlay.h"
|
||||
@@ -67,6 +68,42 @@ bool HookManager::Install(const SymbolResolver& symbols)
|
||||
|
||||
GameObjectFactory::ResolveSymbols(const_cast<SymbolResolver&>(symbols));
|
||||
|
||||
if (symbols.pLoadUVs && symbols.pSimpleIconCtor && symbols.pOperatorNew)
|
||||
{
|
||||
ModAtlas::SetInjectSymbols(symbols.pSimpleIconCtor, symbols.pOperatorNew);
|
||||
if (MH_CreateHook(symbols.pLoadUVs,
|
||||
reinterpret_cast<void*>(&GameHooks::Hooked_LoadUVs),
|
||||
reinterpret_cast<void**>(&GameHooks::Original_LoadUVs)) != MH_OK)
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] Warning: Failed to hook PreStitchedTextureMap::loadUVs (mod textures may not appear)");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] Hooked PreStitchedTextureMap::loadUVs (mod texture injection)");
|
||||
}
|
||||
|
||||
if (symbols.pRegisterIcon)
|
||||
{
|
||||
if (MH_CreateHook(symbols.pRegisterIcon,
|
||||
reinterpret_cast<void*>(&GameHooks::Hooked_RegisterIcon),
|
||||
reinterpret_cast<void**>(&GameHooks::Original_RegisterIcon)) != MH_OK)
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] Warning: Failed to hook PreStitchedTextureMap::registerIcon");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] Hooked PreStitchedTextureMap::registerIcon (mod icon lookup)");
|
||||
// Pass the trampoline to ModAtlas so it can look up vanilla icons
|
||||
// for copying internal state (field_0x48 source image pointer).
|
||||
ModAtlas::SetRegisterIconFn(GameHooks::Original_RegisterIcon);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (symbols.pLoadUVs)
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] Mod texture injection unavailable: SimpleIcon/operator new not resolved");
|
||||
}
|
||||
|
||||
if (symbols.pCreativeStaticCtor)
|
||||
{
|
||||
CreativeInventory::ResolveSymbols(const_cast<SymbolResolver&>(symbols));
|
||||
@@ -113,6 +150,34 @@ bool HookManager::Install(const SymbolResolver& symbols)
|
||||
}
|
||||
}
|
||||
|
||||
if (symbols.pGetString)
|
||||
{
|
||||
if (MH_CreateHook(symbols.pGetString,
|
||||
reinterpret_cast<void*>(&GameHooks::Hooked_GetString),
|
||||
reinterpret_cast<void**>(&GameHooks::Original_GetString)) != MH_OK)
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] Warning: Failed to hook CMinecraftApp::GetString (mod names unavailable)");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] Hooked CMinecraftApp::GetString (mod localization)");
|
||||
}
|
||||
}
|
||||
|
||||
if (symbols.pGetResourceAsStream)
|
||||
{
|
||||
if (MH_CreateHook(symbols.pGetResourceAsStream,
|
||||
reinterpret_cast<void*>(&GameHooks::Hooked_GetResourceAsStream),
|
||||
reinterpret_cast<void**>(&GameHooks::Original_GetResourceAsStream)) != MH_OK)
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] Warning: Failed to hook InputStream::getResourceAsStream (mod atlas unavailable)");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] Hooked InputStream::getResourceAsStream (mod textures)");
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
void* pOutputDbgStr = reinterpret_cast<void*>(
|
||||
GetProcAddress(GetModuleHandleA("kernel32.dll"), "OutputDebugStringA"));
|
||||
|
||||
449
LegacyForgeRuntime/src/ModAtlas.cpp
Normal file
449
LegacyForgeRuntime/src/ModAtlas.cpp
Normal file
@@ -0,0 +1,449 @@
|
||||
#include "ModAtlas.h"
|
||||
#include "LogUtil.h"
|
||||
#include <Windows.h>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||
#include "stb_image.h"
|
||||
#include "stb_image_write.h"
|
||||
|
||||
namespace ModAtlas
|
||||
{
|
||||
static std::string s_mergedDir;
|
||||
static std::vector<ModTextureEntry> s_blockEntries;
|
||||
static std::vector<ModTextureEntry> s_itemEntries;
|
||||
static bool s_hasModTextures = false;
|
||||
static void* s_simpleIconCtor = nullptr;
|
||||
static void* (*s_operatorNew)(size_t) = nullptr;
|
||||
|
||||
// iconType is at offset 8 in PreStitchedTextureMap (verified via getIconType disassembly)
|
||||
|
||||
static std::string ToLower(const std::string& s)
|
||||
{
|
||||
std::string r = s;
|
||||
for (char& c : r)
|
||||
c = (char)tolower((unsigned char)c);
|
||||
return r;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
WIN32_FIND_DATAA fd;
|
||||
std::string search = 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 = modsPath + "\\" + fd.cFileName;
|
||||
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)
|
||||
{
|
||||
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 (GetFileAttributesA(blocksPath.c_str()) == FILE_ATTRIBUTE_DIRECTORY)
|
||||
scanDir(blocksPath, blocks, "blocks");
|
||||
if (GetFileAttributesA(itemsPath.c_str()) == FILE_ATTRIBUTE_DIRECTORY)
|
||||
scanDir(itemsPath, items, "items");
|
||||
}
|
||||
} while (FindNextFileA(h, &fd));
|
||||
FindClose(h);
|
||||
}
|
||||
|
||||
static bool LoadPng(const std::string& path, int* w, int* h, int* comp, unsigned char** data)
|
||||
{
|
||||
*data = stbi_load(path.c_str(), w, h, comp, 4);
|
||||
return *data != nullptr;
|
||||
}
|
||||
|
||||
static void Blit16x16(unsigned char* dst, int dstW, int dstH, int dstX, int dstY,
|
||||
const unsigned char* src, int srcW, int srcH)
|
||||
{
|
||||
for (int y = 0; y < 16; y++)
|
||||
{
|
||||
for (int x = 0; x < 16; x++)
|
||||
{
|
||||
int sx = (srcW > 0) ? (x * srcW / 16) : 0;
|
||||
int sy = (srcH > 0) ? (y * srcH / 16) : 0;
|
||||
int di = ((dstY + y) * dstW + (dstX + x)) * 4;
|
||||
int si = (sy * srcW + sx) * 4;
|
||||
if (si < srcW * srcH * 4 && di < dstW * dstH * 4)
|
||||
{
|
||||
dst[di] = src[si];
|
||||
dst[di + 1] = src[si + 1];
|
||||
dst[di + 2] = src[si + 2];
|
||||
dst[di + 3] = src[si + 3];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool IsCellEmpty(const unsigned char* img, int imgW, int imgH,
|
||||
int cellX, int cellY, int cellSize)
|
||||
{
|
||||
for (int y = 0; y < cellSize; y++)
|
||||
{
|
||||
for (int x = 0; x < cellSize; x++)
|
||||
{
|
||||
int px = cellX + x;
|
||||
int py = cellY + y;
|
||||
if (px >= imgW || py >= imgH) continue;
|
||||
int idx = (py * imgW + px) * 4;
|
||||
if (img[idx + 3] > 0)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool BuildAtlas(const std::string& vanillaPath, const std::string& outPath,
|
||||
const std::vector<std::pair<std::string, std::string>>& modTextures,
|
||||
int gridCols, int gridRows, int iconSize,
|
||||
std::vector<ModTextureEntry>& entries)
|
||||
{
|
||||
int imgW = gridCols * iconSize;
|
||||
int imgH = gridRows * iconSize;
|
||||
|
||||
unsigned char* img = (unsigned char*)calloc(imgW * imgH, 4);
|
||||
if (!img) return false;
|
||||
|
||||
int vw = 0, vh = 0, vc = 0;
|
||||
if (!vanillaPath.empty())
|
||||
{
|
||||
unsigned char* vanilla = stbi_load(vanillaPath.c_str(), &vw, &vh, &vc, 4);
|
||||
if (vanilla)
|
||||
{
|
||||
int copyW = (vw < imgW) ? vw : imgW;
|
||||
int copyH = (vh < imgH) ? vh : imgH;
|
||||
for (int y = 0; y < copyH; y++)
|
||||
memcpy(img + y * imgW * 4, vanilla + y * vw * 4, copyW * 4);
|
||||
stbi_image_free(vanilla);
|
||||
}
|
||||
}
|
||||
|
||||
// Find all empty (fully transparent) cells in the atlas
|
||||
std::vector<std::pair<int, int>> emptyCells;
|
||||
for (int row = 0; row < gridRows; row++)
|
||||
{
|
||||
for (int col = 0; col < gridCols; col++)
|
||||
{
|
||||
if (IsCellEmpty(img, imgW, imgH, col * iconSize, row * iconSize, iconSize))
|
||||
emptyCells.push_back({ row, col });
|
||||
}
|
||||
}
|
||||
|
||||
LogUtil::Log("[LegacyForge] ModAtlas: found %zu empty cells in %dx%d atlas",
|
||||
emptyCells.size(), gridCols, gridRows);
|
||||
|
||||
size_t cellIdx = 0;
|
||||
for (const auto& tex : modTextures)
|
||||
{
|
||||
const std::string& iconName = tex.first;
|
||||
const std::string& path = tex.second;
|
||||
|
||||
if (cellIdx >= emptyCells.size())
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] ModAtlas: no empty cells left for %s!", iconName.c_str());
|
||||
break;
|
||||
}
|
||||
|
||||
int sw = 0, sh = 0, sc = 0;
|
||||
unsigned char* src = nullptr;
|
||||
if (!LoadPng(path, &sw, &sh, &sc, &src))
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] ModAtlas: failed to load %s", path.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
int row = emptyCells[cellIdx].first;
|
||||
int col = emptyCells[cellIdx].second;
|
||||
cellIdx++;
|
||||
|
||||
Blit16x16(img, imgW, imgH, col * iconSize, row * iconSize, src, sw, sh);
|
||||
stbi_image_free(src);
|
||||
|
||||
std::wstring wname(iconName.begin(), iconName.end());
|
||||
entries.push_back({ wname, 0, row, col });
|
||||
|
||||
LogUtil::Log("[LegacyForge] ModAtlas: placed '%s' at row=%d col=%d", iconName.c_str(), row, col);
|
||||
}
|
||||
|
||||
std::string dir = outPath.substr(0, outPath.find_last_of("\\/"));
|
||||
CreateDirectoryA(dir.c_str(), nullptr);
|
||||
|
||||
int ok = stbi_write_png(outPath.c_str(), imgW, imgH, 4, img, imgW * 4);
|
||||
free(img);
|
||||
return ok != 0;
|
||||
}
|
||||
|
||||
std::string BuildAtlases(const std::string& modsPath, const std::string& gameResPath)
|
||||
{
|
||||
s_blockEntries.clear();
|
||||
s_itemEntries.clear();
|
||||
s_hasModTextures = false;
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> blockPaths, itemPaths;
|
||||
FindModTextures(modsPath, blockPaths, itemPaths);
|
||||
|
||||
if (blockPaths.empty() && itemPaths.empty())
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] ModAtlas: no mod textures found");
|
||||
return "";
|
||||
}
|
||||
|
||||
char baseDir[MAX_PATH] = { 0 };
|
||||
GetModuleFileNameA(nullptr, baseDir, MAX_PATH);
|
||||
std::string base(baseDir);
|
||||
size_t pos = base.find_last_of("\\/");
|
||||
if (pos != std::string::npos) base.resize(pos + 1);
|
||||
|
||||
s_mergedDir = base + "mods\\ModLoader\\generated";
|
||||
CreateDirectoryA((base + "mods").c_str(), nullptr);
|
||||
CreateDirectoryA((base + "mods\\ModLoader").c_str(), nullptr);
|
||||
CreateDirectoryA(s_mergedDir.c_str(), nullptr);
|
||||
|
||||
std::string vanillaTerrain = gameResPath + "\\terrain.png";
|
||||
std::string vanillaItems = gameResPath + "\\items.png";
|
||||
std::string outTerrain = s_mergedDir + "\\terrain.png";
|
||||
std::string outItems = s_mergedDir + "\\items.png";
|
||||
|
||||
if (!blockPaths.empty())
|
||||
{
|
||||
if (BuildAtlas(vanillaTerrain, outTerrain, blockPaths, 16, 32, 16, s_blockEntries))
|
||||
{
|
||||
s_hasModTextures = true;
|
||||
LogUtil::Log("[LegacyForge] ModAtlas: built terrain.png with %zu mod textures", s_blockEntries.size());
|
||||
}
|
||||
}
|
||||
|
||||
if (!itemPaths.empty())
|
||||
{
|
||||
if (BuildAtlas(vanillaItems, outItems, itemPaths, 16, 16, 16, s_itemEntries))
|
||||
{
|
||||
s_hasModTextures = true;
|
||||
for (auto& e : s_itemEntries) e.atlasType = 1;
|
||||
LogUtil::Log("[LegacyForge] ModAtlas: built items.png with %zu mod textures", s_itemEntries.size());
|
||||
}
|
||||
}
|
||||
|
||||
return s_hasModTextures ? s_mergedDir : "";
|
||||
}
|
||||
|
||||
std::string GetMergedTerrainPath()
|
||||
{
|
||||
return s_mergedDir.empty() ? "" : s_mergedDir + "\\terrain.png";
|
||||
}
|
||||
|
||||
std::string GetMergedItemsPath()
|
||||
{
|
||||
return s_mergedDir.empty() ? "" : s_mergedDir + "\\items.png";
|
||||
}
|
||||
|
||||
const std::vector<ModTextureEntry>& GetBlockEntries() { return s_blockEntries; }
|
||||
const std::vector<ModTextureEntry>& GetItemEntries() { return s_itemEntries; }
|
||||
bool HasModTextures() { return s_hasModTextures; }
|
||||
|
||||
static std::unordered_map<std::wstring, void*> s_modIcons;
|
||||
static RegisterIcon_fn s_originalRegisterIcon = nullptr;
|
||||
static std::string s_gameResPath;
|
||||
static std::string s_backupTerrainPath;
|
||||
static std::string s_backupItemsPath;
|
||||
|
||||
// Per-atlas-type textureMap pointers, saved during CreateModIcons for FixupModIcons.
|
||||
static void* s_terrainTextureMap = nullptr;
|
||||
static void* s_itemsTextureMap = nullptr;
|
||||
|
||||
void SetInjectSymbols(void* simpleIconCtor, void* operatorNew)
|
||||
{
|
||||
s_simpleIconCtor = simpleIconCtor;
|
||||
s_operatorNew = reinterpret_cast<void* (*)(size_t)>(operatorNew);
|
||||
}
|
||||
|
||||
void SetRegisterIconFn(RegisterIcon_fn fn)
|
||||
{
|
||||
s_originalRegisterIcon = fn;
|
||||
}
|
||||
|
||||
void InstallAtlasFiles(const std::string& gameResPath)
|
||||
{
|
||||
if (!s_hasModTextures) return;
|
||||
s_gameResPath = gameResPath;
|
||||
|
||||
std::string vanillaTerrain = gameResPath + "\\terrain.png";
|
||||
std::string vanillaItems = gameResPath + "\\items.png";
|
||||
std::string mergedTerrain = GetMergedTerrainPath();
|
||||
std::string mergedItems = GetMergedItemsPath();
|
||||
|
||||
if (!mergedTerrain.empty() && !s_blockEntries.empty())
|
||||
{
|
||||
s_backupTerrainPath = vanillaTerrain + ".legacyforge_backup";
|
||||
CopyFileA(vanillaTerrain.c_str(), s_backupTerrainPath.c_str(), FALSE);
|
||||
if (CopyFileA(mergedTerrain.c_str(), vanillaTerrain.c_str(), FALSE))
|
||||
LogUtil::Log("[LegacyForge] ModAtlas: installed merged terrain.png over game file");
|
||||
else
|
||||
LogUtil::Log("[LegacyForge] ModAtlas: WARNING - failed to copy merged terrain.png (err=%lu)", GetLastError());
|
||||
}
|
||||
|
||||
if (!mergedItems.empty() && !s_itemEntries.empty())
|
||||
{
|
||||
s_backupItemsPath = vanillaItems + ".legacyforge_backup";
|
||||
CopyFileA(vanillaItems.c_str(), s_backupItemsPath.c_str(), FALSE);
|
||||
if (CopyFileA(mergedItems.c_str(), vanillaItems.c_str(), FALSE))
|
||||
LogUtil::Log("[LegacyForge] ModAtlas: installed merged items.png over game file");
|
||||
else
|
||||
LogUtil::Log("[LegacyForge] ModAtlas: WARNING - failed to copy merged items.png (err=%lu)", GetLastError());
|
||||
}
|
||||
}
|
||||
|
||||
void CreateModIcons(void* textureMap)
|
||||
{
|
||||
if (!s_hasModTextures || !s_simpleIconCtor || !textureMap) return;
|
||||
if (!s_operatorNew) { LogUtil::Log("[LegacyForge] ModAtlas: operator new not resolved, skipping icon creation"); return; }
|
||||
|
||||
int iconType = *reinterpret_cast<int*>(reinterpret_cast<char*>(textureMap) + 8);
|
||||
LogUtil::Log("[LegacyForge] ModAtlas: CreateModIcons called for atlas type %d (textureMap=%p)", iconType, textureMap);
|
||||
|
||||
if (iconType == 0) s_terrainTextureMap = textureMap;
|
||||
else if (iconType == 1) s_itemsTextureMap = textureMap;
|
||||
|
||||
typedef void (__fastcall* SimpleIconCtor_fn)(void* thisPtr, const std::wstring* name,
|
||||
const std::wstring* filename, float u0, float v0, float u1, float v1);
|
||||
|
||||
auto ctor = reinterpret_cast<SimpleIconCtor_fn>(s_simpleIconCtor);
|
||||
|
||||
auto create = [&](const std::vector<ModTextureEntry>& entries, float vertRatio) {
|
||||
for (const auto& e : entries)
|
||||
{
|
||||
if (e.atlasType != iconType) continue;
|
||||
|
||||
float u0 = static_cast<float>(e.col) / 16.0f;
|
||||
float v0 = static_cast<float>(e.row) * vertRatio;
|
||||
float u1 = static_cast<float>(e.col + 1) / 16.0f;
|
||||
float v1 = static_cast<float>(e.row + 1) * vertRatio;
|
||||
|
||||
void* icon = s_operatorNew(128);
|
||||
if (icon)
|
||||
{
|
||||
memset(icon, 0, 128);
|
||||
ctor(icon, &e.iconName, &e.iconName, u0, v0, u1, v1);
|
||||
s_modIcons[e.iconName] = icon;
|
||||
LogUtil::Log("[LegacyForge] ModAtlas: created icon '%ls' (atlas=%d, row=%d, col=%d)",
|
||||
e.iconName.c_str(), iconType, e.row, e.col);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (iconType == 0)
|
||||
create(s_blockEntries, 1.0f / 32.0f);
|
||||
else if (iconType == 1)
|
||||
create(s_itemEntries, 1.0f / 16.0f);
|
||||
|
||||
LogUtil::Log("[LegacyForge] ModAtlas: s_modIcons now has %zu entries total", s_modIcons.size());
|
||||
}
|
||||
|
||||
void FixupModIcons()
|
||||
{
|
||||
if (s_modIcons.empty() || !s_originalRegisterIcon) return;
|
||||
|
||||
// After Minecraft::init, vanilla icons have field_0x48 properly set.
|
||||
// Grab the source-image pointer from a vanilla icon for each atlas type
|
||||
// and copy it to our mod icons.
|
||||
auto fixForAtlas = [](void* textureMap, int atlasType, const wchar_t* probeName) {
|
||||
if (!textureMap) return;
|
||||
|
||||
std::wstring probeStr(probeName);
|
||||
void* probeIcon = s_originalRegisterIcon(textureMap, probeStr);
|
||||
if (!probeIcon)
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] FixupModIcons: could not find vanilla icon '%ls'", probeName);
|
||||
return;
|
||||
}
|
||||
|
||||
void* srcPtr = *reinterpret_cast<void**>(static_cast<char*>(probeIcon) + 0x48);
|
||||
LogUtil::Log("[LegacyForge] FixupModIcons: vanilla '%ls' field_0x48 = %p", probeName, srcPtr);
|
||||
|
||||
if (!srcPtr)
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] FixupModIcons: WARNING - vanilla source ptr still NULL for atlas %d", atlasType);
|
||||
return;
|
||||
}
|
||||
|
||||
int fixed = 0;
|
||||
for (auto& kv : s_modIcons)
|
||||
{
|
||||
void* icon = kv.second;
|
||||
void* existing = *reinterpret_cast<void**>(static_cast<char*>(icon) + 0x48);
|
||||
if (!existing)
|
||||
{
|
||||
*reinterpret_cast<void**>(static_cast<char*>(icon) + 0x48) = srcPtr;
|
||||
fixed++;
|
||||
}
|
||||
}
|
||||
LogUtil::Log("[LegacyForge] FixupModIcons: patched field_0x48 on %d mod icons (atlas %d, srcPtr=%p)",
|
||||
fixed, atlasType, srcPtr);
|
||||
};
|
||||
|
||||
fixForAtlas(s_terrainTextureMap, 0, L"stone");
|
||||
fixForAtlas(s_itemsTextureMap, 1, L"diamond");
|
||||
|
||||
// Restore backed-up vanilla atlas files
|
||||
if (!s_backupTerrainPath.empty())
|
||||
{
|
||||
std::string vanillaTerrain = s_gameResPath + "\\terrain.png";
|
||||
if (MoveFileExA(s_backupTerrainPath.c_str(), vanillaTerrain.c_str(), MOVEFILE_REPLACE_EXISTING))
|
||||
LogUtil::Log("[LegacyForge] ModAtlas: restored original terrain.png");
|
||||
s_backupTerrainPath.clear();
|
||||
}
|
||||
if (!s_backupItemsPath.empty())
|
||||
{
|
||||
std::string vanillaItems = s_gameResPath + "\\items.png";
|
||||
if (MoveFileExA(s_backupItemsPath.c_str(), vanillaItems.c_str(), MOVEFILE_REPLACE_EXISTING))
|
||||
LogUtil::Log("[LegacyForge] ModAtlas: restored original items.png");
|
||||
s_backupItemsPath.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void* LookupModIcon(const std::wstring& name)
|
||||
{
|
||||
auto it = s_modIcons.find(name);
|
||||
if (it != s_modIcons.end())
|
||||
return it->second;
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
58
LegacyForgeRuntime/src/ModAtlas.h
Normal file
58
LegacyForgeRuntime/src/ModAtlas.h
Normal file
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
|
||||
/// <summary>
|
||||
/// Builds merged terrain.png and items.png atlases from mod assets.
|
||||
/// Scans mods/*/assets/blocks/*.png and items/*.png, stitches into vanilla atlases.
|
||||
/// </summary>
|
||||
namespace ModAtlas
|
||||
{
|
||||
struct ModTextureEntry
|
||||
{
|
||||
std::wstring iconName; // e.g. "examplemod:ruby_ore"
|
||||
int atlasType; // 0=blocks/terrain, 1=items
|
||||
int row, col; // Grid position in atlas
|
||||
};
|
||||
|
||||
/// Call before textures->stitch(). Returns path to generated dir, or empty if none.
|
||||
std::string BuildAtlases(const std::string& modsPath, const std::string& gameResPath);
|
||||
|
||||
/// Get path to merged terrain.png (if built)
|
||||
std::string GetMergedTerrainPath();
|
||||
|
||||
/// Get path to merged items.png (if built)
|
||||
std::string GetMergedItemsPath();
|
||||
|
||||
/// Get mod texture entries for injection into texture map
|
||||
const std::vector<ModTextureEntry>& GetBlockEntries();
|
||||
const std::vector<ModTextureEntry>& GetItemEntries();
|
||||
|
||||
/// Whether atlases were built (have mod textures)
|
||||
bool HasModTextures();
|
||||
|
||||
typedef void* (__fastcall *RegisterIcon_fn)(void* textureMap, const std::wstring& name);
|
||||
|
||||
/// Set resolved symbols for texture injection (called from HookManager)
|
||||
void SetInjectSymbols(void* simpleIconCtor, void* operatorNew);
|
||||
|
||||
/// Set the original registerIcon function for vanilla icon lookups
|
||||
void SetRegisterIconFn(RegisterIcon_fn fn);
|
||||
|
||||
/// Copy merged atlas PNGs over the game's vanilla atlas files.
|
||||
/// Call before Minecraft::init so the game loads our textures.
|
||||
void InstallAtlasFiles(const std::string& gameResPath);
|
||||
|
||||
/// Create SimpleIcon objects for our mod textures after loadUVs runs.
|
||||
/// Call from loadUVs hook after original returns. atlasType is read from textureMap.
|
||||
void CreateModIcons(void* textureMap);
|
||||
|
||||
/// Fix mod icons' internal source-image pointer (field_0x48) by copying
|
||||
/// from a fully-initialized vanilla icon. Call AFTER Minecraft::init completes.
|
||||
void FixupModIcons();
|
||||
|
||||
/// Look up a mod icon by name. Returns the SimpleIcon* or nullptr if not a mod icon.
|
||||
void* LookupModIcon(const std::wstring& name);
|
||||
}
|
||||
39
LegacyForgeRuntime/src/ModStrings.cpp
Normal file
39
LegacyForgeRuntime/src/ModStrings.cpp
Normal file
@@ -0,0 +1,39 @@
|
||||
#include "ModStrings.h"
|
||||
#include "LogUtil.h"
|
||||
#include <vector>
|
||||
#include <cstring>
|
||||
|
||||
namespace ModStrings
|
||||
{
|
||||
static std::mutex s_mutex;
|
||||
static std::unordered_map<int, std::wstring> s_strings;
|
||||
static int s_nextId = MOD_DESC_ID_BASE;
|
||||
|
||||
void Register(int descriptionId, const wchar_t* value)
|
||||
{
|
||||
if (!value) return;
|
||||
std::lock_guard<std::mutex> lock(s_mutex);
|
||||
s_strings[descriptionId] = value;
|
||||
LogUtil::Log("[LegacyForge] ModStrings: registered id=%d -> %ls", descriptionId, value);
|
||||
}
|
||||
|
||||
const wchar_t* Get(int descriptionId)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(s_mutex);
|
||||
auto it = s_strings.find(descriptionId);
|
||||
if (it != s_strings.end())
|
||||
return it->second.c_str();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int AllocateId()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(s_mutex);
|
||||
return s_nextId++;
|
||||
}
|
||||
|
||||
bool IsModId(int id)
|
||||
{
|
||||
return id >= MOD_DESC_ID_BASE;
|
||||
}
|
||||
}
|
||||
20
LegacyForgeRuntime/src/ModStrings.h
Normal file
20
LegacyForgeRuntime/src/ModStrings.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
#include <mutex>
|
||||
|
||||
/// <summary>
|
||||
/// Stores mod-registered display names for blocks and items.
|
||||
/// Maps description IDs (allocated from MOD_DESC_ID_BASE) to wide strings.
|
||||
/// Hooked into app.GetString() so the game displays mod names.
|
||||
/// </summary>
|
||||
namespace ModStrings
|
||||
{
|
||||
constexpr int MOD_DESC_ID_BASE = 10000;
|
||||
|
||||
void Register(int descriptionId, const wchar_t* value);
|
||||
const wchar_t* Get(int descriptionId);
|
||||
int AllocateId();
|
||||
bool IsModId(int id);
|
||||
}
|
||||
@@ -2,11 +2,22 @@
|
||||
#include "IdRegistry.h"
|
||||
#include "CreativeInventory.h"
|
||||
#include "GameObjectFactory.h"
|
||||
#include "ModStrings.h"
|
||||
#include "LogUtil.h"
|
||||
#include <Windows.h>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
static std::wstring Utf8ToWide(const char* utf8)
|
||||
{
|
||||
if (!utf8 || !utf8[0]) return std::wstring();
|
||||
int len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, nullptr, 0);
|
||||
if (len <= 0) return std::wstring();
|
||||
std::wstring result(len - 1, 0);
|
||||
MultiByteToWideChar(CP_UTF8, 0, utf8, -1, &result[0], len);
|
||||
return result;
|
||||
}
|
||||
|
||||
extern "C"
|
||||
{
|
||||
|
||||
@@ -18,7 +29,8 @@ int native_register_block(
|
||||
int soundType,
|
||||
const char* iconName,
|
||||
float lightEmission,
|
||||
int lightBlock)
|
||||
int lightBlock,
|
||||
const char* displayName)
|
||||
{
|
||||
if (!namespacedId) return -1;
|
||||
|
||||
@@ -32,17 +44,18 @@ int native_register_block(
|
||||
LogUtil::Log("[LegacyForge] Registered block '%s' -> ID %d (hardness=%.1f, resistance=%.1f)",
|
||||
namespacedId, id, hardness, resistance);
|
||||
|
||||
// Convert icon name from UTF-8 to wide string
|
||||
std::wstring wIcon;
|
||||
if (iconName)
|
||||
std::wstring wIcon = Utf8ToWide(iconName);
|
||||
|
||||
int descId = -1;
|
||||
if (displayName && displayName[0])
|
||||
{
|
||||
int len = MultiByteToWideChar(CP_UTF8, 0, iconName, -1, nullptr, 0);
|
||||
wIcon.resize(len > 0 ? len - 1 : 0);
|
||||
MultiByteToWideChar(CP_UTF8, 0, iconName, -1, &wIcon[0], len);
|
||||
descId = ModStrings::AllocateId();
|
||||
std::wstring wName = Utf8ToWide(displayName);
|
||||
ModStrings::Register(descId, wName.c_str());
|
||||
}
|
||||
|
||||
if (!GameObjectFactory::CreateTile(id, materialId, hardness, resistance,
|
||||
soundType, wIcon.empty() ? nullptr : wIcon.c_str()))
|
||||
soundType, wIcon.empty() ? nullptr : wIcon.c_str(), descId))
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] Warning: failed to create game Tile for block '%s' id=%d", namespacedId, id);
|
||||
}
|
||||
@@ -54,7 +67,8 @@ int native_register_item(
|
||||
const char* namespacedId,
|
||||
int maxStackSize,
|
||||
int maxDamage,
|
||||
const char* iconName)
|
||||
const char* iconName,
|
||||
const char* displayName)
|
||||
{
|
||||
if (!namespacedId) return -1;
|
||||
|
||||
@@ -68,15 +82,17 @@ int native_register_item(
|
||||
LogUtil::Log("[LegacyForge] Registered item '%s' -> ID %d (stack=%d, durability=%d)",
|
||||
namespacedId, id, maxStackSize, maxDamage);
|
||||
|
||||
std::wstring wIcon;
|
||||
if (iconName && iconName[0])
|
||||
std::wstring wIcon = Utf8ToWide(iconName);
|
||||
|
||||
int descId = -1;
|
||||
if (displayName && displayName[0])
|
||||
{
|
||||
int len = MultiByteToWideChar(CP_UTF8, 0, iconName, -1, nullptr, 0);
|
||||
wIcon.resize(len > 0 ? len - 1 : 0);
|
||||
MultiByteToWideChar(CP_UTF8, 0, iconName, -1, &wIcon[0], len);
|
||||
descId = ModStrings::AllocateId();
|
||||
std::wstring wName = Utf8ToWide(displayName);
|
||||
ModStrings::Register(descId, wName.c_str());
|
||||
}
|
||||
|
||||
if (!GameObjectFactory::CreateItem(id, maxStackSize, wIcon.empty() ? nullptr : wIcon.c_str()))
|
||||
if (!GameObjectFactory::CreateItem(id, maxStackSize, wIcon.empty() ? nullptr : wIcon.c_str(), descId))
|
||||
{
|
||||
LogUtil::Log("[LegacyForge] Warning: failed to create game Item for '%s' id=%d", namespacedId, id);
|
||||
}
|
||||
@@ -84,6 +100,18 @@ int native_register_item(
|
||||
return id;
|
||||
}
|
||||
|
||||
int native_allocate_description_id()
|
||||
{
|
||||
return ModStrings::AllocateId();
|
||||
}
|
||||
|
||||
void native_register_string(int descriptionId, const char* displayName)
|
||||
{
|
||||
if (!displayName) return;
|
||||
std::wstring wName = Utf8ToWide(displayName);
|
||||
ModStrings::Register(descriptionId, wName.c_str());
|
||||
}
|
||||
|
||||
int native_register_entity(
|
||||
const char* namespacedId,
|
||||
float width,
|
||||
|
||||
@@ -13,13 +13,18 @@ extern "C"
|
||||
int soundType,
|
||||
const char* iconName,
|
||||
float lightEmission,
|
||||
int lightBlock);
|
||||
int lightBlock,
|
||||
const char* displayName);
|
||||
|
||||
__declspec(dllexport) int native_register_item(
|
||||
const char* namespacedId,
|
||||
int maxStackSize,
|
||||
int maxDamage,
|
||||
const char* iconName);
|
||||
const char* iconName,
|
||||
const char* displayName);
|
||||
|
||||
__declspec(dllexport) int native_allocate_description_id();
|
||||
__declspec(dllexport) void native_register_string(int descriptionId, const char* displayName);
|
||||
|
||||
__declspec(dllexport) int native_register_entity(
|
||||
const char* namespacedId,
|
||||
|
||||
@@ -12,6 +12,12 @@ static const char* SYM_EXIT_GAME = "?ExitGame@CConsoleMinecraftApp@@U
|
||||
static const char* SYM_CREATIVE_STATIC_CTOR = "?staticCtor@IUIScene_CreativeMenu@@SAXXZ";
|
||||
static const char* SYM_MAINMENU_CUSTOMDRAW = "?customDraw@UIScene_MainMenu@@UEAAXPEAUIggyCustomDrawCallbackRegion@@@Z";
|
||||
static const char* SYM_PRESENT = "?Present@C4JRender@@QEAAXXZ";
|
||||
static const char* SYM_GET_STRING = "?GetString@CMinecraftApp@@SAPEB_WH@Z";
|
||||
static const char* SYM_GET_RESOURCE_AS_STREAM = "?getResourceAsStream@InputStream@@SAPEAV1@AEBV?$basic_string@_WU?$char_traits@_W@std@@V?$allocator@_W@2@@std@@@Z";
|
||||
static const char* SYM_LOAD_UVS = "?loadUVs@PreStitchedTextureMap@@AEAAXXZ";
|
||||
static const char* SYM_SIMPLE_ICON_CTOR = "??0SimpleIcon@@QEAA@AEBV?$basic_string@_WU?$char_traits@_W@std@@V?$allocator@_W@2@@std@@0MMMM@Z";
|
||||
static const char* SYM_OPERATOR_NEW = "??2@YAPEAX_K@Z";
|
||||
static const char* SYM_REGISTER_ICON = "?registerIcon@PreStitchedTextureMap@@UEAAPEAVIcon@@AEBV?$basic_string@_WU?$char_traits@_W@std@@V?$allocator@_W@2@@std@@@Z";
|
||||
|
||||
bool SymbolResolver::Initialize()
|
||||
{
|
||||
@@ -70,6 +76,17 @@ bool SymbolResolver::ResolveGameFunctions()
|
||||
pCreativeStaticCtor = Resolve(SYM_CREATIVE_STATIC_CTOR);
|
||||
pMainMenuCustomDraw = Resolve(SYM_MAINMENU_CUSTOMDRAW);
|
||||
pPresent = Resolve(SYM_PRESENT);
|
||||
pGetString = Resolve(SYM_GET_STRING);
|
||||
pGetResourceAsStream = Resolve(SYM_GET_RESOURCE_AS_STREAM);
|
||||
pLoadUVs = Resolve(SYM_LOAD_UVS);
|
||||
pSimpleIconCtor = Resolve(SYM_SIMPLE_ICON_CTOR);
|
||||
pOperatorNew = Resolve(SYM_OPERATOR_NEW);
|
||||
pRegisterIcon = Resolve(SYM_REGISTER_ICON);
|
||||
if (!pOperatorNew) pOperatorNew = GetProcAddress(GetModuleHandleA("vcruntime140.dll"), SYM_OPERATOR_NEW);
|
||||
if (!pOperatorNew) pOperatorNew = GetProcAddress(GetModuleHandleA("vcruntime140d.dll"), SYM_OPERATOR_NEW);
|
||||
if (!pOperatorNew) pOperatorNew = GetProcAddress(GetModuleHandle(nullptr), SYM_OPERATOR_NEW);
|
||||
if (!pSimpleIconCtor) PdbParser::DumpMatching("??0SimpleIcon@@");
|
||||
if (!pLoadUVs) PdbParser::DumpMatching("loadUVs@PreStitchedTextureMap");
|
||||
|
||||
auto logSym = [](const char* name, void* ptr) {
|
||||
if (ptr)
|
||||
@@ -85,6 +102,12 @@ bool SymbolResolver::ResolveGameFunctions()
|
||||
logSym("CreativeStaticCtor", pCreativeStaticCtor);
|
||||
logSym("MainMenuCustomDraw", pMainMenuCustomDraw);
|
||||
logSym("C4JRender::Present", pPresent);
|
||||
logSym("CMinecraftApp::GetString", pGetString);
|
||||
logSym("InputStream::getResourceAsStream", pGetResourceAsStream);
|
||||
logSym("PreStitchedTextureMap::loadUVs", pLoadUVs);
|
||||
logSym("SimpleIcon::SimpleIcon", pSimpleIconCtor);
|
||||
logSym("operator new", pOperatorNew);
|
||||
logSym("registerIcon", pRegisterIcon);
|
||||
|
||||
bool ok = pRunStaticCtors && pMinecraftTick && pMinecraftInit;
|
||||
if (ok)
|
||||
|
||||
@@ -17,6 +17,12 @@ public:
|
||||
void* pCreativeStaticCtor = nullptr; // IUIScene_CreativeMenu::staticCtor()
|
||||
void* pMainMenuCustomDraw = nullptr; // UIScene_MainMenu::customDraw()
|
||||
void* pPresent = nullptr; // C4JRender::Present()
|
||||
void* pGetString = nullptr; // CMinecraftApp::GetString(int)
|
||||
void* pGetResourceAsStream = nullptr; // InputStream::getResourceAsStream(wstring)
|
||||
void* pLoadUVs = nullptr; // PreStitchedTextureMap::loadUVs()
|
||||
void* pSimpleIconCtor = nullptr; // SimpleIcon::SimpleIcon(wstring,wstring,float*4)
|
||||
void* pOperatorNew = nullptr; // global operator new(size_t) - for texture injection
|
||||
void* pRegisterIcon = nullptr; // PreStitchedTextureMap::registerIcon(const wstring&)
|
||||
|
||||
private:
|
||||
uintptr_t m_moduleBase = 0;
|
||||
|
||||
1
LegacyForgeRuntime/third_party/stb/.gitkeep
vendored
Normal file
1
LegacyForgeRuntime/third_party/stb/.gitkeep
vendored
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
7988
LegacyForgeRuntime/third_party/stb/stb_image.h
vendored
Normal file
7988
LegacyForgeRuntime/third_party/stb/stb_image.h
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1724
LegacyForgeRuntime/third_party/stb/stb_image_write.h
vendored
Normal file
1724
LegacyForgeRuntime/third_party/stb/stb_image_write.h
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user