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(&GameHooks::Original_ServerLevelTickPendingTicks)) != MH_OK) { @@ -735,7 +875,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Level.pMcRegionChunkStorageLoad) { - if (MH_CreateHook(symbols.Level.pMcRegionChunkStorageLoad, + if (CreateHookChecked(symbols, symbols.Level.pMcRegionChunkStorageLoad, reinterpret_cast(&GameHooks::Hooked_McRegionChunkStorageLoad), reinterpret_cast(&GameHooks::Original_McRegionChunkStorageLoad)) != MH_OK) { @@ -749,7 +889,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Level.pMcRegionChunkStorageSave) { - if (MH_CreateHook(symbols.Level.pMcRegionChunkStorageSave, + if (CreateHookChecked(symbols, symbols.Level.pMcRegionChunkStorageSave, reinterpret_cast(&GameHooks::Hooked_McRegionChunkStorageSave), reinterpret_cast(&GameHooks::Original_McRegionChunkStorageSave)) != MH_OK) { @@ -763,7 +903,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Tile.pTileGetResource) { - if (MH_CreateHook(symbols.Tile.pTileGetResource, + if (CreateHookChecked(symbols, symbols.Tile.pTileGetResource, reinterpret_cast(&GameHooks::Hooked_TileGetResource), reinterpret_cast(&GameHooks::Original_TileGetResource)) != MH_OK) { @@ -777,7 +917,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Tile.pTileGetPlacedOnFaceDataValue) { - if (MH_CreateHook(symbols.Tile.pTileGetPlacedOnFaceDataValue, + if (CreateHookChecked(symbols, symbols.Tile.pTileGetPlacedOnFaceDataValue, reinterpret_cast(&GameHooks::Hooked_TileGetPlacedOnFaceDataValue), reinterpret_cast(&GameHooks::Original_TileGetPlacedOnFaceDataValue)) != MH_OK) { @@ -791,7 +931,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Tile.pTileCloneTileId) { - if (MH_CreateHook(symbols.Tile.pTileCloneTileId, + if (CreateHookChecked(symbols, symbols.Tile.pTileCloneTileId, reinterpret_cast(&GameHooks::Hooked_TileCloneTileId), reinterpret_cast(&GameHooks::Original_TileCloneTileId)) != MH_OK) { @@ -805,7 +945,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Tile.pTileAddAABBs) { - if (MH_CreateHook(symbols.Tile.pTileAddAABBs, + if (CreateHookChecked(symbols, symbols.Tile.pTileAddAABBs, reinterpret_cast(&GameHooks::Hooked_TileAddAABBs), reinterpret_cast(&GameHooks::Original_TileAddAABBs)) != MH_OK) { @@ -819,7 +959,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Tile.pTileUpdateDefaultShape) { - if (MH_CreateHook(symbols.Tile.pTileUpdateDefaultShape, + if (CreateHookChecked(symbols, symbols.Tile.pTileUpdateDefaultShape, reinterpret_cast(&GameHooks::Hooked_TileUpdateDefaultShape), reinterpret_cast(&GameHooks::Original_TileUpdateDefaultShape)) != MH_OK) { @@ -833,7 +973,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Tile.pTileIsSolidRender) { - if (MH_CreateHook(symbols.Tile.pTileIsSolidRender, + if (CreateHookChecked(symbols, symbols.Tile.pTileIsSolidRender, reinterpret_cast(&GameHooks::Hooked_TileIsSolidRender), reinterpret_cast(&GameHooks::Original_TileIsSolidRender)) != MH_OK) { @@ -847,7 +987,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Tile.pTileIsCubeShaped) { - if (MH_CreateHook(symbols.Tile.pTileIsCubeShaped, + if (CreateHookChecked(symbols, symbols.Tile.pTileIsCubeShaped, reinterpret_cast(&GameHooks::Hooked_TileIsCubeShaped), reinterpret_cast(&GameHooks::Original_TileIsCubeShaped)) != MH_OK) { @@ -861,7 +1001,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Tile.pTileClip) { - if (MH_CreateHook(symbols.Tile.pTileClip, + if (CreateHookChecked(symbols, symbols.Tile.pTileClip, reinterpret_cast(&GameHooks::Hooked_TileClip), reinterpret_cast(&GameHooks::Original_TileClip)) != MH_OK) { @@ -875,7 +1015,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Level.pLevelClip) { - if (MH_CreateHook(symbols.Level.pLevelClip, + if (CreateHookChecked(symbols, symbols.Level.pLevelClip, reinterpret_cast(&GameHooks::Hooked_LevelClip), reinterpret_cast(&GameHooks::Original_LevelClip)) != MH_OK) { @@ -893,7 +1033,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Tile.pTileRendererTesselateInWorld) { - if (MH_CreateHook(symbols.Tile.pTileRendererTesselateInWorld, + if (CreateHookChecked(symbols, symbols.Tile.pTileRendererTesselateInWorld, reinterpret_cast(&GameHooks::Hooked_TileRendererTesselateInWorld), reinterpret_cast(&GameHooks::Original_TileRendererTesselateInWorld)) != MH_OK) { @@ -907,7 +1047,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Tile.pTileRendererRenderTile) { - if (MH_CreateHook(symbols.Tile.pTileRendererRenderTile, + if (CreateHookChecked(symbols, symbols.Tile.pTileRendererRenderTile, reinterpret_cast(&GameHooks::Hooked_TileRendererRenderTile), reinterpret_cast(&GameHooks::Original_TileRendererRenderTile)) != MH_OK) { @@ -921,7 +1061,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Tile.pStoneSlabGetTexture) { - if (MH_CreateHook(symbols.Tile.pStoneSlabGetTexture, + if (CreateHookChecked(symbols, symbols.Tile.pStoneSlabGetTexture, reinterpret_cast(&GameHooks::Hooked_StoneSlabGetTexture), reinterpret_cast(&GameHooks::Original_StoneSlabGetTexture)) != MH_OK) { @@ -935,7 +1075,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Tile.pWoodSlabGetTexture) { - if (MH_CreateHook(symbols.Tile.pWoodSlabGetTexture, + if (CreateHookChecked(symbols, symbols.Tile.pWoodSlabGetTexture, reinterpret_cast(&GameHooks::Hooked_WoodSlabGetTexture), reinterpret_cast(&GameHooks::Original_WoodSlabGetTexture)) != MH_OK) { @@ -949,7 +1089,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Tile.pStoneSlabGetResource) { - if (MH_CreateHook(symbols.Tile.pStoneSlabGetResource, + if (CreateHookChecked(symbols, symbols.Tile.pStoneSlabGetResource, reinterpret_cast(&GameHooks::Hooked_StoneSlabGetResource), reinterpret_cast(&GameHooks::Original_StoneSlabGetResource)) != MH_OK) { @@ -963,7 +1103,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Tile.pWoodSlabGetResource) { - if (MH_CreateHook(symbols.Tile.pWoodSlabGetResource, + if (CreateHookChecked(symbols, symbols.Tile.pWoodSlabGetResource, reinterpret_cast(&GameHooks::Hooked_WoodSlabGetResource), reinterpret_cast(&GameHooks::Original_WoodSlabGetResource)) != MH_OK) { @@ -977,7 +1117,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Tile.pStoneSlabGetDescriptionId) { - if (MH_CreateHook(symbols.Tile.pStoneSlabGetDescriptionId, + if (CreateHookChecked(symbols, symbols.Tile.pStoneSlabGetDescriptionId, reinterpret_cast(&GameHooks::Hooked_StoneSlabGetDescriptionId), reinterpret_cast(&GameHooks::Original_StoneSlabGetDescriptionId)) != MH_OK) { @@ -991,7 +1131,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Tile.pWoodSlabGetDescriptionId) { - if (MH_CreateHook(symbols.Tile.pWoodSlabGetDescriptionId, + if (CreateHookChecked(symbols, symbols.Tile.pWoodSlabGetDescriptionId, reinterpret_cast(&GameHooks::Hooked_WoodSlabGetDescriptionId), reinterpret_cast(&GameHooks::Original_WoodSlabGetDescriptionId)) != MH_OK) { @@ -1005,7 +1145,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Tile.pStoneSlabGetAuxName) { - if (MH_CreateHook(symbols.Tile.pStoneSlabGetAuxName, + if (CreateHookChecked(symbols, symbols.Tile.pStoneSlabGetAuxName, reinterpret_cast(&GameHooks::Hooked_StoneSlabGetAuxName), reinterpret_cast(&GameHooks::Original_StoneSlabGetAuxName)) != MH_OK) { @@ -1019,7 +1159,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Tile.pWoodSlabGetAuxName) { - if (MH_CreateHook(symbols.Tile.pWoodSlabGetAuxName, + if (CreateHookChecked(symbols, symbols.Tile.pWoodSlabGetAuxName, reinterpret_cast(&GameHooks::Hooked_WoodSlabGetAuxName), reinterpret_cast(&GameHooks::Original_WoodSlabGetAuxName)) != MH_OK) { @@ -1033,7 +1173,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Tile.pStoneSlabRegisterIcons) { - if (MH_CreateHook(symbols.Tile.pStoneSlabRegisterIcons, + if (CreateHookChecked(symbols, symbols.Tile.pStoneSlabRegisterIcons, reinterpret_cast(&GameHooks::Hooked_StoneSlabRegisterIcons), reinterpret_cast(&GameHooks::Original_StoneSlabRegisterIcons)) != MH_OK) { @@ -1048,7 +1188,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Tile.pWoodSlabRegisterIcons) { - if (MH_CreateHook(symbols.Tile.pWoodSlabRegisterIcons, + if (CreateHookChecked(symbols, symbols.Tile.pWoodSlabRegisterIcons, reinterpret_cast(&GameHooks::Hooked_WoodSlabRegisterIcons), reinterpret_cast(&GameHooks::Original_WoodSlabRegisterIcons)) != MH_OK) { @@ -1063,7 +1203,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Tile.pHalfSlabCloneTileId) { - if (MH_CreateHook(symbols.Tile.pHalfSlabCloneTileId, + if (CreateHookChecked(symbols, symbols.Tile.pHalfSlabCloneTileId, reinterpret_cast(&GameHooks::Hooked_HalfSlabCloneTileId), reinterpret_cast(&GameHooks::Original_HalfSlabCloneTileId)) != MH_OK) { @@ -1077,7 +1217,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Tile.pStoneSlabItemGetDescriptionId) { - if (MH_CreateHook(symbols.Tile.pStoneSlabItemGetDescriptionId, + if (CreateHookChecked(symbols, symbols.Tile.pStoneSlabItemGetDescriptionId, reinterpret_cast(&GameHooks::Hooked_StoneSlabItemGetDescriptionId), reinterpret_cast(&GameHooks::Original_StoneSlabItemGetDescriptionId)) != MH_OK) { @@ -1091,7 +1231,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Tile.pStoneSlabItemGetIcon) { - if (MH_CreateHook(symbols.Tile.pStoneSlabItemGetIcon, + if (CreateHookChecked(symbols, symbols.Tile.pStoneSlabItemGetIcon, reinterpret_cast(&GameHooks::Hooked_StoneSlabItemGetIcon), reinterpret_cast(&GameHooks::Original_StoneSlabItemGetIcon)) != MH_OK) { @@ -1105,7 +1245,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Entity.pPlayerCanDestroy) { - if (MH_CreateHook(symbols.Entity.pPlayerCanDestroy, + if (CreateHookChecked(symbols, symbols.Entity.pPlayerCanDestroy, reinterpret_cast(&GameHooks::Hooked_PlayerCanDestroy), reinterpret_cast(&GameHooks::Original_PlayerCanDestroy)) != MH_OK) { @@ -1119,7 +1259,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Entity.pServerPlayerGameModeUseItem) { - if (MH_CreateHook(symbols.Entity.pServerPlayerGameModeUseItem, + if (CreateHookChecked(symbols, symbols.Entity.pServerPlayerGameModeUseItem, reinterpret_cast(&GameHooks::Hooked_ServerPlayerGameModeUseItem), reinterpret_cast(&GameHooks::Original_ServerPlayerGameModeUseItem)) != MH_OK) { @@ -1133,7 +1273,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Entity.pMultiPlayerGameModeUseItem) { - if (MH_CreateHook(symbols.Entity.pMultiPlayerGameModeUseItem, + if (CreateHookChecked(symbols, symbols.Entity.pMultiPlayerGameModeUseItem, reinterpret_cast(&GameHooks::Hooked_MultiPlayerGameModeUseItem), reinterpret_cast(&GameHooks::Original_MultiPlayerGameModeUseItem)) != MH_OK) { @@ -1162,7 +1302,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Texture.pTexturesBindTextureResource) { - if (MH_CreateHook(symbols.Texture.pTexturesBindTextureResource, + if (CreateHookChecked(symbols, symbols.Texture.pTexturesBindTextureResource, reinterpret_cast(&GameHooks::Hooked_TexturesBindTextureResource), reinterpret_cast(&GameHooks::Original_TexturesBindTextureResource)) != MH_OK) { @@ -1176,7 +1316,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Texture.pTexturesLoadTextureByName) { - if (MH_CreateHook(symbols.Texture.pTexturesLoadTextureByName, + if (CreateHookChecked(symbols, symbols.Texture.pTexturesLoadTextureByName, reinterpret_cast(&GameHooks::Hooked_TexturesLoadTextureByName), reinterpret_cast(&GameHooks::Original_TexturesLoadTextureByName)) != MH_OK) { @@ -1190,7 +1330,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Texture.pTexturesLoadTextureByIndex) { - if (MH_CreateHook(symbols.Texture.pTexturesLoadTextureByIndex, + if (CreateHookChecked(symbols, symbols.Texture.pTexturesLoadTextureByIndex, reinterpret_cast(&GameHooks::Hooked_TexturesLoadTextureByIndex), reinterpret_cast(&GameHooks::Original_TexturesLoadTextureByIndex)) != MH_OK) { @@ -1204,7 +1344,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Texture.pTexturesReadImage) { - if (MH_CreateHook(symbols.Texture.pTexturesReadImage, + if (CreateHookChecked(symbols, symbols.Texture.pTexturesReadImage, reinterpret_cast(&GameHooks::Hooked_TexturesReadImage), reinterpret_cast(&GameHooks::Original_TexturesReadImage)) != MH_OK) { @@ -1218,7 +1358,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Texture.pTextureManagerCreateTexture) { - if (MH_CreateHook(symbols.Texture.pTextureManagerCreateTexture, + if (CreateHookChecked(symbols, symbols.Texture.pTextureManagerCreateTexture, reinterpret_cast(&GameHooks::Hooked_TextureManagerCreateTexture), reinterpret_cast(&GameHooks::Original_TextureManagerCreateTexture)) != MH_OK) { @@ -1232,7 +1372,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Texture.pTextureTransferFromImage) { - if (MH_CreateHook(symbols.Texture.pTextureTransferFromImage, + if (CreateHookChecked(symbols, symbols.Texture.pTextureTransferFromImage, reinterpret_cast(&GameHooks::Hooked_TextureTransferFromImage), reinterpret_cast(&GameHooks::Original_TextureTransferFromImage)) != MH_OK) { @@ -1246,7 +1386,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Resource.pAbstractTexturePackGetImageResource) { - if (MH_CreateHook(symbols.Resource.pAbstractTexturePackGetImageResource, + if (CreateHookChecked(symbols, symbols.Resource.pAbstractTexturePackGetImageResource, reinterpret_cast(&GameHooks::Hooked_AbstractTexturePackGetImageResource), reinterpret_cast(&GameHooks::Original_AbstractTexturePackGetImageResource)) != MH_OK) { @@ -1260,7 +1400,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Resource.pDLCTexturePackGetImageResource) { - if (MH_CreateHook(symbols.Resource.pDLCTexturePackGetImageResource, + if (CreateHookChecked(symbols, symbols.Resource.pDLCTexturePackGetImageResource, reinterpret_cast(&GameHooks::Hooked_DLCTexturePackGetImageResource), reinterpret_cast(&GameHooks::Original_DLCTexturePackGetImageResource)) != MH_OK) { @@ -1277,7 +1417,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Texture.pStitchedGetU0) { - if (MH_CreateHook(symbols.Texture.pStitchedGetU0, + if (CreateHookChecked(symbols, symbols.Texture.pStitchedGetU0, reinterpret_cast(&GameHooks::Hooked_StitchedGetU0), reinterpret_cast(&GameHooks::Original_StitchedGetU0)) != MH_OK) { @@ -1286,7 +1426,7 @@ bool HookManager::Install(const SymbolResolver& symbols) } if (symbols.Texture.pStitchedGetU1) { - if (MH_CreateHook(symbols.Texture.pStitchedGetU1, + if (CreateHookChecked(symbols, symbols.Texture.pStitchedGetU1, reinterpret_cast(&GameHooks::Hooked_StitchedGetU1), reinterpret_cast(&GameHooks::Original_StitchedGetU1)) != MH_OK) { @@ -1295,7 +1435,7 @@ bool HookManager::Install(const SymbolResolver& symbols) } if (symbols.Texture.pStitchedGetV0) { - if (MH_CreateHook(symbols.Texture.pStitchedGetV0, + if (CreateHookChecked(symbols, symbols.Texture.pStitchedGetV0, reinterpret_cast(&GameHooks::Hooked_StitchedGetV0), reinterpret_cast(&GameHooks::Original_StitchedGetV0)) != MH_OK) { @@ -1304,7 +1444,7 @@ bool HookManager::Install(const SymbolResolver& symbols) } if (symbols.Texture.pStitchedGetV1) { - if (MH_CreateHook(symbols.Texture.pStitchedGetV1, + if (CreateHookChecked(symbols, symbols.Texture.pStitchedGetV1, reinterpret_cast(&GameHooks::Hooked_StitchedGetV1), reinterpret_cast(&GameHooks::Original_StitchedGetV1)) != MH_OK) { @@ -1314,7 +1454,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Core.pExitGame) { - if (MH_CreateHook(symbols.Core.pExitGame, + if (CreateHookChecked(symbols, symbols.Core.pExitGame, reinterpret_cast(&GameHooks::Hooked_ExitGame), reinterpret_cast(&GameHooks::Original_ExitGame)) != MH_OK) { @@ -1334,6 +1474,23 @@ bool HookManager::Install(const SymbolResolver& symbols) symbols.Entity.pEntityMoveTo, symbols.Entity.pEntitySetPos); GameHooks::SetItemRenderSymbols(symbols.Item.pItemEntityGetItem); + GameHooks::SetLootSymbols( + symbols.Texture.pOperatorNew, + symbols.Item.pItemInstanceCtor, + symbols.Item.pItemInstanceSharedPtrCtor, + symbols.Item.pItemInstanceSharedPtrDtor, + symbols.Item.pEntitySharedPtrCtor, + symbols.Item.pEntitySharedPtrDtor, + symbols.Item.pItemEntitySharedPtrDtor, + symbols.Item.pItemEntityCtorWithItem, + symbols.Item.pItemEntitySetItem, + symbols.Item.pItemEntityMakeShared, + symbols.Entity.pEntitySpawnAtLocation, + symbols.Entity.pEntitySpawnAtLocationInt, + symbols.Item.pItemInstanceSetAuxValue, + symbols.Entity.pEntityGetEncodeId, + symbols.Entity.pEntityGetEncodeIdById, + symbols.Entity.pEntityGetNetworkName); GameHooks::SetUseActionSymbols( symbols.Inventory.pInventoryRemoveResource, symbols.Inventory.pInventoryVtable, @@ -1347,7 +1504,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Entity.pLivingEntityPick) { - if (MH_CreateHook(symbols.Entity.pLivingEntityPick, + if (CreateHookChecked(symbols, symbols.Entity.pLivingEntityPick, reinterpret_cast(&GameHooks::Hooked_LivingEntityPick), reinterpret_cast(&GameHooks::Original_LivingEntityPick)) != MH_OK) { @@ -1361,7 +1518,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Texture.pPreStitchedTextureMapStitch) { - if (MH_CreateHook(symbols.Texture.pPreStitchedTextureMapStitch, + if (CreateHookChecked(symbols, symbols.Texture.pPreStitchedTextureMapStitch, reinterpret_cast(&GameHooks::Hooked_PreStitchedTextureMapStitch), reinterpret_cast(&GameHooks::Original_PreStitchedTextureMapStitch)) != MH_OK) { @@ -1376,7 +1533,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Texture.pLoadUVs && symbols.Texture.pSimpleIconCtor && symbols.Texture.pOperatorNew) { ModAtlas::SetInjectSymbols(symbols.Texture.pSimpleIconCtor, symbols.Texture.pOperatorNew); - if (MH_CreateHook(symbols.Texture.pLoadUVs, + if (CreateHookChecked(symbols, symbols.Texture.pLoadUVs, reinterpret_cast(&GameHooks::Hooked_LoadUVs), reinterpret_cast(&GameHooks::Original_LoadUVs)) != MH_OK) { @@ -1389,7 +1546,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Texture.pRegisterIcon) { - if (MH_CreateHook(symbols.Texture.pRegisterIcon, + if (CreateHookChecked(symbols, symbols.Texture.pRegisterIcon, reinterpret_cast(&GameHooks::Hooked_RegisterIcon), reinterpret_cast(&GameHooks::Original_RegisterIcon)) != MH_OK) { @@ -1413,7 +1570,7 @@ bool HookManager::Install(const SymbolResolver& symbols) { CreativeInventory::ResolveSymbols(const_cast(symbols)); - if (MH_CreateHook(symbols.Ui.pCreativeStaticCtor, + if (CreateHookChecked(symbols, symbols.Ui.pCreativeStaticCtor, reinterpret_cast(&GameHooks::Hooked_CreativeStaticCtor), reinterpret_cast(&GameHooks::Original_CreativeStaticCtor)) != MH_OK) { @@ -1427,7 +1584,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Ui.pMainMenuCustomDraw) { - if (MH_CreateHook(symbols.Ui.pMainMenuCustomDraw, + if (CreateHookChecked(symbols, symbols.Ui.pMainMenuCustomDraw, reinterpret_cast(&GameHooks::Hooked_MainMenuCustomDraw), reinterpret_cast(&GameHooks::Original_MainMenuCustomDraw)) != MH_OK) { @@ -1443,7 +1600,7 @@ bool HookManager::Install(const SymbolResolver& symbols) { MainMenuOverlay::ResolveSymbols(const_cast(symbols)); - if (MH_CreateHook(symbols.Core.pPresent, + if (CreateHookChecked(symbols, symbols.Core.pPresent, reinterpret_cast(&GameHooks::Hooked_Present), reinterpret_cast(&GameHooks::Original_Present)) != MH_OK) { @@ -1460,7 +1617,7 @@ bool HookManager::Install(const SymbolResolver& symbols) // Read GetString prologue bytes BEFORE MinHook overwrites them. ModStrings::CaptureStringTableRef(symbols.Ui.pGetString); - if (MH_CreateHook(symbols.Ui.pGetString, + if (CreateHookChecked(symbols, symbols.Ui.pGetString, reinterpret_cast(&GameHooks::Hooked_GetString), reinterpret_cast(&GameHooks::Original_GetString)) != MH_OK) { @@ -1475,7 +1632,7 @@ bool HookManager::Install(const SymbolResolver& symbols) if (symbols.Resource.pGetResourceAsStream) { - if (MH_CreateHook(symbols.Resource.pGetResourceAsStream, + if (CreateHookChecked(symbols, symbols.Resource.pGetResourceAsStream, reinterpret_cast(&GameHooks::Hooked_GetResourceAsStream), reinterpret_cast(&GameHooks::Original_GetResourceAsStream)) != MH_OK) { @@ -1492,7 +1649,7 @@ bool HookManager::Install(const SymbolResolver& symbols) GetProcAddress(GetModuleHandleA("kernel32.dll"), "OutputDebugStringA")); if (pOutputDbgStr) { - if (MH_CreateHook(pOutputDbgStr, + if (CreateHookChecked(symbols, pOutputDbgStr, reinterpret_cast(&GameHooks::Hooked_OutputDebugStringA), reinterpret_cast(&GameHooks::Original_OutputDebugStringA)) != MH_OK) { diff --git a/WeaveLoaderRuntime/src/ModStrings.cpp b/WeaveLoaderRuntime/src/ModStrings.cpp index cd6a856..b050983 100644 --- a/WeaveLoaderRuntime/src/ModStrings.cpp +++ b/WeaveLoaderRuntime/src/ModStrings.cpp @@ -162,19 +162,19 @@ namespace ModStrings return nullptr; } - void InjectAllIntoGameTable() + bool InjectAllIntoGameTable() { if (!s_pStringTableField) { LogUtil::Log("[WeaveLoader] ModStrings: no string table ref - cannot inject"); - return; + return false; } void* stringTable = *s_pStringTableField; if (!stringTable) { LogUtil::Log("[WeaveLoader] ModStrings: m_stringTable pointer is NULL"); - return; + return false; } LogUtil::Log("[WeaveLoader] ModStrings: StringTable object at %p", stringTable); @@ -183,14 +183,14 @@ namespace ModStrings if (!vec) { LogUtil::Log("[WeaveLoader] ModStrings: FAILED to locate m_stringsVec in StringTable"); - return; + return false; } std::lock_guard lock(s_mutex); if (s_strings.empty()) { LogUtil::Log("[WeaveLoader] ModStrings: no mod strings to inject"); - return; + return false; } // Find the highest ID we need @@ -213,5 +213,6 @@ namespace ModStrings } LogUtil::Log("[WeaveLoader] ModStrings: injected %d mod strings into game string table", count); + return true; } } diff --git a/WeaveLoaderRuntime/src/ModStrings.h b/WeaveLoaderRuntime/src/ModStrings.h index a02011f..59e2a74 100644 --- a/WeaveLoaderRuntime/src/ModStrings.h +++ b/WeaveLoaderRuntime/src/ModStrings.h @@ -21,5 +21,5 @@ namespace ModStrings // After the string table is loaded (e.g. during PreInit), call this // to inject all previously registered mod strings into the game's // m_stringsVec so that inlined GetString calls find them. - void InjectAllIntoGameTable(); + bool InjectAllIntoGameTable(); } diff --git a/WeaveLoaderRuntime/src/NativeExports.cpp b/WeaveLoaderRuntime/src/NativeExports.cpp index 8fc3b7c..a366227 100644 --- a/WeaveLoaderRuntime/src/NativeExports.cpp +++ b/WeaveLoaderRuntime/src/NativeExports.cpp @@ -363,6 +363,11 @@ int native_register_item( { LogUtil::Log("[WeaveLoader] Warning: failed to create game Item for '%s' id=%d", namespacedId, id); } + else if (!GameObjectFactory::IsRuntimeItemValid(id)) + { + LogUtil::Log("[WeaveLoader] ERROR: item '%s' id=%d was created but Item::items[%d] is null", + namespacedId, id, id); + } return id; } @@ -753,6 +758,13 @@ int native_summon_entity_by_id(int numericEntityId, double x, double y, double z return 1; } +int native_spawn_item_from_entity(void* entityPtr, int numericItemId, int count, int aux) +{ + if (!entityPtr || numericItemId < 0 || count <= 0) + return 0; + return GameHooks::SpawnItemFromEntity(entityPtr, numericItemId, count, aux) ? 1 : 0; +} + int native_level_has_neighbor_signal(void* levelPtr, int x, int y, int z) { return (levelPtr && s_levelHasNeighborSignal && s_levelHasNeighborSignal(levelPtr, x, y, z)) ? 1 : 0; diff --git a/WeaveLoaderRuntime/src/NativeExports.h b/WeaveLoaderRuntime/src/NativeExports.h index 2901d1a..71ca0d7 100644 --- a/WeaveLoaderRuntime/src/NativeExports.h +++ b/WeaveLoaderRuntime/src/NativeExports.h @@ -204,6 +204,7 @@ extern "C" __declspec(dllexport) int native_spawn_entity_from_player_look(void* playerPtr, void* playerSharedPtr, int numericEntityId, double speed, double spawnForward, double spawnUp); __declspec(dllexport) int native_summon_entity(const char* namespacedId, double x, double y, double z); __declspec(dllexport) int native_summon_entity_by_id(int numericEntityId, double x, double y, double z); + __declspec(dllexport) int native_spawn_item_from_entity(void* entityPtr, int numericItemId, int count, int aux); __declspec(dllexport) int native_level_has_neighbor_signal(void* levelPtr, int x, int y, int z); __declspec(dllexport) int native_level_set_tile(void* levelPtr, int x, int y, int z, int blockId, int data, int flags); __declspec(dllexport) int native_level_schedule_tick(void* levelPtr, int x, int y, int z, int blockId, int delay); diff --git a/WeaveLoaderRuntime/src/SymbolResolver.cpp b/WeaveLoaderRuntime/src/SymbolResolver.cpp index 967d9c1..3791479 100644 --- a/WeaveLoaderRuntime/src/SymbolResolver.cpp +++ b/WeaveLoaderRuntime/src/SymbolResolver.cpp @@ -115,6 +115,28 @@ void* SymbolResolver::ResolveExact(const char* exactName) return reinterpret_cast(m_moduleBase + rva); } +void* SymbolResolver::ResolveOptional(const char* decoratedName) +{ + if (!m_initialized) return nullptr; + + uint32_t rva = PdbParser::FindSymbolRVA(decoratedName); + if (rva == 0) + return nullptr; + + return reinterpret_cast(m_moduleBase + rva); +} + +void* SymbolResolver::ResolveExactOptional(const char* exactName) +{ + if (!m_initialized) return nullptr; + + uint32_t rva = PdbParser::FindSymbolRVAByName(exactName); + if (rva == 0) + return nullptr; + + return reinterpret_cast(m_moduleBase + rva); +} + bool SymbolResolver::IsStub(void* ptr) const { return ptr == reinterpret_cast(m_moduleBase + 0x31000u); diff --git a/WeaveLoaderRuntime/src/SymbolResolver.h b/WeaveLoaderRuntime/src/SymbolResolver.h index 5ba8906..800c124 100644 --- a/WeaveLoaderRuntime/src/SymbolResolver.h +++ b/WeaveLoaderRuntime/src/SymbolResolver.h @@ -11,6 +11,8 @@ public: void* Resolve(const char* decoratedName); void* ResolveExact(const char* exactName); + void* ResolveOptional(const char* decoratedName); + void* ResolveExactOptional(const char* exactName); bool IsStub(void* ptr) const; CoreSymbols Core; diff --git a/WeaveLoaderRuntime/src/Symbols/SymbolGroups.cpp b/WeaveLoaderRuntime/src/Symbols/SymbolGroups.cpp index 5b54793..e8656b0 100644 --- a/WeaveLoaderRuntime/src/Symbols/SymbolGroups.cpp +++ b/WeaveLoaderRuntime/src/Symbols/SymbolGroups.cpp @@ -14,6 +14,12 @@ namespace LogUtil::Log("[WeaveLoader] MISSING: %s", name); } + static void LogSymOptional(const char* name, void* ptr) + { + if (ptr) + LogUtil::Log("[WeaveLoader] %-25s @ %p", name, ptr); + } + static const char* SYM_RUN_STATIC_CTORS = "?MinecraftWorld_RunStaticCtors@@YAXXZ"; static const char* SYM_MINECRAFT_TICK = "?tick@Minecraft@@QEAAX_N0@Z"; static const char* SYM_MINECRAFT_INIT = "?init@Minecraft@@QEAAXXZ"; @@ -71,16 +77,28 @@ namespace static const char* SYM_ITEMINSTANCE_ONCRAFTEDBY = "?onCraftedBy@ItemInstance@@QEAAXPEAVLevel@@V?$shared_ptr@VPlayer@@@std@@H@Z"; static const char* SYM_ITEMINSTANCE_INTERACTENEMY = "?interactEnemy@ItemInstance@@QEAA_NV?$shared_ptr@VPlayer@@@std@@V?$shared_ptr@VLivingEntity@@@3@@Z"; static const char* SYM_ITEMINSTANCE_HURTENEMY = "?hurtEnemy@ItemInstance@@QEAAXV?$shared_ptr@VLivingEntity@@@std@@V?$shared_ptr@VPlayer@@@3@@Z"; + // Not present in this build's mapping.json/PDB. Keep null so we don't spam missing-symbol logs. + static const char* SYM_ITEMINSTANCE_SETAUXVALUE = nullptr; + static const char* SYM_ITEMINSTANCE_SHAREDPTR_DTOR = "??1?$shared_ptr@VItemInstance@@@std@@QEAA@XZ"; + static const char* SYM_ITEMENTITY_SHAREDPTR_DTOR = "??1?$shared_ptr@VItemEntity@@@std@@QEAA@XZ"; + static const char* SYM_ITEMENTITY_SETITEM = "?setItem@ItemEntity@@QEAAXV?$shared_ptr@VItemInstance@@@std@@@Z"; + static const char* SYM_ITEMINSTANCE_CTOR_INT = "??0ItemInstance@@QEAA@HHH@Z"; + static const char* SYM_ITEMINSTANCE_SHAREDPTR_CTOR = "??$?0VItemInstance@@$0A@@?$shared_ptr@VItemInstance@@@std@@QEAA@PEAVItemInstance@@@Z"; + static const char* SYM_ENTITY_SHAREDPTR_CTOR = "??$?0VEntity@@$0A@@?$shared_ptr@VEntity@@@std@@QEAA@PEAVEntity@@@Z"; + static const char* SYM_ENTITY_SHAREDPTR_DTOR = "??1?$shared_ptr@VEntity@@@std@@QEAA@XZ"; static const char* SYM_ITEM_MINEBLOCK = "?mineBlock@Item@@UEAA_NV?$shared_ptr@VItemInstance@@@std@@PEAVLevel@@HHHHV?$shared_ptr@VLivingEntity@@@3@@Z"; static const char* SYM_DIGGERITEM_MINEBLOCK = "?mineBlock@DiggerItem@@UEAA_NV?$shared_ptr@VItemInstance@@@std@@PEAVLevel@@HHHHV?$shared_ptr@VLivingEntity@@@3@@Z"; static const char* SYM_PICKAXEITEM_GETDESTROYSPEED = "?getDestroySpeed@PickaxeItem@@UEAAMV?$shared_ptr@VItemInstance@@@std@@PEAVTile@@@Z"; static const char* SYM_PICKAXEITEM_CANDESTROYSPECIAL = "?canDestroySpecial@PickaxeItem@@UEAA_NPEAVTile@@@Z"; - static const char* SYM_SHOVELITEM_GETDESTROYSPEED = "?getDestroySpeed@ShovelItem@@UEAAMV?$shared_ptr@VItemInstance@@@std@@PEAVTile@@@Z"; + static const char* SYM_SHOVELITEM_GETDESTROYSPEED = "?getDestroySpeed@DiggerItem@@UEAAMV?$shared_ptr@VItemInstance@@@std@@PEAVTile@@@Z"; static const char* SYM_SHOVELITEM_CANDESTROYSPECIAL = "?canDestroySpecial@ShovelItem@@UEAA_NPEAVTile@@@Z"; static const char* SYM_ITEMENTITY_GETITEM = "?getItem@ItemEntity@@QEAA?AV?$shared_ptr@VItemInstance@@@std@@XZ"; + static const char* SYM_ITEMENTITY_CTOR_WITH_ITEM = "??0ItemEntity@@QEAA@PEAVLevel@@NNNV?$shared_ptr@VItemInstance@@@std@@@Z"; + static const char* SYM_ITEMENTITY_MAKE_SHARED = "??$make_shared@VItemEntity@@AEAPEAVLevel@@AEANNAEANAEAV?$shared_ptr@VItemInstance@@@std@@@std@@YA?AV?$shared_ptr@VItemEntity@@@0@AEAPEAVLevel@@AEAN$$QEAN1AEAV?$shared_ptr@VItemInstance@@@0@@Z"; static const char* SYM_ITEMRENDERER_RENDERGUIITEM = "?renderGuiItem@ItemRenderer@@QEAAXPEAVFont@@PEAVTextures@@V?$shared_ptr@VItemInstance@@@std@@MMMMM_N@Z"; static const char* SYM_ITEMINHANDRENDERER_RENDER = "?render@ItemInHandRenderer@@QEAAXM@Z"; static const char* SYM_ITEMINHANDRENDERER_RENDERITEM = "?renderItem@ItemInHandRenderer@@QEAAXV?$shared_ptr@VLivingEntity@@@std@@V?$shared_ptr@VItemInstance@@@3@H_N@Z"; + static const char* SYM_ENTITY_SPAWNATLOCATION_INT = nullptr; static const char* SYM_TILE_ONPLACE = "?onPlace@Tile@@UEAAXPEAVLevel@@HHH@Z"; static const char* SYM_TILE_NEIGHBORCHANGED = "?neighborChanged@Tile@@UEAAXPEAVLevel@@HHHH@Z"; @@ -158,10 +176,34 @@ namespace static const char* SYM_LIVINGENTITY_GETLOOKANGLE = "?getLookAngle@LivingEntity@@UEAAPEAVVec3@@XZ"; static const char* SYM_ENTITY_GETLOOKANGLE = "?getLookAngle@Entity@@UEAAPEAVVec3@@XZ"; static const char* SYM_LIVINGENTITY_GETVIEWVECTOR = "?getViewVector@LivingEntity@@UEAAPEAVVec3@@M@Z"; - static const char* SYM_LIVINGENTITY_GETPOS = "?getPos@LivingEntity@@QEAAPEAVVec3@@M@Z"; + static const char* SYM_LIVINGENTITY_GETPOS = "?getPos@LivingEntity@@UEAAPEAVVec3@@M@Z"; static const char* SYM_LIVINGENTITY_GETPOS_V = "?getPos@LivingEntity@@UEAAPEAVVec3@@M@Z"; static const char* SYM_LIVINGENTITY_PICK = "?pick@LivingEntity@@UEAAPEAVHitResult@@NM@Z"; static const char* SYM_ENTITY_LERPMOTION = "?lerpMotion@Entity@@UEAAXNNN@Z"; + static const char* SYM_ENTITY_SPAWNATLOCATION = "?spawnAtLocation@Entity@@QEAA?AV?$shared_ptr@VItemEntity@@@std@@V?$shared_ptr@VItemInstance@@@3@M@Z"; + static const char* SYM_ENTITY_GETENCODEID = "?getEncodeId@EntityIO@@SA?AV?$basic_string@_WU?$char_traits@_W@std@@V?$allocator@_W@2@@std@@V?$shared_ptr@VEntity@@@3@@Z"; + static const char* SYM_ENTITY_GETENCODEID_BYID = "?getEncodeId@EntityIO@@SA?AV?$basic_string@_WU?$char_traits@_W@std@@V?$allocator@_W@2@@std@@H@Z"; + static const char* SYM_ENTITY_GETNETWORKNAME = "?getNetworkName@Entity@@UEAA?AV?$basic_string@_WU?$char_traits@_W@std@@V?$allocator@_W@2@@std@@XZ"; + static const char* SYM_LIVINGENTITY_TICKDEATH = "?tickDeath@LivingEntity@@MEAAXXZ"; + static const char* SYM_LIVINGENTITY_DROPDEATHLOOT = "?dropDeathLoot@LivingEntity@@MEAAX_NH@Z"; + static const char* SYM_MOB_DROPDEATHLOOT = "?dropDeathLoot@Mob@@MEAAX_NH@Z"; + static const char* SYM_CHICKEN_DROPDEATHLOOT = "?dropDeathLoot@Chicken@@MEAAX_NH@Z"; + static const char* SYM_COW_DROPDEATHLOOT = "?dropDeathLoot@Cow@@MEAAX_NH@Z"; + static const char* SYM_PIG_DROPDEATHLOOT = "?dropDeathLoot@Pig@@MEAAX_NH@Z"; + static const char* SYM_SHEEP_DROPDEATHLOOT = "?dropDeathLoot@Sheep@@UEAAX_NH@Z"; + static const char* SYM_SQUID_DROPDEATHLOOT = "?dropDeathLoot@Squid@@MEAAX_NH@Z"; + static const char* SYM_OCELOT_DROPDEATHLOOT = "?dropDeathLoot@Ocelot@@MEAAX_NH@Z"; + static const char* SYM_SNOWMAN_DROPDEATHLOOT = "?dropDeathLoot@SnowMan@@MEAAX_NH@Z"; + static const char* SYM_VILLAGERGOLEM_DROPDEATHLOOT = "?dropDeathLoot@VillagerGolem@@MEAAX_NH@Z"; + static const char* SYM_PIGZOMBIE_DROPDEATHLOOT = "?dropDeathLoot@PigZombie@@MEAAX_NH@Z"; + static const char* SYM_SPIDER_DROPDEATHLOOT = "?dropDeathLoot@Spider@@MEAAX_NH@Z"; + static const char* SYM_SKELETON_DROPDEATHLOOT = "?dropDeathLoot@Skeleton@@MEAAX_NH@Z"; + static const char* SYM_WITCH_DROPDEATHLOOT = "?dropDeathLoot@Witch@@MEAAX_NH@Z"; + static const char* SYM_BLAZE_DROPDEATHLOOT = "?dropDeathLoot@Blaze@@MEAAX_NH@Z"; + static const char* SYM_ENDERMAN_DROPDEATHLOOT = "?dropDeathLoot@EnderMan@@MEAAX_NH@Z"; + static const char* SYM_GHAST_DROPDEATHLOOT = "?dropDeathLoot@Ghast@@MEAAX_NH@Z"; + static const char* SYM_LAVASLIME_DROPDEATHLOOT = "?dropDeathLoot@LavaSlime@@MEAAX_NH@Z"; + static const char* SYM_WITHERBOSS_DROPDEATHLOOT = "?dropDeathLoot@WitherBoss@@MEAAX_NH@Z"; static const char* SYM_INVENTORY_REMOVERESOURCE = "?removeResource@Inventory@@QEAA_NH@Z"; static const char* SYM_INVENTORY_VFTABLE = "??_7Inventory@@6B@"; @@ -178,15 +220,15 @@ bool CoreSymbols::Resolve(SymbolResolver& resolver) pMinecraftSetLevel = resolver.Resolve(SYM_MINECRAFT_SETLEVEL); pExitGame = resolver.Resolve(SYM_EXIT_GAME); pPresent = resolver.Resolve(SYM_PRESENT); - pMinecraftApp = resolver.Resolve("?app@@3VCMinecraftApp@@A"); + pMinecraftApp = resolver.ResolveOptional("?app@@3VCMinecraftApp@@A"); if (!pMinecraftApp) - pMinecraftApp = resolver.ResolveExact("app"); - pGetMinecraftLanguage = resolver.Resolve(SYM_GET_MINECRAFT_LANGUAGE); + pMinecraftApp = resolver.ResolveExactOptional("app"); + pGetMinecraftLanguage = resolver.ResolveOptional(SYM_GET_MINECRAFT_LANGUAGE); if (!pGetMinecraftLanguage) - pGetMinecraftLanguage = resolver.ResolveExact("CMinecraftApp::GetMinecraftLanguage"); - pGetMinecraftLocale = resolver.Resolve(SYM_GET_MINECRAFT_LOCALE); + pGetMinecraftLanguage = resolver.ResolveExactOptional("CMinecraftApp::GetMinecraftLanguage"); + pGetMinecraftLocale = resolver.ResolveOptional(SYM_GET_MINECRAFT_LOCALE); if (!pGetMinecraftLocale) - pGetMinecraftLocale = resolver.ResolveExact("CMinecraftApp::GetMinecraftLocale"); + pGetMinecraftLocale = resolver.ResolveExactOptional("CMinecraftApp::GetMinecraftLocale"); return HasCritical(); } @@ -198,9 +240,9 @@ void CoreSymbols::Log() const LogSym("Minecraft::setLevel", pMinecraftSetLevel); LogSym("ExitGame", pExitGame); LogSym("C4JRender::Present", pPresent); - LogSym("app (CMinecraftApp)", pMinecraftApp); - LogSym("CMinecraftApp::GetMinecraftLanguage", pGetMinecraftLanguage); - LogSym("CMinecraftApp::GetMinecraftLocale", pGetMinecraftLocale); + LogSymOptional("app (CMinecraftApp)", pMinecraftApp); + LogSymOptional("CMinecraftApp::GetMinecraftLanguage", pGetMinecraftLanguage); + LogSymOptional("CMinecraftApp::GetMinecraftLocale", pGetMinecraftLocale); } bool CoreSymbols::HasCritical() const @@ -331,6 +373,17 @@ bool ItemSymbols::Resolve(SymbolResolver& resolver) pItemInstanceOnCraftedBy = resolver.Resolve(SYM_ITEMINSTANCE_ONCRAFTEDBY); pItemInstanceInteractEnemy = resolver.Resolve(SYM_ITEMINSTANCE_INTERACTENEMY); pItemInstanceHurtEnemy = resolver.Resolve(SYM_ITEMINSTANCE_HURTENEMY); + if (SYM_ITEMINSTANCE_SETAUXVALUE) + pItemInstanceSetAuxValue = resolver.Resolve(SYM_ITEMINSTANCE_SETAUXVALUE); + pItemInstanceCtor = resolver.Resolve(SYM_ITEMINSTANCE_CTOR_INT); + pItemInstanceSharedPtrCtor = resolver.Resolve(SYM_ITEMINSTANCE_SHAREDPTR_CTOR); + pItemInstanceSharedPtrDtor = resolver.Resolve(SYM_ITEMINSTANCE_SHAREDPTR_DTOR); + pEntitySharedPtrCtor = resolver.Resolve(SYM_ENTITY_SHAREDPTR_CTOR); + pEntitySharedPtrDtor = resolver.Resolve(SYM_ENTITY_SHAREDPTR_DTOR); + pItemEntitySharedPtrDtor = resolver.Resolve(SYM_ITEMENTITY_SHAREDPTR_DTOR); + pItemEntityCtorWithItem = resolver.Resolve(SYM_ITEMENTITY_CTOR_WITH_ITEM); + pItemEntitySetItem = resolver.Resolve(SYM_ITEMENTITY_SETITEM); + pItemEntityMakeShared = resolver.Resolve(SYM_ITEMENTITY_MAKE_SHARED); pItemMineBlock = resolver.Resolve(SYM_ITEM_MINEBLOCK); pDiggerItemMineBlock = resolver.Resolve(SYM_DIGGERITEM_MINEBLOCK); pPickaxeItemGetDestroySpeed = resolver.Resolve(SYM_PICKAXEITEM_GETDESTROYSPEED); @@ -358,11 +411,21 @@ void ItemSymbols::Log() const LogSym("ItemInstance::onCraftedBy", pItemInstanceOnCraftedBy); LogSym("ItemInstance::interactEnemy", pItemInstanceInteractEnemy); LogSym("ItemInstance::hurtEnemy", pItemInstanceHurtEnemy); + LogSymOptional("ItemInstance::setAuxValue", pItemInstanceSetAuxValue); + LogSym("ItemInstance::ItemInstance(int,int,int)", pItemInstanceCtor); + LogSym("std::shared_ptr::shared_ptr(ItemInstance*)", pItemInstanceSharedPtrCtor); + LogSym("std::shared_ptr::~shared_ptr", pItemInstanceSharedPtrDtor); + LogSym("std::shared_ptr::shared_ptr(Entity*)", pEntitySharedPtrCtor); + LogSym("std::shared_ptr::~shared_ptr", pEntitySharedPtrDtor); + LogSym("std::shared_ptr::~shared_ptr", pItemEntitySharedPtrDtor); + LogSym("ItemEntity::ItemEntity(Level,double,double,double,shared_ptr)", pItemEntityCtorWithItem); + LogSym("ItemEntity::setItem", pItemEntitySetItem); + LogSym("std::make_shared", pItemEntityMakeShared); LogSym("Item::mineBlock", pItemMineBlock); LogSym("DiggerItem::mineBlock", pDiggerItemMineBlock); LogSym("PickaxeItem::getDestroySpeed", pPickaxeItemGetDestroySpeed); LogSym("PickaxeItem::canDestroySpecial", pPickaxeItemCanDestroySpecial); - LogSym("ShovelItem::getDestroySpeed", pShovelItemGetDestroySpeed); + LogSym("DiggerItem::getDestroySpeed", pShovelItemGetDestroySpeed); LogSym("ShovelItem::canDestroySpecial", pShovelItemCanDestroySpecial); LogSym("ItemEntity::getItem", pItemEntityGetItem); LogSym("ItemRenderer::renderGuiItem", pItemRendererRenderGuiItem); @@ -528,8 +591,8 @@ bool LevelSymbols::Resolve(SymbolResolver& resolver) pLevelChunkSetTileAndData = resolver.Resolve(SYM_LEVELCHUNK_SETTILEANDDATA); if (!pLevelChunkSetTileAndData) pLevelChunkSetTileAndData = resolver.ResolveExact("LevelChunk::setTileAndData"); - pLevelChunkGetPos = resolver.ResolveExact("LevelChunk::getPos"); - pLevelChunkGetHighestNonEmptyY = resolver.ResolveExact("LevelChunk::getHighestNonEmptyY"); + pLevelChunkGetPos = resolver.ResolveExactOptional("LevelChunk::getPos"); + pLevelChunkGetHighestNonEmptyY = resolver.ResolveExactOptional("LevelChunk::getHighestNonEmptyY"); pCompressedTileStorageSet = resolver.ResolveExact("CompressedTileStorage::set"); return true; } @@ -552,13 +615,23 @@ void LevelSymbols::Log() const LogSym("LevelChunk::setTile", pLevelChunkSetTile); LogSym("LevelChunk::getData", pLevelChunkGetData); LogSym("LevelChunk::setTileAndData", pLevelChunkSetTileAndData); - LogSym("LevelChunk::getPos", pLevelChunkGetPos); - LogSym("LevelChunk::getHighestNonEmptyY", pLevelChunkGetHighestNonEmptyY); + LogSymOptional("LevelChunk::getPos", pLevelChunkGetPos); + LogSymOptional("LevelChunk::getHighestNonEmptyY", pLevelChunkGetHighestNonEmptyY); LogSym("CompressedTileStorage::set", pCompressedTileStorageSet); } bool EntitySymbols::Resolve(SymbolResolver& resolver) { + auto resolveWithExactOptional = [&resolver](const char* decoratedName, const char* exactNameA, const char* exactNameB = nullptr) -> void* + { + void* ptr = resolver.ResolveOptional(decoratedName); + if (!ptr && exactNameA && exactNameA[0]) + ptr = resolver.ResolveExactOptional(exactNameA); + if (!ptr && exactNameB && exactNameB[0]) + ptr = resolver.ResolveExactOptional(exactNameB); + return ptr; + }; + pPlayerCanDestroy = resolver.Resolve(SYM_PLAYER_CANDESTROY); pServerPlayerGameModeUseItem = resolver.Resolve(SYM_SERVER_PLAYER_GAMEMODE_USEITEM); pMultiPlayerGameModeUseItem = resolver.Resolve(SYM_MULTI_PLAYER_GAMEMODE_USEITEM); @@ -579,6 +652,32 @@ bool EntitySymbols::Resolve(SymbolResolver& resolver) if (!pEntityGetLookAngle) pEntityGetLookAngle = resolver.Resolve(SYM_ENTITY_GETLOOKANGLE); pEntityLerpMotion = resolver.Resolve(SYM_ENTITY_LERPMOTION); + pEntitySpawnAtLocation = resolver.Resolve(SYM_ENTITY_SPAWNATLOCATION); + if (SYM_ENTITY_SPAWNATLOCATION_INT) + pEntitySpawnAtLocationInt = resolver.Resolve(SYM_ENTITY_SPAWNATLOCATION_INT); + pEntityGetEncodeId = resolver.Resolve(SYM_ENTITY_GETENCODEID); + pEntityGetEncodeIdById = resolver.ResolveOptional(SYM_ENTITY_GETENCODEID_BYID); + pEntityGetNetworkName = resolver.Resolve(SYM_ENTITY_GETNETWORKNAME); + pLivingEntityTickDeath = resolveWithExactOptional(SYM_LIVINGENTITY_TICKDEATH, "LivingEntity::tickDeath"); + pLivingEntityDropDeathLoot = resolveWithExactOptional(SYM_LIVINGENTITY_DROPDEATHLOOT, "LivingEntity::dropDeathLoot"); + pMobDropDeathLoot = resolveWithExactOptional(SYM_MOB_DROPDEATHLOOT, "Mob::dropDeathLoot"); + pChickenDropDeathLoot = resolveWithExactOptional(SYM_CHICKEN_DROPDEATHLOOT, "Chicken::dropDeathLoot"); + pCowDropDeathLoot = resolveWithExactOptional(SYM_COW_DROPDEATHLOOT, "Cow::dropDeathLoot"); + pPigDropDeathLoot = resolveWithExactOptional(SYM_PIG_DROPDEATHLOOT, "Pig::dropDeathLoot"); + pSheepDropDeathLoot = resolveWithExactOptional(SYM_SHEEP_DROPDEATHLOOT, "Sheep::dropDeathLoot"); + pSquidDropDeathLoot = resolveWithExactOptional(SYM_SQUID_DROPDEATHLOOT, "Squid::dropDeathLoot"); + pOcelotDropDeathLoot = resolveWithExactOptional(SYM_OCELOT_DROPDEATHLOOT, "Ocelot::dropDeathLoot", "Ozelot::dropDeathLoot"); + pSnowManDropDeathLoot = resolveWithExactOptional(SYM_SNOWMAN_DROPDEATHLOOT, "SnowMan::dropDeathLoot"); + pVillagerGolemDropDeathLoot = resolveWithExactOptional(SYM_VILLAGERGOLEM_DROPDEATHLOOT, "VillagerGolem::dropDeathLoot"); + pPigZombieDropDeathLoot = resolveWithExactOptional(SYM_PIGZOMBIE_DROPDEATHLOOT, "PigZombie::dropDeathLoot"); + pSpiderDropDeathLoot = resolveWithExactOptional(SYM_SPIDER_DROPDEATHLOOT, "Spider::dropDeathLoot"); + pSkeletonDropDeathLoot = resolveWithExactOptional(SYM_SKELETON_DROPDEATHLOOT, "Skeleton::dropDeathLoot"); + pWitchDropDeathLoot = resolveWithExactOptional(SYM_WITCH_DROPDEATHLOOT, "Witch::dropDeathLoot"); + pBlazeDropDeathLoot = resolveWithExactOptional(SYM_BLAZE_DROPDEATHLOOT, "Blaze::dropDeathLoot"); + pEnderManDropDeathLoot = resolveWithExactOptional(SYM_ENDERMAN_DROPDEATHLOOT, "EnderMan::dropDeathLoot"); + pGhastDropDeathLoot = resolveWithExactOptional(SYM_GHAST_DROPDEATHLOOT, "Ghast::dropDeathLoot"); + pLavaSlimeDropDeathLoot = resolveWithExactOptional(SYM_LAVASLIME_DROPDEATHLOOT, "LavaSlime::dropDeathLoot"); + pWitherBossDropDeathLoot = resolveWithExactOptional(SYM_WITHERBOSS_DROPDEATHLOOT, "WitherBoss::dropDeathLoot"); return true; } @@ -600,6 +699,36 @@ void EntitySymbols::Log() const LogSym("LivingEntity::getViewVector", pLivingEntityGetViewVector); LogSym("LivingEntity::pick", pLivingEntityPick); LogSym("Entity::lerpMotion", pEntityLerpMotion); + LogSym("Entity::spawnAtLocation", pEntitySpawnAtLocation); + LogSymOptional("Entity::spawnAtLocation(int,int,float)", pEntitySpawnAtLocationInt); + if (SYM_ENTITY_SPAWNATLOCATION_INT && !pEntitySpawnAtLocationInt) + { + // Provide candidates when the int overload is missing. + PdbParser::DumpMatching("spawnAtLocation@Entity@@"); + } + LogSym("EntityIO::getEncodeId", pEntityGetEncodeId); + LogSym("EntityIO::getEncodeId(int)", pEntityGetEncodeIdById); + LogSym("Entity::getNetworkName", pEntityGetNetworkName); + LogSym("LivingEntity::tickDeath", pLivingEntityTickDeath); + LogSym("LivingEntity::dropDeathLoot", pLivingEntityDropDeathLoot); + LogSym("Mob::dropDeathLoot", pMobDropDeathLoot); + LogSym("Chicken::dropDeathLoot", pChickenDropDeathLoot); + LogSym("Cow::dropDeathLoot", pCowDropDeathLoot); + LogSym("Pig::dropDeathLoot", pPigDropDeathLoot); + LogSym("Sheep::dropDeathLoot", pSheepDropDeathLoot); + LogSym("Squid::dropDeathLoot", pSquidDropDeathLoot); + LogSym("Ocelot::dropDeathLoot", pOcelotDropDeathLoot); + LogSym("SnowMan::dropDeathLoot", pSnowManDropDeathLoot); + LogSym("VillagerGolem::dropDeathLoot", pVillagerGolemDropDeathLoot); + LogSym("PigZombie::dropDeathLoot", pPigZombieDropDeathLoot); + LogSym("Spider::dropDeathLoot", pSpiderDropDeathLoot); + LogSym("Skeleton::dropDeathLoot", pSkeletonDropDeathLoot); + LogSym("Witch::dropDeathLoot", pWitchDropDeathLoot); + LogSym("Blaze::dropDeathLoot", pBlazeDropDeathLoot); + LogSym("EnderMan::dropDeathLoot", pEnderManDropDeathLoot); + LogSym("Ghast::dropDeathLoot", pGhastDropDeathLoot); + LogSym("LavaSlime::dropDeathLoot", pLavaSlimeDropDeathLoot); + LogSym("WitherBoss::dropDeathLoot", pWitherBossDropDeathLoot); } bool InventorySymbols::Resolve(SymbolResolver& resolver) diff --git a/WeaveLoaderRuntime/src/Symbols/SymbolGroups.h b/WeaveLoaderRuntime/src/Symbols/SymbolGroups.h index c09b7e3..a5afb33 100644 --- a/WeaveLoaderRuntime/src/Symbols/SymbolGroups.h +++ b/WeaveLoaderRuntime/src/Symbols/SymbolGroups.h @@ -85,6 +85,9 @@ struct ItemSymbols void* pItemInstanceOnCraftedBy = nullptr; void* pItemInstanceInteractEnemy = nullptr; void* pItemInstanceHurtEnemy = nullptr; + void* pItemInstanceCtor = nullptr; + void* pItemInstanceSharedPtrCtor = nullptr; + void* pItemInstanceSetAuxValue = nullptr; void* pItemMineBlock = nullptr; void* pDiggerItemMineBlock = nullptr; void* pPickaxeItemGetDestroySpeed = nullptr; @@ -95,6 +98,13 @@ struct ItemSymbols void* pItemRendererRenderGuiItem = nullptr; void* pItemInHandRendererRender = nullptr; void* pItemInHandRendererRenderItem = nullptr; + void* pItemInstanceSharedPtrDtor = nullptr; + void* pEntitySharedPtrCtor = nullptr; + void* pEntitySharedPtrDtor = nullptr; + void* pItemEntitySharedPtrDtor = nullptr; + void* pItemEntityCtorWithItem = nullptr; + void* pItemEntitySetItem = nullptr; + void* pItemEntityMakeShared = nullptr; bool Resolve(SymbolResolver& resolver); void Log() const; @@ -197,6 +207,31 @@ struct EntitySymbols void* pLivingEntityGetViewVector = nullptr; void* pLivingEntityPick = nullptr; void* pEntityLerpMotion = nullptr; + void* pEntitySpawnAtLocation = nullptr; + void* pEntitySpawnAtLocationInt = nullptr; + void* pEntityGetEncodeId = nullptr; + void* pEntityGetEncodeIdById = nullptr; + void* pEntityGetNetworkName = nullptr; + void* pLivingEntityTickDeath = nullptr; + void* pLivingEntityDropDeathLoot = nullptr; + void* pMobDropDeathLoot = nullptr; + void* pChickenDropDeathLoot = nullptr; + void* pCowDropDeathLoot = nullptr; + void* pPigDropDeathLoot = nullptr; + void* pSheepDropDeathLoot = nullptr; + void* pSquidDropDeathLoot = nullptr; + void* pOcelotDropDeathLoot = nullptr; + void* pSnowManDropDeathLoot = nullptr; + void* pVillagerGolemDropDeathLoot = nullptr; + void* pPigZombieDropDeathLoot = nullptr; + void* pSpiderDropDeathLoot = nullptr; + void* pSkeletonDropDeathLoot = nullptr; + void* pWitchDropDeathLoot = nullptr; + void* pBlazeDropDeathLoot = nullptr; + void* pEnderManDropDeathLoot = nullptr; + void* pGhastDropDeathLoot = nullptr; + void* pLavaSlimeDropDeathLoot = nullptr; + void* pWitherBossDropDeathLoot = nullptr; bool Resolve(SymbolResolver& resolver); void Log() const; diff --git a/WeaveLoaderRuntime/src/WorldIdRemap.cpp b/WeaveLoaderRuntime/src/WorldIdRemap.cpp index fa48cae..2f21498 100644 --- a/WeaveLoaderRuntime/src/WorldIdRemap.cpp +++ b/WeaveLoaderRuntime/src/WorldIdRemap.cpp @@ -70,8 +70,24 @@ namespace LevelChunkGetHighestNonEmptyY_fn s_levelChunkGetHighestNonEmptyY = nullptr; CompressedTileStorageSet_fn s_compressedTileStorageSet = nullptr; - // LevelChunk layout: vtable(8) + byteArray(16) = 24, then lowerBlocks at 24, upperBlocks at 32 + struct ByteArrayLayout + { + unsigned char* data; + unsigned int length; + }; + + // LevelChunk layout: + // lowerBlocks at 24, upperBlocks at 32, heightmap at 488, minHeight at 504, x at 508, z at 512. static constexpr size_t kLevelChunkLowerBlocksOffset = 24; + static constexpr size_t kLevelChunkHeightmapOffset = 488; + static constexpr size_t kLevelChunkMinHeightOffset = 504; + static constexpr size_t kLevelChunkXOffset = 508; + static constexpr size_t kLevelChunkZOffset = 512; + static constexpr uint32_t kBlockIndexV2Flag = 0x80000000u; + static constexpr uint32_t kBlockIndexV2YShift = 8u; + static constexpr uint32_t kBlockIndexV2YBits = 23u; + static constexpr uint32_t kBlockIndexV2YMask = (1u << kBlockIndexV2YBits) - 1u; + static constexpr int kBlockIndexV2YBias = 1 << (kBlockIndexV2YBits - 1u); bool s_missingPlaceholdersReady = false; int s_chunkRemapLogCount = 0; int s_chunkSaveLogCount = 0; @@ -198,7 +214,7 @@ namespace } const auto* entry = reinterpret_cast(fileEntry); const unsigned int fileSize = entry->length; - // Hard cap for corrupted metadata; chunk namespace files should stay tiny. + // Hard cap for corrupted files; chunk namespace files should stay tiny. if (fileSize > (1024u * 1024u)) { save->closeHandle(fileEntry); @@ -242,6 +258,9 @@ namespace return ok && bytesWritten == text.size(); } + static int MakeBlockIndex(int x, int y, int z); + static void DecodeBlockIndex(int index, int* x, int* y, int* z); + static bool WriteChunkNamespaceMap( void* saveFile, const std::wstring& path, @@ -263,12 +282,22 @@ namespace std::istringstream input(text); std::string line; int expectedEntries = -1; + int formatVersion = 1; int parsedEntries = 0; while (std::getline(input, line)) { if (line.empty()) continue; + if (line.rfind("#format ", 0) == 0) + { + std::istringstream fs(line.substr(8)); + int fmt = 1; + if (fs >> fmt && fmt > 0) + formatVersion = fmt; + continue; + } + if (expectedEntries < 0 && line.rfind("#count ", 0) == 0) { std::istringstream hs(line.substr(7)); @@ -278,17 +307,37 @@ namespace continue; } - std::istringstream ss(line); - int blockIndex = -1; - std::string namespacedId; - if (!(ss >> blockIndex >> namespacedId)) - continue; - if (blockIndex < 0 || namespacedId.empty()) - continue; - (*outMap)[blockIndex] = namespacedId; - ++parsedEntries; - if (expectedEntries >= 0 && parsedEntries >= expectedEntries) - break; + if (formatVersion >= 2) + { + std::istringstream ss(line); + int x = 0; + int y = 0; + int z = 0; + std::string namespacedId; + if (!(ss >> x >> y >> z >> namespacedId)) + continue; + if (x < 0 || x >= kChunkWidth || z < 0 || z >= kChunkWidth || namespacedId.empty()) + continue; + const int blockIndex = MakeBlockIndex(x, y, z); + (*outMap)[blockIndex] = namespacedId; + ++parsedEntries; + if (expectedEntries >= 0 && parsedEntries >= expectedEntries) + break; + } + else + { + std::istringstream ss(line); + int blockIndex = -1; + std::string namespacedId; + if (!(ss >> blockIndex >> namespacedId)) + continue; + if (blockIndex < 0 || namespacedId.empty()) + continue; + (*outMap)[blockIndex] = namespacedId; + ++parsedEntries; + if (expectedEntries >= 0 && parsedEntries >= expectedEntries) + break; + } } if (expectedEntries < 0) { @@ -336,22 +385,73 @@ namespace const std::unordered_map& map) { std::ostringstream out; - out << "#count " << map.size() << '\n'; + bool useFormatV2 = false; for (const auto& entry : map) - out << entry.first << ' ' << entry.second << '\n'; + { + const uint32_t u = static_cast(entry.first); + if ((u & kBlockIndexV2Flag) != 0) + { + useFormatV2 = true; + break; + } + } + + if (useFormatV2) + out << "#format 2\n"; + out << "#count " << map.size() << '\n'; + if (useFormatV2) + { + for (const auto& entry : map) + { + int x = 0; + int y = 0; + int z = 0; + DecodeBlockIndex(entry.first, &x, &y, &z); + out << x << ' ' << y << ' ' << z << ' ' << entry.second << '\n'; + } + } + else + { + for (const auto& entry : map) + out << entry.first << ' ' << entry.second << '\n'; + } return SaveWriteAllText(saveFile, path, out.str()); } static int MakeBlockIndex(int x, int y, int z) { - return ((y & 0xFF) << 8) | ((z & 0x0F) << 4) | (x & 0x0F); + if (y >= 0 && y <= 0xFF) + return ((y & 0xFF) << 8) | ((z & 0x0F) << 4) | (x & 0x0F); + + int packedY = y + kBlockIndexV2YBias; + if (packedY < 0) + packedY = 0; + if (packedY > static_cast(kBlockIndexV2YMask)) + packedY = static_cast(kBlockIndexV2YMask); + const uint32_t u = kBlockIndexV2Flag + | ((static_cast(packedY) & kBlockIndexV2YMask) << kBlockIndexV2YShift) + | ((static_cast(z) & 0x0Fu) << 4u) + | (static_cast(x) & 0x0Fu); + return static_cast(u); } static void DecodeBlockIndex(int index, int* x, int* y, int* z) { - if (x) *x = index & 0x0F; - if (z) *z = (index >> 4) & 0x0F; - if (y) *y = (index >> 8) & 0xFF; + const uint32_t u = static_cast(index); + if (x) *x = static_cast(u & 0x0F); + if (z) *z = static_cast((u >> 4u) & 0x0F); + if (y) + { + if ((u & kBlockIndexV2Flag) != 0) + { + const int packedY = static_cast((u >> kBlockIndexV2YShift) & kBlockIndexV2YMask); + *y = packedY - kBlockIndexV2YBias; + } + else + { + *y = static_cast((u >> 8u) & 0xFF); + } + } } static bool TryGetChunkMeta(void* chunkPtr, ChunkMeta* outMeta) @@ -399,14 +499,69 @@ namespace static bool TryResolveChunkCoords(void* levelChunkPtr, int* outX, int* outZ) { - if (!levelChunkPtr || !s_levelChunkGetPos) + if (!levelChunkPtr) return false; - void* posPtr = s_levelChunkGetPos(levelChunkPtr); - if (!posPtr || !IsReadableRange(posPtr, sizeof(ChunkPosLayout))) + + if (s_levelChunkGetPos) + { + void* posPtr = s_levelChunkGetPos(levelChunkPtr); + if (posPtr && IsReadableRange(posPtr, sizeof(ChunkPosLayout))) + { + const auto* pos = reinterpret_cast(posPtr); + if (outX) *outX = pos->x; + if (outZ) *outZ = pos->z; + return true; + } + } + + if (!IsReadableRange(levelChunkPtr, kLevelChunkZOffset + sizeof(int))) return false; - const auto* pos = reinterpret_cast(posPtr); - if (outX) *outX = pos->x; - if (outZ) *outZ = pos->z; + const int x = *reinterpret_cast(static_cast(levelChunkPtr) + kLevelChunkXOffset); + const int z = *reinterpret_cast(static_cast(levelChunkPtr) + kLevelChunkZOffset); + if (outX) *outX = x; + if (outZ) *outZ = z; + return true; + } + + static bool TryGetChunkMinHeight(void* levelChunkPtr, int* outMinY) + { + if (!levelChunkPtr || !outMinY) + return false; + if (!IsReadableRange(levelChunkPtr, kLevelChunkMinHeightOffset + sizeof(int))) + return false; + const int minY = *reinterpret_cast(static_cast(levelChunkPtr) + kLevelChunkMinHeightOffset); + *outMinY = minY; + return true; + } + + static bool TryGetChunkHeightmapMax(void* levelChunkPtr, int* outMaxY) + { + if (!levelChunkPtr) + return false; + if (!IsReadableRange(levelChunkPtr, kLevelChunkHeightmapOffset + sizeof(ByteArrayLayout))) + return false; + + const auto* heightmap = reinterpret_cast( + static_cast(levelChunkPtr) + kLevelChunkHeightmapOffset); + if (!heightmap->data || heightmap->length == 0) + return false; + if (!IsReadableRange(heightmap->data, heightmap->length)) + return false; + + int maxY = -1; + for (unsigned int i = 0; i < heightmap->length; ++i) + { + const int value = static_cast(heightmap->data[i]); + if (value > maxY) + maxY = value; + } + if (maxY < 0) + return false; + int minY = 0; + if (TryGetChunkMinHeight(levelChunkPtr, &minY) && minY != 0 && maxY < minY) + maxY += minY; + if (outMaxY) + *outMaxY = maxY; return true; } @@ -789,7 +944,9 @@ namespace WorldIdRemap SetLoadedChunkNamespaces(levelChunkPtr, entries); const int sanitizeFallbackId = ResolveSafeMissingBlockFallbackId(); - int applyMaxY = 127; + int minY = 0; + TryGetChunkMinHeight(levelChunkPtr, &minY); + int applyMaxY = kChunkMaxY - 1; if (s_levelChunkGetHighestNonEmptyY) { const int highestY = s_levelChunkGetHighestNonEmptyY(levelChunkPtr); @@ -802,12 +959,23 @@ namespace WorldIdRemap } else if (highestY >= 0) { - applyMaxY = kChunkMaxY - 1; + applyMaxY = highestY; } } - - if (applyMaxY > 127) - applyMaxY = 127; + else + { + int highestY = -1; + if (TryGetChunkHeightmapMax(levelChunkPtr, &highestY)) + { + int bound = highestY + 16; + if (bound >= kChunkMaxY) + bound = kChunkMaxY - 1; + if (bound >= 0) + applyMaxY = bound; + } + } + if (applyMaxY < minY) + applyMaxY = minY; int changed = 0; int skippedInvalid = 0; @@ -819,7 +987,7 @@ namespace WorldIdRemap int y = 0; int z = 0; DecodeBlockIndex(entry.first, &x, &y, &z); - if (y < 0 || y > applyMaxY) + if (y < minY || y > applyMaxY) { ++skippedInvalid; continue; @@ -926,6 +1094,8 @@ namespace WorldIdRemap std::unordered_map nextEntries; const int missingBlockFallback = IdRegistry::Instance().GetMissingFallback(IdRegistry::Type::Block); + int minY = 0; + TryGetChunkMinHeight(levelChunkPtr, &minY); int maxY = kChunkMaxY - 1; if (s_levelChunkGetHighestNonEmptyY) { @@ -934,9 +1104,19 @@ namespace WorldIdRemap return; if (highestY >= 0 && highestY < kChunkMaxY) maxY = highestY; + else if (highestY >= 0) + maxY = highestY; } + else + { + int highestY = -1; + if (TryGetChunkHeightmapMax(levelChunkPtr, &highestY)) + maxY = highestY; + } + if (maxY < minY) + maxY = minY; - for (int y = 0; y <= maxY; ++y) + for (int y = minY; y <= maxY; ++y) { for (int z = 0; z < kChunkWidth; ++z) {