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

@@ -195,8 +195,8 @@ public class ExampleMod : IMod
.Hardness(3.0f)
.Resistance(15f)
.Sound(SoundType.Stone)
.Icon("examplemod:ruby_ore") // From assets/blocks/ruby_ore.png
.Name("Ruby Ore")
.Icon("examplemod:block/ruby_ore")
.Name(Text.Translatable("block.examplemod.ruby_ore"))
.RequiredHarvestLevel(2)
.RequiredTool(ToolType.Pickaxe)
.InCreativeTab(CreativeTab.BuildingBlocks));
@@ -207,8 +207,8 @@ public class ExampleMod : IMod
.Hardness(1.5f)
.Resistance(10f)
.Sound(SoundType.Stone)
.Icon("examplemod:ruby_stone")
.Name("Ruby Stone")
.Icon("examplemod:block/ruby_stone")
.Name(Text.Translatable("block.examplemod.ruby_stone"))
.RequiredHarvestLevel(1)
.RequiredTool(ToolType.Pickaxe)
.InCreativeTab(CreativeTab.BuildingBlocks));
@@ -219,8 +219,8 @@ public class ExampleMod : IMod
.Hardness(2.0f)
.Resistance(5f)
.Sound(SoundType.Wood)
.Icon("examplemod:ruby_wood_planks")
.Name("Ruby Wood Planks")
.Icon("examplemod:block/ruby_wood_planks")
.Name(Text.Translatable("block.examplemod.ruby_wood_planks"))
.InCreativeTab(CreativeTab.BuildingBlocks));
RubySand = Registry.Block.Register("examplemod:ruby_sand",
@@ -230,8 +230,8 @@ public class ExampleMod : IMod
.Hardness(0.5f)
.Resistance(2.5f)
.Sound(SoundType.Sand)
.Icon("examplemod:ruby_sand")
.Name("Ruby Sand")
.Icon("examplemod:block/ruby_sand")
.Name(Text.Translatable("block.examplemod.ruby_sand"))
.RequiredTool(ToolType.Shovel)
.InCreativeTab(CreativeTab.BuildingBlocks));
@@ -242,8 +242,8 @@ public class ExampleMod : IMod
.Hardness(1.5f)
.Resistance(10f)
.Sound(SoundType.Stone)
.Icon("examplemod:ruby_stone")
.Name("Ruby Stone Slab")
.Icon("examplemod:block/ruby_stone")
.Name(Text.Translatable("block.examplemod.ruby_stone_slab"))
.RequiredHarvestLevel(1)
.RequiredTool(ToolType.Pickaxe)
.InCreativeTab(CreativeTab.BuildingBlocks));
@@ -255,8 +255,8 @@ public class ExampleMod : IMod
.Hardness(2.0f)
.Resistance(5f)
.Sound(SoundType.Wood)
.Icon("examplemod:ruby_wood_planks")
.Name("Ruby Wood Slab")
.Icon("examplemod:block/ruby_wood_planks")
.Name(Text.Translatable("block.examplemod.ruby_wood_slab"))
.InCreativeTab(CreativeTab.BuildingBlocks));
RubyLamp = Registry.Block.Register("examplemod:ruby_lamp", new RubyLampBlock(false),
@@ -265,8 +265,8 @@ public class ExampleMod : IMod
.Hardness(0.3f)
.Resistance(1.5f)
.Sound(SoundType.Glass)
.Icon("examplemod:ruby_lamp")
.Name("Ruby Lamp")
.Icon("examplemod:block/ruby_lamp")
.Name(Text.Translatable("block.examplemod.ruby_lamp"))
.RequiredHarvestLevel(0)
.RequiredTool(ToolType.Pickaxe)
.AcceptsRedstonePower()
@@ -283,9 +283,9 @@ public class ExampleMod : IMod
.Hardness(0.3f)
.Resistance(1.5f)
.Sound(SoundType.Glass)
.Icon("examplemod:ruby_lamp_on")
.Icon("examplemod:block/ruby_lamp_on")
.LightLevel(1.0f)
.Name("Ruby Lamp")
.Name(Text.Translatable("block.examplemod.ruby_lamp_lit"))
.RequiredHarvestLevel(0)
.RequiredTool(ToolType.Pickaxe)
.AcceptsRedstonePower());
@@ -296,8 +296,8 @@ public class ExampleMod : IMod
.Hardness(5.0f)
.Resistance(30f)
.Sound(SoundType.Metal)
.Icon("examplemod:orichalcum_ore") // From assets/blocks/orichalcum.png
.Name("Orichalcum Ore")
.Icon("examplemod:block/orichalcum_ore")
.Name(Text.Translatable("block.examplemod.orichalcum_ore"))
.RequiredHarvestLevel(4)
.RequiredTool(ToolType.Pickaxe)
.InCreativeTab(CreativeTab.BuildingBlocks));
@@ -305,8 +305,8 @@ public class ExampleMod : IMod
Ruby = Registry.Item.Register("examplemod:ruby",
new ItemProperties()
.MaxStackSize(64)
.Icon("examplemod:ruby") // From assets/items/ruby.png
.Name("Ruby")
.Icon("examplemod:item/ruby")
.Name(Text.Translatable("item.examplemod.ruby"))
.InCreativeTab(CreativeTab.Materials));
Registry.Item.RegisterToolMaterial("examplemod:ruby_material",
@@ -320,8 +320,8 @@ public class ExampleMod : IMod
.MaxStackSize(1)
.MaxDamage(512)
.AttackDamage(8.0f)
.Icon("examplemod:ruby_sword")
.Name("Ruby Sword")
.Icon("examplemod:item/ruby_sword")
.Name(Text.Translatable("item.examplemod.ruby_sword"))
.InCreativeTab(CreativeTab.ToolsAndWeapons));
RubyShovelItem = Registry.Item.Register("examplemod:ruby_shovel", new RubyShovel { CustomMaterialId = "examplemod:ruby_material" },
@@ -329,8 +329,8 @@ public class ExampleMod : IMod
.MaxStackSize(1)
.MaxDamage(512)
.AttackDamage(4.5f)
.Icon("examplemod:ruby_shovel")
.Name("Ruby Shovel")
.Icon("examplemod:item/ruby_shovel")
.Name(Text.Translatable("item.examplemod.ruby_shovel"))
.InCreativeTab(CreativeTab.ToolsAndWeapons));
RubyPickaxeItem = Registry.Item.Register("examplemod:ruby_pickaxe", new RubyPickaxe { CustomMaterialId = "examplemod:ruby_material" },
@@ -338,8 +338,8 @@ public class ExampleMod : IMod
.MaxStackSize(1)
.MaxDamage(512)
.AttackDamage(5.0f)
.Icon("examplemod:ruby_pickaxe") // From assets/items/ruby_pickaxe.png
.Name("Ruby Pickaxe")
.Icon("examplemod:item/ruby_pickaxe")
.Name(Text.Translatable("item.examplemod.ruby_pickaxe"))
.InCreativeTab(CreativeTab.ToolsAndWeapons));
RubyAxeItem = Registry.Item.Register("examplemod:ruby_axe", new RubyAxe { CustomMaterialId = "examplemod:ruby_material" },
@@ -347,8 +347,8 @@ public class ExampleMod : IMod
.MaxStackSize(1)
.MaxDamage(512)
.AttackDamage(7.0f)
.Icon("examplemod:ruby_axe")
.Name("Ruby Axe")
.Icon("examplemod:item/ruby_axe")
.Name(Text.Translatable("item.examplemod.ruby_axe"))
.InCreativeTab(CreativeTab.ToolsAndWeapons));
RubyHoeItem = Registry.Item.Register("examplemod:ruby_hoe", new RubyHoe { CustomMaterialId = "examplemod:ruby_material" },
@@ -356,15 +356,15 @@ public class ExampleMod : IMod
.MaxStackSize(1)
.MaxDamage(512)
.AttackDamage(1.0f)
.Icon("examplemod:ruby_hoe")
.Name("Ruby Hoe")
.Icon("examplemod:item/ruby_hoe")
.Name(Text.Translatable("item.examplemod.ruby_hoe"))
.InCreativeTab(CreativeTab.ToolsAndWeapons));
RubyWandItem = Registry.Item.Register("examplemod:ruby_wand", new RubyWand(),
new ItemProperties()
.MaxStackSize(1)
.Icon("examplemod:ruby_wand") // From assets/items/ruby_wand.png
.Name("Ruby Wand")
.Icon("examplemod:item/ruby_wand")
.Name(Text.Translatable("item.examplemod.ruby_wand"))
.InCreativeTab(CreativeTab.ToolsAndWeapons));
Registry.Recipe.AddFurnace("examplemod:ruby_ore", "examplemod:ruby", 1.0f);

