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. * *

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. * *

Uses @Pseudo annotation - mixin is optional and will be skipped if MCA is not installed. * *

Target class: net.mca.client.model.VillagerEntityBaseModelMCA *

Target method: setupAnim (MCP name, remapped from Yarn by Architectury) */ @Pseudo @Mixin( targets = "forge.net.mca.client.model.VillagerEntityBaseModelMCA", remap = false ) public class MixinVillagerEntityBaseModelMCA { // 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. * *

This completely overrides arm/leg positions when the villager is tied up. * *

Method signature: void setupAnim(T entity, float limbSwing, float limbSwingAmount, float ageInTicks, float netHeadYaw, float headPitch) *

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; } } }