diff --git a/ExampleMod/ExampleMod.cs b/ExampleMod/ExampleMod.cs
index 1b45a3d..47f5828 100644
--- a/ExampleMod/ExampleMod.cs
+++ b/ExampleMod/ExampleMod.cs
@@ -12,6 +12,7 @@ public class ExampleMod : IMod
public static RegisteredBlock? RubyOre;
public static RegisteredItem? Ruby;
public static RegisteredItem? RubyPickaxeItem;
+ public static RegisteredItem? RubyWandItem;
private sealed class RubyPickaxe : PickaxeItem
{
@@ -22,6 +23,53 @@ public class ExampleMod : IMod
}
}
+ private sealed class RubyWand : Item
+ {
+ private const long CooldownMs = 1500;
+ private long _nextClientUseAtMs;
+ private long _nextServerUseAtMs;
+
+ public override UseItemResult OnUseItem(UseItemContext context)
+ {
+ if (context.IsTestUseOnly)
+ return UseItemResult.CancelVanilla;
+
+ long now = Environment.TickCount64;
+ ref long nextUseAtMs = ref context.IsClientSide ? ref _nextClientUseAtMs : ref _nextServerUseAtMs;
+ if (now < nextUseAtMs)
+ {
+ long remaining = nextUseAtMs - now;
+ Logger.Info($"RubyWand is cooling down ({remaining}ms remaining)");
+ return UseItemResult.CancelVanilla;
+ }
+
+ if (!context.ConsumeInventoryItem("minecraft:gunpowder", 1))
+ {
+ Logger.Info("RubyWand needs gunpowder.");
+ return UseItemResult.CancelVanilla;
+ }
+
+ if (context.IsClientSide)
+ {
+ context.DamageItem(10);
+ nextUseAtMs = now + CooldownMs;
+ return UseItemResult.ContinueVanilla;
+ }
+
+ bool spawned = context.SpawnEntityFromLook("minecraft:wither_skull", speed: 1.4, spawnForward: 1.0, spawnUp: 1.2);
+ if (!spawned)
+ {
+ Logger.Info("RubyWand failed to spawn fireball.");
+ return UseItemResult.CancelVanilla;
+ }
+
+ context.DamageItem(10);
+ Logger.Info($"RubyWand cast fireball! (item={context.ItemId})");
+ nextUseAtMs = now + CooldownMs;
+ return UseItemResult.CancelVanilla;
+ }
+ }
+
public void OnInitialize()
{
RubyOre = Registry.Block.Register("examplemod:ruby_ore",
@@ -49,6 +97,13 @@ public class ExampleMod : IMod
.Name("Ruby Pickaxe")
.InCreativeTab(CreativeTab.ToolsAndWeapons));
+ RubyWandItem = Registry.Item.Register("examplemod:ruby_wand", new RubyWand(),
+ new ItemProperties()
+ .MaxStackSize(1)
+ .Icon("examplemod:ruby_wand") // From assets/items/ruby_wand.png
+ .Name("Ruby Wand")
+ .InCreativeTab(CreativeTab.ToolsAndWeapons));
+
Registry.Recipe.AddFurnace("examplemod:ruby_ore", "examplemod:ruby", 1.0f);
GameEvents.OnBlockBreak += OnBlockBroken;
diff --git a/ExampleMod/assets/items/ruby_wand.png b/ExampleMod/assets/items/ruby_wand.png
new file mode 100644
index 0000000..4b72844
Binary files /dev/null and b/ExampleMod/assets/items/ruby_wand.png differ
diff --git a/WeaveLoader.API/Block/BlockRegistry.cs b/WeaveLoader.API/Block/BlockRegistry.cs
index 5563b88..67c44ca 100644
--- a/WeaveLoader.API/Block/BlockRegistry.cs
+++ b/WeaveLoader.API/Block/BlockRegistry.cs
@@ -1,3 +1,5 @@
+using System.Collections.Generic;
+
namespace WeaveLoader.API.Block;
///
@@ -24,6 +26,9 @@ public class RegisteredBlock
///
public static class BlockRegistry
{
+ private static readonly object s_lock = new();
+ private static readonly Dictionary s_idByNumeric = new();
+
///
/// Register a new block with the game engine.
///
@@ -53,6 +58,18 @@ public static class BlockRegistry
}
Logger.Debug($"Registered block '{id}' -> numeric ID {numericId}");
+ lock (s_lock)
+ {
+ s_idByNumeric[numericId] = id;
+ }
return new RegisteredBlock(id, numericId);
}
+
+ internal static bool TryGetIdentifier(int numericId, out Identifier id)
+ {
+ lock (s_lock)
+ {
+ return s_idByNumeric.TryGetValue(numericId, out id);
+ }
+ }
}
diff --git a/WeaveLoader.API/Entity/EntityRegistry.cs b/WeaveLoader.API/Entity/EntityRegistry.cs
index 1488166..7fd9687 100644
--- a/WeaveLoader.API/Entity/EntityRegistry.cs
+++ b/WeaveLoader.API/Entity/EntityRegistry.cs
@@ -1,3 +1,5 @@
+using System.Collections.Generic;
+
namespace WeaveLoader.API.Entity;
///
@@ -21,6 +23,9 @@ public class RegisteredEntity
///
public static class EntityRegistry
{
+ private static readonly object s_lock = new();
+ private static readonly Dictionary s_idByNumeric = new();
+
public static RegisteredEntity Register(Identifier id, EntityDefinition definition)
{
int numericId = NativeInterop.native_register_entity(
@@ -33,6 +38,33 @@ public static class EntityRegistry
throw new InvalidOperationException($"Failed to register entity '{id}'.");
Logger.Debug($"Registered entity '{id}' -> numeric ID {numericId}");
+ lock (s_lock)
+ {
+ s_idByNumeric[numericId] = id;
+ }
return new RegisteredEntity(id, numericId);
}
+
+ public static bool Summon(Identifier id, double x, double y, double z)
+ {
+ int numericId = IdHelper.GetEntityNumericId(id);
+ if (numericId < 0)
+ return false;
+
+ return Summon(numericId, x, y, z);
+ }
+
+ public static bool Summon(int numericId, double x, double y, double z)
+ {
+ int ok = NativeInterop.native_summon_entity_by_id(numericId, x, y, z);
+ return ok != 0;
+ }
+
+ internal static bool TryGetIdentifier(int numericId, out Identifier id)
+ {
+ lock (s_lock)
+ {
+ return s_idByNumeric.TryGetValue(numericId, out id);
+ }
+ }
}
diff --git a/WeaveLoader.API/Events/GameEvents.cs b/WeaveLoader.API/Events/GameEvents.cs
index d3938bd..0279615 100644
--- a/WeaveLoader.API/Events/GameEvents.cs
+++ b/WeaveLoader.API/Events/GameEvents.cs
@@ -34,6 +34,15 @@ public class EntitySpawnEventArgs : EventArgs
public float Z { get; init; }
}
+public class WorldLoadedEventArgs : EventArgs
+{
+ public nint NativeLevelPointer { get; init; }
+}
+
+public class WorldUnloadedEventArgs : EventArgs
+{
+}
+
public class PlayerJoinEventArgs : EventArgs
{
public int PlayerId { get; init; }
@@ -51,10 +60,14 @@ public static class GameEvents
public static event EventHandler? OnChat;
public static event EventHandler? OnEntitySpawn;
public static event EventHandler? OnPlayerJoin;
+ public static event EventHandler? OnWorldLoaded;
+ public static event EventHandler? OnWorldUnloaded;
internal static void FireBlockBreak(BlockBreakEventArgs e) => OnBlockBreak?.Invoke(null, e);
internal static void FireBlockPlace(BlockPlaceEventArgs e) => OnBlockPlace?.Invoke(null, e);
internal static void FireChat(ChatEventArgs e) => OnChat?.Invoke(null, e);
internal static void FireEntitySpawn(EntitySpawnEventArgs e) => OnEntitySpawn?.Invoke(null, e);
internal static void FirePlayerJoin(PlayerJoinEventArgs e) => OnPlayerJoin?.Invoke(null, e);
+ internal static void FireWorldLoaded(WorldLoadedEventArgs e) => OnWorldLoaded?.Invoke(null, e);
+ internal static void FireWorldUnloaded(WorldUnloadedEventArgs e) => OnWorldUnloaded?.Invoke(null, e);
}
diff --git a/WeaveLoader.API/IdHelper.cs b/WeaveLoader.API/IdHelper.cs
new file mode 100644
index 0000000..3d8952c
--- /dev/null
+++ b/WeaveLoader.API/IdHelper.cs
@@ -0,0 +1,497 @@
+using System.Collections.Generic;
+using WeaveLoader.API.Block;
+using WeaveLoader.API.Entity;
+using WeaveLoader.API.Item;
+
+namespace WeaveLoader.API;
+
+public static class IdHelper
+{
+ private static readonly Dictionary s_vanillaBlockToId = new(StringComparer.Ordinal)
+ {
+ ["minecraft:air"] = 0,
+ ["minecraft:stone"] = 1,
+ ["minecraft:grass"] = 2,
+ ["minecraft:dirt"] = 3,
+ ["minecraft:cobblestone"] = 4,
+ ["minecraft:planks"] = 5,
+ ["minecraft:sapling"] = 6,
+ ["minecraft:bedrock"] = 7,
+ ["minecraft:flowing_water"] = 8,
+ ["minecraft:water"] = 9,
+ ["minecraft:flowing_lava"] = 10,
+ ["minecraft:lava"] = 11,
+ ["minecraft:sand"] = 12,
+ ["minecraft:gravel"] = 13,
+ ["minecraft:gold_ore"] = 14,
+ ["minecraft:iron_ore"] = 15,
+ ["minecraft:coal_ore"] = 16,
+ ["minecraft:log"] = 17,
+ ["minecraft:leaves"] = 18,
+ ["minecraft:sponge"] = 19,
+ ["minecraft:glass"] = 20,
+ ["minecraft:lapis_ore"] = 21,
+ ["minecraft:lapis_block"] = 22,
+ ["minecraft:dispenser"] = 23,
+ ["minecraft:sandstone"] = 24,
+ ["minecraft:noteblock"] = 25,
+ ["minecraft:bed"] = 26,
+ ["minecraft:golden_rail"] = 27,
+ ["minecraft:detector_rail"] = 28,
+ ["minecraft:sticky_piston"] = 29,
+ ["minecraft:web"] = 30,
+ ["minecraft:tallgrass"] = 31,
+ ["minecraft:deadbush"] = 32,
+ ["minecraft:piston"] = 33,
+ ["minecraft:piston_head"] = 34,
+ ["minecraft:wool"] = 35,
+ ["minecraft:dandelion"] = 37,
+ ["minecraft:poppy"] = 38,
+ ["minecraft:brown_mushroom"] = 39,
+ ["minecraft:red_mushroom"] = 40,
+ ["minecraft:gold_block"] = 41,
+ ["minecraft:iron_block"] = 42,
+ ["minecraft:double_stone_slab"] = 43,
+ ["minecraft:stone_slab"] = 44,
+ ["minecraft:brick_block"] = 45,
+ ["minecraft:tnt"] = 46,
+ ["minecraft:bookshelf"] = 47,
+ ["minecraft:mossy_cobblestone"] = 48,
+ ["minecraft:obsidian"] = 49,
+ ["minecraft:torch"] = 50,
+ ["minecraft:fire"] = 51,
+ ["minecraft:mob_spawner"] = 52,
+ ["minecraft:oak_stairs"] = 53,
+ ["minecraft:chest"] = 54,
+ ["minecraft:redstone_wire"] = 55,
+ ["minecraft:diamond_ore"] = 56,
+ ["minecraft:diamond_block"] = 57,
+ ["minecraft:crafting_table"] = 58,
+ ["minecraft:wheat"] = 59,
+ ["minecraft:farmland"] = 60,
+ ["minecraft:furnace"] = 61,
+ ["minecraft:lit_furnace"] = 62,
+ ["minecraft:standing_sign"] = 63,
+ ["minecraft:wooden_door"] = 64,
+ ["minecraft:ladder"] = 65,
+ ["minecraft:rail"] = 66,
+ ["minecraft:stone_stairs"] = 67,
+ ["minecraft:wall_sign"] = 68,
+ ["minecraft:lever"] = 69,
+ ["minecraft:stone_pressure_plate"] = 70,
+ ["minecraft:iron_door"] = 71,
+ ["minecraft:wooden_pressure_plate"] = 72,
+ ["minecraft:redstone_ore"] = 73,
+ ["minecraft:lit_redstone_ore"] = 74,
+ ["minecraft:unlit_redstone_torch"] = 75,
+ ["minecraft:redstone_torch"] = 76,
+ ["minecraft:stone_button"] = 77,
+ ["minecraft:snow_layer"] = 78,
+ ["minecraft:ice"] = 79,
+ ["minecraft:snow"] = 80,
+ ["minecraft:cactus"] = 81,
+ ["minecraft:clay"] = 82,
+ ["minecraft:reeds"] = 83,
+ ["minecraft:jukebox"] = 84,
+ ["minecraft:fence"] = 85,
+ ["minecraft:pumpkin"] = 86,
+ ["minecraft:netherrack"] = 87,
+ ["minecraft:soul_sand"] = 88,
+ ["minecraft:glowstone"] = 89,
+ ["minecraft:portal"] = 90,
+ ["minecraft:lit_pumpkin"] = 91,
+ ["minecraft:cake"] = 92,
+ ["minecraft:unpowered_repeater"] = 93,
+ ["minecraft:powered_repeater"] = 94,
+ ["minecraft:stained_glass"] = 95,
+ ["minecraft:trapdoor"] = 96,
+ ["minecraft:monster_egg"] = 97,
+ ["minecraft:stonebrick"] = 98,
+ ["minecraft:brown_mushroom_block"] = 99,
+ ["minecraft:red_mushroom_block"] = 100,
+ ["minecraft:iron_bars"] = 101,
+ ["minecraft:glass_pane"] = 102,
+ ["minecraft:melon_block"] = 103,
+ ["minecraft:pumpkin_stem"] = 104,
+ ["minecraft:melon_stem"] = 105,
+ ["minecraft:vine"] = 106,
+ ["minecraft:fence_gate"] = 107,
+ ["minecraft:brick_stairs"] = 108,
+ ["minecraft:stone_brick_stairs"] = 109,
+ ["minecraft:mycelium"] = 110,
+ ["minecraft:waterlily"] = 111,
+ ["minecraft:nether_brick"] = 112,
+ ["minecraft:nether_brick_fence"] = 113,
+ ["minecraft:nether_brick_stairs"] = 114,
+ ["minecraft:nether_wart"] = 115,
+ ["minecraft:enchanting_table"] = 116,
+ ["minecraft:brewing_stand"] = 117,
+ ["minecraft:cauldron"] = 118,
+ ["minecraft:end_portal"] = 119,
+ ["minecraft:end_portal_frame"] = 120,
+ ["minecraft:end_stone"] = 121,
+ ["minecraft:dragon_egg"] = 122,
+ ["minecraft:redstone_lamp"] = 123,
+ ["minecraft:lit_redstone_lamp"] = 124,
+ ["minecraft:double_wooden_slab"] = 125,
+ ["minecraft:wooden_slab"] = 126,
+ ["minecraft:cocoa"] = 127,
+ ["minecraft:sandstone_stairs"] = 128,
+ ["minecraft:emerald_ore"] = 129,
+ ["minecraft:ender_chest"] = 130,
+ ["minecraft:tripwire_hook"] = 131,
+ ["minecraft:tripwire"] = 132,
+ ["minecraft:emerald_block"] = 133,
+ ["minecraft:spruce_stairs"] = 134,
+ ["minecraft:birch_stairs"] = 135,
+ ["minecraft:jungle_stairs"] = 136,
+ ["minecraft:command_block"] = 137,
+ ["minecraft:beacon"] = 138,
+ ["minecraft:cobblestone_wall"] = 139,
+ ["minecraft:flower_pot"] = 140,
+ ["minecraft:carrots"] = 141,
+ ["minecraft:potatoes"] = 142,
+ ["minecraft:wooden_button"] = 143,
+ ["minecraft:skull"] = 144,
+ ["minecraft:anvil"] = 145,
+ ["minecraft:trapped_chest"] = 146,
+ ["minecraft:light_weighted_pressure_plate"] = 147,
+ ["minecraft:heavy_weighted_pressure_plate"] = 148,
+ ["minecraft:unpowered_comparator"] = 149,
+ ["minecraft:powered_comparator"] = 150,
+ ["minecraft:daylight_detector"] = 151,
+ ["minecraft:redstone_block"] = 152,
+ ["minecraft:quartz_ore"] = 153,
+ ["minecraft:hopper"] = 154,
+ ["minecraft:quartz_block"] = 155,
+ ["minecraft:quartz_stairs"] = 156,
+ ["minecraft:activator_rail"] = 157,
+ ["minecraft:dropper"] = 158,
+ ["minecraft:hardened_clay"] = 159,
+ ["minecraft:stained_glass_pane"] = 160,
+ ["minecraft:leaves2"] = 161,
+ ["minecraft:log2"] = 162,
+ ["minecraft:acacia_stairs"] = 163,
+ ["minecraft:dark_oak_stairs"] = 164,
+ ["minecraft:slime"] = 165,
+ ["minecraft:barrier"] = 166,
+ ["minecraft:iron_trapdoor"] = 167,
+ ["minecraft:prismarine"] = 168,
+ ["minecraft:sea_lantern"] = 169,
+ ["minecraft:hay_block"] = 170,
+ ["minecraft:carpet"] = 171,
+ ["minecraft:stained_hardened_clay"] = 172,
+ ["minecraft:coal_block"] = 173
+ };
+
+ private static readonly Dictionary s_vanillaItemToId = new(StringComparer.Ordinal)
+ {
+ ["minecraft:iron_shovel"] = 256,
+ ["minecraft:iron_pickaxe"] = 257,
+ ["minecraft:iron_axe"] = 258,
+ ["minecraft:flint_and_steel"] = 259,
+ ["minecraft:apple"] = 260,
+ ["minecraft:bow"] = 261,
+ ["minecraft:arrow"] = 262,
+ ["minecraft:coal"] = 263,
+ ["minecraft:diamond"] = 264,
+ ["minecraft:iron_ingot"] = 265,
+ ["minecraft:gold_ingot"] = 266,
+ ["minecraft:iron_sword"] = 267,
+ ["minecraft:wooden_sword"] = 268,
+ ["minecraft:wooden_shovel"] = 269,
+ ["minecraft:wooden_pickaxe"] = 270,
+ ["minecraft:wooden_axe"] = 271,
+ ["minecraft:stone_sword"] = 272,
+ ["minecraft:stone_shovel"] = 273,
+ ["minecraft:stone_pickaxe"] = 274,
+ ["minecraft:stone_axe"] = 275,
+ ["minecraft:diamond_sword"] = 276,
+ ["minecraft:diamond_shovel"] = 277,
+ ["minecraft:diamond_pickaxe"] = 278,
+ ["minecraft:diamond_axe"] = 279,
+ ["minecraft:stick"] = 280,
+ ["minecraft:bowl"] = 281,
+ ["minecraft:mushroom_stew"] = 282,
+ ["minecraft:golden_sword"] = 283,
+ ["minecraft:golden_shovel"] = 284,
+ ["minecraft:golden_pickaxe"] = 285,
+ ["minecraft:golden_axe"] = 286,
+ ["minecraft:string"] = 287,
+ ["minecraft:feather"] = 288,
+ ["minecraft:gunpowder"] = 289,
+ ["minecraft:wooden_hoe"] = 290,
+ ["minecraft:stone_hoe"] = 291,
+ ["minecraft:iron_hoe"] = 292,
+ ["minecraft:diamond_hoe"] = 293,
+ ["minecraft:golden_hoe"] = 294,
+ ["minecraft:wheat_seeds"] = 295,
+ ["minecraft:wheat"] = 296,
+ ["minecraft:bread"] = 297,
+ ["minecraft:leather_helmet"] = 298,
+ ["minecraft:leather_chestplate"] = 299,
+ ["minecraft:leather_leggings"] = 300,
+ ["minecraft:leather_boots"] = 301,
+ ["minecraft:chainmail_helmet"] = 302,
+ ["minecraft:chainmail_chestplate"] = 303,
+ ["minecraft:chainmail_leggings"] = 304,
+ ["minecraft:chainmail_boots"] = 305,
+ ["minecraft:iron_helmet"] = 306,
+ ["minecraft:iron_chestplate"] = 307,
+ ["minecraft:iron_leggings"] = 308,
+ ["minecraft:iron_boots"] = 309,
+ ["minecraft:diamond_helmet"] = 310,
+ ["minecraft:diamond_chestplate"] = 311,
+ ["minecraft:diamond_leggings"] = 312,
+ ["minecraft:diamond_boots"] = 313,
+ ["minecraft:golden_helmet"] = 314,
+ ["minecraft:golden_chestplate"] = 315,
+ ["minecraft:golden_leggings"] = 316,
+ ["minecraft:golden_boots"] = 317,
+ ["minecraft:flint"] = 318,
+ ["minecraft:porkchop"] = 319,
+ ["minecraft:cooked_porkchop"] = 320,
+ ["minecraft:painting"] = 321,
+ ["minecraft:golden_apple"] = 322,
+ ["minecraft:sign"] = 323,
+ ["minecraft:wooden_door"] = 324,
+ ["minecraft:bucket"] = 325,
+ ["minecraft:water_bucket"] = 326,
+ ["minecraft:lava_bucket"] = 327,
+ ["minecraft:minecart"] = 328,
+ ["minecraft:saddle"] = 329,
+ ["minecraft:iron_door"] = 330,
+ ["minecraft:redstone"] = 331,
+ ["minecraft:snowball"] = 332,
+ ["minecraft:boat"] = 333,
+ ["minecraft:leather"] = 334,
+ ["minecraft:milk_bucket"] = 335,
+ ["minecraft:brick"] = 336,
+ ["minecraft:clay_ball"] = 337,
+ ["minecraft:reeds"] = 338,
+ ["minecraft:paper"] = 339,
+ ["minecraft:book"] = 340,
+ ["minecraft:slime_ball"] = 341,
+ ["minecraft:chest_minecart"] = 342,
+ ["minecraft:furnace_minecart"] = 343,
+ ["minecraft:egg"] = 344,
+ ["minecraft:compass"] = 345,
+ ["minecraft:fishing_rod"] = 346,
+ ["minecraft:clock"] = 347,
+ ["minecraft:glowstone_dust"] = 348,
+ ["minecraft:fish"] = 349,
+ ["minecraft:cooked_fished"] = 350,
+ ["minecraft:dye"] = 351,
+ ["minecraft:bone"] = 352,
+ ["minecraft:sugar"] = 353,
+ ["minecraft:cake"] = 354,
+ ["minecraft:bed"] = 355,
+ ["minecraft:repeater"] = 356,
+ ["minecraft:cookie"] = 357,
+ ["minecraft:filled_map"] = 358,
+ ["minecraft:shears"] = 359,
+ ["minecraft:melon"] = 360,
+ ["minecraft:pumpkin_seeds"] = 361,
+ ["minecraft:melon_seeds"] = 362,
+ ["minecraft:beef"] = 363,
+ ["minecraft:cooked_beef"] = 364,
+ ["minecraft:chicken"] = 365,
+ ["minecraft:cooked_chicken"] = 366,
+ ["minecraft:rotten_flesh"] = 367,
+ ["minecraft:ender_pearl"] = 368,
+ ["minecraft:blaze_rod"] = 369,
+ ["minecraft:ghast_tear"] = 370,
+ ["minecraft:gold_nugget"] = 371,
+ ["minecraft:nether_wart"] = 372,
+ ["minecraft:potion"] = 373,
+ ["minecraft:glass_bottle"] = 374,
+ ["minecraft:spider_eye"] = 375,
+ ["minecraft:fermented_spider_eye"] = 376,
+ ["minecraft:blaze_powder"] = 377,
+ ["minecraft:magma_cream"] = 378,
+ ["minecraft:brewing_stand"] = 379,
+ ["minecraft:cauldron"] = 380,
+ ["minecraft:ender_eye"] = 381,
+ ["minecraft:speckled_melon"] = 382,
+ ["minecraft:spawn_egg"] = 383,
+ ["minecraft:experience_bottle"] = 384,
+ ["minecraft:fire_charge"] = 385,
+ ["minecraft:emerald"] = 388,
+ ["minecraft:item_frame"] = 389,
+ ["minecraft:flower_pot"] = 390,
+ ["minecraft:carrot"] = 391,
+ ["minecraft:potato"] = 392,
+ ["minecraft:baked_potato"] = 393,
+ ["minecraft:poisonous_potato"] = 394,
+ ["minecraft:map"] = 395,
+ ["minecraft:golden_carrot"] = 396,
+ ["minecraft:skull"] = 397,
+ ["minecraft:carrot_on_a_stick"] = 398,
+ ["minecraft:nether_star"] = 399,
+ ["minecraft:pumpkin_pie"] = 400,
+ ["minecraft:fireworks"] = 401,
+ ["minecraft:firework_charge"] = 402,
+ ["minecraft:enchanted_book"] = 403,
+ ["minecraft:comparator"] = 404,
+ ["minecraft:netherbrick"] = 405,
+ ["minecraft:quartz"] = 406,
+ ["minecraft:tnt_minecart"] = 407,
+ ["minecraft:hopper_minecart"] = 408,
+ ["minecraft:iron_horse_armor"] = 417,
+ ["minecraft:golden_horse_armor"] = 418,
+ ["minecraft:diamond_horse_armor"] = 419,
+ ["minecraft:lead"] = 420,
+ ["minecraft:name_tag"] = 421,
+ ["minecraft:record_13"] = 2256,
+ ["minecraft:record_cat"] = 2257,
+ ["minecraft:record_blocks"] = 2258,
+ ["minecraft:record_chirp"] = 2259,
+ ["minecraft:record_far"] = 2260,
+ ["minecraft:record_mall"] = 2261,
+ ["minecraft:record_mellohi"] = 2262,
+ ["minecraft:record_stal"] = 2263,
+ ["minecraft:record_strad"] = 2264,
+ ["minecraft:record_ward"] = 2265,
+ ["minecraft:record_11"] = 2266,
+ ["minecraft:record_wait"] = 2267
+ };
+
+ // Entity IDs must match the game's EntityIO::staticCtor() registrations.
+ private static readonly Dictionary s_vanillaEntityToId = new(StringComparer.Ordinal)
+ {
+ ["minecraft:item"] = 1,
+ ["minecraft:xp_orb"] = 2,
+ ["minecraft:leash_knot"] = 8,
+ ["minecraft:painting"] = 9,
+ ["minecraft:arrow"] = 10,
+ ["minecraft:snowball"] = 11,
+ ["minecraft:fireball"] = 12,
+ ["minecraft:small_fireball"] = 13,
+ ["minecraft:ender_pearl"] = 14,
+ ["minecraft:eye_of_ender_signal"] = 15,
+ ["minecraft:thrown_potion"] = 16,
+ ["minecraft:xp_bottle"] = 17,
+ ["minecraft:item_frame"] = 18,
+ ["minecraft:wither_skull"] = 19,
+ ["minecraft:primed_tnt"] = 20,
+ ["minecraft:falling_block"] = 21,
+ ["minecraft:fireworks_rocket"] = 22,
+ ["minecraft:boat"] = 41,
+ ["minecraft:minecart"] = 42,
+ ["minecraft:chest_minecart"] = 43,
+ ["minecraft:furnace_minecart"] = 44,
+ ["minecraft:tnt_minecart"] = 45,
+ ["minecraft:hopper_minecart"] = 46,
+ ["minecraft:spawner_minecart"] = 47,
+ ["minecraft:mob"] = 48,
+ ["minecraft:monster"] = 49,
+ ["minecraft:creeper"] = 50,
+ ["minecraft:skeleton"] = 51,
+ ["minecraft:spider"] = 52,
+ ["minecraft:giant"] = 53,
+ ["minecraft:zombie"] = 54,
+ ["minecraft:slime"] = 55,
+ ["minecraft:ghast"] = 56,
+ ["minecraft:zombie_pigman"] = 57,
+ ["minecraft:enderman"] = 58,
+ ["minecraft:cave_spider"] = 59,
+ ["minecraft:silverfish"] = 60,
+ ["minecraft:blaze"] = 61,
+ ["minecraft:magma_cube"] = 62,
+ ["minecraft:ender_dragon"] = 63,
+ ["minecraft:wither"] = 64,
+ ["minecraft:bat"] = 65,
+ ["minecraft:witch"] = 66,
+ ["minecraft:pig"] = 90,
+ ["minecraft:sheep"] = 91,
+ ["minecraft:cow"] = 92,
+ ["minecraft:chicken"] = 93,
+ ["minecraft:squid"] = 94,
+ ["minecraft:wolf"] = 95,
+ ["minecraft:mooshroom"] = 96,
+ ["minecraft:snow_golem"] = 97,
+ ["minecraft:ocelot"] = 98,
+ ["minecraft:iron_golem"] = 99,
+ ["minecraft:horse"] = 100,
+ ["minecraft:villager"] = 120,
+ ["minecraft:ender_crystal"] = 200,
+ ["minecraft:dragon_fireball"] = 1000
+ };
+
+ private static readonly Dictionary s_vanillaIdToBlock = CreateReverse(s_vanillaBlockToId);
+ private static readonly Dictionary s_vanillaIdToItem = CreateReverse(s_vanillaItemToId);
+ private static readonly Dictionary s_vanillaIdToEntity = CreateReverse(s_vanillaEntityToId);
+
+ public static int GetBlockNumericId(Identifier id)
+ {
+ int numeric = NativeInterop.native_get_block_id(id.ToString());
+ if (numeric >= 0)
+ return numeric;
+ return s_vanillaBlockToId.TryGetValue(id.ToString(), out int vanillaId) ? vanillaId : -1;
+ }
+
+ public static int GetItemNumericId(Identifier id)
+ {
+ int numeric = NativeInterop.native_get_item_id(id.ToString());
+ if (numeric >= 0)
+ return numeric;
+ return s_vanillaItemToId.TryGetValue(id.ToString(), out int vanillaId) ? vanillaId : -1;
+ }
+
+ public static int GetEntityNumericId(Identifier id)
+ {
+ int numeric = NativeInterop.native_get_entity_id(id.ToString());
+ if (numeric >= 0)
+ return numeric;
+ return s_vanillaEntityToId.TryGetValue(id.ToString(), out int vanillaId) ? vanillaId : -1;
+ }
+
+ public static bool TryGetBlockIdentifier(int numericId, out Identifier id)
+ {
+ if (BlockRegistry.TryGetIdentifier(numericId, out id))
+ return true;
+
+ if (s_vanillaIdToBlock.TryGetValue(numericId, out string? vanilla))
+ {
+ id = new Identifier(vanilla);
+ return true;
+ }
+ id = default;
+ return false;
+ }
+
+ public static bool TryGetItemIdentifier(int numericId, out Identifier id)
+ {
+ if (ItemRegistry.TryGetIdentifier(numericId, out id))
+ return true;
+ if (s_vanillaIdToItem.TryGetValue(numericId, out string? vanilla))
+ {
+ id = new Identifier(vanilla);
+ return true;
+ }
+ id = default;
+ return false;
+ }
+
+ public static bool TryGetEntityIdentifier(int numericId, out Identifier id)
+ {
+ if (EntityRegistry.TryGetIdentifier(numericId, out id))
+ return true;
+
+ if (s_vanillaIdToEntity.TryGetValue(numericId, out string? vanilla))
+ {
+ id = new Identifier(vanilla);
+ return true;
+ }
+ id = default;
+ return false;
+ }
+
+ private static Dictionary CreateReverse(Dictionary source)
+ {
+ var reverse = new Dictionary();
+ foreach (var kv in source)
+ reverse[kv.Value] = kv.Key;
+ return reverse;
+ }
+}
diff --git a/WeaveLoader.API/Item/CustomItem.cs b/WeaveLoader.API/Item/CustomItem.cs
index 792521a..da7c6af 100644
--- a/WeaveLoader.API/Item/CustomItem.cs
+++ b/WeaveLoader.API/Item/CustomItem.cs
@@ -20,6 +20,13 @@ public abstract class Item
/// or to skip vanilla handling.
///
public virtual MineBlockResult OnMineBlock(MineBlockContext context) => MineBlockResult.ContinueVanilla;
+
+ ///
+ /// Called when this item is actively used by the player (right-click use path).
+ /// Return to run vanilla logic,
+ /// or to skip vanilla handling.
+ ///
+ public virtual UseItemResult OnUseItem(UseItemContext context) => UseItemResult.ContinueVanilla;
}
///
@@ -31,6 +38,15 @@ public enum MineBlockResult
CancelVanilla = 1
}
+///
+/// Result of managed use-item callback.
+///
+public enum UseItemResult
+{
+ ContinueVanilla = 0,
+ CancelVanilla = 1
+}
+
///
/// Tool tier used by native tool constructors.
///
@@ -73,6 +89,72 @@ public readonly struct MineBlockContext
}
}
+///
+/// Runtime context for item use callback.
+///
+public readonly struct UseItemContext
+{
+ public int ItemId { get; }
+ public bool IsTestUseOnly { get; }
+ public bool IsClientSide { get; }
+ public nint NativeItemInstancePtr { get; }
+ public nint NativePlayerPtr { get; }
+ public nint NativePlayerSharedPtr { get; }
+
+ internal UseItemContext(int itemId, bool isTestUseOnly, bool isClientSide, nint nativeItemInstancePtr, nint nativePlayerPtr, nint nativePlayerSharedPtr)
+ {
+ ItemId = itemId;
+ IsTestUseOnly = isTestUseOnly;
+ IsClientSide = isClientSide;
+ NativeItemInstancePtr = nativeItemInstancePtr;
+ NativePlayerPtr = nativePlayerPtr;
+ NativePlayerSharedPtr = nativePlayerSharedPtr;
+ }
+
+ public bool ConsumeInventoryItem(Identifier id, int count = 1)
+ {
+ if (NativePlayerPtr == 0 || count <= 0)
+ return false;
+
+ int numericId = IdHelper.GetItemNumericId(id);
+ if (numericId < 0)
+ return false;
+
+ return NativeInterop.native_consume_item_from_player(NativePlayerPtr, numericId, count) != 0;
+ }
+
+ public bool DamageItem(int amount)
+ {
+ if (NativeItemInstancePtr == 0 || NativePlayerSharedPtr == 0 || amount <= 0)
+ return false;
+
+ return NativeInterop.native_damage_item_instance(NativeItemInstancePtr, amount, NativePlayerSharedPtr) != 0;
+ }
+
+ public bool SpawnEntityFromLook(Identifier id, double speed = 1.4, double spawnForward = 1.0, double spawnUp = 1.2)
+ {
+ int numericEntityId = IdHelper.GetEntityNumericId(id);
+ if (numericEntityId < 0)
+ return false;
+
+ return SpawnEntityFromLook(numericEntityId, speed, spawnForward, spawnUp);
+ }
+
+ public bool SpawnEntityFromLook(int numericEntityId, double speed = 1.4, double spawnForward = 1.0, double spawnUp = 1.2)
+ {
+ if (NativePlayerPtr == 0 || numericEntityId < 0)
+ return false;
+
+ return NativeInterop.native_spawn_entity_from_player_look(
+ NativePlayerPtr,
+ NativePlayerSharedPtr,
+ numericEntityId,
+ speed,
+ spawnForward,
+ spawnUp) != 0;
+ }
+}
+
[StructLayout(LayoutKind.Sequential)]
internal struct MineBlockNativeArgs
{
@@ -83,6 +165,17 @@ internal struct MineBlockNativeArgs
public int Z;
}
+[StructLayout(LayoutKind.Sequential)]
+internal struct UseItemNativeArgs
+{
+ public int ItemId;
+ public int IsTestUseOnly;
+ public int IsClientSide;
+ public nint ItemInstancePtr;
+ public nint PlayerPtr;
+ public nint PlayerSharedPtr;
+}
+
internal static class ManagedItemDispatcher
{
private static readonly object s_lock = new();
@@ -125,4 +218,32 @@ internal static class ManagedItemDispatcher
// 0 = no managed item, 1 = continue vanilla, 2 = cancel vanilla.
return result == MineBlockResult.CancelVanilla ? 2 : 1;
}
+
+ internal static int HandleUseItem(IntPtr args, int sizeBytes)
+ {
+ if (args == IntPtr.Zero || sizeBytes < Marshal.SizeOf())
+ return 0;
+
+ UseItemNativeArgs nativeArgs = Marshal.PtrToStructure(args);
+
+ Item? item;
+ lock (s_lock)
+ {
+ s_items.TryGetValue(nativeArgs.ItemId, out item);
+ }
+
+ if (item == null)
+ return 0;
+
+ var result = item.OnUseItem(new UseItemContext(
+ nativeArgs.ItemId,
+ nativeArgs.IsTestUseOnly != 0,
+ nativeArgs.IsClientSide != 0,
+ nativeArgs.ItemInstancePtr,
+ nativeArgs.PlayerPtr,
+ nativeArgs.PlayerSharedPtr));
+
+ // 0 = no managed item, 1 = continue vanilla, 2 = cancel vanilla.
+ return result == UseItemResult.CancelVanilla ? 2 : 1;
+ }
}
diff --git a/WeaveLoader.API/Item/ItemRegistry.cs b/WeaveLoader.API/Item/ItemRegistry.cs
index 6384343..c71589d 100644
--- a/WeaveLoader.API/Item/ItemRegistry.cs
+++ b/WeaveLoader.API/Item/ItemRegistry.cs
@@ -1,3 +1,5 @@
+using System.Collections.Generic;
+
namespace WeaveLoader.API.Item;
///
@@ -24,6 +26,9 @@ public class RegisteredItem
///
public static class ItemRegistry
{
+ private static readonly object s_lock = new();
+ private static readonly Dictionary s_idByNumeric = new();
+
///
/// Register a new item with the game engine.
///
@@ -85,6 +90,18 @@ public static class ItemRegistry
}
Logger.Debug($"Registered item '{id}' -> numeric ID {numericId}");
+ lock (s_lock)
+ {
+ s_idByNumeric[numericId] = id;
+ }
return new RegisteredItem(id, numericId);
}
+
+ internal static bool TryGetIdentifier(int numericId, out Identifier id)
+ {
+ lock (s_lock)
+ {
+ return s_idByNumeric.TryGetValue(numericId, out id);
+ }
+ }
}
diff --git a/WeaveLoader.API/NativeInterop.cs b/WeaveLoader.API/NativeInterop.cs
index 7f7a49d..c0b6e06 100644
--- a/WeaveLoader.API/NativeInterop.cs
+++ b/WeaveLoader.API/NativeInterop.cs
@@ -76,6 +76,27 @@ internal static class NativeInterop
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
internal static extern int native_get_entity_id(string namespacedId);
+ [DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
+ internal static extern int native_summon_entity(string namespacedId, double x, double y, double z);
+
+ [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_consume_item_from_player(nint playerPtr, int numericItemId, int count);
+
+ [DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl)]
+ internal static extern int native_damage_item_instance(nint itemInstancePtr, int amount, nint ownerSharedPtr);
+
+ [DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl)]
+ internal static extern int native_spawn_entity_from_player_look(
+ nint playerPtr,
+ nint playerSharedPtr,
+ int numericEntityId,
+ double speed,
+ double spawnForward,
+ double spawnUp);
+
[DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
internal static extern void native_subscribe_event(string eventName, IntPtr managedFnPtr);
diff --git a/WeaveLoader.API/Registry.cs b/WeaveLoader.API/Registry.cs
index 05b4c37..6777ad9 100644
--- a/WeaveLoader.API/Registry.cs
+++ b/WeaveLoader.API/Registry.cs
@@ -34,6 +34,12 @@ public static class Registry
{
public static RegisteredEntity Register(Identifier id, EntityDefinition definition)
=> EntityRegistry.Register(id, definition);
+
+ public static bool Summon(Identifier id, double x, double y, double z)
+ => EntityRegistry.Summon(id, x, y, z);
+
+ public static bool Summon(int numericId, double x, double y, double z)
+ => EntityRegistry.Summon(numericId, x, y, z);
}
/// Recipe registration for crafting and smelting.
diff --git a/WeaveLoader.Core/WeaveLoaderCore.cs b/WeaveLoader.Core/WeaveLoaderCore.cs
index 6b4be6d..be06be7 100644
--- a/WeaveLoader.Core/WeaveLoaderCore.cs
+++ b/WeaveLoader.Core/WeaveLoaderCore.cs
@@ -1,5 +1,6 @@
using System.Runtime.InteropServices;
using WeaveLoader.API;
+using WeaveLoader.API.Events;
using WeaveLoader.API.Item;
namespace WeaveLoader.Core;
@@ -106,4 +107,85 @@ public static class WeaveLoaderCore
return 0;
}
}
+
+ public static int OnItemUse(IntPtr args, int sizeBytes)
+ {
+ try
+ {
+ return ManagedItemDispatcher.HandleUseItem(args, sizeBytes);
+ }
+ catch (Exception ex)
+ {
+ Logger.Error($"OnItemUse EXCEPTION: {ex}");
+ return 0;
+ }
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct WorldLoadedNativeArgs
+ {
+ public IntPtr LevelPtr;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct EntitySummonedNativeArgs
+ {
+ public int EntityNumericId;
+ public float X;
+ public float Y;
+ public float Z;
+ }
+
+ public static int OnWorldLoaded(IntPtr args, int sizeBytes)
+ {
+ try
+ {
+ var native = Marshal.PtrToStructure(args);
+ GameEvents.FireWorldLoaded(new WorldLoadedEventArgs
+ {
+ NativeLevelPointer = native.LevelPtr
+ });
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Logger.Error($"OnWorldLoaded EXCEPTION: {ex}");
+ return 0;
+ }
+ }
+
+ public static int OnWorldUnloaded(IntPtr args, int sizeBytes)
+ {
+ try
+ {
+ GameEvents.FireWorldUnloaded(new WorldUnloadedEventArgs());
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Logger.Error($"OnWorldUnloaded EXCEPTION: {ex}");
+ return 0;
+ }
+ }
+
+ public static int OnEntitySummoned(IntPtr args, int sizeBytes)
+ {
+ try
+ {
+ var native = Marshal.PtrToStructure(args);
+ GameEvents.FireEntitySpawn(new EntitySpawnEventArgs
+ {
+ EntityId = $"entity:{native.EntityNumericId}",
+ X = native.X,
+ Y = native.Y,
+ Z = native.Z
+ });
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Logger.Error($"OnEntitySummoned EXCEPTION: {ex}");
+ return 0;
+ }
+ }
}
diff --git a/WeaveLoaderRuntime/src/CrashHandler.cpp b/WeaveLoaderRuntime/src/CrashHandler.cpp
index 7333378..d03a461 100644
--- a/WeaveLoaderRuntime/src/CrashHandler.cpp
+++ b/WeaveLoaderRuntime/src/CrashHandler.cpp
@@ -3,14 +3,30 @@
#include "PdbParser.h"
#include
#include
+#include
+#include
+#include
#include
#include
#include
+#include
static HMODULE s_runtimeModule = nullptr;
static uintptr_t s_gameBase = 0;
static volatile LONG s_handling = 0;
+#ifndef STATUS_STACK_BUFFER_OVERRUN
+#define STATUS_STACK_BUFFER_OVERRUN ((DWORD)0xC0000409)
+#endif
+
+#ifndef STATUS_INVALID_CRUNTIME_PARAMETER
+#define STATUS_INVALID_CRUNTIME_PARAMETER ((DWORD)0xC0000417)
+#endif
+
+#ifndef STATUS_FAIL_FAST_EXCEPTION
+#define STATUS_FAIL_FAST_EXCEPTION ((DWORD)0xC0000602)
+#endif
+
static const char* ExceptionCodeToString(DWORD code)
{
switch (code)
@@ -34,8 +50,11 @@ static const char* ExceptionCodeToString(DWORD code)
case EXCEPTION_INVALID_DISPOSITION: return "EXCEPTION_INVALID_DISPOSITION";
case EXCEPTION_NONCONTINUABLE_EXCEPTION: return "EXCEPTION_NONCONTINUABLE_EXCEPTION";
case EXCEPTION_PRIV_INSTRUCTION: return "EXCEPTION_PRIV_INSTRUCTION";
- case EXCEPTION_SINGLE_STEP: return "EXCEPTION_SINGLE_STEP";
+ case EXCEPTION_SINGLE_STEP: return "EXCEPTION_SINGLE_STEP";
case EXCEPTION_STACK_OVERFLOW: return "EXCEPTION_STACK_OVERFLOW";
+ case STATUS_STACK_BUFFER_OVERRUN: return "STATUS_STACK_BUFFER_OVERRUN";
+ case STATUS_INVALID_CRUNTIME_PARAMETER: return "STATUS_INVALID_CRUNTIME_PARAMETER";
+ case STATUS_FAIL_FAST_EXCEPTION: return "STATUS_FAIL_FAST_EXCEPTION";
default: return "UNKNOWN_EXCEPTION";
}
}
@@ -60,6 +79,9 @@ static bool IsFatalException(DWORD code)
case EXCEPTION_NONCONTINUABLE_EXCEPTION:
case EXCEPTION_PRIV_INSTRUCTION:
case EXCEPTION_STACK_OVERFLOW:
+ case STATUS_STACK_BUFFER_OVERRUN:
+ case STATUS_INVALID_CRUNTIME_PARAMETER:
+ case STATUS_FAIL_FAST_EXCEPTION:
return true;
default:
return false;
@@ -143,40 +165,18 @@ static void WalkStack(CONTEXT* ctx)
}
}
-static LONG WINAPI VectoredHandler(EXCEPTION_POINTERS* ep)
+static void WriteCrashReport(const char* origin, EXCEPTION_RECORD* er, CONTEXT* ctx)
{
- if (!ep || !ep->ExceptionRecord || !ep->ContextRecord)
- return EXCEPTION_CONTINUE_SEARCH;
+ if (!er || !ctx)
+ return;
- DWORD code = ep->ExceptionRecord->ExceptionCode;
-
- // The game's debug build uses __debugbreak() (int 3) as assertions throughout
- // the texture system and other subsystems. Without a debugger the default
- // handler terminates the process. We skip past the 1-byte int 3 instruction
- // so the game's fallback code (e.g. missing-texture) can run normally.
- if (code == EXCEPTION_BREAKPOINT && s_gameBase != 0)
- {
- DWORD64 rip = ep->ContextRecord->Rip;
- HMODULE hMod = nullptr;
- if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
- GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
- reinterpret_cast(rip), &hMod) &&
- reinterpret_cast(hMod) == s_gameBase)
- {
- ep->ContextRecord->Rip += 1;
- return EXCEPTION_CONTINUE_EXECUTION;
- }
- }
-
- if (!IsFatalException(code))
- return EXCEPTION_CONTINUE_SEARCH;
-
- // Prevent re-entrancy if the crash handler itself crashes
if (InterlockedCompareExchange(&s_handling, 1, 0) != 0)
- return EXCEPTION_CONTINUE_SEARCH;
+ return;
- EXCEPTION_RECORD* er = ep->ExceptionRecord;
- CONTEXT* ctx = ep->ContextRecord;
+ DWORD code = er->ExceptionCode;
+ DWORD64 faultAddr = er->ExceptionAddress
+ ? reinterpret_cast(er->ExceptionAddress)
+ : ctx->Rip;
SYSTEMTIME st;
GetLocalTime(&st);
@@ -190,8 +190,9 @@ static LONG WINAPI VectoredHandler(EXCEPTION_POINTERS* ep)
LogUtil::LogCrash(" %s", timeBuf);
LogUtil::LogCrash("========================================");
LogUtil::LogCrash("");
+ LogUtil::LogCrash("Origin: %s", origin ? origin : "");
LogUtil::LogCrash("Exception: %s (0x%08X)", ExceptionCodeToString(code), code);
- LogUtil::LogCrash("Address: 0x%016llX", reinterpret_cast(er->ExceptionAddress));
+ LogUtil::LogCrash("Address: 0x%016llX", faultAddr);
LogUtil::LogCrash("Thread: %lu", GetCurrentThreadId());
if (code == EXCEPTION_ACCESS_VIOLATION && er->NumberParameters >= 2)
@@ -202,9 +203,7 @@ static LONG WINAPI VectoredHandler(EXCEPTION_POINTERS* ep)
LogUtil::LogCrash("Fault: %s of address 0x%016llX", op, er->ExceptionInformation[1]);
}
- // Module containing the faulting address + PDB symbol resolution
{
- DWORD64 faultAddr = reinterpret_cast(er->ExceptionAddress);
char modPath[MAX_PATH] = {0};
DWORD64 modBase = 0;
GetModuleForAddr(faultAddr, modPath, sizeof(modPath), &modBase);
@@ -237,7 +236,6 @@ static LONG WINAPI VectoredHandler(EXCEPTION_POINTERS* ep)
LogUtil::LogCrash("");
WalkStack(ctx);
- // Loaded modules
LogUtil::LogCrash("");
LogUtil::LogCrash("Loaded modules:");
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId());
@@ -266,6 +264,96 @@ static LONG WINAPI VectoredHandler(EXCEPTION_POINTERS* ep)
LogUtil::Log("[WeaveLoader] CRASH DETECTED - see logs/crash.log for details");
InterlockedExchange(&s_handling, 0);
+}
+
+static void LogCapturedCrash(const char* origin, DWORD code)
+{
+ EXCEPTION_RECORD er{};
+ er.ExceptionCode = code;
+ er.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
+
+ CONTEXT ctx{};
+ RtlCaptureContext(&ctx);
+ er.ExceptionAddress = reinterpret_cast(ctx.Rip);
+
+ WriteCrashReport(origin, &er, &ctx);
+}
+
+static void __cdecl PurecallHandler()
+{
+ LogCapturedCrash("_purecall", STATUS_INVALID_CRUNTIME_PARAMETER);
+ TerminateProcess(GetCurrentProcess(), STATUS_INVALID_CRUNTIME_PARAMETER);
+}
+
+static void __cdecl TerminateHandler()
+{
+ LogCapturedCrash("std::terminate", STATUS_FAIL_FAST_EXCEPTION);
+ TerminateProcess(GetCurrentProcess(), STATUS_FAIL_FAST_EXCEPTION);
+}
+
+static void __cdecl SignalHandler(int sig)
+{
+ DWORD code = STATUS_FAIL_FAST_EXCEPTION;
+ switch (sig)
+ {
+ case SIGABRT: code = STATUS_STACK_BUFFER_OVERRUN; break;
+ case SIGSEGV: code = EXCEPTION_ACCESS_VIOLATION; break;
+ case SIGILL: code = EXCEPTION_ILLEGAL_INSTRUCTION; break;
+ case SIGFPE: code = EXCEPTION_FLT_INVALID_OPERATION; break;
+ default: break;
+ }
+
+ LogCapturedCrash("signal", code);
+ TerminateProcess(GetCurrentProcess(), code);
+}
+
+static void InvalidParameterHandler(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, uintptr_t)
+{
+ LogCapturedCrash("_invalid_parameter", STATUS_INVALID_CRUNTIME_PARAMETER);
+ TerminateProcess(GetCurrentProcess(), STATUS_INVALID_CRUNTIME_PARAMETER);
+}
+
+static LONG WINAPI TopLevelHandler(EXCEPTION_POINTERS* ep)
+{
+ if (!ep || !ep->ExceptionRecord || !ep->ContextRecord)
+ return EXCEPTION_CONTINUE_SEARCH;
+
+ if (!IsFatalException(ep->ExceptionRecord->ExceptionCode))
+ return EXCEPTION_CONTINUE_SEARCH;
+
+ WriteCrashReport("UnhandledExceptionFilter", ep->ExceptionRecord, ep->ContextRecord);
+ return EXCEPTION_CONTINUE_SEARCH;
+}
+
+static LONG WINAPI VectoredHandler(EXCEPTION_POINTERS* ep)
+{
+ if (!ep || !ep->ExceptionRecord || !ep->ContextRecord)
+ return EXCEPTION_CONTINUE_SEARCH;
+
+ DWORD code = ep->ExceptionRecord->ExceptionCode;
+
+ // The game's debug build uses __debugbreak() (int 3) as assertions throughout
+ // the texture system and other subsystems. Without a debugger the default
+ // handler terminates the process. We skip past the 1-byte int 3 instruction
+ // so the game's fallback code (e.g. missing-texture) can run normally.
+ if (code == EXCEPTION_BREAKPOINT && s_gameBase != 0)
+ {
+ DWORD64 rip = ep->ContextRecord->Rip;
+ HMODULE hMod = nullptr;
+ if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
+ GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
+ reinterpret_cast(rip), &hMod) &&
+ reinterpret_cast(hMod) == s_gameBase)
+ {
+ ep->ContextRecord->Rip += 1;
+ return EXCEPTION_CONTINUE_EXECUTION;
+ }
+ }
+
+ if (!IsFatalException(code))
+ return EXCEPTION_CONTINUE_SEARCH;
+
+ WriteCrashReport("VectoredExceptionHandler", ep->ExceptionRecord, ep->ContextRecord);
return EXCEPTION_CONTINUE_SEARCH;
}
@@ -276,6 +364,14 @@ void Install(HMODULE runtimeModule)
{
s_runtimeModule = runtimeModule;
AddVectoredExceptionHandler(1, VectoredHandler);
+ SetUnhandledExceptionFilter(TopLevelHandler);
+ _set_invalid_parameter_handler(InvalidParameterHandler);
+ _set_purecall_handler(PurecallHandler);
+ std::set_terminate(TerminateHandler);
+ std::signal(SIGABRT, SignalHandler);
+ std::signal(SIGSEGV, SignalHandler);
+ std::signal(SIGILL, SignalHandler);
+ std::signal(SIGFPE, SignalHandler);
}
void SetGameBase(uintptr_t base)
diff --git a/WeaveLoaderRuntime/src/DotNetHost.cpp b/WeaveLoaderRuntime/src/DotNetHost.cpp
index 12f4dbc..dc3b765 100644
--- a/WeaveLoaderRuntime/src/DotNetHost.cpp
+++ b/WeaveLoaderRuntime/src/DotNetHost.cpp
@@ -23,6 +23,8 @@ static managed_entry_fn fn_PostInit = nullptr;
static managed_entry_fn fn_Tick = nullptr;
static managed_entry_fn fn_Shutdown = nullptr;
static managed_entry_fn fn_ItemMineBlock = nullptr;
+static managed_entry_fn fn_ItemUse = nullptr;
+static managed_entry_fn fn_EntitySummoned = nullptr;
static bool LoadHostfxr()
{
@@ -179,6 +181,8 @@ bool DotNetHost::Initialize()
ok &= resolve(L"Tick", &fn_Tick);
ok &= resolve(L"Shutdown", &fn_Shutdown);
ok &= resolve(L"OnItemMineBlock", &fn_ItemMineBlock);
+ ok &= resolve(L"OnItemUse", &fn_ItemUse);
+ ok &= resolve(L"OnEntitySummoned", &fn_EntitySummoned);
if (!ok)
{
@@ -236,6 +240,29 @@ int DotNetHost::CallItemMineBlock(const void* args, int sizeBytes)
return fn_ItemMineBlock(const_cast(args), sizeBytes);
}
+int DotNetHost::CallItemUse(const void* args, int sizeBytes)
+{
+ if (!fn_ItemUse || !args || sizeBytes <= 0)
+ return 0;
+ return fn_ItemUse(const_cast(args), sizeBytes);
+}
+
+void DotNetHost::CallEntitySummoned(int entityNumericId, float x, float y, float z)
+{
+ if (!fn_EntitySummoned)
+ return;
+
+ struct EntitySummonedNativeArgs
+ {
+ int entityNumericId;
+ float x;
+ float y;
+ float z;
+ } native{ entityNumericId, x, y, z };
+
+ fn_EntitySummoned(&native, sizeof(native));
+}
+
void DotNetHost::Cleanup()
{
}
diff --git a/WeaveLoaderRuntime/src/DotNetHost.h b/WeaveLoaderRuntime/src/DotNetHost.h
index 9283d88..e756de5 100644
--- a/WeaveLoaderRuntime/src/DotNetHost.h
+++ b/WeaveLoaderRuntime/src/DotNetHost.h
@@ -16,4 +16,6 @@ namespace DotNetHost
void CallTick();
void CallShutdown();
int CallItemMineBlock(const void* args, int sizeBytes);
+ int CallItemUse(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 c2e42de..e7838bc 100644
--- a/WeaveLoaderRuntime/src/GameHooks.cpp
+++ b/WeaveLoaderRuntime/src/GameHooks.cpp
@@ -10,6 +10,9 @@
#include
#include
#include
+#include
+#include
+#include
namespace GameHooks
{
@@ -29,6 +32,9 @@ namespace GameHooks
ItemInstanceMineBlock_fn Original_ItemInstanceMineBlock = nullptr;
ItemMineBlock_fn Original_ItemMineBlock = nullptr;
ItemMineBlock_fn Original_DiggerItemMineBlock = nullptr;
+ GameModeUseItem_fn Original_ServerPlayerGameModeUseItem = nullptr;
+ GameModeUseItem_fn Original_MultiPlayerGameModeUseItem = nullptr;
+ MinecraftSetLevel_fn Original_MinecraftSetLevel = nullptr;
TexturesBindTextureResource_fn Original_TexturesBindTextureResource = nullptr;
TexturesLoadTextureByName_fn Original_TexturesLoadTextureByName = nullptr;
TexturesLoadTextureByIndex_fn Original_TexturesLoadTextureByIndex = nullptr;
@@ -37,6 +43,29 @@ namespace GameHooks
StitchedTextureUV_fn Original_StitchedGetV0 = nullptr;
StitchedTextureUV_fn Original_StitchedGetV1 = nullptr;
static int s_itemMineBlockHookCalls = 0;
+ static void* s_currentLevel = nullptr;
+ static thread_local void* s_activeUseLevel = nullptr;
+ static LevelAddEntity_fn s_levelAddEntity = nullptr;
+ static EntityIONewById_fn s_entityIoNewById = nullptr;
+ static EntityMoveTo_fn s_entityMoveTo = nullptr;
+ static EntitySetPos_fn s_entitySetPos = nullptr;
+ static EntityGetLookAngle_fn s_entityGetLookAngle = nullptr;
+ static LivingEntityGetViewVector_fn s_livingEntityGetViewVector = nullptr;
+ static EntityLerpMotion_fn s_entityLerpMotion = nullptr;
+ static InventoryRemoveResource_fn s_inventoryRemoveResource = nullptr;
+ static void* s_inventoryVtable = nullptr;
+ static ItemInstanceHurtAndBreak_fn s_itemInstanceHurtAndBreak = nullptr;
+ // Verified from compiled Player::inventory accesses in this game build.
+ static constexpr ptrdiff_t kPlayerInventoryOffset = 0x340;
+ static constexpr ptrdiff_t kLevelIsClientSideOffset = 0x268;
+ static constexpr ptrdiff_t kEntityXOffset = 0x78;
+ static constexpr ptrdiff_t kEntityYOffset = 0x80;
+ static constexpr ptrdiff_t kEntityZOffset = 0x88;
+ static constexpr ptrdiff_t kEntityRemovedOffset = 0xC7;
+ static constexpr ptrdiff_t kFireballOwnerOffset = 0x1D0;
+ static constexpr ptrdiff_t kFireballXPowerOffset = 0x1E8;
+ static constexpr ptrdiff_t kFireballYPowerOffset = 0x1F0;
+ static constexpr ptrdiff_t kFireballZPowerOffset = 0x1F8;
static void* s_textureAtlasLocationBlocks = nullptr;
static void* s_textureAtlasLocationItems = nullptr;
static int s_textureAtlasIdBlocks = -1;
@@ -60,6 +89,12 @@ namespace GameHooks
static bool s_pageResourceInit = false;
static int s_pageRouteLogCount = 0;
static int s_forcedTerrainRouteLogCount = 0;
+ static uintptr_t s_gameModuleBase = 0;
+ static uintptr_t s_gameModuleEnd = 0;
+ static std::vector s_spawnedEntities;
+ static int s_outOfWorldGuardLogCount = 0;
+ static int s_pendingServerUseItemId = -1;
+ static ULONGLONG s_pendingServerUseExpiryMs = 0;
static void EnsurePageResourcesInitialized()
{
@@ -71,6 +106,73 @@ namespace GameHooks
s_pageResourceInit = true;
}
+ static void EnsureGameModuleRange()
+ {
+ if (s_gameModuleBase != 0 && s_gameModuleEnd > s_gameModuleBase)
+ return;
+
+ HMODULE exe = GetModuleHandleW(nullptr);
+ if (!exe)
+ return;
+
+ auto* dos = reinterpret_cast(exe);
+ if (!dos || dos->e_magic != IMAGE_DOS_SIGNATURE)
+ return;
+
+ auto* nt = reinterpret_cast(
+ reinterpret_cast(exe) + dos->e_lfanew);
+ if (!nt || nt->Signature != IMAGE_NT_SIGNATURE)
+ return;
+
+ s_gameModuleBase = reinterpret_cast(exe);
+ s_gameModuleEnd = s_gameModuleBase + nt->OptionalHeader.SizeOfImage;
+ }
+
+ static bool IsCanonicalUserPtr(const void* ptr)
+ {
+ uintptr_t p = reinterpret_cast(ptr);
+ if (p < 0x10000ULL)
+ return false;
+ // User-mode canonical range on x64 Windows.
+ if (p >= 0x0000800000000000ULL)
+ return false;
+ return true;
+ }
+
+ static bool IsGameCodePtr(const void* ptr)
+ {
+ if (!ptr)
+ return false;
+ EnsureGameModuleRange();
+ if (s_gameModuleBase == 0 || s_gameModuleEnd <= s_gameModuleBase)
+ return false;
+ uintptr_t p = reinterpret_cast(ptr);
+ return p >= s_gameModuleBase && p < s_gameModuleEnd;
+ }
+
+ static bool IsReadableRange(const void* ptr, size_t bytes)
+ {
+ if (!ptr || bytes == 0 || !IsCanonicalUserPtr(ptr))
+ return false;
+
+ MEMORY_BASIC_INFORMATION mbi{};
+ if (VirtualQuery(ptr, &mbi, sizeof(mbi)) != sizeof(mbi))
+ return false;
+ if (mbi.State != MEM_COMMIT || mbi.Protect == PAGE_NOACCESS || (mbi.Protect & PAGE_GUARD))
+ return false;
+
+ const DWORD readMask = PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY |
+ PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
+ if ((mbi.Protect & readMask) == 0)
+ return false;
+
+ uintptr_t p = reinterpret_cast(ptr);
+ uintptr_t end = reinterpret_cast(mbi.BaseAddress) + mbi.RegionSize;
+ if (p + bytes < p)
+ return false;
+ return (p + bytes) <= end;
+ }
+
static std::wstring BuildVirtualAtlasPath(int atlasType, int page)
{
std::wstring base = L"/modloader/";
@@ -193,6 +295,20 @@ namespace GameHooks
int z;
};
+ struct UseItemNativeArgs
+ {
+ int itemId;
+ int isTestUseOnly;
+ int isClientSide;
+ void* itemInstancePtr;
+ void* playerPtr;
+ void* playerSharedPtr;
+ };
+
+ static bool IsFireballFamilyEntityId(int entityNumericId);
+ static void* DecodeItemInstancePtrFromSharedArg(void* sharedArg);
+ static void* DecodePlayerPtrFromSharedArg(void* sharedArg);
+
static bool TryReadItemId(void* itemInstancePtr, int& outItemId)
{
if (!itemInstancePtr)
@@ -203,16 +319,16 @@ namespace GameHooks
static const int kCandidateOffsets[] = { 0x20, 0x18, 0x10, 0x28 };
for (int off : kCandidateOffsets)
{
- __try
+ const char* idPtr = static_cast(itemInstancePtr) + off;
+ if (!IsReadableRange(idPtr, sizeof(int)))
+ continue;
+
+ int id = *reinterpret_cast(idPtr);
+ if (id > 0 && id < 32000)
{
- int id = *reinterpret_cast(static_cast(itemInstancePtr) + off);
- if (id > 0 && id < 32000)
- {
- outItemId = id;
- return true;
- }
+ outItemId = id;
+ return true;
}
- __except (EXCEPTION_EXECUTE_HANDLER) {}
}
return false;
@@ -232,6 +348,386 @@ namespace GameHooks
return action;
}
+ static int TryDispatchUseItemFromSharedItemArg(void* itemInstanceSharedPtr, void* playerSharedPtr, bool bTestUseOnly, const char* sourceTag)
+ {
+ void* itemInstancePtr = DecodeItemInstancePtrFromSharedArg(itemInstanceSharedPtr);
+ void* playerPtr = DecodePlayerPtrFromSharedArg(playerSharedPtr);
+ if (!itemInstancePtr)
+ return 0;
+
+ int itemId = 0;
+ if (!TryReadItemId(itemInstancePtr, itemId))
+ return 0;
+
+ int isClientSide = 0;
+ if (s_activeUseLevel &&
+ IsReadableRange(static_cast(s_activeUseLevel) + kLevelIsClientSideOffset, sizeof(bool)))
+ {
+ isClientSide =
+ *reinterpret_cast(static_cast(s_activeUseLevel) + kLevelIsClientSideOffset) ? 1 : 0;
+ }
+
+ UseItemNativeArgs args{ itemId, bTestUseOnly ? 1 : 0, isClientSide, itemInstancePtr, playerPtr, playerSharedPtr };
+ return DotNetHost::CallItemUse(&args, sizeof(args));
+ }
+
+ void SetSummonSymbols(void* levelAddEntity,
+ void* entityIoNewById,
+ void* entityMoveTo,
+ void* entitySetPos)
+ {
+ s_levelAddEntity = reinterpret_cast(levelAddEntity);
+ s_entityIoNewById = reinterpret_cast(entityIoNewById);
+ s_entityMoveTo = reinterpret_cast(entityMoveTo);
+ s_entitySetPos = reinterpret_cast(entitySetPos);
+ }
+
+ void SetUseActionSymbols(void* inventoryRemoveResource,
+ void* inventoryVtable,
+ void* itemInstanceHurtAndBreak,
+ void* containerBroadcastChanges,
+ void* entityGetLookAngle,
+ void* livingEntityGetViewVector,
+ void* entityLerpMotion,
+ void* entitySetPos)
+ {
+ s_inventoryRemoveResource = reinterpret_cast(inventoryRemoveResource);
+ s_inventoryVtable = inventoryVtable;
+ s_itemInstanceHurtAndBreak = reinterpret_cast(itemInstanceHurtAndBreak);
+ s_entityGetLookAngle = reinterpret_cast(entityGetLookAngle);
+ s_livingEntityGetViewVector = reinterpret_cast(livingEntityGetViewVector);
+ s_entityLerpMotion = reinterpret_cast(entityLerpMotion);
+ s_entitySetPos = reinterpret_cast(entitySetPos);
+ }
+
+ static bool IsInventoryObjectPtr(void* objectPtr)
+ {
+ if (!objectPtr || !s_inventoryVtable || !IsReadableRange(objectPtr, sizeof(void*)))
+ return false;
+
+ void* vt = *reinterpret_cast(objectPtr);
+ if (!IsCanonicalUserPtr(vt))
+ return false;
+ return vt == s_inventoryVtable;
+ }
+
+ static void* FindInventoryPtrFromPlayer(void* playerPtr)
+ {
+ if (!playerPtr || !s_inventoryRemoveResource || !IsCanonicalUserPtr(playerPtr))
+ return nullptr;
+
+ const char* base = static_cast(playerPtr);
+ const void* slotPtr = base + kPlayerInventoryOffset;
+ if (!IsReadableRange(slotPtr, sizeof(void*)))
+ return nullptr;
+
+ void* inventoryPtr = *reinterpret_cast(slotPtr);
+ if (!IsInventoryObjectPtr(inventoryPtr))
+ return nullptr;
+
+ return inventoryPtr;
+ }
+
+ bool ConsumePlayerResource(void* playerPtr, int itemId, int count)
+ {
+ static int s_consumeFailLogCount = 0;
+ if (!playerPtr || !s_inventoryRemoveResource || !s_inventoryVtable || itemId < 0 || count <= 0)
+ return false;
+
+ void* inventoryPtr = FindInventoryPtrFromPlayer(playerPtr);
+ if (!inventoryPtr)
+ {
+ if (s_consumeFailLogCount < 20)
+ {
+ LogUtil::Log("[WeaveLoader] ConsumePlayerResource: no inventory ptr (player=%p item=%d count=%d)",
+ playerPtr, itemId, count);
+ s_consumeFailLogCount++;
+ }
+ return false;
+ }
+
+ for (int i = 0; i < count; ++i)
+ {
+ __try
+ {
+ if (!s_inventoryRemoveResource(inventoryPtr, itemId))
+ {
+ if (s_consumeFailLogCount < 20)
+ {
+ LogUtil::Log("[WeaveLoader] ConsumePlayerResource: removeResource false (inv=%p item=%d)",
+ inventoryPtr, itemId);
+ s_consumeFailLogCount++;
+ }
+ return false;
+ }
+ }
+ __except (EXCEPTION_EXECUTE_HANDLER)
+ {
+ if (s_consumeFailLogCount < 20)
+ {
+ LogUtil::Log("[WeaveLoader] ConsumePlayerResource: exception (inv=%p item=%d)", inventoryPtr, itemId);
+ s_consumeFailLogCount++;
+ }
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ bool DamageItemInstance(void* itemInstancePtr, int amount, void* ownerSharedPtr)
+ {
+ if (!itemInstancePtr || !ownerSharedPtr || amount <= 0 || !s_itemInstanceHurtAndBreak)
+ return false;
+
+ __try
+ {
+ s_itemInstanceHurtAndBreak(itemInstancePtr, amount, ownerSharedPtr);
+ return true;
+ }
+ __except (EXCEPTION_EXECUTE_HANDLER)
+ {
+ return false;
+ }
+ }
+
+ static bool TryReadLookVector(void* playerPtr, double& dx, double& dy, double& dz)
+ {
+ dx = 0.0;
+ dy = 0.0;
+ dz = 0.0;
+ if (!playerPtr)
+ return false;
+
+ void* vec = nullptr;
+ if (s_livingEntityGetViewVector)
+ vec = s_livingEntityGetViewVector(playerPtr, 1.0f);
+ else if (s_entityGetLookAngle)
+ vec = s_entityGetLookAngle(playerPtr);
+
+ if (!IsReadableRange(vec, sizeof(double) * 3))
+ return false;
+
+ dx = *reinterpret_cast(static_cast(vec) + 0x00);
+ dy = *reinterpret_cast(static_cast(vec) + 0x08);
+ dz = *reinterpret_cast(static_cast(vec) + 0x10);
+ return true;
+ }
+
+ static bool LooksLikeEntityPtr(void* candidate)
+ {
+ if (!candidate || !IsReadableRange(candidate, sizeof(void*)))
+ return false;
+ void* vt = *reinterpret_cast(candidate);
+ if (!IsCanonicalUserPtr(vt))
+ return false;
+ if (!IsGameCodePtr(vt))
+ return false;
+ return true;
+ }
+
+ static bool TryReadPlayerPos(void* playerPtr, double& x, double& y, double& z)
+ {
+ x = y = z = 0.0;
+ if (!playerPtr)
+ return false;
+
+ char* base = static_cast(playerPtr);
+ 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 x > -32000000.0 && x < 32000000.0 &&
+ y > -2048.0 && y < 4096.0 &&
+ z > -32000000.0 && z < 32000000.0;
+ }
+
+ static bool IsEntityMarkedRemoved(void* entityPtr)
+ {
+ if (!entityPtr || !IsReadableRange(static_cast(entityPtr) + kEntityRemovedOffset, sizeof(bool)))
+ return true;
+ return *reinterpret_cast(static_cast(entityPtr) + kEntityRemovedOffset);
+ }
+
+ static void MarkEntityRemoved(void* entityPtr)
+ {
+ if (!entityPtr || !IsReadableRange(static_cast(entityPtr) + kEntityRemovedOffset, sizeof(bool)))
+ return;
+ *reinterpret_cast(static_cast(entityPtr) + kEntityRemovedOffset) = true;
+ }
+
+ static void TrackSpawnedEntity(void* entityPtr)
+ {
+ if (!entityPtr)
+ return;
+ s_spawnedEntities.push_back(entityPtr);
+ }
+
+ static void CullSpawnedEntitiesBelowWorld()
+ {
+ if (s_spawnedEntities.empty())
+ return;
+
+ size_t write = 0;
+ for (size_t read = 0; read < s_spawnedEntities.size(); ++read)
+ {
+ void* entityPtr = s_spawnedEntities[read];
+ if (!entityPtr || !LooksLikeEntityPtr(entityPtr))
+ continue;
+
+ if (IsEntityMarkedRemoved(entityPtr))
+ continue;
+
+ double x = 0.0, y = 0.0, z = 0.0;
+ if (!TryReadPlayerPos(entityPtr, x, y, z))
+ continue;
+
+ if (y < -1.0)
+ {
+ MarkEntityRemoved(entityPtr);
+ if (s_outOfWorldGuardLogCount < 20)
+ {
+ LogUtil::Log("[WeaveLoader] OutOfWorldGuard: removed spawned entity=%p at y=%.3f", entityPtr, y);
+ s_outOfWorldGuardLogCount++;
+ }
+ continue;
+ }
+
+ s_spawnedEntities[write++] = entityPtr;
+ }
+
+ s_spawnedEntities.resize(write);
+ }
+
+ bool SummonEntityFromPlayerLook(void* playerPtr,
+ void* playerSharedPtr,
+ int entityNumericId,
+ double speed,
+ double spawnForward,
+ double spawnUp)
+ {
+ static int s_summonFailLogCount = 0;
+ void* levelPtr = s_activeUseLevel ? s_activeUseLevel : s_currentLevel;
+ if (!levelPtr || !s_levelAddEntity || !playerPtr)
+ {
+ if (s_summonFailLogCount < 20)
+ {
+ LogUtil::Log("[WeaveLoader] SummonFromLook fail: preconditions level=%p add=%p player=%p shared=%p id=%d",
+ levelPtr, s_levelAddEntity, playerPtr, playerSharedPtr, entityNumericId);
+ s_summonFailLogCount++;
+ }
+ return false;
+ }
+
+ double dx = 0.0, dy = 0.0, dz = 0.0;
+ if (!TryReadLookVector(playerPtr, dx, dy, dz))
+ {
+ if (s_summonFailLogCount < 20)
+ {
+ LogUtil::Log("[WeaveLoader] SummonFromLook fail: look vector unavailable (player=%p)", playerPtr);
+ s_summonFailLogCount++;
+ }
+ return false;
+ }
+
+ double px = 0.0, py = 0.0, pz = 0.0;
+ if (!TryReadPlayerPos(playerPtr, px, py, pz))
+ {
+ if (s_summonFailLogCount < 20)
+ {
+ LogUtil::Log("[WeaveLoader] SummonFromLook fail: player position unavailable (player=%p)", playerPtr);
+ s_summonFailLogCount++;
+ }
+ return false;
+ }
+
+ const double sx = px + (dx * spawnForward);
+ const double sy = py + spawnUp + (dy * spawnForward);
+ const double sz = pz + (dz * spawnForward);
+
+ std::shared_ptr entity;
+
+ if (!s_entityIoNewById)
+ {
+ if (s_summonFailLogCount < 20)
+ {
+ LogUtil::Log("[WeaveLoader] SummonFromLook fail: newById unavailable (id=%d)", entityNumericId);
+ s_summonFailLogCount++;
+ }
+ return false;
+ }
+
+ s_entityIoNewById(&entity, entityNumericId, levelPtr);
+ if (!entity)
+ {
+ if (s_summonFailLogCount < 20)
+ {
+ LogUtil::Log("[WeaveLoader] SummonFromLook fail: EntityIO::newById returned null (id=%d)", entityNumericId);
+ s_summonFailLogCount++;
+ }
+ return false;
+ }
+
+ if (s_entityMoveTo)
+ s_entityMoveTo(entity.get(), sx, sy, sz, 0.0f, 0.0f);
+ else if (s_entitySetPos)
+ s_entitySetPos(entity.get(), sx, sy, sz);
+
+ if (s_entityLerpMotion)
+ s_entityLerpMotion(entity.get(), dx * speed, dy * speed, dz * speed);
+
+ if (IsFireballFamilyEntityId(entityNumericId) &&
+ IsReadableRange(entity.get(), kFireballZPowerOffset + sizeof(double)))
+ {
+ // Source + PDB: Fireball packets initialize xPower/yPower/zPower separately from xd/yd/zd.
+ // EntityIO::newById gives us a game-owned shared_ptr; patch the fireball fields in place.
+ *reinterpret_cast(static_cast(entity.get()) + kFireballOwnerOffset) = nullptr;
+ *reinterpret_cast(static_cast(entity.get()) + kFireballXPowerOffset) = dx * 0.10;
+ *reinterpret_cast(static_cast(entity.get()) + kFireballYPowerOffset) = dy * 0.10;
+ *reinterpret_cast(static_cast(entity.get()) + kFireballZPowerOffset) = dz * 0.10;
+ }
+
+ bool added = s_levelAddEntity(levelPtr, &entity);
+ if (!added && s_summonFailLogCount < 20)
+ {
+ LogUtil::Log("[WeaveLoader] SummonFromLook fail: Level::addEntity returned false (id=%d)", entityNumericId);
+ s_summonFailLogCount++;
+ }
+ if (added)
+ {
+ TrackSpawnedEntity(entity.get());
+ DotNetHost::CallEntitySummoned(entityNumericId, static_cast(sx), static_cast(sy), static_cast(sz));
+ }
+ return added;
+ }
+
+ bool SummonEntityByNumericId(int entityNumericId, double x, double y, double z)
+ {
+ void* levelPtr = s_activeUseLevel ? s_activeUseLevel : s_currentLevel;
+ if (!levelPtr || !s_levelAddEntity || !s_entityIoNewById)
+ return false;
+
+ std::shared_ptr entity;
+ s_entityIoNewById(&entity, entityNumericId, levelPtr);
+ if (!entity)
+ return false;
+
+ if (s_entityMoveTo)
+ s_entityMoveTo(entity.get(), x, y, z, 0.0f, 0.0f);
+
+ bool added = s_levelAddEntity(levelPtr, &entity);
+ if (added)
+ {
+ TrackSpawnedEntity(entity.get());
+ DotNetHost::CallEntitySummoned(entityNumericId, static_cast(x), static_cast(y), static_cast(z));
+ }
+ return added;
+ }
+
void SetAtlasLocationPointers(void* blocksLocation, void* itemsLocation)
{
s_textureAtlasLocationBlocks = blocksLocation;
@@ -250,16 +746,34 @@ namespace GameHooks
LogUtil::Log("[WeaveLoader] Atlas IDs: blocks=%d items=%d", s_textureAtlasIdBlocks, s_textureAtlasIdItems);
}
+ static bool TryReadVec3(void* vecPtr, double& x, double& y, double& z)
+ {
+ if (!IsReadableRange(vecPtr, sizeof(double) * 3))
+ return false;
+ x = *reinterpret_cast(static_cast(vecPtr) + 0x00);
+ y = *reinterpret_cast(static_cast(vecPtr) + 0x08);
+ z = *reinterpret_cast(static_cast(vecPtr) + 0x10);
+ return true;
+ }
+
+ static bool IsFireballFamilyEntityId(int entityNumericId)
+ {
+ return entityNumericId == 12
+ || entityNumericId == 13
+ || entityNumericId == 19
+ || entityNumericId == 1000;
+ }
+
static void* DecodeItemInstancePtrFromSharedArg(void* sharedArg)
{
- if (!sharedArg)
+ if (!sharedArg || !IsCanonicalUserPtr(sharedArg))
return nullptr;
// Candidate A: shared_ptr object where first field is raw ItemInstance*.
__try
{
void* p = *reinterpret_cast(sharedArg);
- if (p)
+ if (p && IsCanonicalUserPtr(p))
{
int id = 0;
if (TryReadItemId(p, id)) return p;
@@ -278,6 +792,37 @@ namespace GameHooks
return nullptr;
}
+ static void* DecodePlayerPtrFromSharedArg(void* sharedArg)
+ {
+ static int s_decodePlayerLogCount = 0;
+ if (!sharedArg || !IsCanonicalUserPtr(sharedArg))
+ return nullptr;
+
+ // shared_ptr is passed by reference; first field is raw Player*.
+ __try
+ {
+ void* p = *reinterpret_cast(sharedArg);
+ if (LooksLikeEntityPtr(p))
+ {
+ if (s_decodePlayerLogCount < 8)
+ {
+ LogUtil::Log("[WeaveLoader] DecodePlayer: shared=%p -> player=%p", sharedArg, p);
+ s_decodePlayerLogCount++;
+ }
+ return p;
+ }
+ }
+ __except (EXCEPTION_EXECUTE_HANDLER) {}
+
+ if (s_decodePlayerLogCount < 8)
+ {
+ LogUtil::Log("[WeaveLoader] DecodePlayer: failed shared=%p", sharedArg);
+ s_decodePlayerLogCount++;
+ }
+
+ return nullptr;
+ }
+
void __fastcall Hooked_LoadUVs(void* thisPtr)
{
LogUtil::Log("[WeaveLoader] Hooked_LoadUVs: ENTER (textureMap=%p)", thisPtr);
@@ -510,6 +1055,79 @@ namespace GameHooks
return false;
}
+ bool __fastcall Hooked_ServerPlayerGameModeUseItem(void* thisPtr, void* playerSharedPtr, void* level, void* itemInstanceSharedPtr, bool bTestUseOnly)
+ {
+ void* previousLevel = s_activeUseLevel;
+ s_activeUseLevel = level;
+ static int s_serverUseLogCount = 0;
+ void* itemInstancePtr = DecodeItemInstancePtrFromSharedArg(itemInstanceSharedPtr);
+ int itemId = -1;
+ TryReadItemId(itemInstancePtr, itemId);
+ bool effectiveTestUseOnly = bTestUseOnly;
+ const ULONGLONG nowMs = GetTickCount64();
+ if (effectiveTestUseOnly &&
+ itemId >= 0 &&
+ s_pendingServerUseItemId == itemId &&
+ nowMs <= s_pendingServerUseExpiryMs)
+ {
+ effectiveTestUseOnly = false;
+ s_pendingServerUseItemId = -1;
+ s_pendingServerUseExpiryMs = 0;
+ }
+ if (s_serverUseLogCount < 40)
+ {
+ LogUtil::Log("[WeaveLoader] UseHook ServerPlayerGameMode::useItem test=%d effective=%d item=%d itemPtr=%p playerShared=%p level=%p",
+ bTestUseOnly ? 1 : 0, effectiveTestUseOnly ? 1 : 0, itemId, itemInstancePtr, playerSharedPtr, level);
+ s_serverUseLogCount++;
+ }
+ int action = TryDispatchUseItemFromSharedItemArg(itemInstanceSharedPtr, playerSharedPtr, effectiveTestUseOnly, "ServerPlayerGameMode::useItem");
+ s_activeUseLevel = previousLevel;
+ if (action == 2)
+ return true;
+
+ if (Original_ServerPlayerGameModeUseItem)
+ return Original_ServerPlayerGameModeUseItem(thisPtr, playerSharedPtr, level, itemInstanceSharedPtr, bTestUseOnly);
+ return false;
+ }
+
+ bool __fastcall Hooked_MultiPlayerGameModeUseItem(void* thisPtr, void* playerSharedPtr, void* level, void* itemInstanceSharedPtr, bool bTestUseOnly)
+ {
+ static int s_multiUseLogCount = 0;
+ void* itemInstancePtr = DecodeItemInstancePtrFromSharedArg(itemInstanceSharedPtr);
+ int itemId = -1;
+ TryReadItemId(itemInstancePtr, itemId);
+ if (!bTestUseOnly && itemId >= 0)
+ {
+ s_pendingServerUseItemId = itemId;
+ s_pendingServerUseExpiryMs = GetTickCount64() + 1000;
+ }
+ if (s_multiUseLogCount < 40)
+ {
+ LogUtil::Log("[WeaveLoader] UseHook MultiPlayerGameMode::useItem test=%d item=%d itemPtr=%p playerShared=%p level=%p",
+ bTestUseOnly ? 1 : 0, itemId, itemInstancePtr, playerSharedPtr, level);
+ s_multiUseLogCount++;
+ }
+ void* previousLevel = s_activeUseLevel;
+ s_activeUseLevel = level;
+ int action = TryDispatchUseItemFromSharedItemArg(itemInstanceSharedPtr, playerSharedPtr, bTestUseOnly, "MultiPlayerGameMode::useItem");
+ s_activeUseLevel = previousLevel;
+ if (action == 2)
+ return true;
+ if (Original_MultiPlayerGameModeUseItem)
+ return Original_MultiPlayerGameModeUseItem(thisPtr, playerSharedPtr, level, itemInstanceSharedPtr, bTestUseOnly);
+ return false;
+ }
+
+ void __fastcall Hooked_MinecraftSetLevel(void* thisPtr, void* level, int message, void* forceInsertPlayerSharedPtr, bool doForceStatsSave, bool bPrimaryPlayerSignedOut)
+ {
+ if (Original_MinecraftSetLevel)
+ {
+ Original_MinecraftSetLevel(thisPtr, level, message, forceInsertPlayerSharedPtr, doForceStatsSave, bPrimaryPlayerSignedOut);
+ }
+
+ s_currentLevel = level;
+ }
+
void* Hooked_GetResourceAsStream(const void* fileName)
{
const std::wstring* path = static_cast(fileName);
@@ -587,7 +1205,9 @@ namespace GameHooks
void __fastcall Hooked_MinecraftTick(void* thisPtr, bool bFirst, bool bUpdateTextures)
{
+ CullSpawnedEntitiesBelowWorld();
Original_MinecraftTick(thisPtr, bFirst, bUpdateTextures);
+ CullSpawnedEntitiesBelowWorld();
if (bFirst)
{
diff --git a/WeaveLoaderRuntime/src/GameHooks.h b/WeaveLoaderRuntime/src/GameHooks.h
index af8423f..e74d626 100644
--- a/WeaveLoaderRuntime/src/GameHooks.h
+++ b/WeaveLoaderRuntime/src/GameHooks.h
@@ -21,6 +21,18 @@ typedef void* (__fastcall *RegisterIcon_fn)(void* thisPtr, const std::wstring& n
typedef void* (__fastcall *ItemInstanceGetIcon_fn)(void* thisPtr);
typedef void (__fastcall *ItemInstanceMineBlock_fn)(void* thisPtr, void* level, int tile, int x, int y, int z, void* ownerSharedPtr);
typedef bool (__fastcall *ItemMineBlock_fn)(void* thisPtr, void* itemInstanceSharedPtr, void* level, int tile, int x, int y, int z, void* ownerSharedPtr);
+typedef bool (__fastcall *GameModeUseItem_fn)(void* thisPtr, void* playerSharedPtr, void* level, void* itemInstanceSharedPtr, bool bTestUseOnly);
+typedef void (__fastcall *MinecraftSetLevel_fn)(void* thisPtr, void* level, int message, void* forceInsertPlayerSharedPtr, bool doForceStatsSave, bool bPrimaryPlayerSignedOut);
+typedef bool (__fastcall *LevelAddEntity_fn)(void* thisPtr, void* entitySharedPtr);
+typedef void (__fastcall *EntityMoveTo_fn)(void* thisPtr, double x, double y, double z, float yRot, float xRot);
+typedef void (__fastcall *EntityIONewById_fn)(void* outSharedPtr, int entityNumericId, void* level);
+typedef void (__fastcall *EntitySetPos_fn)(void* thisPtr, double x, double y, double z);
+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 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);
typedef void (__fastcall *TexturesBindTextureResource_fn)(void* thisPtr, void* resourcePtr);
typedef int (__fastcall *TexturesLoadTextureByName_fn)(void* thisPtr, int texId, const std::wstring& resourceName);
typedef int (__fastcall *TexturesLoadTextureByIndex_fn)(void* thisPtr, int idx);
@@ -44,6 +56,9 @@ namespace GameHooks
extern ItemInstanceMineBlock_fn Original_ItemInstanceMineBlock;
extern ItemMineBlock_fn Original_ItemMineBlock;
extern ItemMineBlock_fn Original_DiggerItemMineBlock;
+ extern GameModeUseItem_fn Original_ServerPlayerGameModeUseItem;
+ extern GameModeUseItem_fn Original_MultiPlayerGameModeUseItem;
+ extern MinecraftSetLevel_fn Original_MinecraftSetLevel;
extern TexturesBindTextureResource_fn Original_TexturesBindTextureResource;
extern TexturesLoadTextureByName_fn Original_TexturesLoadTextureByName;
extern TexturesLoadTextureByIndex_fn Original_TexturesLoadTextureByIndex;
@@ -68,6 +83,9 @@ namespace GameHooks
void __fastcall Hooked_ItemInstanceMineBlock(void* thisPtr, void* level, int tile, int x, int y, int z, void* ownerSharedPtr);
bool __fastcall Hooked_ItemMineBlock(void* thisPtr, void* itemInstanceSharedPtr, void* level, int tile, int x, int y, int z, void* ownerSharedPtr);
bool __fastcall Hooked_DiggerItemMineBlock(void* thisPtr, void* itemInstanceSharedPtr, void* level, int tile, int x, int y, int z, void* ownerSharedPtr);
+ bool __fastcall Hooked_ServerPlayerGameModeUseItem(void* thisPtr, void* playerSharedPtr, void* level, void* itemInstanceSharedPtr, bool bTestUseOnly);
+ bool __fastcall Hooked_MultiPlayerGameModeUseItem(void* thisPtr, void* playerSharedPtr, void* level, void* itemInstanceSharedPtr, bool bTestUseOnly);
+ void __fastcall Hooked_MinecraftSetLevel(void* thisPtr, void* level, int message, void* forceInsertPlayerSharedPtr, bool doForceStatsSave, bool bPrimaryPlayerSignedOut);
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);
@@ -76,4 +94,25 @@ namespace GameHooks
float __fastcall Hooked_StitchedGetV0(void* thisPtr, bool adjust);
float __fastcall Hooked_StitchedGetV1(void* thisPtr, bool adjust);
void SetAtlasLocationPointers(void* blocksLocation, void* itemsLocation);
+ void SetSummonSymbols(void* levelAddEntity,
+ void* entityIoNewById,
+ void* entityMoveTo,
+ void* entitySetPos);
+ void SetUseActionSymbols(void* inventoryRemoveResource,
+ void* inventoryVtable,
+ void* itemInstanceHurtAndBreak,
+ void* containerBroadcastChanges,
+ void* entityGetLookAngle,
+ void* livingEntityGetViewVector,
+ void* entityLerpMotion,
+ void* entitySetPos);
+ bool SummonEntityByNumericId(int entityNumericId, double x, double y, double z);
+ bool ConsumePlayerResource(void* playerPtr, int itemId, int count);
+ bool DamageItemInstance(void* itemInstancePtr, int amount, void* ownerSharedPtr);
+ bool SummonEntityFromPlayerLook(void* playerPtr,
+ void* playerSharedPtr,
+ int entityNumericId,
+ double speed,
+ double spawnForward,
+ double spawnUp);
}
diff --git a/WeaveLoaderRuntime/src/HookManager.cpp b/WeaveLoaderRuntime/src/HookManager.cpp
index 6c98264..76a933c 100644
--- a/WeaveLoaderRuntime/src/HookManager.cpp
+++ b/WeaveLoaderRuntime/src/HookManager.cpp
@@ -54,6 +54,20 @@ bool HookManager::Install(const SymbolResolver& symbols)
LogUtil::Log("[WeaveLoader] Hooked Minecraft::init");
}
+ if (symbols.pMinecraftSetLevel)
+ {
+ if (MH_CreateHook(symbols.pMinecraftSetLevel,
+ reinterpret_cast(&GameHooks::Hooked_MinecraftSetLevel),
+ reinterpret_cast(&GameHooks::Original_MinecraftSetLevel)) != MH_OK)
+ {
+ LogUtil::Log("[WeaveLoader] Warning: Failed to hook Minecraft::setLevel");
+ }
+ else
+ {
+ LogUtil::Log("[WeaveLoader] Hooked Minecraft::setLevel (active level tracking)");
+ }
+ }
+
if (symbols.pItemInstanceMineBlock)
{
if (MH_CreateHook(symbols.pItemInstanceMineBlock,
@@ -110,6 +124,34 @@ bool HookManager::Install(const SymbolResolver& symbols)
}
}
+ if (symbols.pServerPlayerGameModeUseItem)
+ {
+ if (MH_CreateHook(symbols.pServerPlayerGameModeUseItem,
+ reinterpret_cast(&GameHooks::Hooked_ServerPlayerGameModeUseItem),
+ reinterpret_cast(&GameHooks::Original_ServerPlayerGameModeUseItem)) != MH_OK)
+ {
+ LogUtil::Log("[WeaveLoader] Warning: Failed to hook ServerPlayerGameMode::useItem");
+ }
+ else
+ {
+ LogUtil::Log("[WeaveLoader] Hooked ServerPlayerGameMode::useItem (managed item callbacks)");
+ }
+ }
+
+ if (symbols.pMultiPlayerGameModeUseItem)
+ {
+ if (MH_CreateHook(symbols.pMultiPlayerGameModeUseItem,
+ reinterpret_cast(&GameHooks::Hooked_MultiPlayerGameModeUseItem),
+ reinterpret_cast(&GameHooks::Original_MultiPlayerGameModeUseItem)) != MH_OK)
+ {
+ LogUtil::Log("[WeaveLoader] Warning: Failed to hook MultiPlayerGameMode::useItem");
+ }
+ else
+ {
+ LogUtil::Log("[WeaveLoader] Hooked MultiPlayerGameMode::useItem (managed item callbacks)");
+ }
+ }
+
GameHooks::SetAtlasLocationPointers(symbols.pTextureAtlasLocationBlocks, symbols.pTextureAtlasLocationItems);
if (symbols.pTexturesBindTextureResource)
@@ -207,6 +249,20 @@ bool HookManager::Install(const SymbolResolver& symbols)
GameObjectFactory::ResolveSymbols(const_cast(symbols));
FurnaceRecipeRegistry::ResolveSymbols(const_cast(symbols));
+ GameHooks::SetSummonSymbols(
+ symbols.pLevelAddEntity,
+ symbols.pEntityIONewById,
+ symbols.pEntityMoveTo,
+ symbols.pEntitySetPos);
+ GameHooks::SetUseActionSymbols(
+ symbols.pInventoryRemoveResource,
+ symbols.pInventoryVtable,
+ symbols.pItemInstanceHurtAndBreak,
+ symbols.pAbstractContainerMenuBroadcastChanges,
+ symbols.pEntityGetLookAngle,
+ symbols.pLivingEntityGetViewVector,
+ symbols.pEntityLerpMotion,
+ symbols.pEntitySetPos);
if (symbols.pLoadUVs && symbols.pSimpleIconCtor && symbols.pOperatorNew)
{
diff --git a/WeaveLoaderRuntime/src/NativeExports.cpp b/WeaveLoaderRuntime/src/NativeExports.cpp
index 6790bbb..384af5f 100644
--- a/WeaveLoaderRuntime/src/NativeExports.cpp
+++ b/WeaveLoaderRuntime/src/NativeExports.cpp
@@ -3,6 +3,7 @@
#include "CreativeInventory.h"
#include "GameObjectFactory.h"
#include "FurnaceRecipeRegistry.h"
+#include "GameHooks.h"
#include "ModStrings.h"
#include "LogUtil.h"
#include
@@ -249,6 +250,57 @@ int native_get_entity_id(const char* namespacedId)
return IdRegistry::Instance().GetNumericId(IdRegistry::Type::Entity, namespacedId);
}
+int native_consume_item_from_player(void* playerPtr, int numericItemId, int count)
+{
+ if (numericItemId < 0 || count <= 0)
+ return 0;
+ return GameHooks::ConsumePlayerResource(playerPtr, numericItemId, count) ? 1 : 0;
+}
+
+int native_damage_item_instance(void* itemInstancePtr, int amount, void* ownerSharedPtr)
+{
+ if (amount <= 0)
+ return 0;
+ return GameHooks::DamageItemInstance(itemInstancePtr, amount, ownerSharedPtr) ? 1 : 0;
+}
+
+int native_spawn_entity_from_player_look(void* playerPtr, void* playerSharedPtr, int numericEntityId, double speed, double spawnForward, double spawnUp)
+{
+ if (numericEntityId < 0)
+ return 0;
+ return GameHooks::SummonEntityFromPlayerLook(playerPtr, playerSharedPtr, numericEntityId, speed, spawnForward, spawnUp) ? 1 : 0;
+}
+
+int native_summon_entity_by_id(int numericEntityId, double x, double y, double z)
+{
+ if (!GameHooks::SummonEntityByNumericId(numericEntityId, x, y, z))
+ {
+ LogUtil::Log("[WeaveLoader] Summon failed: entity=%d at (%.2f, %.2f, %.2f)",
+ numericEntityId, x, y, z);
+ return 0;
+ }
+
+ LogUtil::Log("[WeaveLoader] Summoned entity=%d at (%.2f, %.2f, %.2f)",
+ numericEntityId, x, y, z);
+ return 1;
+}
+
+int native_summon_entity(const char* namespacedId, double x, double y, double z)
+{
+ if (!namespacedId || !namespacedId[0])
+ return 0;
+
+ const int entityNumericId =
+ IdRegistry::Instance().GetNumericId(IdRegistry::Type::Entity, namespacedId);
+ if (entityNumericId < 0)
+ {
+ LogUtil::Log("[WeaveLoader] Summon failed: unknown entity id '%s'", namespacedId);
+ return 0;
+ }
+
+ return native_summon_entity_by_id(entityNumericId, x, y, z);
+}
+
void native_subscribe_event(const char* eventName, void* managedFnPtr)
{
LogUtil::Log("[WeaveLoader] Event subscription: %s", eventName ? eventName : "(null)");
diff --git a/WeaveLoaderRuntime/src/NativeExports.h b/WeaveLoaderRuntime/src/NativeExports.h
index 283959b..ce7615d 100644
--- a/WeaveLoaderRuntime/src/NativeExports.h
+++ b/WeaveLoaderRuntime/src/NativeExports.h
@@ -57,6 +57,11 @@ extern "C"
__declspec(dllexport) int native_get_block_id(const char* namespacedId);
__declspec(dllexport) int native_get_item_id(const char* namespacedId);
__declspec(dllexport) int native_get_entity_id(const char* namespacedId);
+ __declspec(dllexport) int native_consume_item_from_player(void* playerPtr, int numericItemId, int count);
+ __declspec(dllexport) int native_damage_item_instance(void* itemInstancePtr, int amount, void* ownerSharedPtr);
+ __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) void native_subscribe_event(
const char* eventName,
diff --git a/WeaveLoaderRuntime/src/SymbolResolver.cpp b/WeaveLoaderRuntime/src/SymbolResolver.cpp
index 7f26b4a..c055f6d 100644
--- a/WeaveLoaderRuntime/src/SymbolResolver.cpp
+++ b/WeaveLoaderRuntime/src/SymbolResolver.cpp
@@ -22,6 +22,8 @@ static const char* SYM_ITEMINSTANCE_GETICON = "?getIcon@ItemInstance@@QEAAPEAVIc
static const char* SYM_ITEMINSTANCE_MINEBLOCK = "?mineBlock@ItemInstance@@QEAAXPEAVLevel@@HHHHV?$shared_ptr@VPlayer@@@std@@@Z";
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_SERVER_PLAYER_GAMEMODE_USEITEM = "?useItem@ServerPlayerGameMode@@QEAA_NV?$shared_ptr@VPlayer@@@std@@PEAVLevel@@V?$shared_ptr@VItemInstance@@@3@_N@Z";
+static const char* SYM_MULTI_PLAYER_GAMEMODE_USEITEM = "?useItem@MultiPlayerGameMode@@UEAA_NV?$shared_ptr@VPlayer@@@std@@PEAVLevel@@V?$shared_ptr@VItemInstance@@@3@_N@Z";
static const char* SYM_TEXTURES_BIND_RESOURCE = "?bindTexture@Textures@@QEAAXPEAVResourceLocation@@@Z";
static const char* SYM_TEXTURES_LOAD_BY_NAME = "?loadTexture@Textures@@AEAAHW4_TEXTURE_NAME@@AEBV?$basic_string@_WU?$char_traits@_W@std@@V?$allocator@_W@2@@std@@@Z";
static const char* SYM_TEXTURES_LOAD_BY_INDEX_PUBLIC = "?loadTexture@Textures@@QEAAHH@Z";
@@ -30,6 +32,19 @@ static const char* SYM_STITCHED_GETU0 = "?getU0@StitchedTexture@@UEBAM_N@Z";
static const char* SYM_STITCHED_GETU1 = "?getU1@StitchedTexture@@UEBAM_N@Z";
static const char* SYM_STITCHED_GETV0 = "?getV0@StitchedTexture@@UEBAM_N@Z";
static const char* SYM_STITCHED_GETV1 = "?getV1@StitchedTexture@@UEBAM_N@Z";
+static const char* SYM_MINECRAFT_SETLEVEL = "?setLevel@Minecraft@@QEAAXPEAVMultiPlayerLevel@@HV?$shared_ptr@VPlayer@@@std@@_N2@Z";
+static const char* SYM_LEVEL_ADDENTITY = "?addEntity@Level@@UEAA_NV?$shared_ptr@VEntity@@@std@@@Z";
+static const char* SYM_ENTITYIO_NEWBYID = "?newById@EntityIO@@SA?AV?$shared_ptr@VEntity@@@std@@HPEAVLevel@@@Z";
+static const char* SYM_ENTITY_MOVETO = "?moveTo@Entity@@QEAAXNNNMM@Z";
+static const char* SYM_ENTITY_SETPOS = "?setPos@Entity@@QEAAXNNN@Z";
+static const char* SYM_LIVINGENTITY_GETLOOKANGLE = "?getLookAngle@LivingEntity@@UEAAPEAVVec3@@XZ";
+static const char* SYM_LIVINGENTITY_GETVIEWVECTOR = "?getViewVector@LivingEntity@@UEAAPEAVVec3@@M@Z";
+static const char* SYM_ENTITY_GETLOOKANGLE = "?getLookAngle@Entity@@UEAAPEAVVec3@@XZ";
+static const char* SYM_ENTITY_LERPMOTION = "?lerpMotion@Entity@@UEAAXNNN@Z";
+static const char* SYM_INVENTORY_REMOVERESOURCE = "?removeResource@Inventory@@QEAA_NH@Z";
+static const char* SYM_INVENTORY_VFTABLE = "??_7Inventory@@6B@";
+static const char* SYM_ITEMINSTANCE_HURTANDBREAK = "?hurtAndBreak@ItemInstance@@QEAAXHV?$shared_ptr@VLivingEntity@@@std@@@Z";
+static const char* SYM_ABSTRACTCONTAINERMENU_BROADCASTCHANGES = "?broadcastChanges@AbstractContainerMenu@@UEAAXXZ";
static const char* SYM_TEXATLAS_BLOCKS = "?LOCATION_BLOCKS@TextureAtlas@@2VResourceLocation@@A";
static const char* SYM_TEXATLAS_ITEMS = "?LOCATION_ITEMS@TextureAtlas@@2VResourceLocation@@A";
@@ -106,6 +121,8 @@ bool SymbolResolver::ResolveGameFunctions()
pItemInstanceMineBlock = Resolve(SYM_ITEMINSTANCE_MINEBLOCK);
pItemMineBlock = Resolve(SYM_ITEM_MINEBLOCK);
pDiggerItemMineBlock = Resolve(SYM_DIGGERITEM_MINEBLOCK);
+ pServerPlayerGameModeUseItem = Resolve(SYM_SERVER_PLAYER_GAMEMODE_USEITEM);
+ pMultiPlayerGameModeUseItem = Resolve(SYM_MULTI_PLAYER_GAMEMODE_USEITEM);
pTexturesBindTextureResource = Resolve(SYM_TEXTURES_BIND_RESOURCE);
pTexturesLoadTextureByName = Resolve(SYM_TEXTURES_LOAD_BY_NAME);
pTexturesLoadTextureByIndex = Resolve(SYM_TEXTURES_LOAD_BY_INDEX_PUBLIC);
@@ -115,6 +132,20 @@ bool SymbolResolver::ResolveGameFunctions()
pStitchedGetU1 = Resolve(SYM_STITCHED_GETU1);
pStitchedGetV0 = Resolve(SYM_STITCHED_GETV0);
pStitchedGetV1 = Resolve(SYM_STITCHED_GETV1);
+ pMinecraftSetLevel = Resolve(SYM_MINECRAFT_SETLEVEL);
+ pLevelAddEntity = Resolve(SYM_LEVEL_ADDENTITY);
+ pEntityIONewById = Resolve(SYM_ENTITYIO_NEWBYID);
+ pEntityMoveTo = Resolve(SYM_ENTITY_MOVETO);
+ pEntitySetPos = Resolve(SYM_ENTITY_SETPOS);
+ pEntityGetLookAngle = Resolve(SYM_LIVINGENTITY_GETLOOKANGLE);
+ pLivingEntityGetViewVector = Resolve(SYM_LIVINGENTITY_GETVIEWVECTOR);
+ if (!pEntityGetLookAngle)
+ pEntityGetLookAngle = Resolve(SYM_ENTITY_GETLOOKANGLE);
+ pEntityLerpMotion = Resolve(SYM_ENTITY_LERPMOTION);
+ pInventoryRemoveResource = Resolve(SYM_INVENTORY_REMOVERESOURCE);
+ pInventoryVtable = Resolve(SYM_INVENTORY_VFTABLE);
+ pItemInstanceHurtAndBreak = Resolve(SYM_ITEMINSTANCE_HURTANDBREAK);
+ pAbstractContainerMenuBroadcastChanges = Resolve(SYM_ABSTRACTCONTAINERMENU_BROADCASTCHANGES);
pTextureAtlasLocationBlocks = Resolve(SYM_TEXATLAS_BLOCKS);
pTextureAtlasLocationItems = Resolve(SYM_TEXATLAS_ITEMS);
if (!pOperatorNew) pOperatorNew = GetProcAddress(GetModuleHandleA("vcruntime140.dll"), SYM_OPERATOR_NEW);
@@ -147,6 +178,8 @@ bool SymbolResolver::ResolveGameFunctions()
logSym("ItemInstance::mineBlock", pItemInstanceMineBlock);
logSym("Item::mineBlock", pItemMineBlock);
logSym("DiggerItem::mineBlock", pDiggerItemMineBlock);
+ logSym("ServerPlayerGameMode::useItem", pServerPlayerGameModeUseItem);
+ logSym("MultiPlayerGameMode::useItem", pMultiPlayerGameModeUseItem);
logSym("Textures::bindTexture(ResourceLocation)", pTexturesBindTextureResource);
logSym("Textures::loadTexture(TEXTURE_NAME,wstring)", pTexturesLoadTextureByName);
logSym("Textures::loadTexture(int)", pTexturesLoadTextureByIndex);
@@ -154,6 +187,18 @@ bool SymbolResolver::ResolveGameFunctions()
logSym("StitchedTexture::getU1", pStitchedGetU1);
logSym("StitchedTexture::getV0", pStitchedGetV0);
logSym("StitchedTexture::getV1", pStitchedGetV1);
+ logSym("Minecraft::setLevel", pMinecraftSetLevel);
+ logSym("Level::addEntity", pLevelAddEntity);
+ logSym("EntityIO::newById", pEntityIONewById);
+ logSym("Entity::moveTo", pEntityMoveTo);
+ logSym("Entity::setPos", pEntitySetPos);
+ logSym("LivingEntity/Entity::getLookAngle", pEntityGetLookAngle);
+ logSym("LivingEntity::getViewVector", pLivingEntityGetViewVector);
+ logSym("Entity::lerpMotion", pEntityLerpMotion);
+ logSym("Inventory::removeResource", pInventoryRemoveResource);
+ logSym("Inventory::vftable", pInventoryVtable);
+ logSym("ItemInstance::hurtAndBreak", pItemInstanceHurtAndBreak);
+ logSym("AbstractContainerMenu::broadcastChanges", pAbstractContainerMenuBroadcastChanges);
logSym("TextureAtlas::LOCATION_BLOCKS", pTextureAtlasLocationBlocks);
logSym("TextureAtlas::LOCATION_ITEMS", pTextureAtlasLocationItems);
diff --git a/WeaveLoaderRuntime/src/SymbolResolver.h b/WeaveLoaderRuntime/src/SymbolResolver.h
index e95c304..7ed40c5 100644
--- a/WeaveLoaderRuntime/src/SymbolResolver.h
+++ b/WeaveLoaderRuntime/src/SymbolResolver.h
@@ -27,6 +27,8 @@ public:
void* pItemInstanceMineBlock = nullptr; // ItemInstance::mineBlock(Level*,int,int,int,int,shared_ptr)
void* pItemMineBlock = nullptr; // Item::mineBlock(shared_ptr,Level*,int,int,int,int,shared_ptr)
void* pDiggerItemMineBlock = nullptr; // DiggerItem::mineBlock(shared_ptr,Level*,int,int,int,int,shared_ptr)
+ void* pServerPlayerGameModeUseItem = nullptr; // ServerPlayerGameMode::useItem(shared_ptr,Level*,shared_ptr,bool)
+ void* pMultiPlayerGameModeUseItem = nullptr; // MultiPlayerGameMode::useItem(shared_ptr,Level*,shared_ptr,bool)
void* pTexturesBindTextureResource = nullptr; // Textures::bindTexture(ResourceLocation*)
void* pTexturesLoadTextureByName = nullptr; // Textures::loadTexture(TEXTURE_NAME,const wstring&)
void* pTexturesLoadTextureByIndex = nullptr; // Textures::loadTexture(int)
@@ -34,6 +36,18 @@ public:
void* pStitchedGetU1 = nullptr; // StitchedTexture::getU1(bool) const
void* pStitchedGetV0 = nullptr; // StitchedTexture::getV0(bool) const
void* pStitchedGetV1 = nullptr; // StitchedTexture::getV1(bool) const
+ void* pMinecraftSetLevel = nullptr; // Minecraft::setLevel(MultiPlayerLevel*,int,shared_ptr,bool,bool)
+ void* pLevelAddEntity = nullptr; // Level::addEntity(shared_ptr)
+ void* pEntityIONewById = nullptr; // EntityIO::newById(int,Level*)
+ void* pEntityMoveTo = nullptr; // Entity::moveTo(double,double,double,float,float)
+ void* pEntitySetPos = nullptr; // Entity::setPos(double,double,double)
+ void* pEntityGetLookAngle = nullptr; // Entity::getLookAngle()
+ void* pLivingEntityGetViewVector = nullptr; // LivingEntity::getViewVector(float)
+ void* pEntityLerpMotion = nullptr; // Entity::lerpMotion(double,double,double)
+ void* pInventoryRemoveResource = nullptr; // Inventory::removeResource(int)
+ void* pInventoryVtable = nullptr; // Inventory vftable
+ void* pItemInstanceHurtAndBreak = nullptr; // ItemInstance::hurtAndBreak(int,shared_ptr)
+ void* pAbstractContainerMenuBroadcastChanges = nullptr; // AbstractContainerMenu::broadcastChanges()
void* pTextureAtlasLocationBlocks = nullptr; // TextureAtlas::LOCATION_BLOCKS
void* pTextureAtlasLocationItems = nullptr; // TextureAtlas::LOCATION_ITEMS