Files
TiedUp-/src/main/java/com/tiedup/remake/mixin/client/MixinVillagerEntityBaseModelMCA.java
NotEvil 355e2936c9 Refactor V2 animation, furniture, and GLTF rendering
Broad consolidation of the V2 bondage-item, furniture-entity, and
client-side GLTF pipeline.

Parsing and rendering
  - Shared GLB parsing helpers consolidated into GlbParserUtils
    (accessor reads, weight normalization, joint-index clamping,
    coordinate-space conversion, animation parse, primitive loop).
  - Grow-on-demand Matrix4f[] scratch pool in GltfSkinningEngine and
    GltfLiveBoneReader — removes per-frame joint-matrix allocation
    from the render hot path.
  - emitVertex helper dedups three parallel loops in GltfMeshRenderer.
  - TintColorResolver.resolve has a zero-alloc path when the item
    declares no tint channels.
  - itemAnimCache bounded to 256 entries (access-order LRU) with
    atomic get-or-compute under the map's monitor.

Animation correctness
  - First-in-joint-order wins when body and torso both map to the
    same PlayerAnimator slot; duplicate writes log a single WARN.
  - Multi-item composites honor the FullX / FullHeadX opt-in that
    the single-item path already recognized.
  - Seat transforms converted to Minecraft model-def space so
    asymmetric furniture renders passengers at the correct offset.
  - GlbValidator: IBM count / type / presence, JOINTS_0 presence,
    animation channel target validation, multi-skin support.

Furniture correctness and anti-exploit
  - Seat assignment synced via SynchedEntityData (server is
    authoritative; eliminates client-server divergence on multi-seat).
  - Force-mount authorization requires same dimension and a free
    seat; cross-dimension distance checks rejected.
  - Reconnection on login checks for seat takeover before re-mount
    and force-loads the target chunk for cross-dimension cases.
  - tiedup_furniture_lockpick_ctx carries a session UUID nonce so
    stale context can't misroute a body-item lockpick.
  - tiedup_locked_furniture survives death without keepInventory
    (Forge 1.20.1 does not auto-copy persistent data on respawn).

Lifecycle and memory
  - EntityCleanupHandler fans EntityLeaveLevelEvent out to every
    per-entity state map on the client.
  - DogPoseRenderHandler re-keyed by UUID (stable across dimension
    change; entity int ids are recycled).
  - PetBedRenderHandler, PlayerArmHideEventHandler, and
    HeldItemHideHandler use receiveCanceled + sentinel sets so
    Pre-time mutations are restored even when a downstream handler
    cancels the render.

Tests
  - JUnit harness with 76+ tests across GlbParserUtils, GltfPoseConverter,
    FurnitureSeatGeometry, and FurnitureAuthPredicate.
2026-04-18 17:34:03 +02:00

208 lines
7.9 KiB
Java

