feat(items): item display transforms and custom renderers

This commit is contained in:
Jacobwasbeast
2026-03-12 01:51:16 -05:00
parent 7111d83082
commit a7a30ce83f
19 changed files with 869 additions and 1 deletions

View File

@@ -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<ModelBox> Boxes = new();
public Dictionary<ItemDisplayContext, ItemDisplayTransform> 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<ItemDisplayContext, ItemDisplayTransform>();
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<string>(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<ItemDisplayContext, ItemDisplayTransform> display, HashSet<string> 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<ItemDisplayContext, ItemDisplayTransform> 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<ModelBox> boxes)

View File

@@ -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<ItemDisplayContext, ItemDisplayTransform>? 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.
/// </summary>
public ItemProperties Model(string modelName) { ModelValue = modelName; return this; }
/// <summary>
/// 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.
/// </summary>
public ItemProperties DisplayTransform(ItemDisplayContext context, ItemDisplayTransform transform)
{
DisplayTransforms ??= new Dictionary<ItemDisplayContext, ItemDisplayTransform>();
DisplayTransforms[context] = transform;
return this;
}
/// <summary>Register a custom renderer for this item.</summary>
public ItemProperties Renderer(IItemRenderer renderer) { RendererValue = renderer; return this; }
/// <summary>Force the item to be treated as hand-equipped (sword-like pose) by the renderer.</summary>
public ItemProperties HandEquipped(bool handEquipped = true) { HandEquippedValue = handEquipped; return this; }
/// <summary>
/// Set max damage for a tool/armor item. Setting this to a positive value

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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<int, IItemRenderer> 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<ItemRenderNativeArgs>();
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<ItemRenderNativeArgs>(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;
}
}
}

View File

@@ -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,