View File

@@ -2,24 +2,40 @@
## Language files
Language files live in `assets/lang/` with the format `{locale}.lang` (e.g. `en-GB.lang`, `de-DE.lang`).
Language files live in `assets/examplemod/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.
Use `Text.Translatable()` with `BlockProperties.Name()` / `ItemProperties.Name()` to pull localized strings
from these `.lang` files. Format: `key=value` per line, with `#` for comments.
**Future:** Multi-locale support may load from these `.lang` files. Format: `key=value` per line, with `#` for comments.
Example:
```csharp
.Name(Text.Translatable("item.examplemod.ruby"))
.Name(Text.Literal("Ruby")) // literal fallback if you don't want localization
```
## Textures
Mod textures are supported via the dynamic atlas system. Place PNG files in:
Mod textures use Java-style asset paths. 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`)
- **Blocks:** `assets/examplemod/textures/block/{name}.png` → icon `examplemod:block/{name}`
- **Items:** `assets/examplemod/textures/item/{name}.png` → icon `examplemod:item/{name}`
The mod ID is derived from the mod folder name (lowercase, hyphens removed). Use the namespaced icon in `BlockProperties.Icon()` and `ItemProperties.Icon()`:
Use the Java-style 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
.Icon("examplemod:block/ruby_ore") // block from assets/examplemod/textures/block/ruby_ore.png
.Icon("examplemod:item/ruby") // item from assets/examplemod/textures/item/ruby.png
```
Textures must be 16×16 pixels (or any size; they are scaled). For vanilla icons, use names like `gold_ore`, `diamond`, etc.
Textures must be 16×16 pixels (or any size; they are scaled).
## Models (Java-style)
Block and item models are supported using Java-style JSON assets:
- **Blocks:** `assets/examplemod/models/block/{name}.json`
- **Items:** `assets/examplemod/models/item/{name}.json`
- **Entities (future):** `assets/examplemod/models/entity/{name}.json`
The `examplemod` namespace should match your mod ID (lowercase).

