feat(api/runtime): java-style assets and localization sync
@@ -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);
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
@@ -1,5 +0,0 @@
|
||||
# ExampleMod language file (de-DE)
|
||||
# German translations for ExampleMod content.
|
||||
|
||||
block.examplemod.ruby_ore=Rubinerz
|
||||
item.examplemod.ruby=Rubin
|
||||
61
README.md
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||