package com.tiedup.remake.mixin.client;
import com.tiedup.remake.client.animation.BondageAnimationManager;
import com.tiedup.remake.client.animation.StaticPoseApplier;
import com.tiedup.remake.client.animation.util.AnimationIdBuilder;
import com.tiedup.remake.compat.mca.MCACompat;
import com.tiedup.remake.items.base.PoseType;
import com.tiedup.remake.v2.bondage.BindModeHelper;
import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import dev.kosmx.playerAnim.impl.IAnimatedPlayer;
import dev.kosmx.playerAnim.impl.animation.AnimationApplier;
import java.util.UUID;
import net.minecraft.client.model.HumanoidModel;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Pseudo;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Mixin for MCA's VillagerEntityBaseModelMCA to apply tied poses.
*
* <p>This mixin injects at the end of setupAnim (m_6973_) to override MCA's default
* animations when a villager is tied up. Without this, the bondage render layer
* shows the tied pose but the underlying MCA model still shows normal walking/idle
* animations.
*
* <p>Uses @Pseudo annotation - mixin is optional and will be skipped if MCA is not installed.
*
* <p>Target class: net.mca.client.model.VillagerEntityBaseModelMCA
* <p>Target method: setupAnim (MCP name, remapped from Yarn by Architectury)
*/
@Pseudo
@Mixin(
targets = "forge.net.mca.client.model.VillagerEntityBaseModelMCA",
remap = false
)
public class MixinVillagerEntityBaseModelMCA<T extends LivingEntity> {
// Note: Tick tracking moved to MCAAnimationTickCache for cleanup on world unload
/**
* Inject at the end of setupAnim to apply tied pose after MCA has set its animations.
*
* <p>This completely overrides arm/leg positions when the villager is tied up.
*
* <p>Method signature: void setupAnim(T entity, float limbSwing, float limbSwingAmount, float ageInTicks, float netHeadYaw, float headPitch)
* <p>Note: MCA uses Architectury which remaps Yarn's method names to Forge/MCP names
*/
@Inject(method = "m_6973_", at = @At("TAIL"), remap = false)
private void tiedup$applyTiedPose(
T villager,
float limbSwing,
float limbSwingAmount,
float ageInTicks,
float netHeadYaw,
float headPitch,
CallbackInfo ci
) {
// Only process on client side
if (villager.level() == null || !villager.level().isClientSide()) {
return;
}
// Check if MCA is loaded and this villager is tied
if (!MCACompat.isMCALoaded()) {
return;
}
IBondageState state = MCACompat.getKidnappedState(villager);
if (state == null || !state.isTiedUp()) {
return;
}
// Get pose info from bind item
ItemStack bind = state.getEquipment(BodyRegionV2.ARMS);
PoseType poseType = PoseTypeHelper.getPoseType(bind);
// Derive bound state from V2 regions, fallback to V1 bind mode NBT
boolean armsBound = V2EquipmentHelper.isRegionOccupied(
villager,
BodyRegionV2.ARMS
);
boolean legsBound = V2EquipmentHelper.isRegionOccupied(
villager,
BodyRegionV2.LEGS
);
if (!armsBound && !legsBound && BindModeHelper.isBindItem(bind)) {
armsBound = BindModeHelper.hasArmsBound(bind);
legsBound = BindModeHelper.hasLegsBound(bind);
}
// MCA doesn't track struggling state - use false for now
// TODO: Add struggling support to MCA integration
boolean isStruggling = false;
// Cast this mixin to HumanoidModel to apply pose
// MCA's VillagerEntityBaseModelMCA extends HumanoidModel
@SuppressWarnings("unchecked")
HumanoidModel<?> model = (HumanoidModel<?>) (Object) this;
// Check if villager supports PlayerAnimator (via our mixin)
if (villager instanceof IAnimatedPlayer animated) {
// Build animation ID and play animation
String animId = AnimationIdBuilder.build(
poseType,
armsBound,
legsBound,
null,
isStruggling,
true
);
BondageAnimationManager.playAnimation(villager, animId);
// Dedup: tick the animation stack once per game tick, not per
// render frame. Use tickCount (discrete server tick), not the
// partial-tick-interpolated ageInTicks.
int currentTick = villager.tickCount;
UUID entityId = villager.getUUID();
int lastTick =
com.tiedup.remake.client.animation.tick.MCAAnimationTickCache.getLastTick(
entityId
);
if (lastTick != currentTick) {
// New game tick - tick the animation
animated.getAnimationStack().tick();
com.tiedup.remake.client.animation.tick.MCAAnimationTickCache.setLastTick(
entityId,
currentTick
);
}
// Apply animation transforms to model parts
AnimationApplier emote = animated.playerAnimator_getAnimation();
if (emote != null && emote.isActive()) {
// Use correct PlayerAnimator part names (torso, not body)
emote.updatePart("head", model.head);
emote.updatePart("torso", model.body);
emote.updatePart("leftArm", model.leftArm);
emote.updatePart("rightArm", model.rightArm);
emote.updatePart("leftLeg", model.leftLeg);
emote.updatePart("rightLeg", model.rightLeg);
// Force rotations using setRotation to ensure they're applied
model.rightArm.setRotation(
model.rightArm.xRot,
model.rightArm.yRot,
model.rightArm.zRot
);
model.leftArm.setRotation(
model.leftArm.xRot,
model.leftArm.yRot,
model.leftArm.zRot
);
model.rightLeg.setRotation(
model.rightLeg.xRot,
model.rightLeg.yRot,
model.rightLeg.zRot
);
model.leftLeg.setRotation(
model.leftLeg.xRot,
model.leftLeg.yRot,
model.leftLeg.zRot
);
model.body.setRotation(
model.body.xRot,
model.body.yRot,
model.body.zRot
);
model.head.setRotation(
model.head.xRot,
model.head.yRot,
model.head.zRot
);
} else {
// Fallback to static poses if animation not active
StaticPoseApplier.applyStaticPose(
model,
poseType,
armsBound,
legsBound
);
}
} else {
// Fallback: entity doesn't support PlayerAnimator, use static poses
StaticPoseApplier.applyStaticPose(
model,
poseType,
armsBound,
legsBound
);
}
// Hide arms for WRAP/LATEX_SACK poses (like DamselModel does)
if (poseType == PoseType.WRAP || poseType == PoseType.LATEX_SACK) {
model.leftArm.visible = false;
model.rightArm.visible = false;
}
}
}