View File

@@ -1,7 +1,4 @@
# 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
block.examplemod.ruby_stone=Ruby Stone

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1,5 +0,0 @@
# ExampleMod language file (de-DE)
# German translations for ExampleMod content.
block.examplemod.ruby_ore=Rubinerz
item.examplemod.ruby=Rubin

View File

@@ -205,17 +205,17 @@ public class MyMod : IMod
.Hardness(3.0f)
.Resistance(15.0f)
.Sound(SoundType.Stone)
.Icon("mymod:example_ore")
.Icon("mymod:block/example_ore")
.InCreativeTab(CreativeTab.BuildingBlocks)
.Name("Example Ore"));
.Name(Text.Translatable("block.mymod.example_ore")));
// Register an item
var gem = Registry.Item.Register("mymod:example_gem",
new ItemProperties()
.MaxStackSize(64)
.Icon("mymod:example_gem")
.Icon("mymod:item/example_gem")
.InCreativeTab(CreativeTab.Materials)
.Name("Example Gem"));
.Name(Text.Translatable("item.mymod.example_gem")));
// Add a smelting recipe
Registry.Recipe.AddFurnace("mymod:example_ore", "mymod:example_gem", 1.0f);
@@ -239,15 +239,43 @@ Place 16x16 PNG textures in your mod's assets folder:
```
MyMod/
├── assets/
── blocks/
└── example_ore.png # Block texture
└── items/
│ └── example_gem.png # Item texture
── mymod/
└── textures/
├── block/
└── example_ore.png # Block texture
│ └── item/
│ └── example_gem.png # Item texture
├── MyMod.cs
└── MyMod.csproj
```
The icon name in `BlockProperties.Icon()` / `ItemProperties.Icon()` uses the format `"modid:texture_name"` where `texture_name` matches the PNG filename (without extension).
The icon name in `BlockProperties.Icon()` / `ItemProperties.Icon()` uses Java-style names like:
```csharp
.Icon("mymod:block/example_ore")
.Icon("mymod:item/example_gem")
```
### 4b. Add models (block/item/entity)
Place Java-style JSON model files under an `assets/<namespace>/models/` tree inside your mod folder:
```
MyMod/
├── assets/
│ └── mymod/
│ └── models/
│ ├── block/
│ │ └── example_ore.json
│ ├── item/
│ │ └── example_gem.json
│ └── entity/
│ └── example_entity.json # reserved for future entity model support
├── MyMod.cs
└── MyMod.csproj
```
The `namespace` folder should match your mod's ID (lowercase).
### 5. Build and run
@@ -322,6 +350,13 @@ All log output goes to `logs/weaveloader.log` with timestamps and log level pref
Namespaced string IDs follow the `"namespace:path"` convention (e.g., `"mymod:ruby_ore"`). If no namespace is provided, `"minecraft"` is assumed.
### Localization
Use `Text.Translatable("item.examplemod.ruby")` for localized names or `Text.Literal("Ruby")` for fixed text.
Language files live at `assets/<namespace>/lang/<locale>.lang` using `key=value` lines.
By default, WeaveLoader follows the game's current language selection; disable with `Localization.UseGameLanguage = false`.
If the game reports "system default", the locale falls back to the system UI culture; override with `Localization.Locale = "en-GB"`.
## ID Ranges
| Type | Numeric Range | Notes |
@@ -338,7 +373,7 @@ The runtime opens the game's PDB file and parses it using [raw_pdb](https://gith
### Texture Atlas Merging
1. Mod textures are discovered in `mods/*/assets/blocks/` and `mods/*/assets/items/`
1. Mod textures are discovered in `mods/*/assets/<namespace>/textures/block/` and `mods/*/assets/<namespace>/textures/item/`
2. The vanilla `terrain.png` (16x32 grid) and `items.png` (16x16 grid) are loaded via stb_image
3. Empty cells are identified by checking for fully transparent pixels
4. Mod textures are placed into empty cells
@@ -346,6 +381,12 @@ The runtime opens the game's PDB file and parses it using [raw_pdb](https://gith
6. A `CreateFileW` hook temporarily redirects the game's file opens to the merged atlases during init, then is removed once textures are loaded into GPU memory
7. `SimpleIcon` objects are created for each mod texture with correct UV coordinates
### Model Asset Overlay
When the game requests `assets/<namespace>/models/...` via `InputStream::getResourceAsStream`,
WeaveLoader will redirect to a matching file inside `mods/*/assets/<namespace>/models/`.
Block and item models are supported today; `models/entity/` is wired up for future use.
### String Table Injection
The game's `CMinecraftApp::GetString(int)` is a thin wrapper around `StringTable::getString(int)`, which does a vector index lookup. Since MSVC's link-time optimization inlines `GetString` at call sites like `Item::getHoverName`, a MinHook detour alone isn't sufficient. The runtime parses the x64 machine code of `GetString` to locate the RIP-relative reference to `app.m_stringTable`, then directly resizes the string table's internal vector and writes mod strings at the allocated indices.

