diff --git a/.gitignore b/.gitignore
index da701c6..b4416c7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,3 +43,6 @@ src/main/resources/assets/npcgecko/api
# GitHub Pages build output
.github/pages-dist/
.github/pages/node_modules/
+
+# GeckoLib reference clone
+/tmp/
diff --git a/CustomNPC-Plus b/CustomNPC-Plus
index b231f65..17c48fa 160000
--- a/CustomNPC-Plus
+++ b/CustomNPC-Plus
@@ -1 +1 @@
-Subproject commit b231f65bfbd20ee950668d245fdd3cf94619f1aa
+Subproject commit 17c48fab97d1b6171a8f2b5004fc4f7f98ff76c7
diff --git a/GeckoLib b/GeckoLib
index 9e8be5a..95e7806 160000
--- a/GeckoLib
+++ b/GeckoLib
@@ -1 +1 @@
-Subproject commit 9e8be5a5e617fe8cbfd0366b75a319663b038e6c
+Subproject commit 95e7806635f50c5228ac82666445d561f24a509f
diff --git a/src/main/java/com/goodbird/npcgecko/CommonProxy.java b/src/main/java/com/goodbird/npcgecko/CommonProxy.java
index ff1bd03..d3cae5b 100644
--- a/src/main/java/com/goodbird/npcgecko/CommonProxy.java
+++ b/src/main/java/com/goodbird/npcgecko/CommonProxy.java
@@ -3,6 +3,7 @@ package com.goodbird.npcgecko;
import com.goodbird.npcgecko.api.AbstractGeckoAPI;
import com.goodbird.npcgecko.entity.EntityCustomModel;
import com.goodbird.npcgecko.handler.EventHandler;
+import com.goodbird.npcgecko.network.ItemAnimSyncable;
import com.goodbird.npcgecko.network.NetworkHandler;
import com.goodbird.npcgecko.tile.TileEntityCustomModel;
import cpw.mods.fml.common.event.FMLInitializationEvent;
@@ -22,6 +23,7 @@ public class CommonProxy {
public void init(FMLInitializationEvent ev) {
NetworkHandler.init();
+ ItemAnimSyncable.INSTANCE.register();
GameRegistry.registerTileEntity(TileEntityCustomModel.class, "custommodeltile");
NpcAPI.Instance().addGlobalObject("GeckoAPI", AbstractGeckoAPI.Instance());
}
diff --git a/src/main/java/com/goodbird/npcgecko/api/AbstractGeckoAPI.java b/src/main/java/com/goodbird/npcgecko/api/AbstractGeckoAPI.java
index 51e506d..3bb4a8b 100644
--- a/src/main/java/com/goodbird/npcgecko/api/AbstractGeckoAPI.java
+++ b/src/main/java/com/goodbird/npcgecko/api/AbstractGeckoAPI.java
@@ -300,7 +300,10 @@ public abstract class AbstractGeckoAPI {
* itemGecko.setModel("geckolib3:geo/sword.geo.json");
* itemGecko.setTexture("geckolib3:textures/model/sword.png");
* itemGecko.setAnimationFile("geckolib3:animations/sword.animation.json");
- * itemGecko.setIdleAnimation("animation.sword.idle");
+ *
+ * var anim = itemGecko.getItemAnimation();
+ * anim.setIdleAnimation("animation.sword.idle");
+ * anim.setAttackAnimation("animation.sword.attack");
*
*
* @param item the customizable item (IItemCustom or IItemLinked)
diff --git a/src/main/java/com/goodbird/npcgecko/api/IGeckoItemModel.java b/src/main/java/com/goodbird/npcgecko/api/IGeckoItemModel.java
index 90813bb..c33a8dd 100644
--- a/src/main/java/com/goodbird/npcgecko/api/IGeckoItemModel.java
+++ b/src/main/java/com/goodbird/npcgecko/api/IGeckoItemModel.java
@@ -16,7 +16,11 @@ package com.goodbird.npcgecko.api;
* itemGecko.setModel("geckolib3:geo/sword.geo.json");
* itemGecko.setTexture("geckolib3:textures/model/sword.png");
* itemGecko.setAnimationFile("geckolib3:animations/sword.animation.json");
- * itemGecko.setIdleAnimation("animation.sword.idle");
+ *
+ * var anim = itemGecko.getItemAnimation();
+ * anim.setIdleAnimation("animation.sword.idle");
+ * anim.setSwingAnimation("animation.sword.swing");
+ * anim.setAttackAnimation("animation.sword.attack");
*
*
* @see AbstractGeckoAPI#getItemGecko
@@ -76,20 +80,14 @@ public interface IGeckoItemModel {
void setAnimationFile(String animationFile);
/**
- * Get the idle animation name.
+ * Get the item animation configuration interface.
+ * Provides access to getters and setters for all animation types
+ * (idle, swing, attack, use).
*
- * @return the idle animation name, or an empty string if none is set
+ * @return the item animation interface
+ * @see IItemAnimation
*/
- String getIdleAnimation();
-
- /**
- * Set the idle animation name.
- * This animation plays continuously while the item is not performing other actions.
- * The animation must be defined in the currently assigned animation file.
- *
- * @param animation the animation name (e.g. "animation.sword.idle")
- */
- void setIdleAnimation(String animation);
+ IItemAnimation getItemAnimation();
/**
* Get the transition duration between animations in ticks.
diff --git a/src/main/java/com/goodbird/npcgecko/api/IItemAnimation.java b/src/main/java/com/goodbird/npcgecko/api/IItemAnimation.java
new file mode 100644
index 0000000..92473f4
--- /dev/null
+++ b/src/main/java/com/goodbird/npcgecko/api/IItemAnimation.java
@@ -0,0 +1,125 @@
+package com.goodbird.npcgecko.api;
+
+/**
+ * Interface for configuring per-perspective animation names on a GeckoLib item.
+ * Each animation type has a first-person and third-person variant.
+ *
+ *
First-person animations play for the local player's held item view.
+ * Third-person animations play when other players see the item being held.
+ *
+ * Animations are played based on a priority system:
+ *
+ * - Swing - plays when the player swings or attacks with the item
+ * - Use - plays while the player is using the item (holding right-click)
+ * - Idle - loops continuously when no other animation is active
+ *
+ *
+ * Obtain an instance via {@link IGeckoItemModel#getItemAnimation()}.
+ *
+ * Usage example:
+ *
+ * var anim = itemGecko.getItemAnimation();
+ * // Set for both perspectives
+ * anim.setIdleAnimation("animation.sword.idle");
+ * anim.setSwingAnimation("animation.sword.swing");
+ * // Set per-perspective (0 = first person, 1 = third person)
+ * anim.setSwingAnimation("animation.sword.swing_fp", 0);
+ * anim.setSwingAnimation("animation.sword.swing_tp", 1);
+ *
+ *
+ * @see IGeckoItemModel#getItemAnimation()
+ */
+public interface IItemAnimation {
+
+ /**
+ * Get the idle animation name for first person.
+ *
+ * @return the idle animation name, or an empty string if none is set
+ */
+ String getIdleAnimation();
+
+ /**
+ * Get the idle animation name for a specific perspective.
+ *
+ * @param perspective 0 for first person, 1 for third person
+ * @return the idle animation name, or an empty string if none is set
+ */
+ String getIdleAnimation(int perspective);
+
+ /**
+ * Set the idle animation name for both perspectives.
+ *
+ * @param animation the animation name, or an empty string to clear
+ */
+ void setIdleAnimation(String animation);
+
+ /**
+ * Set the idle animation name for a specific perspective.
+ *
+ * @param animation the animation name, or an empty string to clear
+ * @param perspective 0 for first person, 1 for third person
+ */
+ void setIdleAnimation(String animation, int perspective);
+
+ /**
+ * Get the swing animation name for first person.
+ *
+ * @return the swing animation name, or an empty string if none is set
+ */
+ String getSwingAnimation();
+
+ /**
+ * Get the swing animation name for a specific perspective.
+ *
+ * @param perspective 0 for first person, 1 for third person
+ * @return the swing animation name, or an empty string if none is set
+ */
+ String getSwingAnimation(int perspective);
+
+ /**
+ * Set the swing animation name for both perspectives.
+ * This animation plays once when the player swings or attacks with the item.
+ *
+ * @param animation the animation name, or an empty string to clear
+ */
+ void setSwingAnimation(String animation);
+
+ /**
+ * Set the swing animation name for a specific perspective.
+ *
+ * @param animation the animation name, or an empty string to clear
+ * @param perspective 0 for first person, 1 for third person
+ */
+ void setSwingAnimation(String animation, int perspective);
+
+ /**
+ * Get the use animation name for first person.
+ *
+ * @return the use animation name, or an empty string if none is set
+ */
+ String getUseAnimation();
+
+ /**
+ * Get the use animation name for a specific perspective.
+ *
+ * @param perspective 0 for first person, 1 for third person
+ * @return the use animation name, or an empty string if none is set
+ */
+ String getUseAnimation(int perspective);
+
+ /**
+ * Set the use animation name for both perspectives.
+ * This animation plays while the player is actively using the item.
+ *
+ * @param animation the animation name, or an empty string to clear
+ */
+ void setUseAnimation(String animation);
+
+ /**
+ * Set the use animation name for a specific perspective.
+ *
+ * @param animation the animation name, or an empty string to clear
+ * @param perspective 0 for first person, 1 for third person
+ */
+ void setUseAnimation(String animation, int perspective);
+}
diff --git a/src/main/java/com/goodbird/npcgecko/client/ItemAnimationStateTracker.java b/src/main/java/com/goodbird/npcgecko/client/ItemAnimationStateTracker.java
new file mode 100644
index 0000000..644bece
--- /dev/null
+++ b/src/main/java/com/goodbird/npcgecko/client/ItemAnimationStateTracker.java
@@ -0,0 +1,42 @@
+package com.goodbird.npcgecko.client;
+
+import com.goodbird.npcgecko.constants.EnumItemAnimType;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Client-side tracker for item animation states.
+ * Keyed by GeckoLibID (the stable unique ID assigned to each ItemStack).
+ * Tracks both the animation type and a sequence counter for swing re-triggering.
+ */
+public class ItemAnimationStateTracker {
+
+ public static class AnimState {
+ public final EnumItemAnimType type;
+ public final int sequence;
+
+ public AnimState(EnumItemAnimType type, int sequence) {
+ this.type = type;
+ this.sequence = sequence;
+ }
+ }
+
+ private static final Map activeAnimations = new ConcurrentHashMap<>();
+
+ public static void setAnimation(int geckoLibId, EnumItemAnimType type, int sequence) {
+ activeAnimations.put(geckoLibId, new AnimState(type, sequence));
+ }
+
+ public static void clearAnimation(int geckoLibId) {
+ activeAnimations.remove(geckoLibId);
+ }
+
+ public static AnimState getAnimation(int geckoLibId) {
+ return activeAnimations.get(geckoLibId);
+ }
+
+ public static void clear() {
+ activeAnimations.clear();
+ }
+}
diff --git a/src/main/java/com/goodbird/npcgecko/client/ItemRenderContext.java b/src/main/java/com/goodbird/npcgecko/client/ItemRenderContext.java
new file mode 100644
index 0000000..103284d
--- /dev/null
+++ b/src/main/java/com/goodbird/npcgecko/client/ItemRenderContext.java
@@ -0,0 +1,26 @@
+package com.goodbird.npcgecko.client;
+
+import com.goodbird.npcgecko.data.CustomItemModelData;
+import net.minecraft.entity.EntityLivingBase;
+import net.minecraftforge.client.IItemRenderer.ItemRenderType;
+
+/**
+ * Carries render context through the animation pipeline via wrapper userData.
+ * The predicate reads this to determine perspective (FP vs TP) and entity identity.
+ */
+public class ItemRenderContext {
+ public final CustomItemModelData modelData;
+ public final ItemRenderType renderType;
+ public final EntityLivingBase entity;
+ public final int entityId;
+ public final int slotIndex;
+
+ public ItemRenderContext(CustomItemModelData modelData, ItemRenderType renderType,
+ EntityLivingBase entity, int entityId, int slotIndex) {
+ this.modelData = modelData;
+ this.renderType = renderType;
+ this.entity = entity;
+ this.entityId = entityId;
+ this.slotIndex = slotIndex;
+ }
+}
diff --git a/src/main/java/com/goodbird/npcgecko/client/ModelItemRenderUtil.java b/src/main/java/com/goodbird/npcgecko/client/ModelItemRenderUtil.java
index 734ff79..b0963d7 100644
--- a/src/main/java/com/goodbird/npcgecko/client/ModelItemRenderUtil.java
+++ b/src/main/java/com/goodbird/npcgecko/client/ModelItemRenderUtil.java
@@ -4,24 +4,107 @@ import com.goodbird.npcgecko.client.model.ItemModelCustom;
import com.goodbird.npcgecko.data.CustomItemModelData;
import com.goodbird.npcgecko.data.ItemDisplayData;
import com.goodbird.npcgecko.data.ItemDisplayTransform;
+import com.goodbird.npcgecko.item.ItemCustomModelPredicate;
+import com.goodbird.npcgecko.network.ItemAnimSyncable;
+import net.geckominecraft.client.renderer.GlStateManager;
+import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.RenderHelper;
+import net.minecraft.entity.EntityLivingBase;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.util.ResourceLocation;
import net.minecraftforge.client.IItemRenderer.ItemRenderType;
import org.lwjgl.opengl.GL11;
+import software.bernie.geckolib3.core.IAnimatable;
+import software.bernie.geckolib3.core.IAnimatableModel;
+import software.bernie.geckolib3.core.PlayState;
+import software.bernie.geckolib3.core.controller.AnimationController;
+import software.bernie.geckolib3.core.event.predicate.AnimationEvent;
+import software.bernie.geckolib3.core.manager.AnimationData;
+import software.bernie.geckolib3.core.manager.AnimationFactory;
+import software.bernie.geckolib3.core.manager.SingletonAnimationFactory;
+import software.bernie.geckolib3.core.snapshot.BoneSnapshot;
+import software.bernie.geckolib3.core.util.Color;
+import software.bernie.geckolib3.geo.render.built.GeoBone;
+import software.bernie.geckolib3.geo.render.built.GeoModel;
import software.bernie.geckolib3.item.AnimatableStackWrapper;
-import software.bernie.geckolib3.renderers.geo.GeoItemStackRenderer;
+import software.bernie.geckolib3.model.AnimatedGeoModel;
+import software.bernie.geckolib3.renderers.geo.IGeoRenderer;
+import software.bernie.example.config.ConfigHandler;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
import java.util.WeakHashMap;
-public class ModelItemRenderUtil {
- private static final GeoItemStackRenderer RENDERER = new GeoItemStackRenderer(new ItemModelCustom());
+/**
+ * Central rendering orchestrator for gecko item models.
+ * Implements IGeoRenderer directly.
+ *
+ * Equipped items (FP/TP) use a shared SingletonAnimationFactory keyed by
+ * entity-slot composite ID, so different entities holding copies of the same
+ * item get independent animation state.
+ *
+ * INVENTORY/ENTITY renders skip animation entirely and reset bones to bind
+ * pose, preventing the shared mutable GeoModel from leaking equipped
+ * animation transforms into display renders.
+ */
+public class ModelItemRenderUtil implements IGeoRenderer {
+
+ private static final ModelItemRenderUtil INSTANCE = new ModelItemRenderUtil();
+ private static final AnimatedGeoModel MODEL_PROVIDER = new ItemModelCustom();
+
+ /**
+ * Separate factory for INVENTORY/ENTITY rendering.
+ * Prevents display-only renders from interfering with equipped animation state
+ * (they share no AnimationData since they use different factory instances).
+ */
+ private static final SingletonAnimationFactory displayFactory;
+
+ static {
+ // Model fetcher for AnimatableStackWrapper
+ AnimationController.addModelFetcher((IAnimatable object) -> {
+ if (object instanceof AnimatableStackWrapper) {
+ return (IAnimatableModel) MODEL_PROVIDER;
+ }
+ return null;
+ });
+
+ // Display factory — isolated from the equipped/sync factory
+ IAnimatable displayDummy = new IAnimatable() {
+ @Override
+ public void registerControllers(AnimationData data) {
+ data.addAnimationController(
+ new AnimationController<>(this, "item_controller", 0, e -> PlayState.STOP));
+ }
+ @Override
+ public AnimationFactory getFactory() { return displayFactory; }
+ };
+ displayFactory = new SingletonAnimationFactory(displayDummy);
+ }
+
private static final WeakHashMap modelDataCache = new WeakHashMap<>();
+ /**
+ * EQUIPPED cache: keyed by animId (entityId << 4 | slot).
+ * Each entity-slot combination gets its own persistent animation state,
+ * so copies of the same item on different entities animate independently.
+ */
+ private static final Map equippedWrapperCache = new HashMap<>();
+
+ /**
+ * INVENTORY/ENTITY cache: keyed by ItemStack identity.
+ * These wrappers are only used for model/texture lookup — no animation.
+ */
+ private static final WeakHashMap nonEquippedWrapperCache = new WeakHashMap<>();
+
+ // Current render state (set per-render, single-threaded)
+ private AnimatableStackWrapper currentWrapper;
+ private ItemStack currentItemStack;
+ private int currentAnimId;
+
/**
* Quick check if an ItemStack has a gecko model attached.
- * Reads directly from the ItemStack's root NBT tag (top-level "GeckoModelData")
- * to avoid NpcAPI cache dependencies that cause flickering when items are moved.
*/
public static boolean hasGeckoModel(ItemStack itemStack) {
if (itemStack == null || !itemStack.hasTagCompound()) return false;
@@ -30,47 +113,135 @@ public class ModelItemRenderUtil {
/**
* Undoes ForgeHooksClient's sprite path transforms and applies the simpler
- * EQUIPPED_BLOCK translate instead. This normalizes the matrix state so that
- * our EQUIPPED defaults work consistently regardless of arm bone orientation.
- *
- * Sprite path applies: translate(0,-0.3,0) → scale(1.5) → rotate(50,Y) → rotate(335,Z) → translate(-0.9375,-0.0625,0)
- * We undo in reverse, then apply the block path: translate(-0.5,-0.5,-0.5)
+ * EQUIPPED_BLOCK translate instead.
*/
private static void normalizeEquipped() {
- // Undo sprite path (reverse order)
GL11.glTranslatef(0.9375F, 0.0625F, 0.0F);
GL11.glRotatef(-335.0F, 0.0F, 0.0F, 1.0F);
GL11.glRotatef(-50.0F, 0.0F, 1.0F, 0.0F);
GL11.glScalef(1.0F / 1.5F, 1.0F / 1.5F, 1.0F / 1.5F);
GL11.glTranslatef(0.0F, 0.3F, 0.0F);
- // Apply old block path
GL11.glTranslatef(-0.5F, -0.5F, -0.5F);
}
/**
- * Attempts to render a gecko model for the given item.
- * Reads model data directly from the ItemStack's NBT to avoid NpcAPI cache
- * dependencies that cause flickering when items are moved in inventory.
+ * Computes a unique animation ID from entity ID and slot index.
+ * Different entities holding copies of the same item get different IDs.
*/
- public static boolean tryRender(ItemRenderType type, ItemStack itemStack) {
+ private static int computeAnimId(int entityId, int slotIndex) {
+ return (entityId << 4) | (slotIndex & 0xF);
+ }
+
+ /**
+ * Resets all bones in the model to their initial bind pose.
+ * Prevents animation transforms from the equipped render bleeding
+ * into INVENTORY/ENTITY renders via the shared cached GeoModel.
+ */
+ private static void resetBonesToBindPose(GeoModel model) {
+ for (GeoBone bone : model.topLevelBones) {
+ resetBoneRecursive(bone);
+ }
+ }
+
+ private static void resetBoneRecursive(GeoBone bone) {
+ BoneSnapshot initial = bone.getInitialSnapshot();
+ if (initial != null) {
+ bone.setPositionX(initial.positionOffsetX);
+ bone.setPositionY(initial.positionOffsetY);
+ bone.setPositionZ(initial.positionOffsetZ);
+ bone.setRotationX(initial.rotationValueX);
+ bone.setRotationY(initial.rotationValueY);
+ bone.setRotationZ(initial.rotationValueZ);
+ bone.setScaleX(initial.scaleValueX);
+ bone.setScaleY(initial.scaleValueY);
+ bone.setScaleZ(initial.scaleValueZ);
+ }
+ for (GeoBone child : bone.childBones) {
+ resetBoneRecursive(child);
+ }
+ }
+
+ /**
+ * Attempts to render a gecko model for the given item.
+ */
+ public static boolean tryRender(ItemRenderType type, ItemStack itemStack, Object[] data) {
if (itemStack == null || !itemStack.hasTagCompound()) return false;
NBTTagCompound root = itemStack.getTagCompound();
if (!root.hasKey("GeckoModelData")) return false;
+ // Always re-read from NBT so script changes (animations, transitions, etc.)
+ // take effect immediately without needing an ItemStack reference change.
CustomItemModelData modelData = modelDataCache.get(itemStack);
if (modelData == null) {
modelData = new CustomItemModelData();
- modelData.readFromNBT(root.getCompoundTag("GeckoModelData"));
modelDataCache.put(itemStack, modelData);
}
+ modelData.readFromNBT(root.getCompoundTag("GeckoModelData"));
+
+ // Extract entity for EQUIPPED types
+ EntityLivingBase entity = null;
+ int entityId = 0;
+ int slotIndex = -1;
+ if (type == ItemRenderType.EQUIPPED || type == ItemRenderType.EQUIPPED_FIRST_PERSON) {
+ // Try to find entity in the data array (CustomNPC+ may not follow
+ // Forge's standard data[0] = entity convention)
+ if (data != null) {
+ for (int i = 0; i < data.length; i++) {
+ if (data[i] instanceof EntityLivingBase) {
+ entity = (EntityLivingBase) data[i];
+ break;
+ }
+ }
+ }
+ // Fallback: for first-person, we know it's the local player
+ if (entity == null && type == ItemRenderType.EQUIPPED_FIRST_PERSON) {
+ entity = Minecraft.getMinecraft().thePlayer;
+ }
+ if (entity != null) {
+ entityId = entity.getEntityId();
+ if (entity instanceof net.minecraft.entity.player.EntityPlayer) {
+ slotIndex = ((net.minecraft.entity.player.EntityPlayer) entity).inventory.currentItem;
+ } else {
+ slotIndex = 0;
+ }
+ }
+ }
+
+ // Look up or create wrapper
+ AnimatableStackWrapper wrapper;
+ boolean isEquipped = (type == ItemRenderType.EQUIPPED || type == ItemRenderType.EQUIPPED_FIRST_PERSON);
+ int animId = 0;
+
+ if (isEquipped) {
+ animId = computeAnimId(entityId, slotIndex);
+ AnimationFactory sharedFactory = ItemAnimSyncable.INSTANCE.getSharedFactory();
+ wrapper = equippedWrapperCache.get(animId);
+ if (wrapper == null) {
+ int transitionTicks = modelData.getTransitionLengthTicks();
+ wrapper = AnimatableStackWrapper.of(itemStack, sharedFactory)
+ .withControllerRegistrar((w, d) -> {
+ d.addAnimationController(new AnimationController<>(w, "item_controller", transitionTicks, ItemCustomModelPredicate::predicate));
+ });
+ equippedWrapperCache.put(animId, wrapper);
+ } else if (wrapper.getStack() != itemStack) {
+ wrapper.setStack(itemStack);
+ }
+ wrapper.withUserData(new ItemRenderContext(modelData, type, entity, entityId, slotIndex));
+ } else {
+ // INVENTORY/ENTITY: no animation, just model/texture lookup
+ wrapper = nonEquippedWrapperCache.get(itemStack);
+ if (wrapper == null) {
+ wrapper = AnimatableStackWrapper.of(itemStack, displayFactory);
+ nonEquippedWrapperCache.put(itemStack, wrapper);
+ }
+ // userData is still needed for model/texture resolution
+ wrapper.withUserData(new ItemRenderContext(modelData, type, null, 0, -1));
+ }
- AnimatableStackWrapper wrapper = AnimatableStackWrapper.of(itemStack).withUserData(modelData);
ItemDisplayTransform transform = resolveTransform(modelData, type);
GL11.glPushMatrix();
- // For equipped types, undo the sprite path and apply the block path
- // so our tuned defaults work consistently across all arm orientations
if (type == ItemRenderType.EQUIPPED_FIRST_PERSON) {
normalizeEquipped();
}
@@ -79,26 +250,92 @@ public class ModelItemRenderUtil {
applyRotate(transform, type);
applyScale(transform, type);
- // Compensate for GeoItemStackRenderer's internal transforms:
- // translate(0, 0.01, 0) + translate(0.5, 0.5, 0.5) + rotate(90, Y)
- // Apply inverse so our tuned defaults remain the final matrix.
- GL11.glRotatef(-90, 0, 1, 0);
- GL11.glTranslatef(-0.5F, -0.51F, -0.5F);
-
- if(type == ItemRenderType.INVENTORY)
+ if (type == ItemRenderType.INVENTORY)
RenderHelper.disableStandardItemLighting();
- RENDERER.render(wrapper, itemStack);
- if(type == ItemRenderType.INVENTORY)
+ INSTANCE.renderItem(wrapper, itemStack, animId, isEquipped);
+
+ if (type == ItemRenderType.INVENTORY)
RenderHelper.enableStandardItemLighting();
-
GL11.glPopMatrix();
return true;
}
+ /**
+ * Drives GeckoLib model loading, animation ticking, and rendering.
+ *
+ * @param animate true for equipped items (tick animation), false for
+ * INVENTORY/ENTITY (reset to bind pose, no animation).
+ */
+ private void renderItem(AnimatableStackWrapper wrapper, ItemStack itemStack, int animId, boolean animate) {
+ this.currentWrapper = wrapper;
+ this.currentItemStack = itemStack;
+ this.currentAnimId = animId;
+
+ GeoModel model;
+ try {
+ model = MODEL_PROVIDER.getModel(MODEL_PROVIDER.getModelLocation(wrapper));
+ } catch (Exception e) {
+ if (ConfigHandler.debugPrintStacktraces) {
+ e.printStackTrace();
+ }
+ return;
+ }
+
+ if (animate) {
+ AnimationEvent itemEvent = new AnimationEvent<>(wrapper, 0, 0,
+ Minecraft.getMinecraft().timer.renderPartialTicks, false,
+ Collections.singletonList(itemStack));
+ wrapper.getFactory().getOrCreateAnimationData(animId, wrapper);
+ MODEL_PROVIDER.setLivingAnimations(wrapper, animId, itemEvent);
+ } else {
+ // Non-equipped (INVENTORY/ENTITY): no animation runs, so manually
+ // reset bones to bind pose to prevent stale transforms from the
+ // last equipped render bleeding into display renders.
+ resetBonesToBindPose(model);
+ }
+
+ GlStateManager.pushMatrix();
+ try {
+ Minecraft.getMinecraft().renderEngine.bindTexture(getTextureLocation(wrapper));
+ Color renderColor = getRenderColor(wrapper, 0f);
+ render(model, wrapper, 0, (float) renderColor.getRed() / 255f,
+ (float) renderColor.getGreen() / 255f,
+ (float) renderColor.getBlue() / 255f,
+ (float) renderColor.getAlpha() / 255f);
+ } catch (Exception e) {
+ if (ConfigHandler.debugPrintStacktraces) {
+ e.printStackTrace();
+ }
+ } finally {
+ GlStateManager.popMatrix();
+ }
+ }
+
+ @Override
+ public AnimatedGeoModel getGeoModelProvider() {
+ return MODEL_PROVIDER;
+ }
+
+ @Override
+ public ResourceLocation getTextureLocation(AnimatableStackWrapper instance) {
+ return MODEL_PROVIDER.getTextureLocation(instance);
+ }
+
+ @Override
+ public Integer getUniqueID(AnimatableStackWrapper animatable) {
+ // Return entity-slot animId, NOT the GeckoLibID from NBT.
+ // Duplicated stacks share the same GeckoLibID, so using it here
+ // would cause GeckoLib internals to cross-contaminate between entities.
+ return currentAnimId;
+ }
+
+ // ========================================================================
+ // Transform helpers (unchanged)
+ // ========================================================================
+
public static void applyTranslate(ItemDisplayTransform t, ItemRenderType type) {
- // Apply Default Positioning for 1.7.10
switch (type) {
case EQUIPPED_FIRST_PERSON:
GL11.glTranslatef(0.5F, 0.55F, 0.5F);
@@ -115,14 +352,12 @@ public class ModelItemRenderUtil {
default:
break;
}
-
if (t != null && t.hasTranslation()) {
GL11.glTranslatef(t.getTranslateX(), t.getTranslateY(), t.getTranslateZ());
}
}
public static void applyRotate(ItemDisplayTransform t, ItemRenderType type) {
- // Apply Default Rotationing for 1.7.10
switch (type) {
case EQUIPPED_FIRST_PERSON:
GL11.glRotatef(-45F, 0.0F, 1.0F, 0.0F);
@@ -142,7 +377,6 @@ public class ModelItemRenderUtil {
default:
break;
}
-
if (t != null && t.hasRotation()) {
GL11.glRotatef(t.getRotationY(), 0.0F, 1.0F, 0.0F);
GL11.glRotatef(t.getRotationX(), 1.0F, 0.0F, 0.0F);
@@ -151,7 +385,6 @@ public class ModelItemRenderUtil {
}
public static void applyScale(ItemDisplayTransform t, ItemRenderType type) {
- // Apply Default Scaling for 1.7.10
switch (type) {
case EQUIPPED_FIRST_PERSON:
float scale = 2.36f;
@@ -173,20 +406,10 @@ public class ModelItemRenderUtil {
if (t != null && t.hasScale()) {
GL11.glScalef(t.getScaleX(), t.getScaleY(), t.getScaleZ());
}
- // No per-context default scale — GeoItemRenderer doesn't scale per type
}
- /**
- * Resolves the display transform for a given render context.
- * Priority:
- * 1) Per-item NBT override (set via setItemDisplay)
- * 2) Display JSON file (set via setDisplayJSON)
- * 3) null — let the apply methods use their inline defaults
- */
public static ItemDisplayTransform resolveTransform(
CustomItemModelData modelData, ItemRenderType type) {
-
- // 1) Per-item NBT override
ItemDisplayTransform nbtTransform = null;
switch (type) {
case EQUIPPED_FIRST_PERSON: nbtTransform = modelData.getFirstPerson(); break;
@@ -197,7 +420,6 @@ public class ModelItemRenderUtil {
}
if (nbtTransform != null) return nbtTransform;
- // 2) Display JSON file (explicitly assigned via setDisplayJSON)
String displayFile = modelData.getDisplayFile();
if (displayFile != null && !displayFile.isEmpty()) {
ItemDisplayData fileData = ItemDisplayLoader.getInstance().getDisplayData(displayFile);
@@ -211,8 +433,6 @@ public class ModelItemRenderUtil {
}
}
}
-
- // 3) No transform found — apply methods will use inline defaults
return null;
}
}
diff --git a/src/main/java/com/goodbird/npcgecko/client/model/ItemModelCustom.java b/src/main/java/com/goodbird/npcgecko/client/model/ItemModelCustom.java
index b2fff3d..7168790 100644
--- a/src/main/java/com/goodbird/npcgecko/client/model/ItemModelCustom.java
+++ b/src/main/java/com/goodbird/npcgecko/client/model/ItemModelCustom.java
@@ -1,5 +1,6 @@
package com.goodbird.npcgecko.client.model;
+import com.goodbird.npcgecko.client.ItemRenderContext;
import com.goodbird.npcgecko.data.CustomItemModelData;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.ResourceLocation;
@@ -10,8 +11,11 @@ import software.bernie.geckolib3.resource.GeckoLibCache;
public class ItemModelCustom extends AnimatedGeoModel {
private CustomItemModelData getModelData(AnimatableStackWrapper animatable) {
- // Primary: read from userData attached by the mixin renderer
+ // Primary: read from ItemRenderContext attached by the mixin renderer
Object userData = animatable.getUserData();
+ if (userData instanceof ItemRenderContext) {
+ return ((ItemRenderContext) userData).modelData;
+ }
if (userData instanceof CustomItemModelData) {
return (CustomItemModelData) userData;
}
diff --git a/src/main/java/com/goodbird/npcgecko/constants/EnumItemAnimType.java b/src/main/java/com/goodbird/npcgecko/constants/EnumItemAnimType.java
new file mode 100644
index 0000000..8aba7f3
--- /dev/null
+++ b/src/main/java/com/goodbird/npcgecko/constants/EnumItemAnimType.java
@@ -0,0 +1,7 @@
+package com.goodbird.npcgecko.constants;
+
+public enum EnumItemAnimType {
+ SWING,
+ USE_START,
+ USE_STOP
+}
diff --git a/src/main/java/com/goodbird/npcgecko/data/CustomItemModelData.java b/src/main/java/com/goodbird/npcgecko/data/CustomItemModelData.java
index ba872d9..ea52d3a 100644
--- a/src/main/java/com/goodbird/npcgecko/data/CustomItemModelData.java
+++ b/src/main/java/com/goodbird/npcgecko/data/CustomItemModelData.java
@@ -6,8 +6,16 @@ public class CustomItemModelData {
private String model = "geckolib3:geo/sword.geo.json";
private String animFile = "geckolib3:animations/sword.animation.json";
private String texture = "geckolib3:textures/model/sword.png";
- private String idleAnim = "animation.sword.idle";
- private int transitionLengthTicks = 10;
+
+ // Per-perspective animation names
+ private String idleAnimFP = "animation.sword.idle";
+ private String idleAnimTP = "animation.sword.idle";
+ private String swingAnimFP = "";
+ private String swingAnimTP = "";
+ private String useAnimFP = "";
+ private String useAnimTP = "";
+
+ private int transitionLengthTicks = 0;
// Display JSON filename from item_displays/ (e.g. "sword.json"). Null = auto-match by model.
private String displayFile = "";
@@ -24,8 +32,13 @@ public class CustomItemModelData {
public NBTTagCompound writeToNBT(NBTTagCompound nbttagcompound) {
nbttagcompound.setString("Model", model);
nbttagcompound.setString("AnimFile", animFile);
- nbttagcompound.setString("IdleAnim", idleAnim);
nbttagcompound.setString("Texture", texture);
+ nbttagcompound.setString("IdleAnimFP", idleAnimFP);
+ nbttagcompound.setString("IdleAnimTP", idleAnimTP);
+ nbttagcompound.setString("SwingAnimFP", swingAnimFP);
+ nbttagcompound.setString("SwingAnimTP", swingAnimTP);
+ nbttagcompound.setString("UseAnimFP", useAnimFP);
+ nbttagcompound.setString("UseAnimTP", useAnimTP);
nbttagcompound.setInteger("TransitionLengthTicks", transitionLengthTicks);
if (displayFile != null && !displayFile.isEmpty()) {
nbttagcompound.setString("DisplayFile", displayFile);
@@ -43,22 +56,49 @@ public class CustomItemModelData {
return nbttagcompound;
}
- public void readFromNBT(NBTTagCompound nbttagcompound) {
- if (nbttagcompound.hasKey("Model")) {
- model = nbttagcompound.getString("Model");
- animFile = nbttagcompound.getString("AnimFile");
- idleAnim = nbttagcompound.getString("IdleAnim");
- texture = nbttagcompound.getString("Texture");
- if (nbttagcompound.hasKey("TransitionLengthTicks")) {
- transitionLengthTicks = nbttagcompound.getInteger("TransitionLengthTicks");
+ public void readFromNBT(NBTTagCompound nbt) {
+ if (nbt.hasKey("Model")) {
+ model = nbt.getString("Model");
+ animFile = nbt.getString("AnimFile");
+ texture = nbt.getString("Texture");
+
+ // New format: per-perspective keys
+ if (nbt.hasKey("IdleAnimFP")) {
+ idleAnimFP = nbt.getString("IdleAnimFP");
+ idleAnimTP = nbt.getString("IdleAnimTP");
+ swingAnimFP = nbt.getString("SwingAnimFP");
+ swingAnimTP = nbt.getString("SwingAnimTP");
+ useAnimFP = nbt.getString("UseAnimFP");
+ useAnimTP = nbt.getString("UseAnimTP");
+ } else if (nbt.hasKey("IdleAnim")) {
+ // Legacy format: single animation per type -> both perspectives
+ String idle = nbt.getString("IdleAnim");
+ idleAnimFP = idle;
+ idleAnimTP = idle;
+ String swing = nbt.hasKey("SwingAnim") ? nbt.getString("SwingAnim") : "";
+ swingAnimFP = swing;
+ swingAnimTP = swing;
+ // Legacy attack maps to swing
+ if (swingAnimFP.isEmpty() && nbt.hasKey("AttackAnim")) {
+ String attack = nbt.getString("AttackAnim");
+ swingAnimFP = attack;
+ swingAnimTP = attack;
+ }
+ String use = nbt.hasKey("UseAnim") ? nbt.getString("UseAnim") : "";
+ useAnimFP = use;
+ useAnimTP = use;
}
- if (nbttagcompound.hasKey("DisplayFile")) {
- displayFile = nbttagcompound.getString("DisplayFile");
+
+ if (nbt.hasKey("TransitionLengthTicks")) {
+ transitionLengthTicks = nbt.getInteger("TransitionLengthTicks");
+ }
+ if (nbt.hasKey("DisplayFile")) {
+ displayFile = nbt.getString("DisplayFile");
}
}
- if (nbttagcompound.hasKey("Display")) {
- NBTTagCompound displayTag = nbttagcompound.getCompoundTag("Display");
+ if (nbt.hasKey("Display")) {
+ NBTTagCompound displayTag = nbt.getCompoundTag("Display");
if (displayTag.hasKey("FirstPerson")) {
firstPerson = new ItemDisplayTransform();
firstPerson.readFromNBT(displayTag.getCompoundTag("FirstPerson"));
@@ -97,7 +137,7 @@ public class CustomItemModelData {
public String getDisplayFile() { return displayFile; }
public void setDisplayFile(String displayFile) { this.displayFile = displayFile != null ? displayFile : ""; }
- // --- Existing getters/setters ---
+ // --- Model/Anim/Texture ---
public String getModel() { return model; }
public void setModel(String model) { this.model = model; }
@@ -105,12 +145,29 @@ public class CustomItemModelData {
public String getAnimFile() { return animFile; }
public void setAnimFile(String animFile) { this.animFile = animFile; }
- public String getIdleAnim() { return idleAnim; }
- public void setIdleAnim(String idleAnim) { this.idleAnim = idleAnim; }
+ public String getTexture() { return texture; }
+ public void setTexture(String texture) { this.texture = texture; }
public int getTransitionLengthTicks() { return transitionLengthTicks; }
public void setTransitionLengthTicks(int transitionLengthTicks) { this.transitionLengthTicks = transitionLengthTicks; }
- public String getTexture() { return texture; }
- public void setTexture(String texture) { this.texture = texture; }
+ // --- Per-perspective animation getters/setters ---
+
+ public String getIdleAnimFP() { return idleAnimFP; }
+ public void setIdleAnimFP(String v) { this.idleAnimFP = v != null ? v : ""; }
+
+ public String getIdleAnimTP() { return idleAnimTP; }
+ public void setIdleAnimTP(String v) { this.idleAnimTP = v != null ? v : ""; }
+
+ public String getSwingAnimFP() { return swingAnimFP; }
+ public void setSwingAnimFP(String v) { this.swingAnimFP = v != null ? v : ""; }
+
+ public String getSwingAnimTP() { return swingAnimTP; }
+ public void setSwingAnimTP(String v) { this.swingAnimTP = v != null ? v : ""; }
+
+ public String getUseAnimFP() { return useAnimFP; }
+ public void setUseAnimFP(String v) { this.useAnimFP = v != null ? v : ""; }
+
+ public String getUseAnimTP() { return useAnimTP; }
+ public void setUseAnimTP(String v) { this.useAnimTP = v != null ? v : ""; }
}
diff --git a/src/main/java/com/goodbird/npcgecko/handler/EventHandler.java b/src/main/java/com/goodbird/npcgecko/handler/EventHandler.java
index 65aba2f..a17e8a7 100644
--- a/src/main/java/com/goodbird/npcgecko/handler/EventHandler.java
+++ b/src/main/java/com/goodbird/npcgecko/handler/EventHandler.java
@@ -1,17 +1,52 @@
package com.goodbird.npcgecko.handler;
+import com.goodbird.npcgecko.constants.EnumItemAnimType;
import com.goodbird.npcgecko.constants.EnumSyncAutoAnim;
+import com.goodbird.npcgecko.data.CustomItemModelData;
+import com.goodbird.npcgecko.mixin.IScriptCustomItem;
import com.goodbird.npcgecko.network.CPacketSyncAutoAnim;
+import com.goodbird.npcgecko.network.ItemAnimSyncable;
import com.goodbird.npcgecko.network.NetworkHandler;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
+import net.minecraft.entity.EntityLivingBase;
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.entity.player.EntityPlayerMP;
+import net.minecraft.item.ItemStack;
+import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.entity.EntityNPCInterface;
+import noppes.npcs.scripted.event.ItemEvent;
import noppes.npcs.scripted.event.NpcEvent;
+import software.bernie.geckolib3.network.GeckoLibNetwork;
+
+import java.util.HashMap;
+import java.util.Map;
public class EventHandler {
+
+ /**
+ * Server-side swing sequence counter per syncId (entityId << 4 | slot).
+ * Incremented on each swing so the client can detect new swings
+ * even when the tracker already has SWING state.
+ */
+ private static final Map swingSequences = new HashMap<>();
+
+ private static int nextSwingSequence(int syncId) {
+ int seq = swingSequences.getOrDefault(syncId, 0) + 1;
+ swingSequences.put(syncId, seq);
+ return seq;
+ }
+
+ // ========================================================================
+ // NPC Events
+ // ========================================================================
+
@SubscribeEvent
public void onNpcMeleeAttack(NpcEvent.SwingEvent event) {
- if (event != null && event.npc != null && event.npc.getMCEntity() != null)
- NetworkHandler.sendToAll(new CPacketSyncAutoAnim((EntityNPCInterface) event.npc.getMCEntity(), EnumSyncAutoAnim.MELEE_ATTACK));
+ if (event == null || event.npc == null || event.npc.getMCEntity() == null) return;
+ EntityNPCInterface npc = (EntityNPCInterface) event.npc.getMCEntity();
+ NetworkHandler.sendToAll(new CPacketSyncAutoAnim(npc, EnumSyncAutoAnim.MELEE_ATTACK));
+ // Also sync NPC's held item swing animation (if it has a gecko model)
+ syncHeldItemAnim(npc, EnumItemAnimType.SWING);
}
@SubscribeEvent
@@ -25,4 +60,106 @@ public class EventHandler {
if (event != null && event.npc != null && event.npc.getMCEntity() != null)
NetworkHandler.sendToAll(new CPacketSyncAutoAnim((EntityNPCInterface) event.npc.getMCEntity(), EnumSyncAutoAnim.HURT));
}
+
+ // ========================================================================
+ // Item Events — uses GeckoLib ISyncable/GeckoLibNetwork
+ // ========================================================================
+
+ @SubscribeEvent
+ public void onItemAttack(ItemEvent.AttackEvent event) {
+ if (event == null || event.item == null) return;
+ if (!(event.item instanceof IScriptCustomItem)) return;
+ CustomItemModelData data = ((IScriptCustomItem) event.item).getCustomModelData();
+ if (data == null) return;
+
+ String swingFP = data.getSwingAnimFP();
+ String swingTP = data.getSwingAnimTP();
+ boolean hasSwing = (swingFP != null && !swingFP.isEmpty()) || (swingTP != null && !swingTP.isEmpty());
+ if (hasSwing) {
+ syncItemAnim(event.swingingEntity, EnumItemAnimType.SWING);
+ }
+ }
+
+ @SubscribeEvent
+ public void onItemStartUsing(ItemEvent.StartUsingItem event) {
+ if (event == null || event.item == null || event.player == null) return;
+ if (!(event.item instanceof IScriptCustomItem)) return;
+ CustomItemModelData data = ((IScriptCustomItem) event.item).getCustomModelData();
+ if (data == null) return;
+
+ String useFP = data.getUseAnimFP();
+ String useTP = data.getUseAnimTP();
+ boolean hasUse = (useFP != null && !useFP.isEmpty()) || (useTP != null && !useTP.isEmpty());
+ if (hasUse) {
+ syncItemAnimForPlayer(event.player, EnumItemAnimType.USE_START);
+ }
+ }
+
+ @SubscribeEvent
+ public void onItemStopUsing(ItemEvent.StopUsingItem event) {
+ if (event == null || event.player == null) return;
+ syncItemAnimForPlayer(event.player, EnumItemAnimType.USE_STOP);
+ }
+
+ @SubscribeEvent
+ public void onItemFinishUsing(ItemEvent.FinishUsingItem event) {
+ if (event == null || event.player == null) return;
+ syncItemAnimForPlayer(event.player, EnumItemAnimType.USE_STOP);
+ }
+
+ // ========================================================================
+ // Helpers — use GeckoLibNetwork.syncAnimationToAll via ItemAnimSyncable
+ // ========================================================================
+
+ /**
+ * Syncs item animation for a scripted item event entity (player or NPC).
+ */
+ private void syncItemAnim(noppes.npcs.api.entity.IEntity entity, EnumItemAnimType type) {
+ if (entity == null) return;
+ net.minecraft.entity.Entity mcEntity = entity.getMCEntity();
+ if (mcEntity instanceof EntityPlayerMP) {
+ doSyncPlayer((EntityPlayerMP) mcEntity, type);
+ } else if (mcEntity instanceof EntityLivingBase) {
+ syncHeldItemAnim((EntityLivingBase) mcEntity, type);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private void syncItemAnimForPlayer(IPlayer> iPlayer, EnumItemAnimType type) {
+ if (iPlayer == null) return;
+ EntityPlayer mcPlayer = (EntityPlayer) iPlayer.getMCEntity();
+ if (!(mcPlayer instanceof EntityPlayerMP)) return;
+ doSyncPlayer((EntityPlayerMP) mcPlayer, type);
+ }
+
+ /**
+ * Syncs a player's held item animation using their current hotbar slot.
+ */
+ private void doSyncPlayer(EntityPlayerMP player, EnumItemAnimType type) {
+ int slot = player.inventory.currentItem;
+ ItemStack stack = player.inventory.getStackInSlot(slot);
+ if (stack == null) return;
+
+ int syncId = (player.getEntityId() << 4) | (slot & 0xF);
+ int sequence = (type == EnumItemAnimType.SWING) ? nextSwingSequence(syncId) : 0;
+ int state = ItemAnimSyncable.encodeState(type, sequence);
+
+ GeckoLibNetwork.syncAnimationToAll(ItemAnimSyncable.INSTANCE, syncId, state);
+ }
+
+ /**
+ * Syncs a non-player entity's held item animation (e.g., NPC).
+ * Uses slot 0 since non-player entities don't have hotbar slots.
+ */
+ private void syncHeldItemAnim(EntityLivingBase entity, EnumItemAnimType type) {
+ ItemStack heldItem = entity.getHeldItem();
+ if (heldItem == null || !heldItem.hasTagCompound()) return;
+ if (!heldItem.getTagCompound().hasKey("GeckoModelData")) return;
+
+ int syncId = (entity.getEntityId() << 4) | 0;
+ int sequence = (type == EnumItemAnimType.SWING) ? nextSwingSequence(syncId) : 0;
+ int state = ItemAnimSyncable.encodeState(type, sequence);
+
+ GeckoLibNetwork.syncAnimationToAll(ItemAnimSyncable.INSTANCE, syncId, state);
+ }
}
diff --git a/src/main/java/com/goodbird/npcgecko/item/ItemCustomModelPredicate.java b/src/main/java/com/goodbird/npcgecko/item/ItemCustomModelPredicate.java
index 204e4a9..1e89f4c 100644
--- a/src/main/java/com/goodbird/npcgecko/item/ItemCustomModelPredicate.java
+++ b/src/main/java/com/goodbird/npcgecko/item/ItemCustomModelPredicate.java
@@ -1,29 +1,146 @@
package com.goodbird.npcgecko.item;
+import com.goodbird.npcgecko.client.ItemAnimationStateTracker;
+import com.goodbird.npcgecko.client.ItemRenderContext;
+import com.goodbird.npcgecko.constants.EnumItemAnimType;
import com.goodbird.npcgecko.data.CustomItemModelData;
-import com.goodbird.npcgecko.mixin.IScriptCustomItem;
-import net.minecraft.item.Item;
-import net.minecraft.item.ItemStack;
-import noppes.npcs.api.item.IItemCustomizable;
-import noppes.npcs.api.item.IItemStack;
-import noppes.npcs.scripted.NpcAPI;
+import net.minecraft.client.Minecraft;
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraftforge.client.IItemRenderer.ItemRenderType;
+import software.bernie.geckolib3.core.AnimationState;
import software.bernie.geckolib3.core.IAnimatable;
import software.bernie.geckolib3.core.PlayState;
import software.bernie.geckolib3.core.builder.AnimationBuilder;
+import software.bernie.geckolib3.core.controller.AnimationController;
import software.bernie.geckolib3.core.event.predicate.AnimationEvent;
+import software.bernie.geckolib3.item.AnimatableStackWrapper;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Animation predicate for gecko item models (equipped only).
+ * Uses a priority chain: swing > use > idle.
+ *
+ * All tracking maps are keyed by animId (entityId << 4 | slot), NOT geckoLibId.
+ * This ensures copies of the same item on different entities animate independently.
+ *
+ * INVENTORY/ENTITY rendering skips animation entirely (bones are reset to bind
+ * pose in ModelItemRenderUtil), so this predicate is never called for those types.
+ */
public class ItemCustomModelPredicate {
- public static PlayState predicate(AnimationEvent
event) {
- ItemStack stack = (ItemStack) event.getExtraData().get(0);
- IItemStack istack = NpcAPI.Instance().getIItemStack(stack);
- if (!(istack instanceof IItemCustomizable)) return PlayState.STOP;
- if (!(istack instanceof IScriptCustomItem)) return PlayState.STOP;
- IScriptCustomItem item = (IScriptCustomItem) istack;
- CustomItemModelData data = item.getCustomModelData();
+
+ private static final Map lastProcessedSwingSeq = new HashMap<>();
+ private static final Map fpSwingActive = new HashMap<>();
+
+ public static PlayState predicate(AnimationEvent
event) {
+ P animatable = event.getAnimatable();
+ if (!(animatable instanceof AnimatableStackWrapper)) return PlayState.STOP;
+ AnimatableStackWrapper wrapper = (AnimatableStackWrapper) animatable;
+
+ Object userData = wrapper.getUserData();
+ if (!(userData instanceof ItemRenderContext)) return PlayState.STOP;
+ ItemRenderContext ctx = (ItemRenderContext) userData;
+ CustomItemModelData data = ctx.modelData;
if (data == null) return PlayState.STOP;
- String idleAnim = data.getIdleAnim();
- if (idleAnim == null || idleAnim.isEmpty()) return PlayState.STOP;
- event.getController().setAnimation(new AnimationBuilder().loop(idleAnim));
- return PlayState.CONTINUE;
+
+ AnimationController> controller = event.getController();
+ ItemRenderType renderType = ctx.renderType;
+ int animId = (ctx.entityId << 4) | (ctx.slotIndex & 0xF);
+
+ boolean isFirstPerson = (renderType == ItemRenderType.EQUIPPED_FIRST_PERSON);
+
+ // Keep transition length in sync with model data (script may change it at runtime)
+ controller.transitionLengthTicks = data.getTransitionLengthTicks();
+
+ // Select perspective-specific animations
+ String swingAnim = isFirstPerson ? data.getSwingAnimFP() : data.getSwingAnimTP();
+ String useAnim = isFirstPerson ? data.getUseAnimFP() : data.getUseAnimTP();
+ String idleAnim = isFirstPerson ? data.getIdleAnimFP() : data.getIdleAnimTP();
+
+ if (isFirstPerson) {
+ // ================================================================
+ // FIRST PERSON: local player, read state directly from client
+ // ================================================================
+ EntityPlayer localPlayer = Minecraft.getMinecraft().thePlayer;
+ if (localPlayer == null) return PlayState.STOP;
+
+ Boolean fpActive = fpSwingActive.get(animId);
+ boolean swingActive = fpActive != null && fpActive;
+
+ // --- Swing (allows interruption: new clicks restart the animation) ---
+ boolean isSwinging = localPlayer.isSwingInProgress;
+ if (isSwinging && swingAnim != null && !swingAnim.isEmpty()) {
+ // Start OR restart the swing animation on every new click
+ fpSwingActive.put(animId, true);
+ localPlayer.isSwingInProgress = false;
+ localPlayer.swingProgressInt = 0;
+ controller.markNeedsReload();
+ controller.setAnimation(new AnimationBuilder().playOnce(swingAnim));
+ return PlayState.CONTINUE;
+ }
+
+ if (swingActive) {
+ if (controller.getAnimationState() == AnimationState.Stopped) {
+ fpSwingActive.put(animId, false);
+ // Fall through to use/idle
+ } else {
+ // Swing still playing, no new click — suppress vanilla swing
+ localPlayer.isSwingInProgress = false;
+ localPlayer.swingProgressInt = 0;
+ return PlayState.CONTINUE;
+ }
+ }
+
+ // --- Use ---
+ if (localPlayer.isUsingItem() && useAnim != null && !useAnim.isEmpty()) {
+ controller.setAnimation(new AnimationBuilder().loop(useAnim));
+ return PlayState.CONTINUE;
+ }
+
+ } else {
+ // ================================================================
+ // THIRD PERSON: read from ItemAnimationStateTracker (keyed by animId)
+ // ================================================================
+
+ // Clear any stale FP swing flag — prevents getting stuck when
+ // toggling F5 (FP→TP→FP) while a swing was active
+ fpSwingActive.remove(animId);
+
+ ItemAnimationStateTracker.AnimState trackerState = ItemAnimationStateTracker.getAnimation(animId);
+
+ // --- Swing (sequence-based detection) ---
+ if (trackerState != null && trackerState.type == EnumItemAnimType.SWING
+ && swingAnim != null && !swingAnim.isEmpty()) {
+ Integer lastSeq = lastProcessedSwingSeq.get(animId);
+ if (lastSeq == null || lastSeq != trackerState.sequence) {
+ // New swing detected
+ lastProcessedSwingSeq.put(animId, trackerState.sequence);
+ controller.markNeedsReload();
+ controller.setAnimation(new AnimationBuilder().playOnce(swingAnim));
+ return PlayState.CONTINUE;
+ }
+ // Same swing, still playing
+ if (controller.getAnimationState() != AnimationState.Stopped) {
+ return PlayState.CONTINUE;
+ }
+ // Swing finished, fall through to idle
+ }
+
+ // --- Use ---
+ if (trackerState != null && trackerState.type == EnumItemAnimType.USE_START
+ && useAnim != null && !useAnim.isEmpty()) {
+ controller.setAnimation(new AnimationBuilder().loop(useAnim));
+ return PlayState.CONTINUE;
+ }
+ }
+
+ // --- Idle (lowest priority, both perspectives) ---
+ if (idleAnim != null && !idleAnim.isEmpty()) {
+ controller.setAnimation(new AnimationBuilder().loop(idleAnim));
+ return PlayState.CONTINUE;
+ }
+
+ return PlayState.STOP;
}
}
diff --git a/src/main/java/com/goodbird/npcgecko/mixin/impl/MixinCustomItemRenderer.java b/src/main/java/com/goodbird/npcgecko/mixin/impl/MixinCustomItemRenderer.java
index 9110088..73e3fc4 100644
--- a/src/main/java/com/goodbird/npcgecko/mixin/impl/MixinCustomItemRenderer.java
+++ b/src/main/java/com/goodbird/npcgecko/mixin/impl/MixinCustomItemRenderer.java
@@ -38,7 +38,7 @@ public class MixinCustomItemRenderer {
remap = false
)
public void renderItem(ItemRenderType type, ItemStack itemStack, Object[] data, CallbackInfo ci) {
- if (ModelItemRenderUtil.tryRender(type, itemStack)) {
+ if (ModelItemRenderUtil.tryRender(type, itemStack, data)) {
ci.cancel();
}
}
diff --git a/src/main/java/com/goodbird/npcgecko/network/ItemAnimSyncable.java b/src/main/java/com/goodbird/npcgecko/network/ItemAnimSyncable.java
new file mode 100644
index 0000000..95d432b
--- /dev/null
+++ b/src/main/java/com/goodbird/npcgecko/network/ItemAnimSyncable.java
@@ -0,0 +1,79 @@
+package com.goodbird.npcgecko.network;
+
+import com.goodbird.npcgecko.client.ItemAnimationStateTracker;
+import com.goodbird.npcgecko.constants.EnumItemAnimType;
+import software.bernie.geckolib3.core.IAnimatable;
+import software.bernie.geckolib3.core.PlayState;
+import software.bernie.geckolib3.core.controller.AnimationController;
+import software.bernie.geckolib3.core.manager.AnimationData;
+import software.bernie.geckolib3.core.manager.AnimationFactory;
+import software.bernie.geckolib3.core.manager.SingletonAnimationFactory;
+import software.bernie.geckolib3.network.GeckoLibNetwork;
+import software.bernie.geckolib3.network.ISyncable;
+
+/**
+ * Singleton ISyncable that handles animation sync for ALL dynamic/scripted items.
+ * Owns the shared SingletonAnimationFactory used by all AnimatableStackWrappers.
+ *
+ * State encoding: (sequence << 8) | (type.ordinal() & 0xFF)
+ * This allows the client to detect new swings by comparing sequence numbers.
+ */
+public class ItemAnimSyncable implements ISyncable {
+ public static final ItemAnimSyncable INSTANCE = new ItemAnimSyncable();
+ private static final String SYNC_KEY = "npcgecko:item_anim";
+
+ private final SingletonAnimationFactory factory;
+
+ private ItemAnimSyncable() {
+ // Dummy animatable for the factory constructor — actual controllers
+ // are registered per-ID by the AnimatableStackWrapper via the
+ // two-argument getOrCreateAnimationData(id, wrapper) at render time.
+ IAnimatable dummy = new IAnimatable() {
+ @Override
+ public void registerControllers(AnimationData data) {
+ data.addAnimationController(
+ new AnimationController<>(this, "item_controller", 0, e -> PlayState.STOP));
+ }
+ @Override
+ public AnimationFactory getFactory() { return factory; }
+ };
+ this.factory = new SingletonAnimationFactory(dummy);
+ }
+
+ public void register() {
+ GeckoLibNetwork.registerSyncable(this);
+ }
+
+ @Override
+ public String getSyncKey() {
+ return SYNC_KEY;
+ }
+
+ @Override
+ public void onAnimationSync(int id, int state) {
+ int animTypeOrdinal = state & 0xFF;
+ int sequence = state >>> 8;
+
+ EnumItemAnimType[] values = EnumItemAnimType.values();
+ if (animTypeOrdinal < 0 || animTypeOrdinal >= values.length) return;
+ EnumItemAnimType type = values[animTypeOrdinal];
+
+ if (type == EnumItemAnimType.USE_STOP) {
+ ItemAnimationStateTracker.clearAnimation(id);
+ } else {
+ ItemAnimationStateTracker.setAnimation(id, type, sequence);
+ }
+ }
+
+ public AnimationFactory getSharedFactory() {
+ return factory;
+ }
+
+ /**
+ * Encodes an EnumItemAnimType and sequence counter into a single int
+ * for the ISyncable state parameter.
+ */
+ public static int encodeState(EnumItemAnimType type, int sequence) {
+ return (sequence << 8) | (type.ordinal() & 0xFF);
+ }
+}
diff --git a/src/main/java/com/goodbird/npcgecko/wrapper/GeckoItemModelWrapper.java b/src/main/java/com/goodbird/npcgecko/wrapper/GeckoItemModelWrapper.java
index 65712b7..1d37839 100644
--- a/src/main/java/com/goodbird/npcgecko/wrapper/GeckoItemModelWrapper.java
+++ b/src/main/java/com/goodbird/npcgecko/wrapper/GeckoItemModelWrapper.java
@@ -1,5 +1,6 @@
package com.goodbird.npcgecko.wrapper;
+import com.goodbird.npcgecko.api.IItemAnimation;
import com.goodbird.npcgecko.api.IGeckoItemModel;
import com.goodbird.npcgecko.data.CustomItemModelData;
import com.goodbird.npcgecko.data.ItemDisplayTransform;
@@ -91,15 +92,8 @@ public class GeckoItemModelWrapper implements IGeckoItemModel {
}
@Override
- public String getIdleAnimation() {
- CustomItemModelData data = scriptItem.getCustomModelData();
- return data != null ? data.getIdleAnim() : "";
- }
-
- @Override
- public void setIdleAnimation(String animation) {
- getOrCreateData().setIdleAnim(animation);
- persist();
+ public IItemAnimation getItemAnimation() {
+ return new ItemAnimationWrapper(item);
}
@Override
diff --git a/src/main/java/com/goodbird/npcgecko/wrapper/ItemAnimationWrapper.java b/src/main/java/com/goodbird/npcgecko/wrapper/ItemAnimationWrapper.java
new file mode 100644
index 0000000..fb8e6ec
--- /dev/null
+++ b/src/main/java/com/goodbird/npcgecko/wrapper/ItemAnimationWrapper.java
@@ -0,0 +1,129 @@
+package com.goodbird.npcgecko.wrapper;
+
+import com.goodbird.npcgecko.api.IItemAnimation;
+import com.goodbird.npcgecko.data.CustomItemModelData;
+import com.goodbird.npcgecko.mixin.IScriptCustomItem;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NBTTagCompound;
+import noppes.npcs.api.item.IItemCustomizable;
+import noppes.npcs.scripted.item.ScriptCustomizableItem;
+
+public class ItemAnimationWrapper implements IItemAnimation {
+ private final IItemCustomizable item;
+ private final IScriptCustomItem scriptItem;
+
+ public ItemAnimationWrapper(IItemCustomizable item) {
+ this.item = item;
+ this.scriptItem = (IScriptCustomItem) item;
+ }
+
+ private CustomItemModelData getOrCreateData() {
+ if (!scriptItem.hasCustomModel()) {
+ scriptItem.setCustomModelData(new CustomItemModelData());
+ }
+ return scriptItem.getCustomModelData();
+ }
+
+ private void persist() {
+ ((ScriptCustomizableItem) item).saveItemData();
+ CustomItemModelData data = scriptItem.getCustomModelData();
+ ItemStack stack = ((ScriptCustomizableItem) item).getMCItemStack();
+ if (data != null && stack != null) {
+ NBTTagCompound root = stack.getTagCompound();
+ if (root == null) { root = new NBTTagCompound(); stack.setTagCompound(root); }
+ NBTTagCompound tag = new NBTTagCompound();
+ data.writeToNBT(tag);
+ root.setTag("GeckoModelData", tag);
+ }
+ }
+
+ // --- Idle ---
+
+ @Override
+ public String getIdleAnimation() { return getIdleAnimation(0); }
+
+ @Override
+ public String getIdleAnimation(int perspective) {
+ CustomItemModelData data = scriptItem.getCustomModelData();
+ if (data == null) return "";
+ return perspective == 0 ? data.getIdleAnimFP() : data.getIdleAnimTP();
+ }
+
+ @Override
+ public void setIdleAnimation(String animation) {
+ CustomItemModelData data = getOrCreateData();
+ String v = animation != null ? animation : "";
+ data.setIdleAnimFP(v);
+ data.setIdleAnimTP(v);
+ persist();
+ }
+
+ @Override
+ public void setIdleAnimation(String animation, int perspective) {
+ CustomItemModelData data = getOrCreateData();
+ String v = animation != null ? animation : "";
+ if (perspective == 0) data.setIdleAnimFP(v);
+ else data.setIdleAnimTP(v);
+ persist();
+ }
+
+ // --- Swing ---
+
+ @Override
+ public String getSwingAnimation() { return getSwingAnimation(0); }
+
+ @Override
+ public String getSwingAnimation(int perspective) {
+ CustomItemModelData data = scriptItem.getCustomModelData();
+ if (data == null) return "";
+ return perspective == 0 ? data.getSwingAnimFP() : data.getSwingAnimTP();
+ }
+
+ @Override
+ public void setSwingAnimation(String animation) {
+ CustomItemModelData data = getOrCreateData();
+ String v = animation != null ? animation : "";
+ data.setSwingAnimFP(v);
+ data.setSwingAnimTP(v);
+ persist();
+ }
+
+ @Override
+ public void setSwingAnimation(String animation, int perspective) {
+ CustomItemModelData data = getOrCreateData();
+ String v = animation != null ? animation : "";
+ if (perspective == 0) data.setSwingAnimFP(v);
+ else data.setSwingAnimTP(v);
+ persist();
+ }
+
+ // --- Use ---
+
+ @Override
+ public String getUseAnimation() { return getUseAnimation(0); }
+
+ @Override
+ public String getUseAnimation(int perspective) {
+ CustomItemModelData data = scriptItem.getCustomModelData();
+ if (data == null) return "";
+ return perspective == 0 ? data.getUseAnimFP() : data.getUseAnimTP();
+ }
+
+ @Override
+ public void setUseAnimation(String animation) {
+ CustomItemModelData data = getOrCreateData();
+ String v = animation != null ? animation : "";
+ data.setUseAnimFP(v);
+ data.setUseAnimTP(v);
+ persist();
+ }
+
+ @Override
+ public void setUseAnimation(String animation, int perspective) {
+ CustomItemModelData data = getOrCreateData();
+ String v = animation != null ? animation : "";
+ if (perspective == 0) data.setUseAnimFP(v);
+ else data.setUseAnimTP(v);
+ persist();
+ }
+}