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.
208 lines
7.9 KiB
Java
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;
|
|
}
|
|
}
|
|
}
|