View File

@@ -1,3 +1,5 @@
using WeaveLoader.API;
namespace WeaveLoader.API.Block;
/// <summary>
@@ -62,7 +64,7 @@ public class BlockProperties
internal float LightEmissionValue = 0.0f;
internal int LightBlockValue = 255;
internal CreativeTab CreativeTabValue = CreativeTab.None;
internal string? NameValue;
internal Text? NameValue;
internal int RequiredHarvestLevelValue = -1;
internal ToolType RequiredToolValue = ToolType.None;
internal bool AcceptsRedstonePowerValue;
@@ -71,14 +73,19 @@ public class BlockProperties
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>
/// <summary>
/// Icon name in the terrain atlas. Use Java-style names like "examplemod:block/ruby_ore"
/// from assets/examplemod/textures/block/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; }
public BlockProperties Name(string displayName) { NameValue = Text.Literal(displayName); return this; }
/// <summary>Localized display name using a language key (e.g. "block.examplemod.ruby_ore").</summary>
public BlockProperties Name(Text text) { NameValue = text; return this; }
/// <summary>Minimum harvest level required to properly mine this block (e.g. 3 for obsidian). -1 means no requirement.</summary>
public BlockProperties RequiredHarvestLevel(int level) { RequiredHarvestLevelValue = level; return this; }
/// <summary>Tool type required to harvest this block (e.g. Pickaxe for stone-like blocks).</summary>

