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:

+ *
    + *
  1. Swing - plays when the player swings or attacks with the item
  2. + *
  3. Use - plays while the player is using the item (holding right-click)
  4. + *
  5. Idle - loops continuously when no other animation is active
  6. + *
+ * + *

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(); + } +}