Working Multi-Use Item Stack Animation System for Scripted / Linked Items

This commit is contained in:
Kam committed 2026-03-13 04:33:45 -04:00
1 parent f40d4c4a11
commit eefbf99542
19 files changed
+1057 -114

No files matched your search

+3
View File
@@ -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/
Submodule GeckoLib updated: 9e8be5a5e6...95e7806635.
@@ -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());
}
@@ -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");
* </pre>
*
* @param item the customizable item (IItemCustom or IItemLinked)
@@ -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");
* </pre>
*
* @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.
@@ -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.
*
* <p>First-person animations play for the local player's held item view.
* Third-person animations play when other players see the item being held.</p>
*
* <p>Animations are played based on a priority system:</p>
* <ol>
* <li>Swing - plays when the player swings or attacks with the item</li>
* <li>Use - plays while the player is using the item (holding right-click)</li>
* <li>Idle - loops continuously when no other animation is active</li>
* </ol>
*
* <p>Obtain an instance via {@link IGeckoItemModel#getItemAnimation()}.</p>
*
* <p>Usage example:</p>
* <pre>
* 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);
* </pre>
*
* @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);
}
@@ -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<Integer, AnimState> 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();
}
}
@@ -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;
}
}
@@ -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<AnimatableStackWrapper> {
private static final ModelItemRenderUtil INSTANCE = new ModelItemRenderUtil();
private static final AnimatedGeoModel<AnimatableStackWrapper> 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<ItemStack, CustomItemModelData> 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<Integer, AnimatableStackWrapper> 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<ItemStack, AnimatableStackWrapper> 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<AnimatableStackWrapper> 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<AnimatableStackWrapper> 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;
}
}
@@ -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<AnimatableStackWrapper> {
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;
}
@@ -0,0 +1,7 @@
package com.goodbird.npcgecko.constants;
public enum EnumItemAnimType {
SWING,
USE_START,
USE_STOP
}
@@ -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 : ""; }
}
@@ -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<Integer, Integer> 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);
}
}
@@ -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 <P extends Item & IAnimatable> PlayState predicate(AnimationEvent<P> 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<Integer, Integer> lastProcessedSwingSeq = new HashMap<>();
private static final Map<Integer, Boolean> fpSwingActive = new HashMap<>();
public static <P extends IAnimatable> PlayState predicate(AnimationEvent<P> 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;
}
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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
@@ -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();
}
}