View File

@@ -59,7 +59,7 @@ public static class BlockRegistry
properties.IconValue,
properties.LightEmissionValue,
properties.LightBlockValue,
properties.NameValue ?? "",
properties.NameValue?.Resolve() ?? "",
properties.RequiredHarvestLevelValue,
(int)properties.RequiredToolValue,
properties.AcceptsRedstonePowerValue ? 1 : 0);
@@ -98,7 +98,7 @@ public static class BlockRegistry
properties.IconValue,
properties.LightEmissionValue,
properties.LightBlockValue,
properties.NameValue ?? "",
properties.NameValue?.Resolve() ?? "",
properties.RequiredHarvestLevelValue,
(int)properties.RequiredToolValue,
properties.AcceptsRedstonePowerValue ? 1 : 0);
@@ -144,7 +144,7 @@ public static class BlockRegistry
properties.IconValue,
properties.LightEmissionValue,
properties.LightBlockValue,
properties.NameValue ?? "",
properties.NameValue?.Resolve() ?? "",
properties.RequiredHarvestLevelValue,
(int)properties.RequiredToolValue,
properties.AcceptsRedstonePowerValue ? 1 : 0);
@@ -191,7 +191,7 @@ public static class BlockRegistry
properties.IconValue,
properties.LightEmissionValue,
properties.LightBlockValue,
properties.NameValue ?? "",
properties.NameValue?.Resolve() ?? "",
properties.RequiredHarvestLevelValue,
(int)properties.RequiredToolValue,
properties.AcceptsRedstonePowerValue ? 1 : 0,

View File

@@ -1,3 +1,5 @@
using WeaveLoader.API;
namespace WeaveLoader.API.Item;
/// <summary>
@@ -10,10 +12,13 @@ public class ItemProperties
internal float AttackDamageValue = 0.0f;
internal string IconValue = "";
internal CreativeTab CreativeTabValue = CreativeTab.None;
internal string? NameValue;
internal Text? 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>
/// <summary>
/// Icon name in the items atlas. Use Java-style names like "examplemod:item/ruby"
/// from assets/examplemod/textures/item/ruby.png, or vanilla names like "diamond", "ingotIron".
/// </summary>
public ItemProperties Icon(string iconName) { IconValue = iconName; return this; }
/// <summary>
@@ -25,5 +30,7 @@ public class ItemProperties
public ItemProperties AttackDamage(float damage) { AttackDamageValue = damage; 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; }
public ItemProperties Name(string displayName) { NameValue = Text.Literal(displayName); return this; }
/// <summary>Localized display name using a language key (e.g. "item.examplemod.ruby").</summary>
public ItemProperties Name(Text text) { NameValue = text; return this; }
}

