diff --git a/ExampleMod/ExampleMod.cs b/ExampleMod/ExampleMod.cs
index 81063e7..f7a7bda 100644
--- a/ExampleMod/ExampleMod.cs
+++ b/ExampleMod/ExampleMod.cs
@@ -2,6 +2,7 @@ using WeaveLoader.API;
using WeaveLoader.API.Block;
using WeaveLoader.API.Item;
using WeaveLoader.API.Events;
+using WeaveLoader.API.Loot;
namespace ExampleMod;
@@ -11,6 +12,7 @@ public class ExampleMod : IMod
{
private static nint s_currentLevel;
private static bool s_hasLevel;
+ private static bool s_loggedCreeperLootInjection;
public static RegisteredBlock? RubyOre;
public static RegisteredBlock? RubyStone;
public static RegisteredBlock? RubyWoodPlanks;
@@ -561,6 +563,29 @@ public class ExampleMod : IMod
.Name(Text.Translatable("item.examplemod.ruby_wand"))
.InCreativeTab(CreativeTab.ToolsAndWeapons));
+ LootTableEvents.MODIFY.Register((_, _, tableId, tableBuilder, _) =>
+ {
+ if (!tableId.Namespace.Equals("minecraft", StringComparison.OrdinalIgnoreCase) ||
+ !tableId.Path.Equals("entities/creeper", StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+
+ var pool = LootPool.builder()
+ .rolls(ConstantLootNumberProvider.create(1))
+ .conditionally(RandomChanceLootCondition.builder(0.05f))
+ .with(ItemEntry.builder(new Identifier("examplemod:ruby_sand"))
+ .apply(new SetCountLootFunction(1)));
+
+ tableBuilder.pool(pool);
+
+ if (!s_loggedCreeperLootInjection)
+ {
+ s_loggedCreeperLootInjection = true;
+ Logger.Info("ExampleMod: injected creeper loot pool (ruby_sand)");
+ }
+ });
+
Registry.Recipe.AddFurnace("examplemod:ruby_ore", "examplemod:ruby", 1.0f);
GameEvents.OnBlockBreak += OnBlockBroken;
diff --git a/ExampleMod/ExampleMod.csproj b/ExampleMod/ExampleMod.csproj
index 7421baa..22d3b1d 100644
--- a/ExampleMod/ExampleMod.csproj
+++ b/ExampleMod/ExampleMod.csproj
@@ -22,6 +22,7 @@
+
diff --git a/ExampleMod/data/examplemod/loot_tables/blocks/debug_hooks.json b/ExampleMod/data/examplemod/loot_tables/blocks/debug_hooks.json
new file mode 100644
index 0000000..7898277
--- /dev/null
+++ b/ExampleMod/data/examplemod/loot_tables/blocks/debug_hooks.json
@@ -0,0 +1,14 @@
+{
+ "type": "minecraft:block",
+ "pools": [
+ {
+ "rolls": 1,
+ "entries": [
+ {
+ "type": "minecraft:item",
+ "name": "examplemod:debug_item"
+ }
+ ]
+ }
+ ]
+}
diff --git a/ExampleMod/data/minecraft/loot_tables/entities/chicken.json b/ExampleMod/data/minecraft/loot_tables/entities/chicken.json
new file mode 100644
index 0000000..755f3be
--- /dev/null
+++ b/ExampleMod/data/minecraft/loot_tables/entities/chicken.json
@@ -0,0 +1,47 @@
+{
+ "type": "minecraft:entity",
+ "pools": [
+ {
+ "rolls": 1,
+ "entries": [
+ {
+ "type": "minecraft:empty",
+ "weight": 50
+ },
+ {
+ "type": "minecraft:item",
+ "name": "examplemod:ruby",
+ "weight": 30,
+ "functions": [
+ {
+ "function": "minecraft:set_count",
+ "count": 1
+ }
+ ]
+ },
+ {
+ "type": "minecraft:item",
+ "name": "examplemod:ruby",
+ "weight": 15,
+ "functions": [
+ {
+ "function": "minecraft:set_count",
+ "count": 2
+ }
+ ]
+ },
+ {
+ "type": "minecraft:item",
+ "name": "examplemod:ruby",
+ "weight": 5,
+ "functions": [
+ {
+ "function": "minecraft:set_count",
+ "count": 3
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/WeaveLoader.API/Loot/LootConditions.cs b/WeaveLoader.API/Loot/LootConditions.cs
new file mode 100644
index 0000000..16d1be3
--- /dev/null
+++ b/WeaveLoader.API/Loot/LootConditions.cs
@@ -0,0 +1,28 @@
+namespace WeaveLoader.API.Loot;
+
+public interface ILootCondition
+{
+ bool Test(Random random);
+}
+
+public sealed class RandomChanceLootCondition : ILootCondition
+{
+ private readonly float _chance;
+
+ private RandomChanceLootCondition(float chance)
+ {
+ _chance = chance;
+ }
+
+ public static RandomChanceLootCondition builder(float chance)
+ {
+ return new RandomChanceLootCondition(chance);
+ }
+
+ public bool Test(Random random)
+ {
+ if (random == null)
+ return false;
+ return random.NextDouble() < _chance;
+ }
+}
diff --git a/WeaveLoader.API/Loot/LootEntries.cs b/WeaveLoader.API/Loot/LootEntries.cs
new file mode 100644
index 0000000..08dc6a9
--- /dev/null
+++ b/WeaveLoader.API/Loot/LootEntries.cs
@@ -0,0 +1,68 @@
+using WeaveLoader.API;
+
+namespace WeaveLoader.API.Loot;
+
+public abstract class LootEntry
+{
+ public int Weight { get; internal set; } = 1;
+ internal List Functions { get; } = new();
+}
+
+public abstract class LootEntryBuilder
+{
+ internal abstract LootEntry Build();
+}
+
+public sealed class ItemEntry : LootEntry
+{
+ public Identifier ItemId { get; }
+
+ internal ItemEntry(Identifier itemId)
+ {
+ ItemId = itemId;
+ }
+
+ public static ItemEntryBuilder builder(Identifier itemId)
+ {
+ return new ItemEntryBuilder(itemId);
+ }
+
+ public sealed class ItemEntryBuilder : LootEntryBuilder
+ {
+ private readonly Identifier _itemId;
+ private int _weight = 1;
+ private readonly List _functions = new();
+
+ internal ItemEntryBuilder(Identifier itemId)
+ {
+ _itemId = itemId;
+ }
+
+ public ItemEntryBuilder weight(int weight)
+ {
+ _weight = weight;
+ return this;
+ }
+
+ public ItemEntryBuilder apply(ILootFunction function)
+ {
+ _functions.Add(function);
+ return this;
+ }
+
+ internal override LootEntry Build()
+ {
+ var entry = new ItemEntry(_itemId) { Weight = _weight };
+ entry.Functions.AddRange(_functions);
+ return entry;
+ }
+ }
+}
+
+public sealed class EmptyEntry : LootEntry
+{
+ internal EmptyEntry(int weight)
+ {
+ Weight = weight;
+ }
+}
diff --git a/WeaveLoader.API/Loot/LootFunctions.cs b/WeaveLoader.API/Loot/LootFunctions.cs
new file mode 100644
index 0000000..6bf716e
--- /dev/null
+++ b/WeaveLoader.API/Loot/LootFunctions.cs
@@ -0,0 +1,25 @@
+using WeaveLoader.API;
+
+namespace WeaveLoader.API.Loot;
+
+public interface ILootFunction
+{
+ void Apply(ref LootDrop drop, Random random);
+}
+
+public sealed class SetCountLootFunction : ILootFunction
+{
+ private readonly int _count;
+
+ public SetCountLootFunction(int count)
+ {
+ _count = count;
+ }
+
+ public void Apply(ref LootDrop drop, Random random)
+ {
+ drop = drop with { Count = _count };
+ }
+}
+
+public readonly record struct LootDrop(Identifier ItemId, int Count, int Aux);
diff --git a/WeaveLoader.API/Loot/LootManager.cs b/WeaveLoader.API/Loot/LootManager.cs
new file mode 100644
index 0000000..fecd973
--- /dev/null
+++ b/WeaveLoader.API/Loot/LootManager.cs
@@ -0,0 +1,9 @@
+namespace WeaveLoader.API.Loot;
+
+///
+/// Placeholder loot manager exposed to event callbacks.
+///
+public sealed class LootManager
+{
+ internal LootManager() { }
+}
diff --git a/WeaveLoader.API/Loot/LootNumbers.cs b/WeaveLoader.API/Loot/LootNumbers.cs
new file mode 100644
index 0000000..17e1b6f
--- /dev/null
+++ b/WeaveLoader.API/Loot/LootNumbers.cs
@@ -0,0 +1,23 @@
+namespace WeaveLoader.API.Loot;
+
+public interface ILootNumberProvider
+{
+ int NextInt(Random random);
+}
+
+public sealed class ConstantLootNumberProvider : ILootNumberProvider
+{
+ private readonly int _value;
+
+ private ConstantLootNumberProvider(int value)
+ {
+ _value = value;
+ }
+
+ public static ConstantLootNumberProvider create(int value)
+ {
+ return new ConstantLootNumberProvider(value);
+ }
+
+ public int NextInt(Random random) => _value;
+}
diff --git a/WeaveLoader.API/Loot/LootPool.cs b/WeaveLoader.API/Loot/LootPool.cs
new file mode 100644
index 0000000..e904e3c
--- /dev/null
+++ b/WeaveLoader.API/Loot/LootPool.cs
@@ -0,0 +1,54 @@
+namespace WeaveLoader.API.Loot;
+
+public sealed class LootPool
+{
+ internal ILootNumberProvider Rolls { get; }
+ internal List Conditions { get; }
+ internal List Entries { get; }
+
+ private LootPool(ILootNumberProvider rolls, List conditions, List entries)
+ {
+ Rolls = rolls;
+ Conditions = conditions;
+ Entries = entries;
+ }
+
+ public static Builder builder() => new();
+
+ public sealed class Builder
+ {
+ private ILootNumberProvider? _rolls;
+ private readonly List _conditions = new();
+ private readonly List _entries = new();
+
+ public Builder rolls(ILootNumberProvider rolls)
+ {
+ _rolls = rolls;
+ return this;
+ }
+
+ public Builder conditionally(ILootCondition condition)
+ {
+ _conditions.Add(condition);
+ return this;
+ }
+
+ public Builder with(LootEntryBuilder entryBuilder)
+ {
+ _entries.Add(entryBuilder.Build());
+ return this;
+ }
+
+ public Builder with(LootEntry entry)
+ {
+ _entries.Add(entry);
+ return this;
+ }
+
+ internal LootPool Build()
+ {
+ ILootNumberProvider rolls = _rolls ?? ConstantLootNumberProvider.create(1);
+ return new LootPool(rolls, _conditions, _entries);
+ }
+ }
+}
diff --git a/WeaveLoader.API/Loot/LootResourceManager.cs b/WeaveLoader.API/Loot/LootResourceManager.cs
new file mode 100644
index 0000000..806c4c7
--- /dev/null
+++ b/WeaveLoader.API/Loot/LootResourceManager.cs
@@ -0,0 +1,9 @@
+namespace WeaveLoader.API.Loot;
+
+///
+/// Placeholder resource manager for loot event callbacks.
+///
+public sealed class LootResourceManager
+{
+ internal LootResourceManager() { }
+}
diff --git a/WeaveLoader.API/Loot/LootTableBuilder.cs b/WeaveLoader.API/Loot/LootTableBuilder.cs
new file mode 100644
index 0000000..8d55202
--- /dev/null
+++ b/WeaveLoader.API/Loot/LootTableBuilder.cs
@@ -0,0 +1,12 @@
+namespace WeaveLoader.API.Loot;
+
+public sealed class LootTableBuilder
+{
+ internal List Pools { get; } = new();
+
+ public LootTableBuilder pool(LootPool.Builder builder)
+ {
+ Pools.Add(builder.Build());
+ return this;
+ }
+}
diff --git a/WeaveLoader.API/Loot/LootTableEvents.cs b/WeaveLoader.API/Loot/LootTableEvents.cs
new file mode 100644
index 0000000..cdd55cb
--- /dev/null
+++ b/WeaveLoader.API/Loot/LootTableEvents.cs
@@ -0,0 +1,41 @@
+using System;
+using WeaveLoader.API;
+
+namespace WeaveLoader.API.Loot;
+
+public delegate void LootTableModifyHandler(
+ LootResourceManager resourceManager,
+ LootManager lootManager,
+ Identifier id,
+ LootTableBuilder tableBuilder,
+ LootTableSource source);
+
+public sealed class LootTableModifyEvent
+{
+ private event LootTableModifyHandler? _handlers;
+ internal bool HasHandlers => _handlers != null;
+
+ public void Register(LootTableModifyHandler handler)
+ {
+ _handlers += handler;
+ }
+
+ public void register(LootTableModifyHandler handler)
+ {
+ Register(handler);
+ }
+
+ internal void Fire(LootResourceManager resourceManager,
+ LootManager lootManager,
+ Identifier id,
+ LootTableBuilder tableBuilder,
+ LootTableSource source)
+ {
+ _handlers?.Invoke(resourceManager, lootManager, id, tableBuilder, source);
+ }
+}
+
+public static class LootTableEvents
+{
+ public static LootTableModifyEvent MODIFY { get; } = new();
+}
diff --git a/WeaveLoader.API/Loot/LootTableSource.cs b/WeaveLoader.API/Loot/LootTableSource.cs
new file mode 100644
index 0000000..2ffc2de
--- /dev/null
+++ b/WeaveLoader.API/Loot/LootTableSource.cs
@@ -0,0 +1,11 @@
+namespace WeaveLoader.API.Loot;
+
+///
+/// Origin of a loot table.
+///
+public enum LootTableSource
+{
+ Vanilla = 0,
+ Mod = 1,
+ BuiltIn = 2
+}
diff --git a/WeaveLoader.API/NativeInterop.cs b/WeaveLoader.API/NativeInterop.cs
index 9e6f68b..8595f8e 100644
--- a/WeaveLoader.API/NativeInterop.cs
+++ b/WeaveLoader.API/NativeInterop.cs
@@ -226,6 +226,9 @@ internal static class NativeInterop
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl)]
internal static extern int native_summon_entity_by_id(int numericEntityId, double x, double y, double z);
+ [DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl)]
+ internal static extern int native_spawn_item_from_entity(nint entityPtr, int numericItemId, int count, int aux);
+
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl)]
internal static extern int native_consume_item_from_player(nint playerPtr, int numericItemId, int count);
diff --git a/WeaveLoader.Core/Loot/LootSystem.cs b/WeaveLoader.Core/Loot/LootSystem.cs
new file mode 100644
index 0000000..ffa2757
--- /dev/null
+++ b/WeaveLoader.Core/Loot/LootSystem.cs
@@ -0,0 +1,468 @@
+using System.Text.Json;
+using WeaveLoader.API;
+using WeaveLoader.API.Loot;
+
+namespace WeaveLoader.Core.Loot;
+
+internal sealed class LootSystem
+{
+ private readonly LootIndex _index;
+ private readonly LootManager _lootManager;
+ private readonly LootResourceManager _resourceManager;
+ private readonly Random _random = new();
+ private readonly object _lock = new();
+ private readonly string _modsPath;
+ private static readonly object _logLock = new();
+ private static readonly HashSet _loggedTableIds = new(StringComparer.OrdinalIgnoreCase);
+ private static readonly List _emptyDrops = new();
+ private static readonly object _tableCacheLock = new();
+ private static readonly Dictionary _tableCache = new(StringComparer.OrdinalIgnoreCase);
+
+ public LootSystem(string modsPath)
+ {
+ _modsPath = modsPath;
+ _index = LootIndex.Build(modsPath);
+ _lootManager = new LootManager();
+ _resourceManager = new LootResourceManager();
+ }
+
+ public LootResult GetEntityLoot(Identifier entityId)
+ {
+ Identifier tableId = new(entityId.Namespace, $"entities/{entityId.Path}");
+ return GetLoot(tableId);
+ }
+
+ public LootResult GetBlockLoot(Identifier blockId)
+ {
+ Identifier tableId = new(blockId.Namespace, $"blocks/{blockId.Path}");
+ return GetLoot(tableId);
+ }
+
+ private LootResult GetLoot(Identifier tableId)
+ {
+ var tables = _index.FindTables(tableId);
+ LogTablesOnce(tableId, tables);
+
+ if (tables.Count == 0 && !LootTableEvents.MODIFY.HasHandlers)
+ return new LootResult(_emptyDrops, false);
+
+ bool overrideVanilla = false;
+ var builder = new LootTableBuilder();
+
+ foreach (var table in tables)
+ {
+ if (table.Namespace == "minecraft")
+ overrideVanilla = true;
+
+ MergeTableIntoBuilder(builder, table.Path);
+ }
+
+ LootTableSource source = tableId.Namespace == "minecraft" ? LootTableSource.Vanilla : LootTableSource.Mod;
+ LootTableEvents.MODIFY.Fire(_resourceManager, _lootManager, tableId, builder, source);
+
+ List drops;
+ lock (_lock)
+ {
+ drops = LootEvaluator.Evaluate(builder, _random);
+ }
+
+ return new LootResult(drops, overrideVanilla);
+ }
+
+ private void LogTablesOnce(Identifier tableId, List tables)
+ {
+ string key = tableId.ToString();
+ lock (_logLock)
+ {
+ if (_loggedTableIds.Contains(key))
+ return;
+ _loggedTableIds.Add(key);
+ }
+
+ if (tables.Count == 0)
+ {
+ Logger.Info($"Loot tables not found for {tableId} (modsPath={_modsPath})");
+ return;
+ }
+
+ string list = string.Join(", ", tables.Select(t => $"{t.Namespace}:{t.Path}"));
+ Logger.Info($"Loot tables for {tableId}: {list}");
+ }
+
+ public static Identifier NormalizeEntityId(string encodeId)
+ {
+ if (string.IsNullOrWhiteSpace(encodeId))
+ return new Identifier("minecraft", "pig");
+
+ string trimmed = encodeId.Trim();
+ if (trimmed.Contains(':'))
+ return new Identifier(trimmed.ToLowerInvariant());
+
+ if (LegacyEntityIdMap.TryGetValue(trimmed, out string? mapped))
+ return new Identifier(mapped);
+
+ string snake = ToSnakeCase(trimmed);
+ return new Identifier("minecraft", snake.ToLowerInvariant());
+ }
+
+ private static void MergeTableIntoBuilder(LootTableBuilder builder, string path)
+ {
+ CachedTable table;
+ lock (_tableCacheLock)
+ {
+ if (!_tableCache.TryGetValue(path, out table!))
+ {
+ table = ParseTable(path);
+ _tableCache[path] = table;
+ }
+ }
+
+ foreach (CachedPool cachedPool in table.Pools)
+ {
+ var poolBuilder = LootPool.builder();
+
+ if (cachedPool.Rolls.HasValue)
+ poolBuilder.rolls(ConstantLootNumberProvider.create(cachedPool.Rolls.Value));
+
+ if (cachedPool.RandomChance.HasValue)
+ poolBuilder.conditionally(RandomChanceLootCondition.builder(cachedPool.RandomChance.Value));
+
+ foreach (CachedEntry entry in cachedPool.Entries)
+ {
+ if (entry.IsEmpty)
+ {
+ poolBuilder.with(new EmptyEntry(entry.Weight));
+ continue;
+ }
+
+ var itemBuilder = ItemEntry.builder(entry.ItemId);
+ itemBuilder.weight(entry.Weight);
+ if (entry.Count.HasValue)
+ itemBuilder.apply(new SetCountLootFunction(entry.Count.Value));
+ poolBuilder.with(itemBuilder);
+ }
+
+ builder.pool(poolBuilder);
+ }
+ }
+
+ private static CachedTable ParseTable(string path)
+ {
+ var cached = new CachedTable();
+ try
+ {
+ string json = File.ReadAllText(path);
+ using JsonDocument doc = JsonDocument.Parse(json);
+ if (!doc.RootElement.TryGetProperty("pools", out JsonElement pools) || pools.ValueKind != JsonValueKind.Array)
+ return cached;
+
+ foreach (JsonElement poolElem in pools.EnumerateArray())
+ {
+ var cachedPool = new CachedPool();
+
+ if (poolElem.TryGetProperty("rolls", out JsonElement rollsElem) &&
+ rollsElem.ValueKind == JsonValueKind.Number &&
+ rollsElem.TryGetInt32(out int rolls))
+ {
+ cachedPool.Rolls = rolls;
+ }
+
+ if (poolElem.TryGetProperty("conditions", out JsonElement conditionsElem) &&
+ conditionsElem.ValueKind == JsonValueKind.Array)
+ {
+ foreach (JsonElement cond in conditionsElem.EnumerateArray())
+ ParseCondition(cond, cachedPool);
+ }
+
+ if (poolElem.TryGetProperty("entries", out JsonElement entriesElem) &&
+ entriesElem.ValueKind == JsonValueKind.Array)
+ {
+ foreach (JsonElement entry in entriesElem.EnumerateArray())
+ ParseEntry(entry, cachedPool);
+ }
+
+ cached.Pools.Add(cachedPool);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Error($"Loot table parse failed for {path}: {ex.Message}");
+ }
+ return cached;
+ }
+
+ private static void ParseCondition(JsonElement element, CachedPool pool)
+ {
+ if (!element.TryGetProperty("condition", out JsonElement typeElem))
+ return;
+
+ string? type = typeElem.GetString();
+ if (type == "minecraft:random_chance" && element.TryGetProperty("chance", out JsonElement chanceElem))
+ {
+ if (chanceElem.ValueKind == JsonValueKind.Number && chanceElem.TryGetSingle(out float chance))
+ {
+ pool.RandomChance = chance;
+ }
+ }
+ }
+
+ private static void ParseEntry(JsonElement entry, CachedPool pool)
+ {
+ if (!entry.TryGetProperty("type", out JsonElement typeElem))
+ return;
+
+ string? type = typeElem.GetString();
+ int weight = 1;
+ if (entry.TryGetProperty("weight", out JsonElement weightElem) && weightElem.ValueKind == JsonValueKind.Number)
+ weightElem.TryGetInt32(out weight);
+
+ if (type == "minecraft:empty")
+ {
+ pool.Entries.Add(new CachedEntry
+ {
+ IsEmpty = true,
+ Weight = weight
+ });
+ return;
+ }
+
+ if (type == "minecraft:item" && entry.TryGetProperty("name", out JsonElement nameElem))
+ {
+ string? name = nameElem.GetString();
+ if (string.IsNullOrWhiteSpace(name))
+ return;
+
+ var cachedEntry = new CachedEntry
+ {
+ IsEmpty = false,
+ ItemId = new Identifier(name),
+ Weight = weight,
+ Count = null
+ };
+
+ if (entry.TryGetProperty("functions", out JsonElement funcsElem) && funcsElem.ValueKind == JsonValueKind.Array)
+ {
+ foreach (JsonElement func in funcsElem.EnumerateArray())
+ {
+ if (!func.TryGetProperty("function", out JsonElement fnTypeElem))
+ continue;
+ string? fnType = fnTypeElem.GetString();
+ if (fnType == "minecraft:set_count" && func.TryGetProperty("count", out JsonElement countElem))
+ {
+ if (countElem.ValueKind == JsonValueKind.Number && countElem.TryGetInt32(out int count))
+ cachedEntry.Count = count;
+ }
+ }
+ }
+
+ pool.Entries.Add(cachedEntry);
+ }
+ }
+
+ private sealed class CachedTable
+ {
+ public readonly List Pools = new();
+ }
+
+ private sealed class CachedPool
+ {
+ public int? Rolls;
+ public float? RandomChance;
+ public readonly List Entries = new();
+ }
+
+ private sealed class CachedEntry
+ {
+ public bool IsEmpty;
+ public Identifier ItemId = default!;
+ public int Weight;
+ public int? Count;
+ }
+
+ private static string ToSnakeCase(string input)
+ {
+ if (string.IsNullOrEmpty(input))
+ return input;
+
+ var result = new System.Text.StringBuilder(input.Length + 8);
+ for (int i = 0; i < input.Length; i++)
+ {
+ char c = input[i];
+ if (char.IsUpper(c))
+ {
+ if (i > 0 && (char.IsLower(input[i - 1]) || char.IsDigit(input[i - 1])))
+ result.Append('_');
+ result.Append(char.ToLowerInvariant(c));
+ }
+ else
+ {
+ result.Append(c);
+ }
+ }
+ return result.ToString();
+ }
+
+ private static readonly Dictionary LegacyEntityIdMap = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["PigZombie"] = "minecraft:zombie_pigman",
+ ["LavaSlime"] = "minecraft:magma_cube",
+ ["VillagerGolem"] = "minecraft:iron_golem",
+ ["SnowMan"] = "minecraft:snow_golem",
+ ["Ozelot"] = "minecraft:ocelot",
+ ["EnderMan"] = "minecraft:enderman",
+ ["WitherBoss"] = "minecraft:wither",
+ ["MushroomCow"] = "minecraft:mooshroom",
+ ["Giant"] = "minecraft:giant",
+ ["CaveSpider"] = "minecraft:cave_spider",
+ ["MinecartRideable"] = "minecraft:minecart",
+ ["MinecartChest"] = "minecraft:chest_minecart",
+ ["MinecartFurnace"] = "minecraft:furnace_minecart",
+ ["MinecartTNT"] = "minecraft:tnt_minecart",
+ ["MinecartHopper"] = "minecraft:hopper_minecart",
+ ["MinecartSpawner"] = "minecraft:spawner_minecart",
+ ["EyeOfEnderSignal"] = "minecraft:eye_of_ender_signal",
+ ["ThrownEnderpearl"] = "minecraft:ender_pearl",
+ ["ThrownExpBottle"] = "minecraft:xp_bottle",
+ ["ThrownPotion"] = "minecraft:potion",
+ ["FireworksRocketEntity"] = "minecraft:fireworks_rocket",
+ ["PrimedTnt"] = "minecraft:primed_tnt",
+ ["FallingSand"] = "minecraft:falling_block",
+ ["XPOrb"] = "minecraft:xp_orb"
+ };
+
+ private sealed record TableRef(string Namespace, string Path);
+
+ private sealed class LootIndex
+ {
+ private readonly Dictionary> _tables = new();
+ private readonly Dictionary> _tablesByPath = new(StringComparer.OrdinalIgnoreCase);
+ private static readonly List _emptyTables = new();
+
+ public static LootIndex Build(string modsPath)
+ {
+ var index = new LootIndex();
+ if (!Directory.Exists(modsPath))
+ return index;
+
+ foreach (string modFolder in Directory.GetDirectories(modsPath))
+ {
+ string dataDir = Path.Combine(modFolder, "data");
+ if (!Directory.Exists(dataDir))
+ continue;
+
+ foreach (string nsDir in Directory.GetDirectories(dataDir))
+ {
+ string ns = Path.GetFileName(nsDir).ToLowerInvariant();
+ string lootDir = Path.Combine(nsDir, "loot_tables");
+ if (!Directory.Exists(lootDir))
+ continue;
+
+ foreach (string file in Directory.GetFiles(lootDir, "*.json", SearchOption.AllDirectories))
+ {
+ string rel = Path.GetRelativePath(lootDir, file).Replace('\\', '/');
+ if (rel.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
+ rel = rel[..^5];
+ var id = new Identifier(ns, rel.ToLowerInvariant());
+
+ if (!index._tables.TryGetValue(id, out var list))
+ {
+ list = new List();
+ index._tables[id] = list;
+ }
+ list.Add(new TableRef(ns, file));
+
+ if (!index._tablesByPath.TryGetValue(id.Path, out var pathList))
+ {
+ pathList = new List();
+ index._tablesByPath[id.Path] = pathList;
+ }
+ pathList.Add(new TableRef(ns, file));
+ }
+ }
+ }
+
+ return index;
+ }
+
+ public List FindTables(Identifier id)
+ {
+ if (id.Namespace == "minecraft")
+ {
+ if (_tablesByPath.TryGetValue(id.Path, out var byPath))
+ return byPath;
+ }
+ if (_tables.TryGetValue(id, out var list))
+ return list;
+ return _emptyTables;
+ }
+ }
+
+ public readonly record struct LootResult(List Drops, bool OverrideVanilla);
+
+ private static class LootEvaluator
+ {
+ public static List Evaluate(LootTableBuilder builder, Random random)
+ {
+ var drops = new List();
+ foreach (LootPool pool in builder.Pools)
+ {
+ if (!CheckConditions(pool, random))
+ continue;
+
+ int rolls = pool.Rolls.NextInt(random);
+ if (rolls < 1)
+ continue;
+
+ for (int i = 0; i < rolls; i++)
+ {
+ LootEntry? entry = ChooseEntry(pool.Entries, random);
+ if (entry is ItemEntry itemEntry)
+ {
+ var drop = new LootDrop(itemEntry.ItemId, 1, 0);
+ foreach (ILootFunction fn in itemEntry.Functions)
+ fn.Apply(ref drop, random);
+ if (drop.Count > 0)
+ drops.Add(drop);
+ }
+ }
+ }
+
+ return drops;
+ }
+
+ private static bool CheckConditions(LootPool pool, Random random)
+ {
+ foreach (ILootCondition cond in pool.Conditions)
+ {
+ if (!cond.Test(random))
+ return false;
+ }
+ return true;
+ }
+
+ private static LootEntry? ChooseEntry(List entries, Random random)
+ {
+ if (entries.Count == 0)
+ return null;
+
+ int totalWeight = 0;
+ foreach (LootEntry entry in entries)
+ totalWeight += entry.Weight > 0 ? entry.Weight : 0;
+
+ if (totalWeight <= 0)
+ return null;
+
+ int roll = random.Next(totalWeight);
+ int accum = 0;
+ foreach (LootEntry entry in entries)
+ {
+ int weight = entry.Weight > 0 ? entry.Weight : 0;
+ accum += weight;
+ if (roll < accum)
+ return entry;
+ }
+
+ return entries[0];
+ }
+ }
+}
diff --git a/WeaveLoader.Core/WeaveLoaderCore.cs b/WeaveLoader.Core/WeaveLoaderCore.cs
index b78c53d..b23b6a4 100644
--- a/WeaveLoader.Core/WeaveLoaderCore.cs
+++ b/WeaveLoader.Core/WeaveLoaderCore.cs
@@ -3,13 +3,23 @@ using WeaveLoader.API;
using WeaveLoader.API.Events;
using WeaveLoader.API.Block;
using WeaveLoader.API.Item;
+using WeaveLoader.Core.Loot;
namespace WeaveLoader.Core;
public static class WeaveLoaderCore
{
+ private static readonly object _lootLogLock = new();
+ private static readonly HashSet _loggedEntityLoot = new(StringComparer.OrdinalIgnoreCase);
+ private static readonly object _lootResultLock = new();
+ private static readonly HashSet _loggedEntityLootResult = new(StringComparer.OrdinalIgnoreCase);
+ private static readonly object _lootMissingItemLock = new();
+ private static readonly HashSet _loggedMissingLootItems = new(StringComparer.OrdinalIgnoreCase);
+ private static int _lootSpawnLogCount;
private static ModManager? _modManager;
private static bool _initialized;
+ private static string _modsPath = "mods";
+ private static LootSystem? _lootSystem;
public static int Initialize(IntPtr args, int sizeBytes)
{
@@ -44,6 +54,8 @@ public static class WeaveLoaderCore
else
modsPath = "mods";
+ _modsPath = modsPath;
+ _lootSystem = null;
Logger.Info($"Discovering mods in: {modsPath}");
Logger.Info($"Directory exists: {Directory.Exists(modsPath)}");
@@ -357,6 +369,142 @@ public static class WeaveLoaderCore
}
}
+ public static int OnBlockLoot(IntPtr args, int sizeBytes)
+ {
+ try
+ {
+ if (args == IntPtr.Zero || sizeBytes <= 0)
+ return -1;
+
+ var native = Marshal.PtrToStructure(args);
+ if (native.BlockNumericId < 0)
+ return -1;
+
+ if (!IdHelper.TryGetBlockIdentifier(native.BlockNumericId, out Identifier blockId))
+ return -1;
+
+ _lootSystem ??= new LootSystem(_modsPath);
+ var result = _lootSystem.GetBlockLoot(blockId);
+ if (result.Drops.Count == 0)
+ return -1;
+
+ Identifier dropId = result.Drops[0].ItemId;
+ int numericId = IdHelper.GetItemNumericId(dropId);
+ if (numericId < 0)
+ numericId = IdHelper.GetBlockNumericId(dropId);
+
+ return numericId;
+ }
+ catch (Exception ex)
+ {
+ Logger.Error($"OnBlockLoot EXCEPTION: {ex}");
+ return -1;
+ }
+ }
+
+ public static int OnEntityLoot(IntPtr args, int sizeBytes)
+ {
+ try
+ {
+ if (args == IntPtr.Zero || sizeBytes <= 0)
+ return 0;
+
+ var native = Marshal.PtrToStructure(args);
+ Identifier entityId;
+ string? encodeId = Marshal.PtrToStringUTF8(native.EntityId);
+ if (!string.IsNullOrWhiteSpace(encodeId))
+ {
+ entityId = LootSystem.NormalizeEntityId(encodeId);
+ }
+ else if (native.EntityNumericId >= 0 &&
+ IdHelper.TryGetEntityIdentifier(native.EntityNumericId, out Identifier numericEntityId))
+ {
+ entityId = numericEntityId;
+ }
+ else
+ {
+ return 0;
+ }
+
+ LogEntityLootOnce(entityId, encodeId, native.EntityNumericId);
+
+ _lootSystem ??= new LootSystem(_modsPath);
+ var result = _lootSystem.GetEntityLoot(entityId);
+ LogEntityLootResultOnce(entityId, result);
+
+ int spawnCount = 0;
+ foreach (var drop in result.Drops)
+ {
+ int numericId = IdHelper.GetItemNumericId(drop.ItemId);
+ if (numericId < 0)
+ numericId = IdHelper.GetBlockNumericId(drop.ItemId);
+ if (numericId < 0)
+ {
+ LogMissingLootItemOnce(entityId, drop.ItemId);
+ continue;
+ }
+
+ int logIndex = System.Threading.Interlocked.Increment(ref _lootSpawnLogCount);
+ if (logIndex <= 100)
+ {
+ Logger.Info($"Loot spawn attempt entity={entityId} item={drop.ItemId} numericId={numericId} count={drop.Count} aux={drop.Aux}");
+ }
+
+ int ok = NativeInterop.native_spawn_item_from_entity(
+ native.EntityPtr, numericId, drop.Count, drop.Aux);
+ if (ok != 0)
+ spawnCount++;
+ }
+
+ if (result.OverrideVanilla)
+ return 2;
+ return spawnCount == 0 ? 0 : 1;
+ }
+ catch (Exception ex)
+ {
+ Logger.Error($"OnEntityLoot EXCEPTION: {ex}");
+ return 0;
+ }
+ }
+
+ private static void LogEntityLootOnce(Identifier entityId, string? encodeId, int numericId)
+ {
+ lock (_lootLogLock)
+ {
+ if (_loggedEntityLoot.Contains(entityId.ToString()))
+ return;
+ _loggedEntityLoot.Add(entityId.ToString());
+ }
+
+ string enc = string.IsNullOrWhiteSpace(encodeId) ? "" : encodeId!;
+ Logger.Info($"EntityLoot resolved id={entityId} encodeId={enc} numericId={numericId}");
+ }
+
+ private static void LogEntityLootResultOnce(Identifier entityId, LootSystem.LootResult result)
+ {
+ lock (_lootResultLock)
+ {
+ if (_loggedEntityLootResult.Contains(entityId.ToString()))
+ return;
+ _loggedEntityLootResult.Add(entityId.ToString());
+ }
+
+ Logger.Info($"EntityLoot result id={entityId} drops={result.Drops.Count} overrideVanilla={result.OverrideVanilla}");
+ }
+
+ private static void LogMissingLootItemOnce(Identifier entityId, Identifier itemId)
+ {
+ string key = $"{entityId}->{itemId}";
+ lock (_lootMissingItemLock)
+ {
+ if (_loggedMissingLootItems.Contains(key))
+ return;
+ _loggedMissingLootItems.Add(key);
+ }
+
+ Logger.Info($"Loot drop item id not found: entity={entityId} item={itemId}");
+ }
+
[StructLayout(LayoutKind.Sequential)]
private struct WorldLoadedNativeArgs
{
@@ -372,6 +520,24 @@ public static class WeaveLoaderCore
public float Z;
}
+ [StructLayout(LayoutKind.Sequential)]
+ private struct EntityLootNativeArgs
+ {
+ public int EntityNumericId;
+ public IntPtr EntityId;
+ public IntPtr EntityPtr;
+ public int WasKilledByPlayer;
+ public int PlayerBonusLevel;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct BlockLootNativeArgs
+ {
+ public int BlockNumericId;
+ public int BlockData;
+ public int PlayerBonusLevel;
+ }
+
public static int OnWorldLoaded(IntPtr args, int sizeBytes)
{
try
diff --git a/WeaveLoaderRuntime/src/CrashHandler.cpp b/WeaveLoaderRuntime/src/CrashHandler.cpp
index a97731c..ec2fd53 100644
--- a/WeaveLoaderRuntime/src/CrashHandler.cpp
+++ b/WeaveLoaderRuntime/src/CrashHandler.cpp
@@ -93,6 +93,35 @@ static bool ShouldWriteVectoredReport(EXCEPTION_RECORD* er)
if (!er)
return false;
+ if (s_runtimeModule)
+ {
+ void* frames[64] = {};
+ USHORT count = RtlCaptureStackBackTrace(0, 64, frames, nullptr);
+ for (USHORT i = 0; i < count; ++i)
+ {
+ HMODULE hMod = nullptr;
+ if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
+ GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
+ reinterpret_cast(frames[i]), &hMod) &&
+ hMod == s_runtimeModule)
+ {
+ return true;
+ }
+ }
+ }
+
+ if (s_runtimeModule)
+ {
+ HMODULE hMod = nullptr;
+ if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
+ GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
+ reinterpret_cast(er->ExceptionAddress), &hMod) &&
+ hMod == s_runtimeModule)
+ {
+ return true;
+ }
+ }
+
switch (er->ExceptionCode)
{
case STATUS_STACK_BUFFER_OVERRUN:
diff --git a/WeaveLoaderRuntime/src/DotNetHost.cpp b/WeaveLoaderRuntime/src/DotNetHost.cpp
index 04f426f..d1607c8 100644
--- a/WeaveLoaderRuntime/src/DotNetHost.cpp
+++ b/WeaveLoaderRuntime/src/DotNetHost.cpp
@@ -42,6 +42,8 @@ static managed_entry_fn fn_BlockDestroyed = nullptr;
static managed_entry_fn fn_BlockPlayerDestroy = nullptr;
static managed_entry_fn fn_BlockPlayerWillDestroy = nullptr;
static managed_entry_fn fn_BlockPlacedBy = nullptr;
+static managed_entry_fn fn_BlockLoot = nullptr;
+static managed_entry_fn fn_EntityLoot = nullptr;
static managed_entry_fn fn_EntitySummoned = nullptr;
static bool LoadHostfxr()
@@ -218,7 +220,9 @@ bool DotNetHost::Initialize()
ok &= resolve(L"OnBlockPlayerDestroy", &fn_BlockPlayerDestroy);
ok &= resolve(L"OnBlockPlayerWillDestroy", &fn_BlockPlayerWillDestroy);
ok &= resolve(L"OnBlockPlacedBy", &fn_BlockPlacedBy);
+ ok &= resolve(L"OnEntityLoot", &fn_EntityLoot);
ok &= resolve(L"OnEntitySummoned", &fn_EntitySummoned);
+ resolve(L"OnBlockLoot", &fn_BlockLoot);
if (!ok)
{
@@ -409,6 +413,20 @@ int DotNetHost::CallBlockPlacedBy(const void* args, int sizeBytes)
return fn_BlockPlacedBy(const_cast(args), sizeBytes);
}
+int DotNetHost::CallBlockLoot(const void* args, int sizeBytes)
+{
+ if (!fn_BlockLoot || !args || sizeBytes <= 0)
+ return -1;
+ return fn_BlockLoot(const_cast(args), sizeBytes);
+}
+
+int DotNetHost::CallEntityLoot(const void* args, int sizeBytes)
+{
+ if (!fn_EntityLoot || !args || sizeBytes <= 0)
+ return 0;
+ return fn_EntityLoot(const_cast(args), sizeBytes);
+}
+
void DotNetHost::CallEntitySummoned(int entityNumericId, float x, float y, float z)
{
diff --git a/WeaveLoaderRuntime/src/DotNetHost.h b/WeaveLoaderRuntime/src/DotNetHost.h
index 40471e4..56e77c2 100644
--- a/WeaveLoaderRuntime/src/DotNetHost.h
+++ b/WeaveLoaderRuntime/src/DotNetHost.h
@@ -35,5 +35,7 @@ namespace DotNetHost
int CallBlockPlayerDestroy(const void* args, int sizeBytes);
int CallBlockPlayerWillDestroy(const void* args, int sizeBytes);
int CallBlockPlacedBy(const void* args, int sizeBytes);
+ int CallBlockLoot(const void* args, int sizeBytes);
+ int CallEntityLoot(const void* args, int sizeBytes);
void CallEntitySummoned(int entityNumericId, float x, float y, float z);
}
diff --git a/WeaveLoaderRuntime/src/GameHooks.cpp b/WeaveLoaderRuntime/src/GameHooks.cpp
index 4f80b19..20a751e 100644
--- a/WeaveLoaderRuntime/src/GameHooks.cpp
+++ b/WeaveLoaderRuntime/src/GameHooks.cpp
@@ -14,6 +14,7 @@
#include "WorldIdRemap.h"
#include "ModelRegistry.h"
#include "ItemRenderRegistry.h"
+#include "GameObjectFactory.h"
#include "PdbParser.h"
#include
#include
@@ -36,9 +37,16 @@
#include
#include
+class ItemInstance;
+class ItemEntity;
+
namespace GameHooks
{
static bool IsReadableRange(const void* ptr, size_t bytes);
+ static bool LooksLikeEntityPtrStrict(void* candidate);
+ static bool IsEntityMarkedRemoved(void* entityPtr);
+ static bool TryReadEntityPos(void* entityPtr, double& x, double& y, double& z);
+ static bool TryReadEntityPosViaVirtual(void* entityPtr, double& x, double& y, double& z);
RunStaticCtors_fn Original_RunStaticCtors = nullptr;
MinecraftTick_fn Original_MinecraftTick = nullptr;
MinecraftInit_fn Original_MinecraftInit = nullptr;
@@ -124,6 +132,25 @@ namespace GameHooks
MinecraftSetLevel_fn Original_MinecraftSetLevel = nullptr;
EntityPlayStepSound_fn Original_EntityPlayStepSound = nullptr;
EntityCheckInsideTiles_fn Original_EntityCheckInsideTiles = nullptr;
+ LivingEntityDropDeathLoot_fn Original_LivingEntityDropDeathLoot = nullptr;
+ LivingEntityDropDeathLoot_fn Original_MobDropDeathLoot = nullptr;
+ LivingEntityDropDeathLoot_fn Original_ChickenDropDeathLoot = nullptr;
+ LivingEntityDropDeathLoot_fn Original_CowDropDeathLoot = nullptr;
+ LivingEntityDropDeathLoot_fn Original_PigDropDeathLoot = nullptr;
+ LivingEntityDropDeathLoot_fn Original_SheepDropDeathLoot = nullptr;
+ LivingEntityDropDeathLoot_fn Original_SquidDropDeathLoot = nullptr;
+ LivingEntityDropDeathLoot_fn Original_OcelotDropDeathLoot = nullptr;
+ LivingEntityDropDeathLoot_fn Original_SnowManDropDeathLoot = nullptr;
+ LivingEntityDropDeathLoot_fn Original_VillagerGolemDropDeathLoot = nullptr;
+ LivingEntityDropDeathLoot_fn Original_PigZombieDropDeathLoot = nullptr;
+ LivingEntityDropDeathLoot_fn Original_SpiderDropDeathLoot = nullptr;
+ LivingEntityDropDeathLoot_fn Original_SkeletonDropDeathLoot = nullptr;
+ LivingEntityDropDeathLoot_fn Original_WitchDropDeathLoot = nullptr;
+ LivingEntityDropDeathLoot_fn Original_BlazeDropDeathLoot = nullptr;
+ LivingEntityDropDeathLoot_fn Original_EnderManDropDeathLoot = nullptr;
+ LivingEntityDropDeathLoot_fn Original_GhastDropDeathLoot = nullptr;
+ LivingEntityDropDeathLoot_fn Original_LavaSlimeDropDeathLoot = nullptr;
+ LivingEntityDropDeathLoot_fn Original_WitherBossDropDeathLoot = nullptr;
TexturesBindTextureResource_fn Original_TexturesBindTextureResource = nullptr;
TexturesLoadTextureByName_fn Original_TexturesLoadTextureByName = nullptr;
TexturesLoadTextureByIndex_fn Original_TexturesLoadTextureByIndex = nullptr;
@@ -158,6 +185,11 @@ namespace GameHooks
namespace
{
+ using EntitySpawnAtLocation_fn = void (__fastcall *)(void* outSharedPtr, void* thisPtr, void* itemSharedPtr, float yOffset);
+ using EntitySpawnAtLocationInt_fn = void (__fastcall *)(void* outSharedPtr, void* thisPtr, int resource, int count, float yOffset);
+ using ItemEntitySetItemTyped_fn = void (__fastcall *)(void* thisPtr, std::shared_ptr itemInstance);
+ using ItemEntityMakeShared_fn = void (__fastcall *)(void* outSharedPtr, void* levelRefPtr, double* xRef, double yValue, double* zRef, void* itemSharedRef);
+
struct AABBRaw
{
double x0, y0, z0;
@@ -179,6 +211,23 @@ namespace GameHooks
void* pos;
};
+ static OperatorNew_fn s_operatorNew = nullptr;
+ static ItemInstanceCtor_fn s_itemInstanceCtor = nullptr;
+ static SharedPtrItemInstanceCtor_fn s_itemInstanceSharedPtrCtor = nullptr;
+ static SharedPtrItemInstanceDtor_fn s_itemInstanceSharedPtrDtor = nullptr;
+ static SharedPtrEntityCtor_fn s_entitySharedPtrCtor = nullptr;
+ static SharedPtrEntityDtor_fn s_entitySharedPtrDtor = nullptr;
+ static SharedPtrItemEntityDtor_fn s_itemEntitySharedPtrDtor = nullptr;
+ static ItemEntityCtorWithItem_fn s_itemEntityCtorWithItem = nullptr;
+ static ItemEntitySetItem_fn s_itemEntitySetItem = nullptr;
+ static ItemEntityMakeShared_fn s_itemEntityMakeShared = nullptr;
+ static EntitySpawnAtLocation_fn s_entitySpawnAtLocation = nullptr;
+ static EntitySpawnAtLocationInt_fn s_entitySpawnAtLocationInt = nullptr;
+ static ItemInstanceSetAuxValue_fn s_itemInstanceSetAuxValue = nullptr;
+ static EntityGetEncodeId_fn s_entityGetEncodeId = nullptr;
+ static EntityGetEncodeIdById_fn s_entityGetEncodeIdById = nullptr;
+ static EntityGetNetworkName_fn s_entityGetNetworkName = nullptr;
+
static bool Intersects(const AABBRaw* box, double x0, double y0, double z0, double x1, double y1, double z1)
{
@@ -281,6 +330,8 @@ namespace GameHooks
static int s_entityLevelOffset = -1;
static std::atomic s_entityBbOffsetTried{false};
static int s_entityBbOffset = -1;
+ static std::atomic s_entityIdOffsetTried{false};
+ static int s_entityIdOffset = -1;
static int s_rotationLogCount = 0;
static bool ReadFileToString(const char* path, std::string& out)
@@ -498,6 +549,36 @@ namespace GameHooks
return true;
}
+ static bool TryResolveEntityIdOffset()
+ {
+ bool expected = false;
+ if (!s_entityIdOffsetTried.compare_exchange_strong(expected, true))
+ return s_entityIdOffset >= 0;
+
+ const char* baseDir = LogUtil::GetBaseDir();
+ if (!baseDir || baseDir[0] == '\0')
+ return false;
+
+ std::string json;
+ std::string path = std::string(baseDir) + "metadata\\offsets.json";
+ if (!ReadFileToString(path.c_str(), json))
+ {
+ path = std::string(baseDir) + "offsets.json";
+ if (!ReadFileToString(path.c_str(), json))
+ return false;
+ }
+
+ int offset = -1;
+ if (!ExtractOffsetForField(json, "Entity", "entityId", offset))
+ return false;
+ if (offset < 0)
+ return false;
+
+ s_entityIdOffset = offset;
+ LogUtil::Log("[WeaveLoader] ModelRegistry: Entity.entityId offset = 0x%X", s_entityIdOffset);
+ return true;
+ }
+
int GetTileId(void* tilePtr)
{
@@ -804,7 +885,8 @@ namespace GameHooks
static thread_local int s_firstPersonRenderDepth = 0;
static std::string s_modsPath;
static std::unordered_map s_modAssetRoots;
- static bool s_modAssetsIndexed = false;
+ static std::unordered_map s_modDataRoots;
+ static std::atomic s_modAssetsIndexed{false};
static std::atomic s_modAssetsIndexing{false};
static std::mutex s_modAssetsMutex;
static std::unordered_map s_itemRenderLogCount;
@@ -817,6 +899,7 @@ namespace GameHooks
static constexpr ptrdiff_t kEntityXOffset = 0x78;
static constexpr ptrdiff_t kEntityYOffset = 0x80;
static constexpr ptrdiff_t kEntityZOffset = 0x88;
+ static constexpr ptrdiff_t kEntityLevelOffset = 0x58;
static constexpr ptrdiff_t kEntityRemovedOffset = 0xC7;
static constexpr ptrdiff_t kFireballOwnerOffset = 0x1D0;
static constexpr ptrdiff_t kFireballXPowerOffset = 0x1E8;
@@ -855,6 +938,13 @@ namespace GameHooks
static uintptr_t s_gameModuleEnd = 0;
static std::vector s_spawnedEntities;
static int s_outOfWorldGuardLogCount = 0;
+ struct RecentLootDispatch
+ {
+ ULONGLONG timeMs;
+ int result;
+ };
+ static std::unordered_map s_recentLootDispatch;
+ static std::mutex s_recentLootDispatchMutex;
static int s_pendingServerUseItemId = -1;
static LevelGetTile_fn s_levelGetTile = nullptr;
static std::atomic s_tickCounter{0};
@@ -877,6 +967,8 @@ namespace GameHooks
static bool s_preInitCalled = false;
static bool s_initCalled = false;
static bool s_postInitCalled = false;
+ static bool s_modStringsInjected = false;
+ static uint32_t s_modStringsLastAttemptTick = 0;
static thread_local bool s_inventoryShapeOverrideActive = false;
static thread_local void* s_inventoryShapeOverrideTile = nullptr;
static thread_local ModelBox s_inventoryShapeOverrideBox{};
@@ -891,6 +983,18 @@ namespace GameHooks
s_pageResourceInit = true;
}
+ static void TryInjectModStrings()
+ {
+ if (s_modStringsInjected)
+ return;
+ const uint32_t tick = s_tickCounter.load(std::memory_order_relaxed);
+ if (s_modStringsLastAttemptTick != 0 && tick < s_modStringsLastAttemptTick + 20)
+ return;
+ s_modStringsLastAttemptTick = tick;
+ if (ModStrings::InjectAllIntoGameTable())
+ s_modStringsInjected = true;
+ }
+
static void EnsureGameModuleRange()
{
if (s_gameModuleBase != 0 && s_gameModuleEnd > s_gameModuleBase)
@@ -1096,8 +1200,9 @@ namespace GameHooks
static void BuildModAssetIndexLocked()
{
+ s_modAssetsIndexed.store(false, std::memory_order_relaxed);
s_modAssetRoots.clear();
- s_modAssetsIndexed = true;
+ s_modDataRoots.clear();
if (s_modsPath.empty())
return;
@@ -1114,54 +1219,90 @@ namespace GameHooks
std::string modFolder = fd.cFileName;
std::string assetsPath = s_modsPath + "\\" + modFolder + "\\assets";
+ std::string dataPath = s_modsPath + "\\" + modFolder + "\\data";
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 (attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY))
{
- 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())
+ WIN32_FIND_DATAA nsfd;
+ std::string nsSearch = assetsPath + "\\*";
+ HANDLE hNs = FindFirstFileA(nsSearch.c_str(), &nsfd);
+ if (hNs != INVALID_HANDLE_VALUE)
{
- s_modAssetRoots.emplace(nsName, assetsPath);
+ 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);
}
- else
+ }
+
+ attr = GetFileAttributesA(dataPath.c_str());
+ if (attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY))
+ {
+ WIN32_FIND_DATAA nsfd;
+ std::string nsSearch = dataPath + "\\*";
+ HANDLE hNs = FindFirstFileA(nsSearch.c_str(), &nsfd);
+ if (hNs != INVALID_HANDLE_VALUE)
{
- LogUtil::Log("[WeaveLoader] ModAssets: duplicate namespace '%s' (folder=%s) ignored",
- nsName.c_str(), modFolder.c_str());
+ 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_modDataRoots.find(nsName) == s_modDataRoots.end())
+ {
+ s_modDataRoots.emplace(nsName, dataPath);
+ }
+ else
+ {
+ LogUtil::Log("[WeaveLoader] ModData: duplicate namespace '%s' (folder=%s) ignored",
+ nsName.c_str(), modFolder.c_str());
+ }
+ } while (FindNextFileA(hNs, &nsfd));
+ FindClose(hNs);
}
- } while (FindNextFileA(hNs, &nsfd));
- FindClose(hNs);
+ }
} while (FindNextFileA(h, &fd));
FindClose(h);
+ s_modAssetsIndexed.store(true, std::memory_order_release);
}
static void StartModAssetIndexAsync()
{
if (s_modsPath.empty())
return;
- if (s_modAssetsIndexed || s_modAssetsIndexing.load())
+ if (s_modAssetsIndexed.load(std::memory_order_acquire) || s_modAssetsIndexing.load())
return;
s_modAssetsIndexing = true;
std::thread([]()
{
+ size_t nsCount = 0;
{
std::lock_guard guard(s_modAssetsMutex);
BuildModAssetIndexLocked();
+ nsCount = s_modAssetRoots.size();
}
s_modAssetsIndexing = false;
- LogUtil::Log("[WeaveLoader] ModAssets: async index complete (%zu namespaces)", s_modAssetRoots.size());
+ LogUtil::Log("[WeaveLoader] ModAssets: async index complete (%zu namespaces)", nsCount);
}).detach();
}
@@ -1172,10 +1313,10 @@ namespace GameHooks
StartModAssetIndexAsync();
return;
}
- if (s_modAssetsIndexed)
+ if (s_modAssetsIndexed.load(std::memory_order_acquire))
return;
std::lock_guard guard(s_modAssetsMutex);
- if (!s_modAssetsIndexed)
+ if (!s_modAssetsIndexed.load(std::memory_order_relaxed))
BuildModAssetIndexLocked();
}
@@ -1221,10 +1362,10 @@ namespace GameHooks
if (IsAsyncModAssetsEnabled())
{
- if (!s_modAssetsIndexed)
+ if (!s_modAssetsIndexed.load(std::memory_order_acquire))
{
StartModAssetIndexAsync();
- if (!s_modAssetsIndexed)
+ if (!s_modAssetsIndexed.load(std::memory_order_acquire))
return false;
}
}
@@ -1232,11 +1373,87 @@ namespace GameHooks
{
EnsureModAssetIndex();
}
- auto it = s_modAssetRoots.find(nsKey);
- if (it == s_modAssetRoots.end())
+ std::string rootPath;
+ {
+ std::lock_guard guard(s_modAssetsMutex);
+ auto it = s_modAssetRoots.find(nsKey);
+ if (it == s_modAssetRoots.end())
+ return false;
+ rootPath = it->second;
+ }
+
+ std::wstring rootW(rootPath.begin(), rootPath.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;
- std::wstring rootW(it->second.begin(), it->second.end());
+ outPath = fullPath;
+ return true;
+ }
+
+ static bool TryResolveModDataPath(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 kData = L"/data/";
+ size_t dataPos = lower.find(kData);
+ if (dataPos == std::wstring::npos)
+ return false;
+
+ size_t nsStart = dataPos + kData.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;
+
+ if (IsAsyncModAssetsEnabled())
+ {
+ if (!s_modAssetsIndexed.load(std::memory_order_acquire))
+ {
+ StartModAssetIndexAsync();
+ if (!s_modAssetsIndexed.load(std::memory_order_acquire))
+ return false;
+ }
+ }
+ else
+ {
+ EnsureModAssetIndex();
+ }
+ std::string rootPath;
+ {
+ std::lock_guard guard(s_modAssetsMutex);
+ auto it = s_modDataRoots.find(nsKey);
+ if (it == s_modDataRoots.end())
+ return false;
+ rootPath = it->second;
+ }
+
+ std::wstring rootW(rootPath.begin(), rootPath.end());
std::wstring relW = ns + L"/" + rel;
for (wchar_t& ch : relW)
{
@@ -1710,6 +1927,372 @@ namespace GameHooks
s_itemEntityGetItem = reinterpret_cast(itemEntityGetItem);
}
+ void SetLootSymbols(void* operatorNew,
+ void* itemInstanceCtor,
+ void* itemInstanceSharedPtrCtor,
+ void* itemInstanceSharedPtrDtor,
+ void* entitySharedPtrCtor,
+ void* entitySharedPtrDtor,
+ void* itemEntitySharedPtrDtor,
+ void* itemEntityCtorWithItem,
+ void* itemEntitySetItem,
+ void* itemEntityMakeShared,
+ void* entitySpawnAtLocation,
+ void* entitySpawnAtLocationInt,
+ void* itemInstanceSetAuxValue,
+ void* entityGetEncodeId,
+ void* entityGetEncodeIdById,
+ void* entityGetNetworkName)
+ {
+ s_operatorNew = reinterpret_cast(operatorNew);
+ s_itemInstanceCtor = reinterpret_cast(itemInstanceCtor);
+ s_itemInstanceSharedPtrCtor = reinterpret_cast(itemInstanceSharedPtrCtor);
+ s_itemInstanceSharedPtrDtor = reinterpret_cast(itemInstanceSharedPtrDtor);
+ s_entitySharedPtrCtor = reinterpret_cast(entitySharedPtrCtor);
+ s_entitySharedPtrDtor = reinterpret_cast(entitySharedPtrDtor);
+ s_itemEntitySharedPtrDtor = reinterpret_cast(itemEntitySharedPtrDtor);
+ s_itemEntityCtorWithItem = reinterpret_cast(itemEntityCtorWithItem);
+ s_itemEntitySetItem = reinterpret_cast(itemEntitySetItem);
+ s_itemEntityMakeShared = reinterpret_cast(itemEntityMakeShared);
+ s_entitySpawnAtLocation = reinterpret_cast(entitySpawnAtLocation);
+ s_entitySpawnAtLocationInt = reinterpret_cast(entitySpawnAtLocationInt);
+ s_itemInstanceSetAuxValue = reinterpret_cast(itemInstanceSetAuxValue);
+ s_entityGetEncodeId = reinterpret_cast(entityGetEncodeId);
+ s_entityGetEncodeIdById = reinterpret_cast(entityGetEncodeIdById);
+ s_entityGetNetworkName = reinterpret_cast(entityGetNetworkName);
+ }
+
+ static bool LooksLikeLevelPtr(void* levelPtr)
+ {
+ if (!levelPtr || !IsReadableRange(levelPtr, sizeof(void*)))
+ return false;
+ void* vt = *reinterpret_cast(levelPtr);
+ if (!IsCanonicalUserPtr(vt) || !IsGameCodePtr(vt))
+ return false;
+ if (!IsReadableRange(static_cast(levelPtr) + kLevelIsClientSideOffset, sizeof(bool)))
+ return false;
+ return true;
+ }
+
+ static bool TryFindLevelPtrInEntity(void* entityPtr, void*& outLevelPtr)
+ {
+ outLevelPtr = nullptr;
+ if (!entityPtr)
+ return false;
+ if (TryGetEntityLevel(entityPtr, outLevelPtr) && LooksLikeLevelPtr(outLevelPtr))
+ return true;
+
+ const char* base = static_cast(entityPtr);
+ constexpr size_t kScanBytes = 0x200;
+ for (size_t off = 0; off + sizeof(void*) <= kScanBytes; off += sizeof(void*))
+ {
+ const void* addr = base + off;
+ if (!IsReadableRange(addr, sizeof(void*)))
+ continue;
+ void* candidate = *reinterpret_cast(addr);
+ if (LooksLikeLevelPtr(candidate))
+ {
+ outLevelPtr = candidate;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ static bool TryGetLevelIsClientSide(void* levelPtr, bool& outIsClientSide)
+ {
+ outIsClientSide = false;
+ if (!LooksLikeLevelPtr(levelPtr))
+ return false;
+ outIsClientSide =
+ *reinterpret_cast(static_cast(levelPtr) + kLevelIsClientSideOffset);
+ return true;
+ }
+
+ static uint64_t MakeLootDispatchCacheKey(void* entityPtr, int dispatchKind)
+ {
+ const uint64_t p = static_cast(reinterpret_cast(entityPtr));
+ return (p << 1) ^ static_cast(dispatchKind & 1);
+ }
+
+ static bool TryGetCachedLootDispatchResult(void* entityPtr, int dispatchKind, int& outResult)
+ {
+ if (!entityPtr)
+ return false;
+ const uint64_t key = MakeLootDispatchCacheKey(entityPtr, dispatchKind);
+ const ULONGLONG nowMs = GetTickCount64();
+ constexpr ULONGLONG kDedupWindowMs = 250;
+ constexpr ULONGLONG kPurgeWindowMs = 10000;
+
+ std::lock_guard guard(s_recentLootDispatchMutex);
+ if (s_recentLootDispatch.size() > 4096)
+ {
+ for (auto it = s_recentLootDispatch.begin(); it != s_recentLootDispatch.end(); )
+ {
+ if (nowMs > it->second.timeMs && (nowMs - it->second.timeMs) > kPurgeWindowMs)
+ it = s_recentLootDispatch.erase(it);
+ else
+ ++it;
+ }
+ }
+
+ auto it = s_recentLootDispatch.find(key);
+ if (it == s_recentLootDispatch.end())
+ return false;
+ if (nowMs < it->second.timeMs || (nowMs - it->second.timeMs) > kDedupWindowMs)
+ return false;
+ outResult = it->second.result;
+ return true;
+ }
+
+ static void CacheLootDispatchResult(void* entityPtr, int dispatchKind, int result)
+ {
+ if (!entityPtr)
+ return;
+ const uint64_t key = MakeLootDispatchCacheKey(entityPtr, dispatchKind);
+ std::lock_guard guard(s_recentLootDispatchMutex);
+ s_recentLootDispatch[key] = { GetTickCount64(), result };
+ }
+
+ static bool TryCallItemInstanceCtor(void* rawItem, int itemId, int count, int aux)
+ {
+ if (!rawItem || !s_itemInstanceCtor)
+ return false;
+ __try
+ {
+ s_itemInstanceCtor(rawItem, itemId, count, aux);
+ }
+ __except (EXCEPTION_EXECUTE_HANDLER)
+ {
+ return false;
+ }
+ return true;
+ }
+
+ static bool TryCallItemEntityCtorWithItem(void* rawEntity, void* levelPtr, double x, double y, double z, void* itemSharedPtr)
+ {
+ if (!rawEntity || !levelPtr || !itemSharedPtr || !s_itemEntityCtorWithItem)
+ return false;
+ __try
+ {
+ s_itemEntityCtorWithItem(rawEntity, levelPtr, x, y, z, itemSharedPtr);
+ }
+ __except (EXCEPTION_EXECUTE_HANDLER)
+ {
+ return false;
+ }
+ return true;
+ }
+
+ static void TrackSpawnedEntity(void* entityPtr);
+ static void MarkEntityRemoved(void* entityPtr);
+
+ static bool TrySpawnItemEntityDirect(void* sourceEntityPtr, SharedPtrBlob& itemShared)
+ {
+ if (!sourceEntityPtr || !s_operatorNew || !s_itemEntityCtorWithItem || !s_entitySharedPtrCtor || !s_levelAddEntity)
+ return false;
+
+ void* levelPtr = nullptr;
+ if (!TryFindLevelPtrInEntity(sourceEntityPtr, levelPtr) || !LooksLikeLevelPtr(levelPtr))
+ return false;
+
+ double x = 0.0;
+ double y = 0.0;
+ double z = 0.0;
+ if (!TryReadEntityPos(sourceEntityPtr, x, y, z) &&
+ !TryReadEntityPosViaVirtual(sourceEntityPtr, x, y, z))
+ {
+ return false;
+ }
+
+ constexpr size_t kItemEntityAllocSize = 0x400;
+ void* rawEntity = s_operatorNew(kItemEntityAllocSize);
+ if (!rawEntity)
+ return false;
+
+ memset(rawEntity, 0, kItemEntityAllocSize);
+ if (!TryCallItemEntityCtorWithItem(rawEntity, levelPtr, x, y, z, &itemShared))
+ return false;
+
+ SharedPtrBlob entityShared{};
+ __try
+ {
+ s_entitySharedPtrCtor(&entityShared, rawEntity);
+ }
+ __except (EXCEPTION_EXECUTE_HANDLER)
+ {
+ entityShared.ptr = nullptr;
+ entityShared.ref = nullptr;
+ }
+
+ if (!entityShared.ptr)
+ return false;
+
+ bool added = false;
+ __try
+ {
+ added = s_levelAddEntity(levelPtr, &entityShared);
+ }
+ __except (EXCEPTION_EXECUTE_HANDLER)
+ {
+ added = false;
+ }
+
+ void* spawnedPtr = DecodeEntityPtrFromSharedArg(&entityShared);
+ if (added && spawnedPtr && LooksLikeEntityPtr(spawnedPtr))
+ TrackSpawnedEntity(spawnedPtr);
+
+ if (s_entitySharedPtrDtor)
+ s_entitySharedPtrDtor(&entityShared);
+ else if (s_itemEntitySharedPtrDtor)
+ s_itemEntitySharedPtrDtor(&entityShared);
+
+ return added;
+ }
+
+ static bool TrySpawnItemEntityViaEntityIO(void* entityPtr, void* itemSharedPtr, void* levelHint)
+ {
+ if (!entityPtr || !itemSharedPtr)
+ return false;
+ if (!s_entityIoNewById || !s_itemEntitySetItem || !s_levelAddEntity)
+ return false;
+
+ void* levelPtr = levelHint;
+ if (!LooksLikeLevelPtr(levelPtr))
+ {
+ if (!TryGetEntityLevel(entityPtr, levelPtr) || !LooksLikeLevelPtr(levelPtr))
+ levelPtr = nullptr;
+ }
+ if (!levelPtr && LooksLikeLevelPtr(s_currentLevel))
+ levelPtr = s_currentLevel;
+ if (!LooksLikeLevelPtr(levelPtr))
+ return false;
+
+ std::shared_ptr entity;
+ s_entityIoNewById(&entity, 1, levelPtr);
+ if (!entity)
+ return false;
+
+ ItemEntitySetItemTyped_fn setItemTyped =
+ reinterpret_cast(s_itemEntitySetItem);
+ std::shared_ptr& typedItem =
+ *reinterpret_cast*>(itemSharedPtr);
+ setItemTyped(entity.get(), typedItem);
+
+ // ItemEntity::throwTime lives at +0x1DC in this build. LegacyMinecraft sets it to 10
+ // in Entity::spawnAtLocation so the drop does not get picked up immediately.
+ *reinterpret_cast(reinterpret_cast(entity.get()) + 0x1DC) = 10;
+
+ double x = 0.0, y = 0.0, z = 0.0;
+ if (TryReadEntityPos(entityPtr, x, y, z) ||
+ TryReadEntityPosViaVirtual(entityPtr, x, y, z))
+ {
+ if (s_entityMoveTo)
+ s_entityMoveTo(entity.get(), x, y, z, 0.0f, 0.0f);
+ else if (s_entitySetPos)
+ s_entitySetPos(entity.get(), x, y, z);
+ }
+
+ bool added = s_levelAddEntity(levelPtr, &entity);
+ if (added)
+ {
+ TrackSpawnedEntity(entity.get());
+ }
+ return added;
+ }
+
+ bool SpawnItemFromEntity(void* entityPtr, int itemId, int count, int aux)
+ {
+ static int s_spawnItemAttemptLogCount = 0;
+ if (s_spawnItemAttemptLogCount < 50)
+ {
+ LogUtil::Log("[WeaveLoader] SpawnItemFromEntity attempt itemId=%d count=%d aux=%d entity=%p",
+ itemId, count, aux, entityPtr);
+ s_spawnItemAttemptLogCount++;
+ }
+ if (!entityPtr || itemId <= 0 || count <= 0)
+ return false;
+ if (!GameObjectFactory::IsRuntimeItemValid(itemId))
+ {
+ static int s_invalidLootItemLogCount = 0;
+ if (s_invalidLootItemLogCount < 20)
+ {
+ LogUtil::Log("[WeaveLoader] SpawnItemFromEntity rejected unresolved item id=%d count=%d aux=%d",
+ itemId, count, aux);
+ s_invalidLootItemLogCount++;
+ }
+ return false;
+ }
+ if (!LooksLikeEntityPtr(entityPtr))
+ return false;
+ if (IsEntityMarkedRemoved(entityPtr))
+ return false;
+ void* levelPtr = nullptr;
+ if ((!TryGetEntityLevel(entityPtr, levelPtr) || !LooksLikeLevelPtr(levelPtr)) &&
+ LooksLikeLevelPtr(s_currentLevel))
+ {
+ levelPtr = s_currentLevel;
+ }
+ bool isClientSide = false;
+ if (!TryGetLevelIsClientSide(levelPtr, isClientSide))
+ return false;
+ if (isClientSide)
+ {
+ static int s_clientLootRejectLogCount = 0;
+ if (s_clientLootRejectLogCount < 10)
+ {
+ LogUtil::Log("[WeaveLoader] SpawnItemFromEntity rejected on client side entity=%p id=%d count=%d aux=%d",
+ entityPtr, itemId, count, aux);
+ s_clientLootRejectLogCount++;
+ }
+ return false;
+ }
+
+ if (s_operatorNew && s_itemInstanceCtor && s_itemInstanceSharedPtrCtor && s_itemInstanceSharedPtrDtor)
+ {
+ constexpr size_t kItemInstanceAllocSize = 256;
+ void* rawItem = s_operatorNew(kItemInstanceAllocSize);
+ if (!rawItem)
+ return false;
+ memset(rawItem, 0, kItemInstanceAllocSize);
+ if (!TryCallItemInstanceCtor(rawItem, itemId, count, aux))
+ return false;
+
+ SharedPtrBlob itemShared{};
+ s_itemInstanceSharedPtrCtor(&itemShared, rawItem);
+ if (!DecodeItemInstancePtrFromSharedArg(&itemShared))
+ {
+ s_itemInstanceSharedPtrDtor(&itemShared);
+ return false;
+ }
+
+ if (TrySpawnItemEntityViaEntityIO(entityPtr, &itemShared, levelPtr))
+ {
+ s_itemInstanceSharedPtrDtor(&itemShared);
+ return true;
+ }
+
+ s_itemInstanceSharedPtrDtor(&itemShared);
+ }
+
+ static int s_spawnMissingItemLogCount = 0;
+ if (s_spawnMissingItemLogCount < 10)
+ {
+ LogUtil::Log("[WeaveLoader] SpawnItemFromEntity missing supported path for id=%d count=%d aux=%d (entityIo=%p setItem=%p addEntity=%p new=%p ctor=%p spCtor=%p spDtor=%p)",
+ itemId, count, aux, s_entityIoNewById, s_itemEntitySetItem, s_levelAddEntity, s_operatorNew, s_itemInstanceCtor, s_itemInstanceSharedPtrCtor, s_itemInstanceSharedPtrDtor);
+ s_spawnMissingItemLogCount++;
+ }
+
+ static int s_spawnFailedLogCount = 0;
+ if (s_spawnFailedLogCount < 10)
+ {
+ LogUtil::Log("[WeaveLoader] SpawnItemFromEntity failed to spawn id=%d count=%d aux=%d (entityIo=%p setItem=%p addEntity=%p)",
+ itemId, count, aux, s_entityIoNewById, s_itemEntitySetItem, s_levelAddEntity);
+ s_spawnFailedLogCount++;
+ }
+ return false;
+ }
+
void SetUseActionSymbols(void* inventoryRemoveResource,
void* inventoryVtable,
void* itemInstanceHurtAndBreak,
@@ -1892,6 +2475,77 @@ namespace GameHooks
return true;
}
+ static bool TryReadEntityPos(void* entityPtr, double& x, double& y, double& z)
+ {
+ x = y = z = 0.0;
+ if (!entityPtr)
+ return false;
+
+ char* base = static_cast(entityPtr);
+ if (!IsReadableRange(base + kEntityXOffset, sizeof(double)) ||
+ !IsReadableRange(base + kEntityYOffset, sizeof(double)) ||
+ !IsReadableRange(base + kEntityZOffset, sizeof(double)))
+ return false;
+
+ x = *reinterpret_cast(base + kEntityXOffset);
+ y = *reinterpret_cast(base + kEntityYOffset);
+ z = *reinterpret_cast(base + kEntityZOffset);
+ return std::isfinite(x) && std::isfinite(y) && std::isfinite(z);
+ }
+
+ static bool TryReadEntityPosViaVirtual(void* entityPtr, double& x, double& y, double& z)
+ {
+ x = y = z = 0.0;
+ if (!entityPtr || !s_livingEntityGetPos)
+ return false;
+ __try
+ {
+ void* vec = s_livingEntityGetPos(entityPtr, 1.0f);
+ if (!IsReadableRange(vec, sizeof(double) * 3))
+ return false;
+ x = *reinterpret_cast(static_cast(vec) + 0x00);
+ y = *reinterpret_cast(static_cast(vec) + 0x08);
+ z = *reinterpret_cast(static_cast(vec) + 0x10);
+ return std::isfinite(x) && std::isfinite(y) && std::isfinite(z);
+ }
+ __except (EXCEPTION_EXECUTE_HANDLER)
+ {
+ return false;
+ }
+ }
+
+ static bool LooksLikeEntityPtrStrict(void* candidate)
+ {
+ if (!LooksLikeEntityPtr(candidate))
+ return false;
+ void* vt = *reinterpret_cast(candidate);
+ if (!IsReadableRange(vt, sizeof(void*)))
+ return false;
+ void* vtFn = *reinterpret_cast(vt);
+ if (!IsCanonicalUserPtr(vtFn) || !IsGameCodePtr(vtFn))
+ return false;
+ double x = 0.0, y = 0.0, z = 0.0;
+ if (!TryReadEntityPos(candidate, x, y, z))
+ return false;
+ return true;
+ }
+
+ static bool TryReadEntityNumericId(void* entityPtr, int& outId)
+ {
+ if (!entityPtr)
+ return false;
+ if (s_entityIdOffset < 0 && !TryResolveEntityIdOffset())
+ return false;
+ const char* base = static_cast(entityPtr);
+ if (!IsReadableRange(base + s_entityIdOffset, sizeof(int)))
+ return false;
+ int id = *reinterpret_cast(base + s_entityIdOffset);
+ if (id < 0 || id > 9999)
+ return false;
+ outId = id;
+ return true;
+ }
+
static bool TryReadPlayerPos(void* playerPtr, double& x, double& y, double& z)
{
x = y = z = 0.0;
@@ -2127,6 +2781,98 @@ namespace GameHooks
return true;
}
+ static std::string WideToUtf8(const std::wstring& w)
+ {
+ if (w.empty())
+ return {};
+ if (w.size() > 256)
+ return {};
+ int len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast(w.size()), nullptr, 0, nullptr, nullptr);
+ if (len <= 0)
+ return {};
+ std::string out(len, '\0');
+ WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast(w.size()), out.data(), len, nullptr, nullptr);
+ return out;
+ }
+
+ static void NoopEntityDeleter(Entity*) {}
+
+ static bool TryCallEntityEncodeId(EntityGetEncodeId_fn fn, void* entityPtr, std::wstring* out)
+ {
+ if (!fn || !entityPtr || !out)
+ return false;
+ if (!LooksLikeEntityPtrStrict(entityPtr))
+ return false;
+ std::shared_ptr entityShared(reinterpret_cast(entityPtr), NoopEntityDeleter);
+ std::wstring result = fn(entityShared);
+ if (result.empty())
+ return false;
+ if (result.size() > 256)
+ return false;
+ *out = std::move(result);
+ return true;
+ }
+
+ static bool TryCallEntityNetworkName(EntityGetNetworkName_fn fn, void* entityPtr, std::wstring* out)
+ {
+ if (!fn || !entityPtr || !out)
+ return false;
+ if (!LooksLikeEntityPtrStrict(entityPtr))
+ return false;
+ __try
+ {
+ fn(out, entityPtr);
+ return true;
+ }
+ __except (EXCEPTION_EXECUTE_HANDLER) {}
+ return false;
+ }
+
+ static bool TryGetEntityEncodeId(void* entityPtr, std::string& outId)
+ {
+ if (!entityPtr)
+ return false;
+ if (!LooksLikeEntityPtrStrict(entityPtr))
+ return false;
+
+ std::wstring wName;
+ if (TryCallEntityEncodeId(s_entityGetEncodeId, entityPtr, &wName))
+ {
+ outId = WideToUtf8(wName);
+ if (!outId.empty())
+ return true;
+ }
+
+ if (TryCallEntityNetworkName(s_entityGetNetworkName, entityPtr, &wName))
+ {
+ std::string networkName = WideToUtf8(wName);
+ if (!networkName.empty())
+ {
+ // Common legacy format: "entity.Creeper.name" -> "Creeper"
+ const std::string prefix = "entity.";
+ const std::string suffix = ".name";
+ if (networkName.size() > (prefix.size() + suffix.size()) &&
+ networkName.compare(0, prefix.size(), prefix) == 0 &&
+ networkName.compare(networkName.size() - suffix.size(), suffix.size(), suffix) == 0)
+ {
+ networkName = networkName.substr(prefix.size(), networkName.size() - prefix.size() - suffix.size());
+ }
+ outId = std::move(networkName);
+ if (!outId.empty())
+ return true;
+ }
+ }
+
+ int numericId = -1;
+ if (!TryReadEntityNumericId(entityPtr, numericId) || !s_entityGetEncodeIdById)
+ return false;
+ wName = s_entityGetEncodeIdById(numericId);
+ if (wName.empty())
+ return false;
+ outId = WideToUtf8(wName);
+ return !outId.empty();
+ }
+
static bool IsFireballFamilyEntityId(int entityNumericId)
{
return entityNumericId == 12
@@ -2236,6 +2982,111 @@ namespace GameHooks
}
}
+ struct EntityLootNativeArgs
+ {
+ int entityNumericId;
+ const char* entityId;
+ void* entityPtr;
+ int wasKilledByPlayer;
+ int playerBonusLevel;
+ };
+
+ static int DispatchEntityLoot(void* entityPtr, bool wasKilledByPlayer, int playerBonusLevel)
+ {
+ if (!entityPtr)
+ return 0;
+ if (!LooksLikeEntityPtrStrict(entityPtr))
+ return 0;
+ int cachedResult = 0;
+ if (TryGetCachedLootDispatchResult(entityPtr, 0, cachedResult))
+ return cachedResult;
+
+ void* levelPtr = nullptr;
+ if ((!TryGetEntityLevel(entityPtr, levelPtr) || !LooksLikeLevelPtr(levelPtr)) &&
+ LooksLikeLevelPtr(s_currentLevel))
+ {
+ levelPtr = s_currentLevel;
+ }
+ bool isClientSide = false;
+ if (!TryGetLevelIsClientSide(levelPtr, isClientSide))
+ return 0;
+ if (isClientSide)
+ return 0;
+ int numericId = -1;
+ std::string encodeId;
+ const char* encodeIdPtr = nullptr;
+
+ if (TryGetEntityEncodeId(entityPtr, encodeId))
+ encodeIdPtr = encodeId.c_str();
+
+ if (!TryReadEntityNumericId(entityPtr, numericId))
+ numericId = -1;
+
+ if (!encodeIdPtr && numericId < 0)
+ return 0;
+
+ EntityLootNativeArgs args
+ {
+ numericId,
+ encodeIdPtr,
+ entityPtr,
+ wasKilledByPlayer ? 1 : 0,
+ playerBonusLevel
+ };
+
+ const int result = DotNetHost::CallEntityLoot(&args, static_cast(sizeof(args)));
+ CacheLootDispatchResult(entityPtr, 0, result);
+ return result;
+ }
+
+ static bool ShouldSkipVanillaLoot(void* entityPtr, bool wasKilledByPlayer, int playerBonusLevel)
+ {
+ const int result = DispatchEntityLoot(entityPtr, wasKilledByPlayer, playerBonusLevel);
+ return result == 2;
+ }
+
+ static int DispatchEntityLootExplicit(void* entityPtr, const char* encodeId, bool wasKilledByPlayer, int playerBonusLevel)
+ {
+ if (!entityPtr || !encodeId || encodeId[0] == '\0')
+ return 0;
+ if (!LooksLikeEntityPtrStrict(entityPtr))
+ return 0;
+ int cachedResult = 0;
+ if (TryGetCachedLootDispatchResult(entityPtr, 1, cachedResult))
+ return cachedResult;
+
+ void* levelPtr = nullptr;
+ if ((!TryGetEntityLevel(entityPtr, levelPtr) || !LooksLikeLevelPtr(levelPtr)) &&
+ LooksLikeLevelPtr(s_currentLevel))
+ {
+ levelPtr = s_currentLevel;
+ }
+ bool isClientSide = false;
+ if (!TryGetLevelIsClientSide(levelPtr, isClientSide))
+ return 0;
+ if (isClientSide)
+ return 0;
+
+ EntityLootNativeArgs args
+ {
+ -1,
+ encodeId,
+ entityPtr,
+ wasKilledByPlayer ? 1 : 0,
+ playerBonusLevel
+ };
+
+ const int result = DotNetHost::CallEntityLoot(&args, static_cast(sizeof(args)));
+ CacheLootDispatchResult(entityPtr, 1, result);
+ return result;
+ }
+
+ static bool ShouldSkipVanillaLootExplicit(void* entityPtr, const char* encodeId, bool wasKilledByPlayer, int playerBonusLevel)
+ {
+ const int result = DispatchEntityLootExplicit(entityPtr, encodeId, wasKilledByPlayer, playerBonusLevel);
+ return result == 2;
+ }
+
static void DispatchManagedBlockById(int blockId, void* level, int x, int y, int z, int eventKind, int neighborBlockId)
{
if (!ManagedBlockRegistry::IsManaged(blockId))
@@ -2580,6 +3431,80 @@ namespace GameHooks
}
}
+#define DEFINE_ENTITY_LOOT_HOOK(name) \
+ void __fastcall Hooked_##name(void* thisPtr, bool wasKilledByPlayer, int playerBonusLevel) \
+ { \
+ const bool skipVanilla = ShouldSkipVanillaLoot(thisPtr, wasKilledByPlayer, playerBonusLevel); \
+ if (!skipVanilla && Original_##name) \
+ Original_##name(thisPtr, wasKilledByPlayer, playerBonusLevel); \
+ }
+
+ DEFINE_ENTITY_LOOT_HOOK(LivingEntityDropDeathLoot)
+
+ void __fastcall Hooked_MobDropDeathLoot(void* thisPtr, bool wasKilledByPlayer, int playerBonusLevel)
+ {
+ static int s_logCount = 0;
+ if (s_logCount < 20)
+ {
+ int numericId = -1;
+ std::string encodeId;
+ const bool hasNumeric = TryReadEntityNumericId(thisPtr, numericId);
+ const bool hasEncode = TryGetEntityEncodeId(thisPtr, encodeId);
+ LogUtil::Log("[WeaveLoader] LootHook Mob observed encode=%s numeric=%d",
+ hasEncode ? encodeId.c_str() : "",
+ hasNumeric ? numericId : -1);
+ s_logCount++;
+ }
+
+ const bool skipVanilla = ShouldSkipVanillaLoot(thisPtr, wasKilledByPlayer, playerBonusLevel);
+ if (!skipVanilla && Original_MobDropDeathLoot)
+ Original_MobDropDeathLoot(thisPtr, wasKilledByPlayer, playerBonusLevel);
+ }
+
+ void __fastcall Hooked_ChickenDropDeathLoot(void* thisPtr, bool wasKilledByPlayer, int playerBonusLevel)
+ {
+ const bool skipVanilla = ShouldSkipVanillaLootExplicit(thisPtr, "Chicken", wasKilledByPlayer, playerBonusLevel);
+ static int s_logCount = 0;
+ if (s_logCount < 5)
+ {
+ int id = -1;
+ TryReadEntityNumericId(thisPtr, id);
+ LogUtil::Log("[WeaveLoader] LootHook Chicken skipVanilla=%d entityId=%d", skipVanilla ? 1 : 0, id);
+ s_logCount++;
+ }
+ if (!skipVanilla && Original_ChickenDropDeathLoot)
+ Original_ChickenDropDeathLoot(thisPtr, wasKilledByPlayer, playerBonusLevel);
+ }
+
+#define DEFINE_ENTITY_LOOT_HOOK_KNOWN(name, encodeIdLiteral) \
+ void __fastcall Hooked_##name(void* thisPtr, bool wasKilledByPlayer, int playerBonusLevel) \
+ { \
+ const bool skipVanilla = ShouldSkipVanillaLootExplicit(thisPtr, encodeIdLiteral, wasKilledByPlayer, playerBonusLevel); \
+ if (!skipVanilla && Original_##name) \
+ Original_##name(thisPtr, wasKilledByPlayer, playerBonusLevel); \
+ }
+
+ DEFINE_ENTITY_LOOT_HOOK_KNOWN(CowDropDeathLoot, "Cow")
+ DEFINE_ENTITY_LOOT_HOOK_KNOWN(PigDropDeathLoot, "Pig")
+ DEFINE_ENTITY_LOOT_HOOK_KNOWN(SheepDropDeathLoot, "Sheep")
+ DEFINE_ENTITY_LOOT_HOOK_KNOWN(SquidDropDeathLoot, "Squid")
+ DEFINE_ENTITY_LOOT_HOOK_KNOWN(OcelotDropDeathLoot, "Ozelot")
+ DEFINE_ENTITY_LOOT_HOOK_KNOWN(SnowManDropDeathLoot, "SnowMan")
+ DEFINE_ENTITY_LOOT_HOOK_KNOWN(VillagerGolemDropDeathLoot, "VillagerGolem")
+ DEFINE_ENTITY_LOOT_HOOK_KNOWN(PigZombieDropDeathLoot, "PigZombie")
+ DEFINE_ENTITY_LOOT_HOOK_KNOWN(SpiderDropDeathLoot, "Spider")
+ DEFINE_ENTITY_LOOT_HOOK_KNOWN(SkeletonDropDeathLoot, "Skeleton")
+ DEFINE_ENTITY_LOOT_HOOK_KNOWN(WitchDropDeathLoot, "Witch")
+ DEFINE_ENTITY_LOOT_HOOK_KNOWN(BlazeDropDeathLoot, "Blaze")
+ DEFINE_ENTITY_LOOT_HOOK_KNOWN(EnderManDropDeathLoot, "EnderMan")
+ DEFINE_ENTITY_LOOT_HOOK_KNOWN(GhastDropDeathLoot, "Ghast")
+ DEFINE_ENTITY_LOOT_HOOK_KNOWN(LavaSlimeDropDeathLoot, "LavaSlime")
+ DEFINE_ENTITY_LOOT_HOOK_KNOWN(WitherBossDropDeathLoot, "WitherBoss")
+
+#undef DEFINE_ENTITY_LOOT_HOOK_KNOWN
+
+#undef DEFINE_ENTITY_LOOT_HOOK
+
void __fastcall Hooked_TileStepOn(void* thisPtr, void* level, int x, int y, int z, void* entitySharedPtr)
{
if (Original_TileStepOn)
@@ -2997,9 +3922,26 @@ namespace GameHooks
int __fastcall Hooked_TileGetResource(void* thisPtr, int data, void* random, int playerBonusLevel)
{
- const ManagedBlockRegistry::Definition* def = ManagedBlockRegistry::Find(TryReadTileId(thisPtr));
+ const int tileId = TryReadTileId(thisPtr);
+ const ManagedBlockRegistry::Definition* def = ManagedBlockRegistry::Find(tileId);
if (def && def->dropBlockId >= 0)
return def->dropBlockId;
+
+ struct BlockLootNativeArgs
+ {
+ int blockNumericId;
+ int blockData;
+ int playerBonusLevel;
+ };
+
+ if (tileId >= 0)
+ {
+ const BlockLootNativeArgs args{ tileId, data, playerBonusLevel };
+ const int managedDropId = DotNetHost::CallBlockLoot(&args, static_cast(sizeof(args)));
+ if (managedDropId >= 0)
+ return managedDropId;
+ }
+
return Original_TileGetResource ? Original_TileGetResource(thisPtr, data, random, playerBonusLevel) : 0;
}
@@ -4438,6 +5380,14 @@ namespace GameHooks
return Original_GetResourceAsStream(&modAssetPath);
}
+ std::wstring modDataPath;
+ if (TryResolveModDataPath(*path, modDataPath))
+ {
+ LogUtil::Log("[WeaveLoader] getResourceAsStream: redirecting %ls -> %ls",
+ path->c_str(), modDataPath.c_str());
+ return Original_GetResourceAsStream(&modDataPath);
+ }
+
return Original_GetResourceAsStream(fileName);
}
@@ -4478,15 +5428,13 @@ namespace GameHooks
DotNetHost::CallInit();
s_initCalled = true;
- // Inject mod strings directly into the game's StringTable vector.
- // This is necessary because the compiler inlines GetString at call
- // sites like Item::getHoverName, bypassing our GetString hook.
- ModStrings::InjectAllIntoGameTable();
+ // Inject mod strings later when the StringTable is stable.
}
void __fastcall Hooked_MinecraftTick(void* thisPtr, bool bFirst, bool bUpdateTextures)
{
s_tickCounter.fetch_add(1, std::memory_order_relaxed);
+ TryInjectModStrings();
ModAtlas::PollAsyncBuild();
CullSpawnedEntitiesBelowWorld();
Original_MinecraftTick(thisPtr, bFirst, bUpdateTextures);
@@ -4564,7 +5512,6 @@ namespace GameHooks
LogUtil::Log("[WeaveLoader] Hook: Minecraft::init -- late Init fallback");
DotNetHost::CallInit();
s_initCalled = true;
- ModStrings::InjectAllIntoGameTable();
}
if (!s_postInitCalled)
@@ -4573,6 +5520,7 @@ namespace GameHooks
DotNetHost::CallPostInit();
s_postInitCalled = true;
}
+
}
void __fastcall Hooked_ExitGame(void* thisPtr)
diff --git a/WeaveLoaderRuntime/src/GameHooks.h b/WeaveLoaderRuntime/src/GameHooks.h
index 2a7a129..41b3472 100644
--- a/WeaveLoaderRuntime/src/GameHooks.h
+++ b/WeaveLoaderRuntime/src/GameHooks.h
@@ -2,10 +2,13 @@
#include
#include
#include
+#include
/// Function pointer typedefs matching the game's actual function signatures.
/// x64 MSVC uses __fastcall-like convention (this in rcx, args in rdx/r8/r9).
+class Entity;
+
typedef void (*RunStaticCtors_fn)();
typedef void (__fastcall *MinecraftTick_fn)(void* thisPtr, bool bFirst, bool bUpdateTextures);
typedef void (__fastcall *MinecraftInit_fn)(void* thisPtr);
@@ -36,6 +39,22 @@ typedef void (__fastcall *ItemInstanceInventoryTick_fn)(void* thisPtr, void* lev
typedef void (__fastcall *ItemInstanceOnCraftedBy_fn)(void* thisPtr, void* level, void* playerSharedPtr, int amount);
typedef bool (__fastcall *ItemInstanceInteractEnemy_fn)(void* thisPtr, void* playerSharedPtr, void* targetSharedPtr);
typedef void (__fastcall *ItemInstanceHurtEnemy_fn)(void* thisPtr, void* targetSharedPtr, void* playerSharedPtr);
+typedef void (__fastcall *ItemInstanceSetAuxValue_fn)(void* thisPtr, int value);
+typedef void (__fastcall *ItemEntitySetItem_fn)(void* thisPtr, void* itemSharedPtr);
+typedef void (__fastcall *ItemEntityCtorWithItem_fn)(void* thisPtr, void* levelPtr, double x, double y, double z, void* itemSharedPtr);
+struct SharedPtrBlob
+{
+ void* ptr;
+ void* ref;
+};
+
+typedef void* (__cdecl *OperatorNew_fn)(size_t size);
+typedef void (__fastcall *ItemInstanceCtor_fn)(void* thisPtr, int itemId, int count, int aux);
+typedef void (__fastcall *SharedPtrItemInstanceCtor_fn)(void* thisPtr, void* rawPtr);
+typedef void (__fastcall *SharedPtrItemInstanceDtor_fn)(void* thisPtr);
+typedef void (__fastcall *SharedPtrItemEntityDtor_fn)(void* thisPtr);
+typedef void (__fastcall *SharedPtrEntityCtor_fn)(void* thisPtr, void* rawPtr);
+typedef void (__fastcall *SharedPtrEntityDtor_fn)(void* thisPtr);
typedef bool (__fastcall *ItemMineBlock_fn)(void* thisPtr, void* itemInstanceSharedPtr, void* level, int tile, int x, int y, int z, void* ownerSharedPtr);
typedef float (__fastcall *PickaxeGetDestroySpeed_fn)(void* thisPtr, void* itemInstanceSharedPtr, void* tilePtr);
typedef bool (__fastcall *PickaxeCanDestroySpecial_fn)(void* thisPtr, void* tilePtr);
@@ -85,6 +104,11 @@ typedef void (__fastcall *EntitySetPos_fn)(void* thisPtr, double x, double y, do
typedef void* (__fastcall *EntityGetLookAngle_fn)(void* thisPtr);
typedef void* (__fastcall *LivingEntityGetViewVector_fn)(void* thisPtr, float partialTicks);
typedef void (__fastcall *EntityLerpMotion_fn)(void* thisPtr, double x, double y, double z);
+typedef std::wstring (__fastcall *EntityGetEncodeId_fn)(std::shared_ptr entitySharedPtr);
+typedef std::wstring (__fastcall *EntityGetEncodeIdById_fn)(int entityIoValue);
+typedef void (__fastcall *EntityGetNetworkName_fn)(std::wstring* out, void* thisPtr);
+typedef void (__fastcall *LivingEntityTickDeath_fn)(void* thisPtr);
+typedef void (__fastcall *LivingEntityDropDeathLoot_fn)(void* thisPtr, bool killedByPlayer, int lootingLevel);
typedef bool (__fastcall *InventoryRemoveResource_fn)(void* thisPtr, int itemId);
typedef void (__fastcall *ItemInstanceHurtAndBreak_fn)(void* thisPtr, int amount, void* ownerSharedPtr);
typedef void (__fastcall *AbstractContainerMenuBroadcastChanges_fn)(void* thisPtr);
@@ -204,6 +228,25 @@ namespace GameHooks
extern MinecraftSetLevel_fn Original_MinecraftSetLevel;
extern EntityPlayStepSound_fn Original_EntityPlayStepSound;
extern EntityCheckInsideTiles_fn Original_EntityCheckInsideTiles;
+ extern LivingEntityDropDeathLoot_fn Original_LivingEntityDropDeathLoot;
+ extern LivingEntityDropDeathLoot_fn Original_MobDropDeathLoot;
+ extern LivingEntityDropDeathLoot_fn Original_ChickenDropDeathLoot;
+ extern LivingEntityDropDeathLoot_fn Original_CowDropDeathLoot;
+ extern LivingEntityDropDeathLoot_fn Original_PigDropDeathLoot;
+ extern LivingEntityDropDeathLoot_fn Original_SheepDropDeathLoot;
+ extern LivingEntityDropDeathLoot_fn Original_SquidDropDeathLoot;
+ extern LivingEntityDropDeathLoot_fn Original_OcelotDropDeathLoot;
+ extern LivingEntityDropDeathLoot_fn Original_SnowManDropDeathLoot;
+ extern LivingEntityDropDeathLoot_fn Original_VillagerGolemDropDeathLoot;
+ extern LivingEntityDropDeathLoot_fn Original_PigZombieDropDeathLoot;
+ extern LivingEntityDropDeathLoot_fn Original_SpiderDropDeathLoot;
+ extern LivingEntityDropDeathLoot_fn Original_SkeletonDropDeathLoot;
+ extern LivingEntityDropDeathLoot_fn Original_WitchDropDeathLoot;
+ extern LivingEntityDropDeathLoot_fn Original_BlazeDropDeathLoot;
+ extern LivingEntityDropDeathLoot_fn Original_EnderManDropDeathLoot;
+ extern LivingEntityDropDeathLoot_fn Original_GhastDropDeathLoot;
+ extern LivingEntityDropDeathLoot_fn Original_LavaSlimeDropDeathLoot;
+ extern LivingEntityDropDeathLoot_fn Original_WitherBossDropDeathLoot;
extern TexturesBindTextureResource_fn Original_TexturesBindTextureResource;
extern TexturesLoadTextureByName_fn Original_TexturesLoadTextureByName;
extern TexturesLoadTextureByIndex_fn Original_TexturesLoadTextureByIndex;
@@ -320,6 +363,25 @@ namespace GameHooks
void __fastcall Hooked_MinecraftSetLevel(void* thisPtr, void* level, int message, void* forceInsertPlayerSharedPtr, bool doForceStatsSave, bool bPrimaryPlayerSignedOut);
void __fastcall Hooked_EntityPlayStepSound(void* thisPtr, int xt, int yt, int zt, int t);
void __fastcall Hooked_EntityCheckInsideTiles(void* thisPtr);
+ void __fastcall Hooked_LivingEntityDropDeathLoot(void* thisPtr, bool wasKilledByPlayer, int playerBonusLevel);
+ void __fastcall Hooked_MobDropDeathLoot(void* thisPtr, bool wasKilledByPlayer, int playerBonusLevel);
+ void __fastcall Hooked_ChickenDropDeathLoot(void* thisPtr, bool wasKilledByPlayer, int playerBonusLevel);
+ void __fastcall Hooked_CowDropDeathLoot(void* thisPtr, bool wasKilledByPlayer, int playerBonusLevel);
+ void __fastcall Hooked_PigDropDeathLoot(void* thisPtr, bool wasKilledByPlayer, int playerBonusLevel);
+ void __fastcall Hooked_SheepDropDeathLoot(void* thisPtr, bool wasKilledByPlayer, int playerBonusLevel);
+ void __fastcall Hooked_SquidDropDeathLoot(void* thisPtr, bool wasKilledByPlayer, int playerBonusLevel);
+ void __fastcall Hooked_OcelotDropDeathLoot(void* thisPtr, bool wasKilledByPlayer, int playerBonusLevel);
+ void __fastcall Hooked_SnowManDropDeathLoot(void* thisPtr, bool wasKilledByPlayer, int playerBonusLevel);
+ void __fastcall Hooked_VillagerGolemDropDeathLoot(void* thisPtr, bool wasKilledByPlayer, int playerBonusLevel);
+ void __fastcall Hooked_PigZombieDropDeathLoot(void* thisPtr, bool wasKilledByPlayer, int playerBonusLevel);
+ void __fastcall Hooked_SpiderDropDeathLoot(void* thisPtr, bool wasKilledByPlayer, int playerBonusLevel);
+ void __fastcall Hooked_SkeletonDropDeathLoot(void* thisPtr, bool wasKilledByPlayer, int playerBonusLevel);
+ void __fastcall Hooked_WitchDropDeathLoot(void* thisPtr, bool wasKilledByPlayer, int playerBonusLevel);
+ void __fastcall Hooked_BlazeDropDeathLoot(void* thisPtr, bool wasKilledByPlayer, int playerBonusLevel);
+ void __fastcall Hooked_EnderManDropDeathLoot(void* thisPtr, bool wasKilledByPlayer, int playerBonusLevel);
+ void __fastcall Hooked_GhastDropDeathLoot(void* thisPtr, bool wasKilledByPlayer, int playerBonusLevel);
+ void __fastcall Hooked_LavaSlimeDropDeathLoot(void* thisPtr, bool wasKilledByPlayer, int playerBonusLevel);
+ void __fastcall Hooked_WitherBossDropDeathLoot(void* thisPtr, bool wasKilledByPlayer, int playerBonusLevel);
void __fastcall Hooked_TexturesBindTextureResource(void* thisPtr, void* resourcePtr);
int __fastcall Hooked_TexturesLoadTextureByName(void* thisPtr, int texId, const std::wstring& resourceName);
int __fastcall Hooked_TexturesLoadTextureByIndex(void* thisPtr, int idx);
@@ -350,6 +412,23 @@ namespace GameHooks
void* entityMoveTo,
void* entitySetPos);
void SetItemRenderSymbols(void* itemEntityGetItem);
+ void SetLootSymbols(void* operatorNew,
+ void* itemInstanceCtor,
+ void* itemInstanceSharedPtrCtor,
+ void* itemInstanceSharedPtrDtor,
+ void* entitySharedPtrCtor,
+ void* entitySharedPtrDtor,
+ void* itemEntitySharedPtrDtor,
+ void* itemEntityCtorWithItem,
+ void* itemEntitySetItem,
+ void* itemEntityMakeShared,
+ void* entitySpawnAtLocation,
+ void* entitySpawnAtLocationInt,
+ void* itemInstanceSetAuxValue,
+ void* entityGetEncodeId,
+ void* entityGetEncodeIdById,
+ void* entityGetNetworkName);
+ bool SpawnItemFromEntity(void* entityPtr, int itemId, int count, int aux);
void SetUseActionSymbols(void* inventoryRemoveResource,
void* inventoryVtable,
void* itemInstanceHurtAndBreak,
diff --git a/WeaveLoaderRuntime/src/GameObjectFactory.cpp b/WeaveLoaderRuntime/src/GameObjectFactory.cpp
index a391237..2993c72 100644
--- a/WeaveLoaderRuntime/src/GameObjectFactory.cpp
+++ b/WeaveLoaderRuntime/src/GameObjectFactory.cpp
@@ -66,6 +66,7 @@ static StoneSlabItemCtor_fn fnStoneSlabItemCtor = nullptr;
static ItemSetIconName_fn fnItemSetIconName= nullptr;
static ItemSetUseDescriptionId_fn fnItemSetUseDescriptionId = nullptr;
static int s_itemDescIdOffset = -1; // offset of descriptionId field in Item, extracted from getDescriptionId
+static void* s_itemArrayObject = nullptr;
// Store ADDRESSES of Material*/SoundType* statics so we can dereference lazily
// (they're NULL at resolve time because staticCtor hasn't run yet).
@@ -116,6 +117,14 @@ static const void* GetTier(int idx)
namespace GameObjectFactory
{
+namespace
+{
+ struct ArrayWithLengthRaw
+ {
+ void** data;
+ unsigned int length;
+ };
+}
bool ResolveSymbols(SymbolResolver& resolver)
{
@@ -160,6 +169,7 @@ bool ResolveSymbols(SymbolResolver& resolver)
"?setIconName@Item@@QEAAPEAV1@AEBV?$basic_string@_WU?$char_traits@_W@std@@V?$allocator@_W@2@@std@@@Z");
fnItemSetUseDescriptionId = (ItemSetUseDescriptionId_fn)resolver.Resolve(
"?setUseDescriptionId@Item@@QEAAPEAV1@I@Z");
+ s_itemArrayObject = resolver.ResolveOptional("?items@Item@@2V?$arrayWithLength@PEAVItem@@@@A");
// Item::setDescriptionId is inlined — extract the field offset from getDescriptionId instead.
// getDescriptionId(int) is "mov eax, [rcx+offset]; ret" so we parse the offset from its opcodes.
void* fnItemGetDescId = resolver.Resolve("?getDescriptionId@Item@@UEAAIH@Z");
@@ -257,6 +267,7 @@ bool ResolveSymbols(SymbolResolver& resolver)
logSym("StoneSlabTileItem::StoneSlabTileItem", (void*)fnStoneSlabItemCtor);
logSym("Item::setIconName", (void*)fnItemSetIconName);
logSym("Item::setUseDescriptionId", (void*)fnItemSetUseDescriptionId);
+ logSym("Item::items", s_itemArrayObject);
logSym("Material::stone addr", (void*)s_materialAddrs[1]);
logSym("SOUND_STONE addr", (void*)s_soundAddrs[1]);
@@ -635,4 +646,22 @@ void* FindItem(int itemId)
return it->second;
}
+bool IsRuntimeItemValid(int itemId)
+{
+ if (itemId < 0 || itemId >= 32000)
+ return false;
+
+ if (s_itemArrayObject)
+ {
+ const auto* items = reinterpret_cast(s_itemArrayObject);
+ if (items->data && itemId < static_cast(items->length))
+ return items->data[itemId] != nullptr;
+ }
+
+ if (itemId >= 3000)
+ return FindItem(itemId) != nullptr;
+
+ return true;
+}
+
} // namespace GameObjectFactory
diff --git a/WeaveLoaderRuntime/src/GameObjectFactory.h b/WeaveLoaderRuntime/src/GameObjectFactory.h
index cb44fa9..89494e1 100644
--- a/WeaveLoaderRuntime/src/GameObjectFactory.h
+++ b/WeaveLoaderRuntime/src/GameObjectFactory.h
@@ -41,4 +41,5 @@ namespace GameObjectFactory
bool CreateSwordItem(int itemId, int tier, int maxDamage,
const wchar_t* iconName, int descriptionId = -1);
void* FindItem(int itemId);
+ bool IsRuntimeItemValid(int itemId);
}
diff --git a/WeaveLoaderRuntime/src/HookManager.cpp b/WeaveLoaderRuntime/src/HookManager.cpp
index 088fb22..8a10130 100644
--- a/WeaveLoaderRuntime/src/HookManager.cpp
+++ b/WeaveLoaderRuntime/src/HookManager.cpp
@@ -10,7 +10,39 @@
#include "NativeExports.h"
#include "WorldIdRemap.h"
#include "LogUtil.h"
+#include
#include
+#include
+
+namespace
+{
+ static bool IsExecutablePtr(void* ptr)
+ {
+ if (!ptr)
+ return false;
+ MEMORY_BASIC_INFORMATION mbi{};
+ if (!VirtualQuery(ptr, &mbi, sizeof(mbi)))
+ return false;
+ if (mbi.State != MEM_COMMIT)
+ return false;
+ const DWORD protect = mbi.Protect & 0xFF;
+ return protect == PAGE_EXECUTE
+ || protect == PAGE_EXECUTE_READ
+ || protect == PAGE_EXECUTE_READWRITE
+ || protect == PAGE_EXECUTE_WRITECOPY;
+ }
+
+ static MH_STATUS CreateHookChecked(const SymbolResolver& symbols, void* target, void* detour, void** original)
+ {
+ if (!target)
+ return MH_ERROR_NOT_EXECUTABLE;
+ if (symbols.IsStub(target))
+ return MH_ERROR_NOT_EXECUTABLE;
+ if (!IsExecutablePtr(target))
+ return MH_ERROR_NOT_EXECUTABLE;
+ return MH_CreateHook(target, detour, original);
+ }
+}
bool HookManager::Install(const SymbolResolver& symbols)
{
@@ -52,7 +84,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Core.pRunStaticCtors)
{
- if (MH_CreateHook(symbols.Core.pRunStaticCtors,
+ if (CreateHookChecked(symbols, symbols.Core.pRunStaticCtors,
reinterpret_cast(&GameHooks::Hooked_RunStaticCtors),
reinterpret_cast(&GameHooks::Original_RunStaticCtors)) != MH_OK)
{
@@ -64,7 +96,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Core.pMinecraftTick)
{
- if (MH_CreateHook(symbols.Core.pMinecraftTick,
+ if (CreateHookChecked(symbols, symbols.Core.pMinecraftTick,
reinterpret_cast(&GameHooks::Hooked_MinecraftTick),
reinterpret_cast(&GameHooks::Original_MinecraftTick)) != MH_OK)
{
@@ -76,7 +108,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Core.pMinecraftInit)
{
- if (MH_CreateHook(symbols.Core.pMinecraftInit,
+ if (CreateHookChecked(symbols, symbols.Core.pMinecraftInit,
reinterpret_cast(&GameHooks::Hooked_MinecraftInit),
reinterpret_cast(&GameHooks::Original_MinecraftInit)) != MH_OK)
{
@@ -88,7 +120,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Core.pMinecraftSetLevel)
{
- if (MH_CreateHook(symbols.Core.pMinecraftSetLevel,
+ if (CreateHookChecked(symbols, symbols.Core.pMinecraftSetLevel,
reinterpret_cast(&GameHooks::Hooked_MinecraftSetLevel),
reinterpret_cast(&GameHooks::Original_MinecraftSetLevel)) != MH_OK)
{
@@ -102,7 +134,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Item.pItemInstanceMineBlock)
{
- if (MH_CreateHook(symbols.Item.pItemInstanceMineBlock,
+ if (CreateHookChecked(symbols, symbols.Item.pItemInstanceMineBlock,
reinterpret_cast(&GameHooks::Hooked_ItemInstanceMineBlock),
reinterpret_cast(&GameHooks::Original_ItemInstanceMineBlock)) != MH_OK)
{
@@ -116,7 +148,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Entity.pServerPlayerGameModeUseItemOn)
{
- if (MH_CreateHook(symbols.Entity.pServerPlayerGameModeUseItemOn,
+ if (CreateHookChecked(symbols, symbols.Entity.pServerPlayerGameModeUseItemOn,
reinterpret_cast(&GameHooks::Hooked_ServerPlayerGameModeUseItemOn),
reinterpret_cast(&GameHooks::Original_ServerPlayerGameModeUseItemOn)) != MH_OK)
{
@@ -130,7 +162,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Entity.pMultiPlayerGameModeUseItemOn)
{
- if (MH_CreateHook(symbols.Entity.pMultiPlayerGameModeUseItemOn,
+ if (CreateHookChecked(symbols, symbols.Entity.pMultiPlayerGameModeUseItemOn,
reinterpret_cast(&GameHooks::Hooked_MultiPlayerGameModeUseItemOn),
reinterpret_cast(&GameHooks::Original_MultiPlayerGameModeUseItemOn)) != MH_OK)
{
@@ -144,7 +176,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Item.pItemInstanceUseOn)
{
- if (MH_CreateHook(symbols.Item.pItemInstanceUseOn,
+ if (CreateHookChecked(symbols, symbols.Item.pItemInstanceUseOn,
reinterpret_cast(&GameHooks::Hooked_ItemInstanceUseOn),
reinterpret_cast(&GameHooks::Original_ItemInstanceUseOn)) != MH_OK)
{
@@ -158,7 +190,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Item.pItemInstanceSave)
{
- if (MH_CreateHook(symbols.Item.pItemInstanceSave,
+ if (CreateHookChecked(symbols, symbols.Item.pItemInstanceSave,
reinterpret_cast(&GameHooks::Hooked_ItemInstanceSave),
reinterpret_cast(&GameHooks::Original_ItemInstanceSave)) != MH_OK)
{
@@ -172,7 +204,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Item.pItemInstanceLoad)
{
- if (MH_CreateHook(symbols.Item.pItemInstanceLoad,
+ if (CreateHookChecked(symbols, symbols.Item.pItemInstanceLoad,
reinterpret_cast(&GameHooks::Hooked_ItemInstanceLoad),
reinterpret_cast(&GameHooks::Original_ItemInstanceLoad)) != MH_OK)
{
@@ -186,7 +218,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Item.pItemInstanceInventoryTick)
{
- if (MH_CreateHook(symbols.Item.pItemInstanceInventoryTick,
+ if (CreateHookChecked(symbols, symbols.Item.pItemInstanceInventoryTick,
reinterpret_cast(&GameHooks::Hooked_ItemInstanceInventoryTick),
reinterpret_cast(&GameHooks::Original_ItemInstanceInventoryTick)) != MH_OK)
{
@@ -200,7 +232,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Item.pItemInstanceOnCraftedBy)
{
- if (MH_CreateHook(symbols.Item.pItemInstanceOnCraftedBy,
+ if (CreateHookChecked(symbols, symbols.Item.pItemInstanceOnCraftedBy,
reinterpret_cast(&GameHooks::Hooked_ItemInstanceOnCraftedBy),
reinterpret_cast(&GameHooks::Original_ItemInstanceOnCraftedBy)) != MH_OK)
{
@@ -214,7 +246,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Item.pItemInstanceInteractEnemy)
{
- if (MH_CreateHook(symbols.Item.pItemInstanceInteractEnemy,
+ if (CreateHookChecked(symbols, symbols.Item.pItemInstanceInteractEnemy,
reinterpret_cast(&GameHooks::Hooked_ItemInstanceInteractEnemy),
reinterpret_cast(&GameHooks::Original_ItemInstanceInteractEnemy)) != MH_OK)
{
@@ -228,7 +260,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Item.pItemInstanceHurtEnemy)
{
- if (MH_CreateHook(symbols.Item.pItemInstanceHurtEnemy,
+ if (CreateHookChecked(symbols, symbols.Item.pItemInstanceHurtEnemy,
reinterpret_cast(&GameHooks::Hooked_ItemInstanceHurtEnemy),
reinterpret_cast(&GameHooks::Original_ItemInstanceHurtEnemy)) != MH_OK)
{
@@ -242,7 +274,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Entity.pEntityPlayStepSound)
{
- if (MH_CreateHook(symbols.Entity.pEntityPlayStepSound,
+ if (CreateHookChecked(symbols, symbols.Entity.pEntityPlayStepSound,
reinterpret_cast(&GameHooks::Hooked_EntityPlayStepSound),
reinterpret_cast(&GameHooks::Original_EntityPlayStepSound)) != MH_OK)
{
@@ -256,7 +288,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Entity.pEntityCheckInsideTiles)
{
- if (MH_CreateHook(symbols.Entity.pEntityCheckInsideTiles,
+ if (CreateHookChecked(symbols, symbols.Entity.pEntityCheckInsideTiles,
reinterpret_cast(&GameHooks::Hooked_EntityCheckInsideTiles),
reinterpret_cast(&GameHooks::Original_EntityCheckInsideTiles)) != MH_OK)
{
@@ -268,10 +300,118 @@ bool HookManager::Install(const SymbolResolver& symbols)
}
}
+ std::unordered_map installedDropLootTargets;
+ auto hookDropLoot = [&](void* target, void* hookFn, void** originalFn, const char* name)
+ {
+ if (!target)
+ return;
+ if (target == symbols.Tile.pTileOnPlace)
+ {
+ LogUtil::Log("[WeaveLoader] Hook coverage: skipping %s (shared stub address)", name);
+ return;
+ }
+ auto it = installedDropLootTargets.find(target);
+ if (it != installedDropLootTargets.end())
+ {
+ LogUtil::Log("[WeaveLoader] Hook coverage: %s shares address with %s", name, it->second);
+ return;
+ }
+
+ const MH_STATUS status = CreateHookChecked(symbols, target, hookFn, originalFn);
+ if (status != MH_OK)
+ {
+ if (status == MH_ERROR_ALREADY_CREATED)
+ {
+ LogUtil::Log("[WeaveLoader] Hook coverage: %s already hooked elsewhere", name);
+ return;
+ }
+
+ LogUtil::Log("[WeaveLoader] Warning: Failed to hook %s (status=%d)", name, static_cast(status));
+ }
+ else
+ {
+ installedDropLootTargets[target] = name;
+ LogUtil::Log("[WeaveLoader] Hooked %s (loot tables)", name);
+ }
+ };
+
+ hookDropLoot(symbols.Entity.pMobDropDeathLoot,
+ reinterpret_cast(&GameHooks::Hooked_MobDropDeathLoot),
+ reinterpret_cast(&GameHooks::Original_MobDropDeathLoot),
+ "Mob::dropDeathLoot");
+ hookDropLoot(symbols.Entity.pChickenDropDeathLoot,
+ reinterpret_cast(&GameHooks::Hooked_ChickenDropDeathLoot),
+ reinterpret_cast(&GameHooks::Original_ChickenDropDeathLoot),
+ "Chicken::dropDeathLoot");
+ hookDropLoot(symbols.Entity.pCowDropDeathLoot,
+ reinterpret_cast(&GameHooks::Hooked_CowDropDeathLoot),
+ reinterpret_cast(&GameHooks::Original_CowDropDeathLoot),
+ "Cow::dropDeathLoot");
+ hookDropLoot(symbols.Entity.pPigDropDeathLoot,
+ reinterpret_cast(&GameHooks::Hooked_PigDropDeathLoot),
+ reinterpret_cast(&GameHooks::Original_PigDropDeathLoot),
+ "Pig::dropDeathLoot");
+ hookDropLoot(symbols.Entity.pSheepDropDeathLoot,
+ reinterpret_cast(&GameHooks::Hooked_SheepDropDeathLoot),
+ reinterpret_cast(&GameHooks::Original_SheepDropDeathLoot),
+ "Sheep::dropDeathLoot");
+ hookDropLoot(symbols.Entity.pSquidDropDeathLoot,
+ reinterpret_cast(&GameHooks::Hooked_SquidDropDeathLoot),
+ reinterpret_cast(&GameHooks::Original_SquidDropDeathLoot),
+ "Squid::dropDeathLoot");
+ hookDropLoot(symbols.Entity.pOcelotDropDeathLoot,
+ reinterpret_cast(&GameHooks::Hooked_OcelotDropDeathLoot),
+ reinterpret_cast(&GameHooks::Original_OcelotDropDeathLoot),
+ "Ocelot::dropDeathLoot");
+ hookDropLoot(symbols.Entity.pSnowManDropDeathLoot,
+ reinterpret_cast(&GameHooks::Hooked_SnowManDropDeathLoot),
+ reinterpret_cast(&GameHooks::Original_SnowManDropDeathLoot),
+ "SnowMan::dropDeathLoot");
+ hookDropLoot(symbols.Entity.pVillagerGolemDropDeathLoot,
+ reinterpret_cast(&GameHooks::Hooked_VillagerGolemDropDeathLoot),
+ reinterpret_cast(&GameHooks::Original_VillagerGolemDropDeathLoot),
+ "VillagerGolem::dropDeathLoot");
+ hookDropLoot(symbols.Entity.pPigZombieDropDeathLoot,
+ reinterpret_cast(&GameHooks::Hooked_PigZombieDropDeathLoot),
+ reinterpret_cast(&GameHooks::Original_PigZombieDropDeathLoot),
+ "PigZombie::dropDeathLoot");
+ hookDropLoot(symbols.Entity.pSpiderDropDeathLoot,
+ reinterpret_cast(&GameHooks::Hooked_SpiderDropDeathLoot),
+ reinterpret_cast(&GameHooks::Original_SpiderDropDeathLoot),
+ "Spider::dropDeathLoot");
+ hookDropLoot(symbols.Entity.pSkeletonDropDeathLoot,
+ reinterpret_cast(&GameHooks::Hooked_SkeletonDropDeathLoot),
+ reinterpret_cast(&GameHooks::Original_SkeletonDropDeathLoot),
+ "Skeleton::dropDeathLoot");
+ hookDropLoot(symbols.Entity.pWitchDropDeathLoot,
+ reinterpret_cast(&GameHooks::Hooked_WitchDropDeathLoot),
+ reinterpret_cast(&GameHooks::Original_WitchDropDeathLoot),
+ "Witch::dropDeathLoot");
+ hookDropLoot(symbols.Entity.pBlazeDropDeathLoot,
+ reinterpret_cast(&GameHooks::Hooked_BlazeDropDeathLoot),
+ reinterpret_cast(&GameHooks::Original_BlazeDropDeathLoot),
+ "Blaze::dropDeathLoot");
+ hookDropLoot(symbols.Entity.pEnderManDropDeathLoot,
+ reinterpret_cast(&GameHooks::Hooked_EnderManDropDeathLoot),
+ reinterpret_cast(&GameHooks::Original_EnderManDropDeathLoot),
+ "EnderMan::dropDeathLoot");
+ hookDropLoot(symbols.Entity.pGhastDropDeathLoot,
+ reinterpret_cast(&GameHooks::Hooked_GhastDropDeathLoot),
+ reinterpret_cast(&GameHooks::Original_GhastDropDeathLoot),
+ "Ghast::dropDeathLoot");
+ hookDropLoot(symbols.Entity.pLavaSlimeDropDeathLoot,
+ reinterpret_cast(&GameHooks::Hooked_LavaSlimeDropDeathLoot),
+ reinterpret_cast(&GameHooks::Original_LavaSlimeDropDeathLoot),
+ "LavaSlime::dropDeathLoot");
+ hookDropLoot(symbols.Entity.pWitherBossDropDeathLoot,
+ reinterpret_cast(&GameHooks::Hooked_WitherBossDropDeathLoot),
+ reinterpret_cast(&GameHooks::Original_WitherBossDropDeathLoot),
+ "WitherBoss::dropDeathLoot");
+
if (symbols.Item.pItemInstanceGetIcon)
{
- if (MH_CreateHook(symbols.Item.pItemInstanceGetIcon,
+ if (CreateHookChecked(symbols, symbols.Item.pItemInstanceGetIcon,
reinterpret_cast(&GameHooks::Hooked_ItemInstanceGetIcon),
reinterpret_cast(&GameHooks::Original_ItemInstanceGetIcon)) != MH_OK)
{
@@ -285,7 +425,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Texture.pEntityRendererBindTextureResource)
{
- if (MH_CreateHook(symbols.Texture.pEntityRendererBindTextureResource,
+ if (CreateHookChecked(symbols, symbols.Texture.pEntityRendererBindTextureResource,
reinterpret_cast(&GameHooks::Hooked_EntityRendererBindTextureResource),
reinterpret_cast(&GameHooks::Original_EntityRendererBindTextureResource)) != MH_OK)
{
@@ -299,7 +439,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Texture.pItemRendererRenderItemBillboard)
{
- if (MH_CreateHook(symbols.Texture.pItemRendererRenderItemBillboard,
+ if (CreateHookChecked(symbols, symbols.Texture.pItemRendererRenderItemBillboard,
reinterpret_cast(&GameHooks::Hooked_ItemRendererRenderItemBillboard),
reinterpret_cast(&GameHooks::Original_ItemRendererRenderItemBillboard)) != MH_OK)
{
@@ -313,7 +453,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Item.pItemRendererRenderGuiItem)
{
- if (MH_CreateHook(symbols.Item.pItemRendererRenderGuiItem,
+ if (CreateHookChecked(symbols, symbols.Item.pItemRendererRenderGuiItem,
reinterpret_cast(&GameHooks::Hooked_ItemRendererRenderGuiItem),
reinterpret_cast(&GameHooks::Original_ItemRendererRenderGuiItem)) != MH_OK)
{
@@ -327,7 +467,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Item.pItemInHandRendererRender)
{
- if (MH_CreateHook(symbols.Item.pItemInHandRendererRender,
+ if (CreateHookChecked(symbols, symbols.Item.pItemInHandRendererRender,
reinterpret_cast(&GameHooks::Hooked_ItemInHandRendererRender),
reinterpret_cast(&GameHooks::Original_ItemInHandRendererRender)) != MH_OK)
{
@@ -341,7 +481,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Item.pItemInHandRendererRenderItem)
{
- if (MH_CreateHook(symbols.Item.pItemInHandRendererRenderItem,
+ if (CreateHookChecked(symbols, symbols.Item.pItemInHandRendererRenderItem,
reinterpret_cast(&GameHooks::Hooked_ItemInHandRendererRenderItem),
reinterpret_cast(&GameHooks::Original_ItemInHandRendererRenderItem)) != MH_OK)
{
@@ -355,7 +495,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Texture.pCompassTextureCycleFrames)
{
- if (MH_CreateHook(symbols.Texture.pCompassTextureCycleFrames,
+ if (CreateHookChecked(symbols, symbols.Texture.pCompassTextureCycleFrames,
reinterpret_cast(&GameHooks::Hooked_CompassTextureCycleFrames),
reinterpret_cast(&GameHooks::Original_CompassTextureCycleFrames)) != MH_OK)
{
@@ -369,7 +509,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Texture.pClockTextureCycleFrames)
{
- if (MH_CreateHook(symbols.Texture.pClockTextureCycleFrames,
+ if (CreateHookChecked(symbols, symbols.Texture.pClockTextureCycleFrames,
reinterpret_cast(&GameHooks::Hooked_ClockTextureCycleFrames),
reinterpret_cast(&GameHooks::Original_ClockTextureCycleFrames)) != MH_OK)
{
@@ -383,7 +523,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Texture.pCompassTextureGetSourceWidth)
{
- if (MH_CreateHook(symbols.Texture.pCompassTextureGetSourceWidth,
+ if (CreateHookChecked(symbols, symbols.Texture.pCompassTextureGetSourceWidth,
reinterpret_cast(&GameHooks::Hooked_CompassTextureGetSourceWidth),
reinterpret_cast(&GameHooks::Original_CompassTextureGetSourceWidth)) != MH_OK)
{
@@ -397,7 +537,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Texture.pCompassTextureGetSourceHeight)
{
- if (MH_CreateHook(symbols.Texture.pCompassTextureGetSourceHeight,
+ if (CreateHookChecked(symbols, symbols.Texture.pCompassTextureGetSourceHeight,
reinterpret_cast(&GameHooks::Hooked_CompassTextureGetSourceHeight),
reinterpret_cast(&GameHooks::Original_CompassTextureGetSourceHeight)) != MH_OK)
{
@@ -411,7 +551,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Texture.pClockTextureGetSourceWidth)
{
- if (MH_CreateHook(symbols.Texture.pClockTextureGetSourceWidth,
+ if (CreateHookChecked(symbols, symbols.Texture.pClockTextureGetSourceWidth,
reinterpret_cast(&GameHooks::Hooked_ClockTextureGetSourceWidth),
reinterpret_cast(&GameHooks::Original_ClockTextureGetSourceWidth)) != MH_OK)
{
@@ -425,7 +565,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Texture.pClockTextureGetSourceHeight)
{
- if (MH_CreateHook(symbols.Texture.pClockTextureGetSourceHeight,
+ if (CreateHookChecked(symbols, symbols.Texture.pClockTextureGetSourceHeight,
reinterpret_cast(&GameHooks::Hooked_ClockTextureGetSourceHeight),
reinterpret_cast(&GameHooks::Original_ClockTextureGetSourceHeight)) != MH_OK)
{
@@ -439,7 +579,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Item.pItemMineBlock)
{
- if (MH_CreateHook(symbols.Item.pItemMineBlock,
+ if (CreateHookChecked(symbols, symbols.Item.pItemMineBlock,
reinterpret_cast(&GameHooks::Hooked_ItemMineBlock),
reinterpret_cast(&GameHooks::Original_ItemMineBlock)) != MH_OK)
{
@@ -453,7 +593,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Item.pDiggerItemMineBlock)
{
- if (MH_CreateHook(symbols.Item.pDiggerItemMineBlock,
+ if (CreateHookChecked(symbols, symbols.Item.pDiggerItemMineBlock,
reinterpret_cast(&GameHooks::Hooked_DiggerItemMineBlock),
reinterpret_cast(&GameHooks::Original_DiggerItemMineBlock)) != MH_OK)
{
@@ -467,7 +607,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Item.pPickaxeItemGetDestroySpeed)
{
- if (MH_CreateHook(symbols.Item.pPickaxeItemGetDestroySpeed,
+ if (CreateHookChecked(symbols, symbols.Item.pPickaxeItemGetDestroySpeed,
reinterpret_cast(&GameHooks::Hooked_PickaxeItemGetDestroySpeed),
reinterpret_cast(&GameHooks::Original_PickaxeItemGetDestroySpeed)) != MH_OK)
{
@@ -481,7 +621,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Item.pPickaxeItemCanDestroySpecial)
{
- if (MH_CreateHook(symbols.Item.pPickaxeItemCanDestroySpecial,
+ if (CreateHookChecked(symbols, symbols.Item.pPickaxeItemCanDestroySpecial,
reinterpret_cast(&GameHooks::Hooked_PickaxeItemCanDestroySpecial),
reinterpret_cast(&GameHooks::Original_PickaxeItemCanDestroySpecial)) != MH_OK)
{
@@ -495,7 +635,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Item.pShovelItemGetDestroySpeed)
{
- if (MH_CreateHook(symbols.Item.pShovelItemGetDestroySpeed,
+ if (CreateHookChecked(symbols, symbols.Item.pShovelItemGetDestroySpeed,
reinterpret_cast(&GameHooks::Hooked_ShovelItemGetDestroySpeed),
reinterpret_cast(&GameHooks::Original_ShovelItemGetDestroySpeed)) != MH_OK)
{
@@ -509,7 +649,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Item.pShovelItemCanDestroySpecial)
{
- if (MH_CreateHook(symbols.Item.pShovelItemCanDestroySpecial,
+ if (CreateHookChecked(symbols, symbols.Item.pShovelItemCanDestroySpecial,
reinterpret_cast(&GameHooks::Hooked_ShovelItemCanDestroySpecial),
reinterpret_cast(&GameHooks::Original_ShovelItemCanDestroySpecial)) != MH_OK)
{
@@ -523,7 +663,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Level.pLevelSetTileAndData)
{
- if (MH_CreateHook(symbols.Level.pLevelSetTileAndData,
+ if (CreateHookChecked(symbols, symbols.Level.pLevelSetTileAndData,
reinterpret_cast(&GameHooks::Hooked_LevelSetTileAndData),
reinterpret_cast(&GameHooks::Original_LevelSetTileAndData)) != MH_OK)
{
@@ -537,7 +677,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Level.pLevelSetData)
{
- if (MH_CreateHook(symbols.Level.pLevelSetData,
+ if (CreateHookChecked(symbols, symbols.Level.pLevelSetData,
reinterpret_cast(&GameHooks::Hooked_LevelSetData),
reinterpret_cast(&GameHooks::Original_LevelSetData)) != MH_OK)
{
@@ -551,7 +691,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Level.pLevelUpdateNeighborsAt)
{
- if (MH_CreateHook(symbols.Level.pLevelUpdateNeighborsAt,
+ if (CreateHookChecked(symbols, symbols.Level.pLevelUpdateNeighborsAt,
reinterpret_cast(&GameHooks::Hooked_LevelUpdateNeighborsAt),
reinterpret_cast(&GameHooks::Original_LevelUpdateNeighborsAt)) != MH_OK)
{
@@ -565,7 +705,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Tile.pTileUse)
{
- if (MH_CreateHook(symbols.Tile.pTileUse,
+ if (CreateHookChecked(symbols, symbols.Tile.pTileUse,
reinterpret_cast(&GameHooks::Hooked_TileUse),
reinterpret_cast(&GameHooks::Original_TileUse)) != MH_OK)
{
@@ -591,7 +731,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
const bool useSharedActionHook = sharedActionTarget && sharedActionCount >= 2;
if (useSharedActionHook)
{
- if (MH_CreateHook(sharedActionTarget,
+ if (CreateHookChecked(symbols, sharedActionTarget,
reinterpret_cast(&GameHooks::Hooked_TileSharedAction),
reinterpret_cast(&GameHooks::Original_TileSharedAction)) != MH_OK)
{
@@ -606,7 +746,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
{
if (symbols.Tile.pTileStepOn)
{
- if (MH_CreateHook(symbols.Tile.pTileStepOn,
+ if (CreateHookChecked(symbols, symbols.Tile.pTileStepOn,
reinterpret_cast(&GameHooks::Hooked_TileStepOn),
reinterpret_cast(&GameHooks::Original_TileStepOn)) != MH_OK)
{
@@ -620,7 +760,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Tile.pTileFallOn)
{
- if (MH_CreateHook(symbols.Tile.pTileFallOn,
+ if (CreateHookChecked(symbols, symbols.Tile.pTileFallOn,
reinterpret_cast(&GameHooks::Hooked_TileFallOn),
reinterpret_cast(&GameHooks::Original_TileFallOn)) != MH_OK)
{
@@ -648,7 +788,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
const bool useSharedLifecycleHook = sharedLifecycleTarget && sharedLifecycleCount >= 2;
if (useSharedLifecycleHook)
{
- if (MH_CreateHook(sharedLifecycleTarget,
+ if (CreateHookChecked(symbols, sharedLifecycleTarget,
reinterpret_cast(&GameHooks::Hooked_TileSharedLifecycle),
reinterpret_cast(&GameHooks::Original_TileSharedLifecycle)) != MH_OK)
{
@@ -663,7 +803,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
{
if (symbols.Tile.pTileDestroy)
{
- if (MH_CreateHook(symbols.Tile.pTileDestroy,
+ if (CreateHookChecked(symbols, symbols.Tile.pTileDestroy,
reinterpret_cast(&GameHooks::Hooked_TileDestroy),
reinterpret_cast(&GameHooks::Original_TileDestroy)) != MH_OK)
{
@@ -679,7 +819,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Tile.pTilePlayerDestroy)
{
- if (MH_CreateHook(symbols.Tile.pTilePlayerDestroy,
+ if (CreateHookChecked(symbols, symbols.Tile.pTilePlayerDestroy,
reinterpret_cast(&GameHooks::Hooked_TilePlayerDestroy),
reinterpret_cast(&GameHooks::Original_TilePlayerDestroy)) != MH_OK)
{
@@ -693,7 +833,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Tile.pTilePlayerWillDestroy)
{
- if (MH_CreateHook(symbols.Tile.pTilePlayerWillDestroy,
+ if (CreateHookChecked(symbols, symbols.Tile.pTilePlayerWillDestroy,
reinterpret_cast(&GameHooks::Hooked_TilePlayerWillDestroy),
reinterpret_cast(&GameHooks::Original_TilePlayerWillDestroy)) != MH_OK)
{
@@ -707,7 +847,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Tile.pTileSetPlacedBy)
{
- if (MH_CreateHook(symbols.Tile.pTileSetPlacedBy,
+ if (CreateHookChecked(symbols, symbols.Tile.pTileSetPlacedBy,
reinterpret_cast(&GameHooks::Hooked_TileSetPlacedBy),
reinterpret_cast(&GameHooks::Original_TileSetPlacedBy)) != MH_OK)
{
@@ -721,7 +861,7 @@ bool HookManager::Install(const SymbolResolver& symbols)
if (symbols.Level.pServerLevelTickPendingTicks)
{
- if (MH_CreateHook(symbols.Level.pServerLevelTickPendingTicks,
+ if (CreateHookChecked(symbols, symbols.Level.pServerLevelTickPendingTicks,
reinterpret_cast(&GameHooks::Hooked_ServerLevelTickPendingTicks),
reinterpret_cast