mirror of
https://github.com/KAMKEEL/CustomNPC-Gecko-Addon.git
synced 2026-11-13 04:17:37 +00:00
Working Multi-Use Item Stack Animation System for Scripted / Linked Items
This commit is contained in:
1 parent
f40d4c4a11
commit
eefbf99542
19 files changed
+1057
-114
No files matched your search
@@ -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/
|
||||
+1
-1
Submodule CustomNPC-Plus updated: b231f65bfb...17c48fab97.
+1
-1
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();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user