From 0b4c87acbcbbf3636eeb48d90121828b7d4e78bc Mon Sep 17 00:00:00 2001 From: Jacobwasbeast Date: Sat, 7 Mar 2026 19:54:52 -0600 Subject: [PATCH] feat(modloader): add item use and summon APIs Add managed item use hooks, server-authoritative summon support, and side-aware use item context. Include IdHelper mappings, example ruby wand usage, and related runtime/crash-handler updates. --- ExampleMod/ExampleMod.cs | 55 ++ ExampleMod/assets/items/ruby_wand.png | Bin 0 -> 4017 bytes WeaveLoader.API/Block/BlockRegistry.cs | 17 + WeaveLoader.API/Entity/EntityRegistry.cs | 32 ++ WeaveLoader.API/Events/GameEvents.cs | 13 + WeaveLoader.API/IdHelper.cs | 497 +++++++++++++++++ WeaveLoader.API/Item/CustomItem.cs | 121 ++++ WeaveLoader.API/Item/ItemRegistry.cs | 17 + WeaveLoader.API/NativeInterop.cs | 21 + WeaveLoader.API/Registry.cs | 6 + WeaveLoader.Core/WeaveLoaderCore.cs | 82 +++ WeaveLoaderRuntime/src/CrashHandler.cpp | 166 ++++-- WeaveLoaderRuntime/src/DotNetHost.cpp | 27 + WeaveLoaderRuntime/src/DotNetHost.h | 2 + WeaveLoaderRuntime/src/GameHooks.cpp | 640 +++++++++++++++++++++- WeaveLoaderRuntime/src/GameHooks.h | 39 ++ WeaveLoaderRuntime/src/HookManager.cpp | 56 ++ WeaveLoaderRuntime/src/NativeExports.cpp | 52 ++ WeaveLoaderRuntime/src/NativeExports.h | 5 + WeaveLoaderRuntime/src/SymbolResolver.cpp | 45 ++ WeaveLoaderRuntime/src/SymbolResolver.h | 14 + 21 files changed, 1862 insertions(+), 45 deletions(-) create mode 100644 ExampleMod/assets/items/ruby_wand.png create mode 100644 WeaveLoader.API/IdHelper.cs 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 0000000000000000000000000000000000000000..4b728440dd33a99067de9cbee2eed691001fe111 GIT binary patch literal 4017 zcmc&%dpy(o|KH4IZbcLaF{4u0W^9$i`D5XuIM$6tDQ*-eK{PVU}d5jkO#$79$d1 zPqwC4w;JaBshEy z7Oim52p*4(fx}~BVqh_*Fcv2qZenR^2}dB|NTjidVa$zV@<_49Os<|-VoifU;f8T& zY#xopgorgsp{!^g4hj|Fkk8_jSlU;0CiipsA|v2p1a1OD!2e8VN3eJ-ZUl?{s0ggYlO#xIFsYfx}dlds$Ev{(w=pFm?!m|T(RI4IieZ(zW; z;1|=@REaFINYr#S?knVk$GdY_R2p4`aXlS(LY(aJXcJ2`+87C2JAl|nj1z+tPI09$ z!+8;3l79vKXym`|iwmSji#x=JNfTLuT+O>0;{O%HLCulyKcxxyM@V#O7^0}V$s`_$ zK;nr`ibNpM#t2hmgt@l~3WGvnOi{)NGzNiK&9cg8k!jSp|BWr~Xb4usClWEcXj~qP z6Sumx*5+Q6sI}GFl1^LgZ*hl<`J^y$bvP)S!y-qAQOK*QSNmU#a#>Vf42eUr3m085 z4r)iG(nLPQLEOcS4nabYFft`Yt;y+n?#?TMzMXuQI| z%vaIk%Y>#dMPr#Gn!jdMZUX=SFqtNLX1;k=004uW=$vL3keyPRjmnk@kd(2J5ud>D zPDqLfQ+{YrZl5_;b#pH#J^T2*AiiYa9bb1PM#9DqY7vJ1^8Ab7P~$Th=pBN+uxtqR z%v0fFgH^{Hqgd#6pN1U=?cBn9*ap8A@AF?+4t_gP`u%Qq@9zAf%_rRltY<6O6O0D0 zs*g7ltI?YpyvxfsX?rM|>S1-U^=C}9MwFSpr<`5iyEb;`%FB&f#y_4?OH+X*TG1Wa zOUG!ci`$$n_O$PR^!Uisl;nc9ukp~mIHw>i&fKt$*s}3BLYM!(fSl}gaIZ(mEC)Pe=M#*S(uv%(m`18Xo znNjpd`qRZHIgC$QDNC}jU%Ec__10$W;iYrR zv&!7t07P^5>n|3sT^m=_5h^Kj+N@HU?LZv~1@{R86c@ruFj|Ckg*qJCBBy8Ge5$f3RJWo};7`LZ9vB`3;{-`%QS=>TdV`@EB^A*~p4H zDyYnD7_)RBz&~mTa3O&=tv5{bXtX)_2YPJy258>{I>=vWt=5Ep+xaFTN#}~9PAN{-`0?Po(S6Yp>q417KC{o>JqewX&^aEg6}^2~g5%+| z&-w_c|3TpGcx6Oc>S95e9rJXn+aPS*V%@-JI+0k6X0P3Ykc?Y2Yf>V1S4%b_P4{QP-VC|6(Zhxa|q8tLdGe$#%&(!>bL8 zE;LHn9eLCJ)VqzDWfrZ|zoHyd@87#15AIU7aP~#yIF-G7x11Orq>AuJ=2l#EdccXTn2w*`stIuO*6%anjTZ3tr?-u@yWW=Z)J*+h_XfQRy~N^RD_K>R zaL5D@*Czn)5vtF1yST-9g7vWt*gpvNbiL>Uo#WFF+|(B!uajTjnbm$tl>^qaw}M_* zm#zn7G)6f|^8GXKR8MCBdBZu) zlOX}%q{z3}QgjYy>)m6C0)>ASZy>rP0H`Z=O0b^%9sYI_hu^kmIU+mUOKVyZgdUyy z3p0G7tXsdMVHrH(ijGaMY4S(yV%#!aklc(XZS_e2_+}+N2u{t_H6x@vJ0Lrd%{NiN zEs~Rt6R*rl&95{b_~S#4#PUzdSJm_;3T`Xe$*wbTWK#^ob5rvtGnOYjY*QM2B^(yO)?ZoB0l74^Yjzlacp&Xl8P(>< z;@a*+XmKFNCiO`A_$#AF+Vyo~_hByc;&ShK zv)v!J1>fY2IO<+|sA1<=X!Qwvdcd~RV5S^$ZNO1ii7<&uXL zo|oq@rq55b*IkL|7wBJZN+XS*UH*M)_TU^rC8M?VWBYgf?faFVTyI&Nex7fz^{9LT zd)aK~uzA?H9JmN_Vs1#L3Vc%)O=yGqD$9bv1Fd(D+FLc3hK@_V^2unFP%J6IU)5+Z zZPchq4{z}7@WBpQ+r{vloOaJj6_X1&u3(o>hqtED;6QM zaFW+9r$053C^+kJ`dFMl(z-b6Tj5Q&F*dW(%@{Fy4_~I!clC(bv0xFCwQn9 zVcgI?ZgWR+|I5f^mVHwE{h9iAv!{vOK2PO<`zcZLP2=to-*5T^3)0n#uvb6^l@x+% z_*EgaKU5J*Ki(B4yuln6mIaPoa;_?zsy83$dTBar(zx<4{)8|k-|MzTahkPy>Rym@ z%`M2?W1ZSge%-y=GF(YBAa*H=s1T+fc~KS>jY;###^d1(*krJq6W1qyggW!8 zD))E7rK2qU?bNawkmkk58Wr`{KivEs@BN6pEuTc->)&t))IZ_eEq+dNsqSi#$0X?Xpk5p8ip%=#mcx6fop0{-^C^ Lznf6AGlc&?^^#Uw literal 0 HcmV?d00001 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