View File

@@ -116,7 +116,7 @@ public static class ItemRegistry
(int)nativeTier,
maxDamage,
properties.IconValue,
properties.NameValue ?? "");
properties.NameValue?.Resolve() ?? "");
if (numericId >= 0)
ConfigureToolMaterial(id, numericId, ToolKind.Pickaxe, material, properties);
@@ -129,7 +129,7 @@ public static class ItemRegistry
(int)(material?.BaseTierValue ?? shovelItem.Tier),
properties.MaxDamageValue,
properties.IconValue,
properties.NameValue ?? "");
properties.NameValue?.Resolve() ?? "");
if (numericId >= 0)
ConfigureToolMaterial(id, numericId, ToolKind.Shovel, material, properties);
@@ -142,7 +142,7 @@ public static class ItemRegistry
(int)(material?.BaseTierValue ?? hoeItem.Tier),
properties.MaxDamageValue,
properties.IconValue,
properties.NameValue ?? "");
properties.NameValue?.Resolve() ?? "");
if (numericId >= 0)
ConfigureToolMaterial(id, numericId, ToolKind.Hoe, material, properties);
@@ -155,7 +155,7 @@ public static class ItemRegistry
(int)(material?.BaseTierValue ?? axeItem.Tier),
properties.MaxDamageValue,
properties.IconValue,
properties.NameValue ?? "");
properties.NameValue?.Resolve() ?? "");
if (numericId >= 0)
ConfigureToolMaterial(id, numericId, ToolKind.Axe, material, properties);
@@ -168,7 +168,7 @@ public static class ItemRegistry
(int)(material?.BaseTierValue ?? swordItem.Tier),
properties.MaxDamageValue,
properties.IconValue,
properties.NameValue ?? "");
properties.NameValue?.Resolve() ?? "");
if (numericId >= 0)
ConfigureToolMaterial(id, numericId, ToolKind.Sword, material, properties);
@@ -180,7 +180,7 @@ public static class ItemRegistry
properties.MaxStackSizeValue,
properties.MaxDamageValue,
properties.IconValue,
properties.NameValue ?? "");
properties.NameValue?.Resolve() ?? "");
}
if (numericId < 0)

View File

