diff --git a/ExampleMod/ExampleMod.cs b/ExampleMod/ExampleMod.cs index 72af762..0f536e7 100644 --- a/ExampleMod/ExampleMod.cs +++ b/ExampleMod/ExampleMod.cs @@ -392,6 +392,7 @@ public class ExampleMod : IMod new ItemProperties() .MaxStackSize(1) .Icon("examplemod:item/ruby_wand") + .Model("examplemod:item/ruby_wand") .Name(Text.Translatable("item.examplemod.ruby_wand")) .InCreativeTab(CreativeTab.ToolsAndWeapons)); diff --git a/ExampleMod/assets/examplemod/models/item/handheld.json b/ExampleMod/assets/examplemod/models/item/handheld.json new file mode 100644 index 0000000..51ea90f --- /dev/null +++ b/ExampleMod/assets/examplemod/models/item/handheld.json @@ -0,0 +1,25 @@ +{ + "parent": "item/generated", + "display": { + "thirdperson_righthand": { + "rotation": [ 0, -90, 55 ], + "translation": [ 0, 4.0, 0.5 ], + "scale": [ 0.85, 0.85, 0.85 ] + }, + "thirdperson_lefthand": { + "rotation": [ 0, 90, -55 ], + "translation": [ 0, 4.0, 0.5 ], + "scale": [ 0.85, 0.85, 0.85 ] + }, + "firstperson_righthand": { + "rotation": [ 0, -90, 25 ], + "translation": [ 1.13, 3.2, 1.13 ], + "scale": [ 0.68, 0.68, 0.68 ] + }, + "firstperson_lefthand": { + "rotation": [ 0, 90, -25 ], + "translation": [ 1.13, 3.2, 1.13 ], + "scale": [ 0.68, 0.68, 0.68 ] + } + } +} diff --git a/ExampleMod/assets/examplemod/models/item/ruby_wand.json b/ExampleMod/assets/examplemod/models/item/ruby_wand.json new file mode 100644 index 0000000..96deffd --- /dev/null +++ b/ExampleMod/assets/examplemod/models/item/ruby_wand.json @@ -0,0 +1,6 @@ +{ + "parent": "item/handheld", + "textures": { + "layer0": "examplemod:item/ruby_wand" + } +} diff --git a/WeaveLoader.API/Assets/ModelResolver.cs b/WeaveLoader.API/Assets/ModelResolver.cs index dfa9779..ae6a963 100644 --- a/WeaveLoader.API/Assets/ModelResolver.cs +++ b/WeaveLoader.API/Assets/ModelResolver.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; +using WeaveLoader.API.Item; namespace WeaveLoader.API.Assets; @@ -12,6 +13,7 @@ internal static class ModelResolver { public string IconName = ""; public List Boxes = new(); + public Dictionary DisplayTransforms = new(); } private enum ModelKind @@ -132,6 +134,18 @@ internal static class ModelResolver properties.IconValue = model.IconName; Logger.Debug($"Model resolved for item '{id}' -> icon '{model.IconName}'"); } + if (model.DisplayTransforms.Count > 0) + { + properties.DisplayTransforms ??= new Dictionary(); + foreach (var entry in model.DisplayTransforms) + { + if (!properties.DisplayTransforms.ContainsKey(entry.Key)) + properties.DisplayTransforms[entry.Key] = entry.Value; + } + Logger.Info($"Item display transforms loaded for '{id}': {model.DisplayTransforms.Count}"); + if (!properties.HandEquippedValue.HasValue && HasHandTransforms(model.DisplayTransforms)) + properties.HandEquippedValue = true; + } } else if (!string.IsNullOrWhiteSpace(properties.ModelValue)) { @@ -203,6 +217,8 @@ internal static class ModelResolver } TryParseElements(modelPath, data.Boxes); + if (kind == ModelKind.Item) + TryParseDisplay(modelPath, modelNamespace, data.DisplayTransforms, new HashSet(StringComparer.OrdinalIgnoreCase)); model = data; return !string.IsNullOrWhiteSpace(data.IconName) || data.Boxes.Count > 0; } @@ -349,13 +365,160 @@ internal static class ModelResolver continue; textures[prop.Name] = value!; } - return textures.Count > 0; } catch (Exception ex) { Logger.Warning($"Failed to parse model JSON '{modelPath}': {ex.Message}"); return false; } + + return textures.Count > 0; + } + + private static void TryParseDisplay(string modelPath, string modelNamespace, Dictionary display, HashSet visited) + { + if (string.IsNullOrWhiteSpace(modelPath) || string.IsNullOrWhiteSpace(modelNamespace)) + return; + if (visited.Contains(modelPath)) + return; + + visited.Add(modelPath); + + try + { + using var doc = JsonDocument.Parse(File.ReadAllText(modelPath)); + JsonElement root = doc.RootElement; + + if (root.TryGetProperty("parent", out JsonElement parentEl) && parentEl.ValueKind == JsonValueKind.String) + { + string? parentValue = parentEl.GetString(); + if (!string.IsNullOrWhiteSpace(parentValue) && + TryGetParentModelFilePath(parentValue!, modelNamespace, out string parentPath, out string parentNamespace) && + File.Exists(parentPath)) + { + TryParseDisplay(parentPath, parentNamespace, display, visited); + } + } + + if (root.TryGetProperty("display", out JsonElement displayEl) && displayEl.ValueKind == JsonValueKind.Object) + { + foreach (var entry in displayEl.EnumerateObject()) + { + if (!TryMapDisplayContext(entry.Name, out ItemDisplayContext context)) + continue; + if (entry.Value.ValueKind != JsonValueKind.Object) + continue; + ItemDisplayTransform transform = ParseDisplayTransform(entry.Value); + display[context] = transform; + } + } + } + catch (Exception ex) + { + Logger.Warning($"Failed to parse item display data '{modelPath}': {ex.Message}"); + } + } + + private static bool TryGetParentModelFilePath(string parentValue, string currentNamespace, out string modelPath, out string modelNamespace) + { + modelPath = ""; + modelNamespace = ""; + + if (string.IsNullOrWhiteSpace(ModContext.ModFolder)) + return false; + + string value = parentValue.Replace('\\', '/'); + string ns = currentNamespace; + int colon = value.IndexOf(':'); + if (colon >= 0) + { + ns = value.Substring(0, colon); + value = value.Substring(colon + 1); + } + + if (string.IsNullOrWhiteSpace(value)) + return false; + + string relative = value.Replace('/', Path.DirectorySeparatorChar); + string fullPath = Path.Combine(ModContext.ModFolder!, "assets", ns, "models", relative + ".json"); + modelPath = fullPath; + modelNamespace = ns; + return true; + } + + private static bool TryMapDisplayContext(string name, out ItemDisplayContext context) + { + switch (name.Trim().ToLowerInvariant()) + { + case "gui": + context = ItemDisplayContext.Gui; + return true; + case "ground": + context = ItemDisplayContext.Ground; + return true; + case "fixed": + context = ItemDisplayContext.Fixed; + return true; + case "head": + context = ItemDisplayContext.Head; + return true; + case "firstperson_righthand": + context = ItemDisplayContext.FirstPersonRightHand; + return true; + case "firstperson_lefthand": + context = ItemDisplayContext.FirstPersonLeftHand; + return true; + case "thirdperson_righthand": + context = ItemDisplayContext.ThirdPersonRightHand; + return true; + case "thirdperson_lefthand": + context = ItemDisplayContext.ThirdPersonLeftHand; + return true; + default: + context = ItemDisplayContext.Gui; + return false; + } + } + + private static ItemDisplayTransform ParseDisplayTransform(JsonElement element) + { + float rX = 0.0f; + float rY = 0.0f; + float rZ = 0.0f; + float tX = 0.0f; + float tY = 0.0f; + float tZ = 0.0f; + float sX = 1.0f; + float sY = 1.0f; + float sZ = 1.0f; + + ReadVec3(element, "rotation", ref rX, ref rY, ref rZ); + ReadVec3(element, "translation", ref tX, ref tY, ref tZ); + ReadVec3(element, "scale", ref sX, ref sY, ref sZ); + + return new ItemDisplayTransform(rX, rY, rZ, tX, tY, tZ, sX, sY, sZ); + } + + private static void ReadVec3(JsonElement parent, string name, ref float x, ref float y, ref float z) + { + if (!parent.TryGetProperty(name, out JsonElement el) || el.ValueKind != JsonValueKind.Array) + return; + + int count = el.GetArrayLength(); + if (count > 0 && el[0].ValueKind == JsonValueKind.Number) + x = (float)el[0].GetDouble(); + if (count > 1 && el[1].ValueKind == JsonValueKind.Number) + y = (float)el[1].GetDouble(); + if (count > 2 && el[2].ValueKind == JsonValueKind.Number) + z = (float)el[2].GetDouble(); + } + + private static bool HasHandTransforms(Dictionary display) + { + return display.ContainsKey(ItemDisplayContext.FirstPersonRightHand) || + display.ContainsKey(ItemDisplayContext.FirstPersonLeftHand) || + display.ContainsKey(ItemDisplayContext.ThirdPersonRightHand) || + display.ContainsKey(ItemDisplayContext.ThirdPersonLeftHand); } private static void TryParseElements(string modelPath, List boxes) diff --git a/WeaveLoader.API/Item/ItemProperties.cs b/WeaveLoader.API/Item/ItemProperties.cs index b18a28b..d955fe6 100644 --- a/WeaveLoader.API/Item/ItemProperties.cs +++ b/WeaveLoader.API/Item/ItemProperties.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using WeaveLoader.API; namespace WeaveLoader.API.Item; @@ -12,6 +13,9 @@ public class ItemProperties internal float AttackDamageValue = 0.0f; internal string IconValue = ""; internal string? ModelValue; + internal Dictionary? DisplayTransforms; + internal IItemRenderer? RendererValue; + internal bool? HandEquippedValue; internal CreativeTab CreativeTabValue = CreativeTab.None; internal CreativePlacement? CreativePlacementValue; internal Text? NameValue; @@ -28,6 +32,20 @@ public class ItemProperties /// and use its texture for the item icon. /// public ItemProperties Model(string modelName) { ModelValue = modelName; return this; } + /// + /// Override the Java-style display transform for a rendering context (gui, ground, first/third person). + /// Values are interpreted the same way as Minecraft Java item model "display" transforms. + /// + public ItemProperties DisplayTransform(ItemDisplayContext context, ItemDisplayTransform transform) + { + DisplayTransforms ??= new Dictionary(); + DisplayTransforms[context] = transform; + return this; + } + /// Register a custom renderer for this item. + public ItemProperties Renderer(IItemRenderer renderer) { RendererValue = renderer; return this; } + /// Force the item to be treated as hand-equipped (sword-like pose) by the renderer. + public ItemProperties HandEquipped(bool handEquipped = true) { HandEquippedValue = handEquipped; return this; } /// /// Set max damage for a tool/armor item. Setting this to a positive value diff --git a/WeaveLoader.API/Item/ItemRegistry.cs b/WeaveLoader.API/Item/ItemRegistry.cs index b7ad363..6ac0725 100644 --- a/WeaveLoader.API/Item/ItemRegistry.cs +++ b/WeaveLoader.API/Item/ItemRegistry.cs @@ -190,6 +190,19 @@ public static class ItemRegistry throw new InvalidOperationException($"Failed to register item '{id}'. No free IDs or invalid parameters."); } + if (properties.DisplayTransforms != null && properties.DisplayTransforms.Count > 0) + { + foreach (var entry in properties.DisplayTransforms) + { + NativeInterop.native_register_item_display_transform(numericId, (int)entry.Key, entry.Value); + } + } + + if (properties.RendererValue != null) + ManagedItemRendererDispatcher.Register(numericId, properties.RendererValue); + if (properties.HandEquippedValue.HasValue) + NativeInterop.native_set_item_hand_equipped(numericId, properties.HandEquippedValue.Value ? 1 : 0); + if (properties.CreativeTabValue != CreativeTab.None) { bool added = false; diff --git a/WeaveLoader.API/Item/ItemRendering.cs b/WeaveLoader.API/Item/ItemRendering.cs new file mode 100644 index 0000000..7abcb5d --- /dev/null +++ b/WeaveLoader.API/Item/ItemRendering.cs @@ -0,0 +1,102 @@ +using System; +using System.Runtime.InteropServices; + +namespace WeaveLoader.API.Item; + +public enum ItemDisplayContext +{ + Gui = 0, + Ground = 1, + Fixed = 2, + Head = 3, + FirstPersonRightHand = 4, + FirstPersonLeftHand = 5, + ThirdPersonRightHand = 6, + ThirdPersonLeftHand = 7, +} + +[StructLayout(LayoutKind.Sequential)] +public struct ItemDisplayTransform +{ + public float RotationX; + public float RotationY; + public float RotationZ; + public float TranslationX; + public float TranslationY; + public float TranslationZ; + public float ScaleX; + public float ScaleY; + public float ScaleZ; + + public ItemDisplayTransform( + float rotationX, + float rotationY, + float rotationZ, + float translationX, + float translationY, + float translationZ, + float scaleX, + float scaleY, + float scaleZ) + { + RotationX = rotationX; + RotationY = rotationY; + RotationZ = rotationZ; + TranslationX = translationX; + TranslationY = translationY; + TranslationZ = translationZ; + ScaleX = scaleX; + ScaleY = scaleY; + ScaleZ = scaleZ; + } + + public static ItemDisplayTransform Identity => new( + 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, + 1.0f, 1.0f, 1.0f); +} + +public readonly struct ItemRenderContext +{ + public int ItemId { get; } + public ItemDisplayContext DisplayContext { get; } + public IntPtr RendererPtr { get; } + public IntPtr ItemInstancePtr { get; } + public float X { get; } + public float Y { get; } + public float ScaleX { get; } + public float ScaleY { get; } + public float Alpha { get; } + + internal ItemRenderContext(ItemRenderNativeArgs args) + { + ItemId = args.ItemId; + DisplayContext = (ItemDisplayContext)args.Context; + RendererPtr = args.RendererPtr; + ItemInstancePtr = args.ItemInstancePtr; + X = args.X; + Y = args.Y; + ScaleX = args.ScaleX; + ScaleY = args.ScaleY; + Alpha = args.Alpha; + } +} + +public interface IItemRenderer +{ + bool Render(ItemRenderContext context); +} + +[StructLayout(LayoutKind.Sequential)] +internal struct ItemRenderNativeArgs +{ + public int ItemId; + public int Context; + public IntPtr RendererPtr; + public IntPtr ItemInstancePtr; + public float X; + public float Y; + public float ScaleX; + public float ScaleY; + public float Alpha; +} diff --git a/WeaveLoader.API/Item/ManagedItemRendererDispatcher.cs b/WeaveLoader.API/Item/ManagedItemRendererDispatcher.cs new file mode 100644 index 0000000..208c56e --- /dev/null +++ b/WeaveLoader.API/Item/ManagedItemRendererDispatcher.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace WeaveLoader.API.Item; + +internal static class ManagedItemRendererDispatcher +{ + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate int NativeItemRenderDelegate(IntPtr args, int sizeBytes); + + private static readonly Dictionary s_renderers = new(); + private static readonly NativeItemRenderDelegate s_callback = HandleRender; + private static readonly IntPtr s_callbackPtr = Marshal.GetFunctionPointerForDelegate(s_callback); + private static readonly int s_argsSize = Marshal.SizeOf(); + + internal static void Register(int itemId, IItemRenderer renderer) + { + s_renderers[itemId] = renderer; + NativeInterop.native_register_item_renderer(itemId, s_callbackPtr); + } + + private static int HandleRender(IntPtr args, int sizeBytes) + { + if (args == IntPtr.Zero || sizeBytes < s_argsSize) + return 0; + + ItemRenderNativeArgs nativeArgs = Marshal.PtrToStructure(args); + if (!s_renderers.TryGetValue(nativeArgs.ItemId, out IItemRenderer? renderer) || renderer == null) + return 0; + + try + { + return renderer.Render(new ItemRenderContext(nativeArgs)) ? 1 : 0; + } + catch (Exception ex) + { + Logger.Error($"Item renderer for {nativeArgs.ItemId} threw: {ex.Message}"); + return 0; + } + } +} diff --git a/WeaveLoader.API/NativeInterop.cs b/WeaveLoader.API/NativeInterop.cs index 58d2b60..9e6f68b 100644 --- a/WeaveLoader.API/NativeInterop.cs +++ b/WeaveLoader.API/NativeInterop.cs @@ -97,6 +97,22 @@ internal static class NativeInterop string iconName, string displayName); + [DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl)] + internal static extern void native_register_item_display_transform( + int numericItemId, + int context, + Item.ItemDisplayTransform transform); + + [DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl)] + internal static extern void native_register_item_renderer( + int numericItemId, + nint rendererFnPtr); + + [DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl)] + internal static extern void native_set_item_hand_equipped( + int numericItemId, + int isHandEquipped); + [DllImport(RuntimeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] internal static extern int native_register_pickaxe_item( string namespacedId, diff --git a/WeaveLoaderRuntime/CMakeLists.txt b/WeaveLoaderRuntime/CMakeLists.txt index f2ebc9f..e78958f 100644 --- a/WeaveLoaderRuntime/CMakeLists.txt +++ b/WeaveLoaderRuntime/CMakeLists.txt @@ -103,6 +103,7 @@ add_library(WeaveLoaderRuntime SHARED src/GameObjectFactory.cpp src/ModStrings.cpp src/ModAtlas.cpp + src/ItemRenderRegistry.cpp ) target_include_directories(WeaveLoaderRuntime PRIVATE @@ -115,6 +116,7 @@ target_link_libraries(WeaveLoaderRuntime PRIVATE minhook raw_pdb bcrypt + opengl32 ) if(EXISTS "${NETHOST_LIB}") diff --git a/WeaveLoaderRuntime/src/GameHooks.cpp b/WeaveLoaderRuntime/src/GameHooks.cpp index d0e6aab..1e8606c 100644 --- a/WeaveLoaderRuntime/src/GameHooks.cpp +++ b/WeaveLoaderRuntime/src/GameHooks.cpp @@ -13,7 +13,9 @@ #include "LogUtil.h" #include "WorldIdRemap.h" #include "ModelRegistry.h" +#include "ItemRenderRegistry.h" #include +#include #include #include #include @@ -51,6 +53,9 @@ namespace GameHooks ItemInstanceGetIcon_fn Original_ItemInstanceGetIcon = nullptr; EntityRendererBindTextureResource_fn Original_EntityRendererBindTextureResource = nullptr; ItemRendererRenderItemBillboard_fn Original_ItemRendererRenderItemBillboard = nullptr; + ItemRendererRenderGuiItem_fn Original_ItemRendererRenderGuiItem = nullptr; + ItemInHandRendererRender_fn Original_ItemInHandRendererRender = nullptr; + ItemInHandRendererRenderItem_fn Original_ItemInHandRendererRenderItem = nullptr; AnimatedTextureCycleFrames_fn Original_CompassTextureCycleFrames = nullptr; AnimatedTextureCycleFrames_fn Original_ClockTextureCycleFrames = nullptr; TextureGetSourceDim_fn Original_CompassTextureGetSourceWidth = nullptr; @@ -658,11 +663,14 @@ namespace GameHooks static InventoryRemoveResource_fn s_inventoryRemoveResource = nullptr; static void* s_inventoryVtable = nullptr; static ItemInstanceHurtAndBreak_fn s_itemInstanceHurtAndBreak = nullptr; + static ItemEntityGetItem_fn s_itemEntityGetItem = nullptr; + static thread_local int s_firstPersonRenderDepth = 0; static std::string s_modsPath; static std::unordered_map s_modAssetRoots; static bool s_modAssetsIndexed = false; static std::atomic s_modAssetsIndexing{false}; static std::mutex s_modAssetsMutex; + static std::unordered_map s_itemRenderLogCount; // Verified from compiled Player::inventory accesses in this game build. static constexpr ptrdiff_t kPlayerInventoryOffset = 0x340; static constexpr ptrdiff_t kLevelIsClientSideOffset = 0x268; @@ -1210,7 +1218,21 @@ namespace GameHooks void* playerSharedPtr; }; + struct ItemRenderNativeArgs + { + int itemId; + int context; + void* rendererPtr; + void* itemInstancePtr; + float x; + float y; + float scaleX; + float scaleY; + float alpha; + }; + static bool IsFireballFamilyEntityId(int entityNumericId); + static bool LooksLikeEntityPtr(void* candidate); static void* DecodeItemInstancePtrFromSharedArg(void* sharedArg); static void* DecodePlayerPtrFromSharedArg(void* sharedArg); @@ -1239,6 +1261,136 @@ namespace GameHooks return false; } + static bool TryGetItemIdFromItemInstanceShared(void* itemInstanceSharedPtr, int& outItemId, void** outItemInstancePtr) + { + void* itemInstancePtr = DecodeItemInstancePtrFromSharedArg(itemInstanceSharedPtr); + if (!itemInstancePtr) + return false; + int itemId = 0; + if (!TryReadItemId(itemInstancePtr, itemId)) + return false; + outItemId = itemId; + if (outItemInstancePtr) + *outItemInstancePtr = itemInstancePtr; + return true; + } + + static void* DecodeEntityPtrFromSharedArg(void* sharedArg) + { + if (!sharedArg || !IsCanonicalUserPtr(sharedArg)) + return nullptr; + + __try + { + void* p = *reinterpret_cast(sharedArg); + if (LooksLikeEntityPtr(p)) + return p; + } + __except (EXCEPTION_EXECUTE_HANDLER) {} + + if (LooksLikeEntityPtr(sharedArg)) + return sharedArg; + + return nullptr; + } + + static bool TryGetItemIdFromItemEntityShared(void* itemEntitySharedPtr, int& outItemId, void** outItemInstancePtr) + { + if (!s_itemEntityGetItem) + return false; + + void* itemEntityPtr = DecodeEntityPtrFromSharedArg(itemEntitySharedPtr); + if (!itemEntityPtr) + return false; + + std::shared_ptr itemShared; + s_itemEntityGetItem(&itemShared, itemEntityPtr); + void* itemInstancePtr = DecodeItemInstancePtrFromSharedArg(&itemShared); + if (!itemInstancePtr) + return false; + + int itemId = 0; + if (!TryReadItemId(itemInstancePtr, itemId)) + return false; + + outItemId = itemId; + if (outItemInstancePtr) + *outItemInstancePtr = itemInstancePtr; + return true; + } + + static float GetTranslationScaleForContext(int context) + { + return context == ItemDisplay_Gui ? 1.0f : (1.0f / 16.0f); + } + + static void ApplyItemDisplayTransform(const ItemDisplayTransformNative& transform, int context) + { + float tScale = GetTranslationScaleForContext(context); + glTranslatef(transform.transX * tScale, transform.transY * tScale, transform.transZ * tScale); + glRotatef(transform.rotZ, 0.0f, 0.0f, 1.0f); + glRotatef(transform.rotY, 0.0f, 1.0f, 0.0f); + glRotatef(transform.rotX, 1.0f, 0.0f, 0.0f); + glScalef(transform.scaleX, transform.scaleY, transform.scaleZ); + } + + static int BeginModelViewTransform() + { + GLint prevMode = GL_MODELVIEW; + glGetIntegerv(GL_MATRIX_MODE, &prevMode); + if (prevMode != GL_MODELVIEW) + glMatrixMode(GL_MODELVIEW); + return prevMode; + } + + static void EndModelViewTransform(int prevMode) + { + if (prevMode != GL_MODELVIEW) + glMatrixMode(prevMode); + } + + static bool TryGetDisplayTransformWithFallback(int itemId, int context, ItemDisplayTransformNative& outTransform) + { + if (ItemRenderRegistry::TryGetDisplayTransform(itemId, context, outTransform)) + return true; + + if (context == ItemDisplay_FirstPersonRightHand) + return ItemRenderRegistry::TryGetDisplayTransform(itemId, ItemDisplay_FirstPersonLeftHand, outTransform); + if (context == ItemDisplay_ThirdPersonRightHand) + return ItemRenderRegistry::TryGetDisplayTransform(itemId, ItemDisplay_ThirdPersonLeftHand, outTransform); + + return false; + } + + static bool TryRenderCustomItem(int itemId, + int context, + void* rendererPtr, + void* itemInstancePtr, + float x, + float y, + float scaleX, + float scaleY, + float alpha) + { + ManagedItemRenderFn fn = ItemRenderRegistry::GetCustomRenderer(itemId); + if (!fn) + return false; + + ItemRenderNativeArgs args{}; + args.itemId = itemId; + args.context = context; + args.rendererPtr = rendererPtr; + args.itemInstancePtr = itemInstancePtr; + args.x = x; + args.y = y; + args.scaleX = scaleX; + args.scaleY = scaleY; + args.alpha = alpha; + + int handled = fn(&args, sizeof(args)); + return handled != 0; + } + static int TryDispatchMineBlockFromItemInstancePtr(void* itemInstancePtr, int tile, int x, int y, int z, const char* sourceTag) { if (!itemInstancePtr) @@ -1287,6 +1439,11 @@ namespace GameHooks s_entitySetPos = reinterpret_cast(entitySetPos); } + void SetItemRenderSymbols(void* itemEntityGetItem) + { + s_itemEntityGetItem = reinterpret_cast(itemEntityGetItem); + } + void SetUseActionSymbols(void* inventoryRemoveResource, void* inventoryVtable, void* itemInstanceHurtAndBreak, @@ -1458,6 +1615,9 @@ namespace GameHooks { if (!candidate || !IsReadableRange(candidate, sizeof(void*))) return false; + // Reject pointers that are already in game code space (likely vtable pointers). + if (IsGameCodePtr(candidate)) + return false; void* vt = *reinterpret_cast(candidate); if (!IsCanonicalUserPtr(vt)) return false; @@ -2645,6 +2805,113 @@ namespace GameHooks s_forcedBillboardPage = prevPage; } + void __fastcall Hooked_ItemRendererRenderGuiItem(void* thisPtr, void* fontPtr, void* texturesPtr, void* itemInstanceSharedPtr, float x, float y, float scaleX, float scaleY, float alpha, bool useCompiled) + { + if (!Original_ItemRendererRenderGuiItem) + return; + + int itemId = -1; + void* itemInstancePtr = nullptr; + bool hasItemId = TryGetItemIdFromItemInstanceShared(itemInstanceSharedPtr, itemId, &itemInstancePtr); + if (!hasItemId) + { + Original_ItemRendererRenderGuiItem(thisPtr, fontPtr, texturesPtr, itemInstanceSharedPtr, x, y, scaleX, scaleY, alpha, useCompiled); + return; + } + + ItemDisplayTransformNative transform{}; + bool hasTransform = TryGetDisplayTransformWithFallback(itemId, ItemDisplay_Gui, transform); + bool hasCustom = ItemRenderRegistry::GetCustomRenderer(itemId) != nullptr; + + if (!hasTransform && !hasCustom) + { + Original_ItemRendererRenderGuiItem(thisPtr, fontPtr, texturesPtr, itemInstanceSharedPtr, x, y, scaleX, scaleY, alpha, useCompiled); + return; + } + + int prevMode = BeginModelViewTransform(); + glPushMatrix(); + glTranslatef(x, y, 0.0f); + glScalef(scaleX, scaleY, 1.0f); + + if (hasTransform) + ApplyItemDisplayTransform(transform, ItemDisplay_Gui); + + bool handled = hasCustom + ? TryRenderCustomItem(itemId, ItemDisplay_Gui, thisPtr, itemInstancePtr, x, y, scaleX, scaleY, alpha) + : false; + + if (!handled) + Original_ItemRendererRenderGuiItem(thisPtr, fontPtr, texturesPtr, itemInstanceSharedPtr, 0.0f, 0.0f, 1.0f, 1.0f, alpha, useCompiled); + + glPopMatrix(); + EndModelViewTransform(prevMode); + } + + void __fastcall Hooked_ItemInHandRendererRender(void* thisPtr, float a) + { + s_firstPersonRenderDepth++; + if (Original_ItemInHandRendererRender) + Original_ItemInHandRendererRender(thisPtr, a); + s_firstPersonRenderDepth--; + } + + void __fastcall Hooked_ItemInHandRendererRenderItem(void* thisPtr, void* entitySharedPtr, void* itemInstanceSharedPtr, int layer, bool setColor) + { + if (!Original_ItemInHandRendererRenderItem) + return; + + int itemId = -1; + void* itemInstancePtr = nullptr; + bool hasItemId = TryGetItemIdFromItemInstanceShared(itemInstanceSharedPtr, itemId, &itemInstancePtr); + if (!hasItemId) + { + Original_ItemInHandRendererRenderItem(thisPtr, entitySharedPtr, itemInstanceSharedPtr, layer, setColor); + return; + } + + int context = s_firstPersonRenderDepth > 0 ? ItemDisplay_FirstPersonRightHand : ItemDisplay_ThirdPersonRightHand; + ItemDisplayTransformNative transform{}; + bool hasTransform = TryGetDisplayTransformWithFallback(itemId, context, transform); + bool hasCustom = ItemRenderRegistry::GetCustomRenderer(itemId) != nullptr; + + if (hasTransform) + { + int& count = s_itemRenderLogCount[itemId]; + if (count < 5) + { + LogUtil::Log("[WeaveLoader] ItemRender: item=%d ctx=%d rot=(%.2f,%.2f,%.2f) trans=(%.2f,%.2f,%.2f) scale=(%.2f,%.2f,%.2f)", + itemId, + context, + transform.rotX, transform.rotY, transform.rotZ, + transform.transX, transform.transY, transform.transZ, + transform.scaleX, transform.scaleY, transform.scaleZ); + count++; + } + } + + if (!hasTransform && !hasCustom) + { + Original_ItemInHandRendererRenderItem(thisPtr, entitySharedPtr, itemInstanceSharedPtr, layer, setColor); + return; + } + + int prevMode = BeginModelViewTransform(); + glPushMatrix(); + if (hasTransform) + ApplyItemDisplayTransform(transform, context); + + bool handled = hasCustom + ? TryRenderCustomItem(itemId, context, thisPtr, itemInstancePtr, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f) + : false; + + if (!handled) + Original_ItemInHandRendererRenderItem(thisPtr, entitySharedPtr, itemInstanceSharedPtr, layer, setColor); + + glPopMatrix(); + EndModelViewTransform(prevMode); + } + static void LogAnimatedTextureGuard(const char* what, void* thisPtr) { if (s_animatedTextureGuardLogCount >= 12) diff --git a/WeaveLoaderRuntime/src/GameHooks.h b/WeaveLoaderRuntime/src/GameHooks.h index fd45466..4e091f6 100644 --- a/WeaveLoaderRuntime/src/GameHooks.h +++ b/WeaveLoaderRuntime/src/GameHooks.h @@ -22,6 +22,10 @@ typedef void* (__fastcall *RegisterIcon_fn)(void* thisPtr, const std::wstring& n typedef void* (__fastcall *ItemInstanceGetIcon_fn)(void* thisPtr); typedef void (__fastcall *EntityRendererBindTextureResource_fn)(void* thisPtr, void* resourcePtr); typedef void (__fastcall *ItemRendererRenderItemBillboard_fn)(void* thisPtr, void* entitySharedPtr, void* iconPtr, int count, float a, float red, float green, float blue); +typedef void (__fastcall *ItemRendererRenderGuiItem_fn)(void* thisPtr, void* fontPtr, void* texturesPtr, void* itemInstanceSharedPtr, float x, float y, float scaleX, float scaleY, float alpha, bool useCompiled); +typedef void (__fastcall *ItemInHandRendererRender_fn)(void* thisPtr, float a); +typedef void (__fastcall *ItemInHandRendererRenderItem_fn)(void* thisPtr, void* entitySharedPtr, void* itemInstanceSharedPtr, int layer, bool setColor); +typedef void (__fastcall *ItemEntityGetItem_fn)(void* outSharedPtr, void* thisPtr); typedef void (__fastcall *AnimatedTextureCycleFrames_fn)(void* thisPtr); typedef int (__fastcall *TextureGetSourceDim_fn)(void* thisPtr); typedef void (__fastcall *ItemInstanceMineBlock_fn)(void* thisPtr, void* level, int tile, int x, int y, int z, void* ownerSharedPtr); @@ -113,6 +117,9 @@ namespace GameHooks extern ItemInstanceGetIcon_fn Original_ItemInstanceGetIcon; extern EntityRendererBindTextureResource_fn Original_EntityRendererBindTextureResource; extern ItemRendererRenderItemBillboard_fn Original_ItemRendererRenderItemBillboard; + extern ItemRendererRenderGuiItem_fn Original_ItemRendererRenderGuiItem; + extern ItemInHandRendererRender_fn Original_ItemInHandRendererRender; + extern ItemInHandRendererRenderItem_fn Original_ItemInHandRendererRenderItem; extern AnimatedTextureCycleFrames_fn Original_CompassTextureCycleFrames; extern AnimatedTextureCycleFrames_fn Original_ClockTextureCycleFrames; extern TextureGetSourceDim_fn Original_CompassTextureGetSourceWidth; @@ -209,6 +216,9 @@ namespace GameHooks void* __fastcall Hooked_ItemInstanceGetIcon(void* thisPtr); void __fastcall Hooked_EntityRendererBindTextureResource(void* thisPtr, void* resourcePtr); void __fastcall Hooked_ItemRendererRenderItemBillboard(void* thisPtr, void* entitySharedPtr, void* iconPtr, int count, float a, float red, float green, float blue); + void __fastcall Hooked_ItemRendererRenderGuiItem(void* thisPtr, void* fontPtr, void* texturesPtr, void* itemInstanceSharedPtr, float x, float y, float scaleX, float scaleY, float alpha, bool useCompiled); + void __fastcall Hooked_ItemInHandRendererRender(void* thisPtr, float a); + void __fastcall Hooked_ItemInHandRendererRenderItem(void* thisPtr, void* entitySharedPtr, void* itemInstanceSharedPtr, int layer, bool setColor); void __fastcall Hooked_CompassTextureCycleFrames(void* thisPtr); void __fastcall Hooked_ClockTextureCycleFrames(void* thisPtr); int __fastcall Hooked_CompassTextureGetSourceWidth(void* thisPtr); @@ -285,6 +295,7 @@ namespace GameHooks void* entityIoNewById, void* entityMoveTo, void* entitySetPos); + void SetItemRenderSymbols(void* itemEntityGetItem); void SetUseActionSymbols(void* inventoryRemoveResource, void* inventoryVtable, void* itemInstanceHurtAndBreak, diff --git a/WeaveLoaderRuntime/src/HookManager.cpp b/WeaveLoaderRuntime/src/HookManager.cpp index f45a601..c4f3c9d 100644 --- a/WeaveLoaderRuntime/src/HookManager.cpp +++ b/WeaveLoaderRuntime/src/HookManager.cpp @@ -226,6 +226,48 @@ bool HookManager::Install(const SymbolResolver& symbols) } } + if (symbols.Item.pItemRendererRenderGuiItem) + { + if (MH_CreateHook(symbols.Item.pItemRendererRenderGuiItem, + reinterpret_cast(&GameHooks::Hooked_ItemRendererRenderGuiItem), + reinterpret_cast(&GameHooks::Original_ItemRendererRenderGuiItem)) != MH_OK) + { + LogUtil::Log("[WeaveLoader] Warning: Failed to hook ItemRenderer::renderGuiItem"); + } + else + { + LogUtil::Log("[WeaveLoader] Hooked ItemRenderer::renderGuiItem (item display transforms)"); + } + } + + if (symbols.Item.pItemInHandRendererRender) + { + if (MH_CreateHook(symbols.Item.pItemInHandRendererRender, + reinterpret_cast(&GameHooks::Hooked_ItemInHandRendererRender), + reinterpret_cast(&GameHooks::Original_ItemInHandRendererRender)) != MH_OK) + { + LogUtil::Log("[WeaveLoader] Warning: Failed to hook ItemInHandRenderer::render"); + } + else + { + LogUtil::Log("[WeaveLoader] Hooked ItemInHandRenderer::render (first-person context)"); + } + } + + if (symbols.Item.pItemInHandRendererRenderItem) + { + if (MH_CreateHook(symbols.Item.pItemInHandRendererRenderItem, + reinterpret_cast(&GameHooks::Hooked_ItemInHandRendererRenderItem), + reinterpret_cast(&GameHooks::Original_ItemInHandRendererRenderItem)) != MH_OK) + { + LogUtil::Log("[WeaveLoader] Warning: Failed to hook ItemInHandRenderer::renderItem"); + } + else + { + LogUtil::Log("[WeaveLoader] Hooked ItemInHandRenderer::renderItem (item display transforms)"); + } + } + if (symbols.Texture.pCompassTextureCycleFrames) { if (MH_CreateHook(symbols.Texture.pCompassTextureCycleFrames, @@ -1050,6 +1092,7 @@ bool HookManager::Install(const SymbolResolver& symbols) symbols.Entity.pEntityIONewById, symbols.Entity.pEntityMoveTo, symbols.Entity.pEntitySetPos); + GameHooks::SetItemRenderSymbols(symbols.Item.pItemEntityGetItem); GameHooks::SetUseActionSymbols( symbols.Inventory.pInventoryRemoveResource, symbols.Inventory.pInventoryVtable, diff --git a/WeaveLoaderRuntime/src/ItemRenderRegistry.cpp b/WeaveLoaderRuntime/src/ItemRenderRegistry.cpp new file mode 100644 index 0000000..7c32dda --- /dev/null +++ b/WeaveLoaderRuntime/src/ItemRenderRegistry.cpp @@ -0,0 +1,69 @@ +#include "ItemRenderRegistry.h" +#include "LogUtil.h" +#include +#include +#include + +namespace +{ + struct ItemRenderEntry + { + std::array transforms{}; + std::array hasTransform{}; + ManagedItemRenderFn renderer = nullptr; + }; + + std::unordered_map g_entries; + std::mutex g_mutex; +} + +void ItemRenderRegistry::RegisterDisplayTransform(int itemId, int context, const ItemDisplayTransformNative& transform) +{ + if (itemId < 0) + return; + if (context < 0 || context >= ItemDisplay_ContextCount) + return; + + std::lock_guard guard(g_mutex); + ItemRenderEntry& entry = g_entries[itemId]; + entry.transforms[context] = transform; + entry.hasTransform[context] = 1; + LogUtil::Log("[WeaveLoader] ItemRenderRegistry: display transform %d registered for item %d", context, itemId); +} + +bool ItemRenderRegistry::TryGetDisplayTransform(int itemId, int context, ItemDisplayTransformNative& outTransform) +{ + if (itemId < 0) + return false; + if (context < 0 || context >= ItemDisplay_ContextCount) + return false; + + std::lock_guard guard(g_mutex); + auto it = g_entries.find(itemId); + if (it == g_entries.end()) + return false; + if (!it->second.hasTransform[context]) + return false; + outTransform = it->second.transforms[context]; + return true; +} + +void ItemRenderRegistry::RegisterCustomRenderer(int itemId, ManagedItemRenderFn fn) +{ + if (itemId < 0) + return; + std::lock_guard guard(g_mutex); + g_entries[itemId].renderer = fn; + LogUtil::Log("[WeaveLoader] ItemRenderRegistry: custom renderer registered for item %d", itemId); +} + +ManagedItemRenderFn ItemRenderRegistry::GetCustomRenderer(int itemId) +{ + if (itemId < 0) + return nullptr; + std::lock_guard guard(g_mutex); + auto it = g_entries.find(itemId); + if (it == g_entries.end()) + return nullptr; + return it->second.renderer; +} diff --git a/WeaveLoaderRuntime/src/ItemRenderRegistry.h b/WeaveLoaderRuntime/src/ItemRenderRegistry.h new file mode 100644 index 0000000..e75d126 --- /dev/null +++ b/WeaveLoaderRuntime/src/ItemRenderRegistry.h @@ -0,0 +1,39 @@ +#pragma once + +#include + +struct ItemDisplayTransformNative +{ + float rotX; + float rotY; + float rotZ; + float transX; + float transY; + float transZ; + float scaleX; + float scaleY; + float scaleZ; +}; + +enum ItemDisplayContextNative : int +{ + ItemDisplay_Gui = 0, + ItemDisplay_Ground = 1, + ItemDisplay_Fixed = 2, + ItemDisplay_Head = 3, + ItemDisplay_FirstPersonRightHand = 4, + ItemDisplay_FirstPersonLeftHand = 5, + ItemDisplay_ThirdPersonRightHand = 6, + ItemDisplay_ThirdPersonLeftHand = 7, + ItemDisplay_ContextCount = 8 +}; + +using ManagedItemRenderFn = int(__cdecl *)(const void* args, int sizeBytes); + +namespace ItemRenderRegistry +{ + void RegisterDisplayTransform(int itemId, int context, const ItemDisplayTransformNative& transform); + bool TryGetDisplayTransform(int itemId, int context, ItemDisplayTransformNative& outTransform); + void RegisterCustomRenderer(int itemId, ManagedItemRenderFn fn); + ManagedItemRenderFn GetCustomRenderer(int itemId); +} diff --git a/WeaveLoaderRuntime/src/NativeExports.cpp b/WeaveLoaderRuntime/src/NativeExports.cpp index d7489b7..8fc3b7c 100644 --- a/WeaveLoaderRuntime/src/NativeExports.cpp +++ b/WeaveLoaderRuntime/src/NativeExports.cpp @@ -367,6 +367,30 @@ int native_register_item( return id; } +void native_register_item_display_transform(int numericItemId, int context, ItemDisplayTransformNative transform) +{ + ItemRenderRegistry::RegisterDisplayTransform(numericItemId, context, transform); +} + +void native_register_item_renderer(int numericItemId, void* rendererFn) +{ + ItemRenderRegistry::RegisterCustomRenderer(numericItemId, reinterpret_cast(rendererFn)); +} + +void native_set_item_hand_equipped(int numericItemId, int isHandEquipped) +{ + void* itemPtr = GameObjectFactory::FindItem(numericItemId); + if (!itemPtr) + { + LogUtil::Log("[WeaveLoader] HandEquipped: item %d not found", numericItemId); + return; + } + + constexpr ptrdiff_t kHandEquippedOffset = 0x40; + *reinterpret_cast(static_cast(itemPtr) + kHandEquippedOffset) = (isHandEquipped != 0) ? 1 : 0; + LogUtil::Log("[WeaveLoader] HandEquipped: item %d -> %d", numericItemId, isHandEquipped != 0); +} + int native_register_pickaxe_item( const char* namespacedId, int tier, diff --git a/WeaveLoaderRuntime/src/NativeExports.h b/WeaveLoaderRuntime/src/NativeExports.h index ff86cdc..2901d1a 100644 --- a/WeaveLoaderRuntime/src/NativeExports.h +++ b/WeaveLoaderRuntime/src/NativeExports.h @@ -3,6 +3,7 @@ #include #include #include "ModelRegistry.h" +#include "ItemRenderRegistry.h" namespace NativeExports { @@ -111,6 +112,16 @@ extern "C" int maxDamage, const char* iconName, const char* displayName); + __declspec(dllexport) void native_register_item_display_transform( + int numericItemId, + int context, + ItemDisplayTransformNative transform); + __declspec(dllexport) void native_register_item_renderer( + int numericItemId, + void* rendererFn); + __declspec(dllexport) void native_set_item_hand_equipped( + int numericItemId, + int isHandEquipped); __declspec(dllexport) int native_register_pickaxe_item( const char* namespacedId, diff --git a/WeaveLoaderRuntime/src/Symbols/SymbolGroups.cpp b/WeaveLoaderRuntime/src/Symbols/SymbolGroups.cpp index 3699ad0..e2d739e 100644 --- a/WeaveLoaderRuntime/src/Symbols/SymbolGroups.cpp +++ b/WeaveLoaderRuntime/src/Symbols/SymbolGroups.cpp @@ -73,6 +73,10 @@ namespace static const char* SYM_PICKAXEITEM_CANDESTROYSPECIAL = "?canDestroySpecial@PickaxeItem@@UEAA_NPEAVTile@@@Z"; static const char* SYM_SHOVELITEM_GETDESTROYSPEED = "?getDestroySpeed@ShovelItem@@UEAAMV?$shared_ptr@VItemInstance@@@std@@PEAVTile@@@Z"; static const char* SYM_SHOVELITEM_CANDESTROYSPECIAL = "?canDestroySpecial@ShovelItem@@UEAA_NPEAVTile@@@Z"; + static const char* SYM_ITEMENTITY_GETITEM = "?getItem@ItemEntity@@QEAA?AV?$shared_ptr@VItemInstance@@@std@@XZ"; + static const char* SYM_ITEMRENDERER_RENDERGUIITEM = "?renderGuiItem@ItemRenderer@@QEAAXPEAVFont@@PEAVTextures@@V?$shared_ptr@VItemInstance@@@std@@MMMMM_N@Z"; + static const char* SYM_ITEMINHANDRENDERER_RENDER = "?render@ItemInHandRenderer@@QEAAXM@Z"; + static const char* SYM_ITEMINHANDRENDERER_RENDERITEM = "?renderItem@ItemInHandRenderer@@QEAAXV?$shared_ptr@VLivingEntity@@@std@@V?$shared_ptr@VItemInstance@@@3@H_N@Z"; static const char* SYM_TILE_ONPLACE = "?onPlace@Tile@@UEAAXPEAVLevel@@HHH@Z"; static const char* SYM_TILE_NEIGHBORCHANGED = "?neighborChanged@Tile@@UEAAXPEAVLevel@@HHHH@Z"; @@ -313,6 +317,10 @@ bool ItemSymbols::Resolve(SymbolResolver& resolver) pPickaxeItemCanDestroySpecial = resolver.Resolve(SYM_PICKAXEITEM_CANDESTROYSPECIAL); pShovelItemGetDestroySpeed = resolver.Resolve(SYM_SHOVELITEM_GETDESTROYSPEED); pShovelItemCanDestroySpecial = resolver.Resolve(SYM_SHOVELITEM_CANDESTROYSPECIAL); + pItemEntityGetItem = resolver.Resolve(SYM_ITEMENTITY_GETITEM); + pItemRendererRenderGuiItem = resolver.Resolve(SYM_ITEMRENDERER_RENDERGUIITEM); + pItemInHandRendererRender = resolver.Resolve(SYM_ITEMINHANDRENDERER_RENDER); + pItemInHandRendererRenderItem = resolver.Resolve(SYM_ITEMINHANDRENDERER_RENDERITEM); if (!pShovelItemGetDestroySpeed) pShovelItemGetDestroySpeed = resolver.ResolveExact("DiggerItem::getDestroySpeed"); return true; @@ -332,6 +340,10 @@ void ItemSymbols::Log() const LogSym("PickaxeItem::canDestroySpecial", pPickaxeItemCanDestroySpecial); LogSym("ShovelItem::getDestroySpeed", pShovelItemGetDestroySpeed); LogSym("ShovelItem::canDestroySpecial", pShovelItemCanDestroySpecial); + LogSym("ItemEntity::getItem", pItemEntityGetItem); + LogSym("ItemRenderer::renderGuiItem", pItemRendererRenderGuiItem); + LogSym("ItemInHandRenderer::render", pItemInHandRendererRender); + LogSym("ItemInHandRenderer::renderItem", pItemInHandRendererRenderItem); } bool TileSymbols::Resolve(SymbolResolver& resolver) diff --git a/WeaveLoaderRuntime/src/Symbols/SymbolGroups.h b/WeaveLoaderRuntime/src/Symbols/SymbolGroups.h index 8a3d138..a0f07af 100644 --- a/WeaveLoaderRuntime/src/Symbols/SymbolGroups.h +++ b/WeaveLoaderRuntime/src/Symbols/SymbolGroups.h @@ -87,6 +87,10 @@ struct ItemSymbols void* pPickaxeItemCanDestroySpecial = nullptr; void* pShovelItemGetDestroySpeed = nullptr; void* pShovelItemCanDestroySpecial = nullptr; + void* pItemEntityGetItem = nullptr; + void* pItemRendererRenderGuiItem = nullptr; + void* pItemInHandRendererRender = nullptr; + void* pItemInHandRendererRenderItem = nullptr; bool Resolve(SymbolResolver& resolver); void Log() const;