@@ -145,6 +145,15 @@ internal static class NativeInterop
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
internal static extern void native_register_string(int descriptionId, string displayName);
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl)]
internal static extern int native_get_minecraft_language();
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl)]
internal static extern int native_get_minecraft_locale();
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl)]
internal static extern nint native_get_mods_path();
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
internal static extern int native_register_entity(
string namespacedId,

286
WeaveLoader.API/Text.cs Normal file
View File

@@ -0,0 +1,286 @@
using System.Globalization;
using System.Runtime.InteropServices;
namespace WeaveLoader.API;
/// <summary>
/// Localized text value used for names, tooltips, and UI strings.
/// </summary>
public sealed class Text
{
public enum TextKind
{
Literal = 0,
Translatable = 1
}
public TextKind Kind { get; }
public string Value { get; }
private Text(TextKind kind, string value)
{
ArgumentException.ThrowIfNullOrWhiteSpace(value);
Kind = kind;
Value = value;
}
public static Text Literal(string value) => new(TextKind.Literal, value);
public static Text Translatable(string key) => new(TextKind.Translatable, key);
internal string Resolve()
{
return Kind == TextKind.Literal ? Value : Localization.Resolve(Value);
}
}
/// <summary>
/// Simple language table loader for mod translations (Java-style .lang files).
/// </summary>
public static class Localization
{
private static readonly object s_lock = new();
private static string s_locale = GetDefaultLocale();
private static string? s_loadedLocale;
private static Dictionary<string, string> s_entries = new(StringComparer.Ordinal);
private static int s_lastGameLanguage = int.MinValue;
/// <summary>When true, follow the game's current language selection.</summary>
public static bool UseGameLanguage { get; set; } = true;
/// <summary>Active locale used when resolving translatable keys (e.g. "en-GB").</summary>
public static string Locale
{
get => s_locale;
set
{
ArgumentException.ThrowIfNullOrWhiteSpace(value);
lock (s_lock)
{
s_locale = value;
s_loadedLocale = null;
}
}
}
/// <summary>Reload language files for the current locale.</summary>
public static void Reload()
{
lock (s_lock)
{
s_loadedLocale = null;
}
}
internal static string Resolve(string key)
{
EnsureLoaded();
return s_entries.TryGetValue(key, out var value) ? value : key;
}
private static void EnsureLoaded()
{
lock (s_lock)
{
if (UseGameLanguage)
{
string? gameLocale = TryGetGameLocale();
if (!string.IsNullOrWhiteSpace(gameLocale) && !string.Equals(gameLocale, s_locale, StringComparison.OrdinalIgnoreCase))
{
s_locale = gameLocale;
s_loadedLocale = null;
}
}
if (s_loadedLocale == s_locale)
return;
s_entries = LoadLocaleTable(s_locale);
s_loadedLocale = s_locale;
}
}
private static string GetDefaultLocale()
{
string? gameLocale = TryGetGameLocale();
if (!string.IsNullOrWhiteSpace(gameLocale))
return gameLocale!;
var env = Environment.GetEnvironmentVariable("WEAVELOADER_LOCALE");
if (!string.IsNullOrWhiteSpace(env))
return env;
try
{
var culture = CultureInfo.CurrentUICulture;
if (!string.IsNullOrWhiteSpace(culture.Name))
return culture.Name.Replace('_', '-');
}
catch
{
// ignore
}
return "en-GB";
}
private static string? TryGetGameLocale()
{
try
{
int lang = NativeInterop.native_get_minecraft_language();
if (lang == s_lastGameLanguage)
return null;
s_lastGameLanguage = lang;
return MapLanguageToLocale(lang);
}
catch
{
return null;
}
}
private static string? MapLanguageToLocale(int lang)
{
// 0 = default (system); return null so we fall back to system culture / env.
return lang switch
{
1 => "en-GB",
2 => "ja-JP",
3 => "de-DE",
4 => "fr-FR",
5 => "es-ES",
6 => "it-IT",
7 => "ko-KR",
8 => "zh-TW",
9 => "pt-PT",
10 => "pt-BR",
11 => "ru-RU",
12 => "nl-NL",
13 => "fi-FI",
14 => "sv-SE",
15 => "da-DK",
16 => "no-NO",
17 => "pl-PL",
18 => "tr-TR",
19 => "es-MX",
20 => "el-GR",
_ => null
};
}
private static Dictionary<string, string> LoadLocaleTable(string locale)
{
var entries = new Dictionary<string, string>(StringComparer.Ordinal);
var locales = BuildLocaleFallbacks(locale);
foreach (var loc in locales)
LoadLocaleFiles(entries, loc);
return entries;
}
private static List<string> BuildLocaleFallbacks(string locale)
{
var list = new List<string>();
void Add(string value)
{
if (string.IsNullOrWhiteSpace(value))
return;
if (!list.Contains(value, StringComparer.OrdinalIgnoreCase))
list.Add(value);
}
Add(locale);
Add(locale.Replace('_', '-'));
Add(locale.Replace('-', '_'));
Add("en-GB");
Add("en-US");
return list;
}
private static void LoadLocaleFiles(Dictionary<string, string> entries, string locale)
{
foreach (var modsPath in GetModsRoots())
{
foreach (var modDir in Directory.EnumerateDirectories(modsPath))
{
var assetsDir = Path.Combine(modDir, "assets");
if (!Directory.Exists(assetsDir))
continue;
foreach (var nsDir in Directory.EnumerateDirectories(assetsDir))
{
var langFile = Path.Combine(nsDir, "lang", $"{locale}.lang");
if (!File.Exists(langFile))
continue;
ParseLangFile(langFile, entries);
}
}
}
}
private static void ParseLangFile(string path, Dictionary<string, string> entries)
{
foreach (var rawLine in File.ReadLines(path))
{
var line = rawLine.Trim();
if (line.Length == 0 || line.StartsWith('#'))
continue;
int idx = line.IndexOf('=');
if (idx <= 0 || idx >= line.Length - 1)
continue;
string key = line[..idx].Trim();
string value = line[(idx + 1)..].Trim();
if (key.Length == 0)
continue;
entries[key] = value;
}
}
private static List<string> GetModsRoots()
{
var roots = new List<string>();
void AddCandidate(string? path)
{
if (string.IsNullOrWhiteSpace(path))
return;
string full;
try
{
full = Path.GetFullPath(path);
}
catch
{
return;
}
if (!Directory.Exists(full))
return;
if (!roots.Contains(full, StringComparer.OrdinalIgnoreCase))
roots.Add(full);
}
AddCandidate(GetNativeModsPath());
var baseDir = AppContext.BaseDirectory;
AddCandidate(Path.Combine(baseDir, "mods"));
AddCandidate(Path.Combine(baseDir, "..", "mods"));
AddCandidate(Path.Combine(baseDir, "..", "..", "mods"));
var cwd = Directory.GetCurrentDirectory();
AddCandidate(Path.Combine(cwd, "mods"));
AddCandidate(Path.Combine(cwd, "..", "mods"));
return roots;
}
private static string? GetNativeModsPath()
{
try
{
var ptr = NativeInterop.native_get_mods_path();
if (ptr == nint.Zero)
return null;
return Marshal.PtrToStringAnsi(ptr);
}
catch
{
return null;
}
}
}

View File

@@ -15,10 +15,10 @@
<!-- Mod textures: use Target so they only copy when API builds to mods/, not when copied as dependency -->
<Target Name="CopyModAssets" AfterTargets="Build">
<MakeDir Directories="$(OutputPath)assets\blocks" />
<MakeDir Directories="$(OutputPath)assets\items" />
<Copy SourceFiles="mod_assets\blocks\missing_block.png" DestinationFolder="$(OutputPath)assets\blocks\" SkipUnchangedFiles="true" />
<Copy SourceFiles="mod_assets\items\missing_item.png" DestinationFolder="$(OutputPath)assets\items\" SkipUnchangedFiles="true" />
<MakeDir Directories="$(OutputPath)assets\weaveloader.api\textures\block" />
<MakeDir Directories="$(OutputPath)assets\weaveloader.api\textures\item" />
<Copy SourceFiles="mod_assets\weaveloader.api\textures\block\missing_block.png" DestinationFolder="$(OutputPath)assets\weaveloader.api\textures\block\" SkipUnchangedFiles="true" />
<Copy SourceFiles="mod_assets\weaveloader.api\textures\item\missing_item.png" DestinationFolder="$(OutputPath)assets\weaveloader.api\textures\item\" SkipUnchangedFiles="true" />
</Target>
</Project>

View File

@@ -2,9 +2,9 @@
Placeholder textures for the missing block and missing item (unresolved mod content).
Stored in `mod_assets/` to avoid conflict with the `Assets/` C# source folder (Windows case-insensitivity).
Copied to `assets/blocks/` and `assets/items/` in the build output.
Copied to `assets/weaveloader.api/textures/block/` and `assets/weaveloader.api/textures/item/` in the build output.
- **Blocks:** `mod_assets/blocks/missing_block.png` → icon `weaveloader.api:missing_block`
- **Items:** `mod_assets/items/missing_item.png` → icon `weaveloader.api:missing_item`
- **Blocks:** `mod_assets/weaveloader.api/textures/block/missing_block.png` → icon `weaveloader.api:block/missing_block`
- **Items:** `mod_assets/weaveloader.api/textures/item/missing_item.png` → icon `weaveloader.api:item/missing_item`
These are used when a world contains blocks or items from mods that are no longer installed.

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

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);
}