package com.tiedup.remake.state; import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.entities.LeashProxyEntity; import com.tiedup.remake.items.base.ILockable; import com.tiedup.remake.network.sync.SyncManager; import com.tiedup.remake.state.components.PlayerCaptivity; import com.tiedup.remake.state.components.PlayerClothesPermission; import com.tiedup.remake.state.components.PlayerDataRetrieval; import com.tiedup.remake.state.components.PlayerEquipment; import com.tiedup.remake.state.components.PlayerLifecycle; import com.tiedup.remake.state.components.PlayerMovement; import com.tiedup.remake.state.components.PlayerSale; import com.tiedup.remake.state.components.PlayerShockCollar; import com.tiedup.remake.state.components.PlayerSpecialActions; import com.tiedup.remake.state.components.PlayerStateQuery; import com.tiedup.remake.state.components.PlayerStruggle; import com.tiedup.remake.state.components.PlayerTaskManagement; import com.tiedup.remake.state.hosts.IPlayerBindStateHost; import com.tiedup.remake.util.RestraintEffectUtils; import com.tiedup.remake.util.teleport.Position; import com.tiedup.remake.v2.BodyRegionV2; import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import org.jetbrains.annotations.Nullable; /** * Core state management for player restraints and bondage equipment. * Singleton pattern - one instance per player, tracked by UUID. * * Based on original PlayerBindState from 1.12.2. * * Responsibilities: * - Track player restraint states (tied, gagged, blindfolded, etc.) * - Manage bondage equipment lifecycle (put on/take off items) * - Lifecycle management (connection, death, respawn) * - Enslavement lifecycle (can be enslaved, act as master) * - Advanced collar features (shocks, GPS tracking) * * Thread Safety: All mutating methods must be called from the server thread * (via enqueueWork for packet handlers). The only cross-thread access is * the StruggleSnapshot (atomic volatile record) and the Player reference (volatile). * The instances map uses ConcurrentHashMap for thread-safe lookup. * * Refactoring: Component-Host pattern */ public class PlayerBindState implements IRestrainable, IPlayerBindStateHost { // ========== Singleton Pattern ========== /** Server-side instances - one per player. Thread-safe map with atomic computeIfAbsent. */ private static final Map instances = new ConcurrentHashMap<>(); /** Client-side instances - one per player. Thread-safe map with atomic computeIfAbsent. */ private static final Map instancesClient = new ConcurrentHashMap<>(); // Note: playersBeingFreed removed - no longer needed with proxy-based leash system /** * Get or create a PlayerBindState instance for a player. * * @param player The player entity * @return The state instance associated with this player */ @Nullable public static PlayerBindState getInstance(Player player) { if (player == null) return null; Map map = player.level().isClientSide ? instancesClient : instances; UUID uuid = player.getUUID(); PlayerBindState state = map.computeIfAbsent(uuid, k -> new PlayerBindState(player) ); // This fixes the bug where remote players' animations don't appear after observer reconnects if (state.player != player) { state.player = player; } return state; } /** * Clears all client-side instances. * Must be called on world unload to prevent memory leaks from stale player references. */ public static void clearClientInstances() { instancesClient.clear(); } /** * Cleans up the instance map when a player leaves the server. */ public static void removeInstance(UUID uuid, boolean isClient) { Map map = isClient ? instancesClient : instances; map.remove(uuid); } // ========== Instance Fields ========== private final UUID playerUUID; // volatile: accessed from multiple threads (network, tick, render) private volatile Player player; private boolean online; // ========== Core Components ========== private final PlayerTaskManagement taskManagement; private final PlayerStateQuery stateQuery; private final PlayerDataRetrieval dataRetrieval; private final PlayerSale sale; // ========== Struggle Components ========== private final PlayerSpecialActions specialActions; private final PlayerClothesPermission clothesPermission; // ========== Animation Components ========== private final PlayerEquipment equipment; private final PlayerStruggle struggle; private final PlayerShockCollar shockCollar; // ========== Captivity Components ========== private final PlayerLifecycle lifecycle; private final PlayerCaptivity captivity; private ICaptor captor; private PlayerCaptorManager captorManager; // Note: transport field removed - now using IPlayerLeashAccess mixin // Struggle animation state — atomic snapshot for cross-thread visibility. // Server thread writes via setStruggling(), render thread reads via isStruggling()/getStruggleStartTick(). // Single volatile reference guarantees both fields are visible atomically. private record StruggleSnapshot(boolean struggling, long startTick) {} private volatile StruggleSnapshot struggleState = new StruggleSnapshot(false, 0); // ========== Movement Style State ========== // Encapsulated in PlayerMovement component. Managed exclusively by MovementStyleManager. private final PlayerMovement movement; /** Access movement style state (hop, crawl, position tracking, etc.). */ public PlayerMovement getMovement() { return movement; } /** Access task management component (tying, untying, feeding tasks). */ public PlayerTaskManagement tasks() { return taskManagement; } /** Access struggle component (struggle, tighten, cooldowns). */ public PlayerStruggle getStruggle() { return struggle; } /** * Resets all movement style state to defaults. * Called on death, logout, and dimension change to ensure clean re-activation. */ public void clearMovementState() { movement.clear(); } // ========== Constructor ========== private PlayerBindState(Player player) { this.playerUUID = player.getUUID(); this.player = player; this.online = true; // Initialize core components this.taskManagement = new PlayerTaskManagement(); this.stateQuery = new PlayerStateQuery(this); this.dataRetrieval = new PlayerDataRetrieval(this); this.sale = new PlayerSale(); // Initialize struggle components this.specialActions = new PlayerSpecialActions(this); this.clothesPermission = new PlayerClothesPermission(this); // Initialize animation components this.equipment = new PlayerEquipment(this); this.struggle = new PlayerStruggle(this); this.shockCollar = new PlayerShockCollar(this); // Initialize captivity components this.lifecycle = new PlayerLifecycle(this); this.captivity = new PlayerCaptivity(this); // Initialize movement component this.movement = new PlayerMovement(); this.captor = null; this.captorManager = new PlayerCaptorManager(player); } // ========== State Query Methods ========== // Delegated to PlayerStateQuery component /** Check if player has ropes/ties equipped. */ @Override public boolean isTiedUp() { return stateQuery.isTiedUp(); } /** Check if player is currently gagged. */ public boolean isGagged() { return stateQuery.isGagged(); } /** Check if player is blindfolded. */ public boolean isBlindfolded() { return stateQuery.isBlindfolded(); } /** Check if player has earplugs. */ public boolean hasEarplugs() { return stateQuery.hasEarplugs(); } public boolean isEarplugged() { return stateQuery.isEarplugged(); } /** Check if player is wearing a collar. */ @Override public boolean hasCollar() { return stateQuery.hasCollar(); } /** Returns the current collar ItemStack, or empty if none. */ public ItemStack getCurrentCollar() { return stateQuery.getCurrentCollar(); } public boolean hasClothes() { return stateQuery.hasClothes(); } /** Check if player has mittens equipped. Mittens system */ public boolean hasMittens() { return stateQuery.hasMittens(); } // ========== Item Management Methods ========== // Delegated to PlayerEquipment component /** Equips a bind item and applies speed reduction. */ public void putBindOn(ItemStack bind) { equipment.putBindOn(bind); } /** Equips a gag (enables gag talk if implemented). Issue #14 fix: now calls onEquipped. */ public void putGagOn(ItemStack gag) { equipment.putGagOn(gag); } /** Equips a blindfold (restricts vision). Issue #14 fix: now calls onEquipped. */ public void putBlindfoldOn(ItemStack blindfold) { equipment.putBlindfoldOn(blindfold); } /** Equips a collar (starts GPS/Shock monitoring). Issue #14 fix: now calls onEquipped. */ public void putCollarOn(ItemStack collar) { equipment.putCollarOn(collar); } /** Issue #14 fix: now calls onEquipped. Syncs clothes config to all clients. */ public void putClothesOn(ItemStack clothes) { equipment.putClothesOn(clothes); } /** Equips mittens (blocks hand interactions). Mittens system. Issue #14 fix: now calls onEquipped. */ public void putMittensOn(ItemStack mittens) { equipment.putMittensOn(mittens); } /** Removes binds and restores speed. */ public ItemStack takeBindOff() { return equipment.takeBindOff(); } public ItemStack takeGagOff() { return equipment.takeGagOff(); } public ItemStack takeBlindfoldOff() { return equipment.takeBlindfoldOff(); } /** Removes mittens. Mittens system */ public ItemStack takeMittensOff() { return equipment.takeMittensOff(); } /** Helper to drop an item at the kidnapped player's feet. */ public void kidnappedDropItem(ItemStack stack) { equipment.kidnappedDropItem(stack); } // ========== Lifecycle Methods ========== /** * Resets the player instance upon reconnection or respawn. * Delegated to PlayerLifecycle component. * * IMPORTANT: Handle transport restoration for pole binding. * With proxy-based leash system, leashes are not persisted through disconnection. * The leash proxy is ephemeral and will be recreated if needed. * * Leg Binding: Speed reduction based on hasLegsBound(), not isTiedUp(). */ public void resetNewConnection(Player player) { // Update player reference (must be done here in host) this.player = player; // Clear movement style state so it re-resolves on the new entity // (transient AttributeModifiers are lost when the entity is recreated, // e.g., after dimension change or respawn) clearMovementState(); // Delegate to lifecycle component lifecycle.resetNewConnection(player); } // onDeathKidnapped() removed - consolidated into IRestrainable override version below // ========== IPlayerBindStateHost Implementation ========== // Note: Some methods are implemented elsewhere in the class with @Override @Override public boolean isOnline() { return online; } @Override public void setOnline(boolean online) { this.online = online; } @Override public Player getPlayer() { return player; } @Override public UUID getPlayerUUID() { return playerUUID; } @Override public Level getLevel() { return player != null ? player.level() : null; } @Override public boolean isClientSide() { Level level = getLevel(); return level != null && level.isClientSide; } @Override public void syncClothesConfig() { SyncManager.syncClothesConfig(player); } @Override public void syncEnslavement() { SyncManager.syncEnslavement(player); } @Override public IRestrainable getKidnapped() { return this; // PlayerBindState implements IRestrainable } @Override public void setCaptor(ICaptor captor) { this.captor = captor; } // Note: getCaptor(), getCaptorManager(), setStruggling(), setStrugglingClient(), // isStruggling(), getStruggleStartTick() are implemented elsewhere in the class // ========== Struggle & Resistance Methods ========== // Non-delegation methods only — delegation removed, use getStruggle() accessor. // tighten(Player) is interface-mandated by ICoercible — cannot be removed. /** Restores resistance to base values when a master tightens the ties. */ public void tighten(Player tightener) { struggle.tighten(tightener); } // ========== v2.5: Knife Cut Target Methods ========== // Delegated to PlayerSpecialActions component /** * Set the body region target for knife cutting. * Used when player selects "Cut" from StruggleChoiceScreen. * * @param region The body region to cut (NECK, MOUTH, etc.) */ public void setKnifeCutTarget(BodyRegionV2 region) { specialActions.setKnifeCutTarget(region); } /** * Get the current knife cut target region. * * @return The target region, or null if none */ public BodyRegionV2 getKnifeCutTarget() { return specialActions.getKnifeCutTarget(); } /** * Clear the knife cut target. */ public void clearKnifeCutTarget() { specialActions.clearKnifeCutTarget(); } /** * Check if player is currently playing struggle animation. * Thread-safe: reads from atomic StruggleSnapshot. */ @Override public boolean isStruggling() { return struggleState.struggling(); } /** * Get the tick when struggle animation started. * Thread-safe: reads from atomic StruggleSnapshot. */ @Override public long getStruggleStartTick() { return struggleState.startTick(); } /** * Set struggle animation state (server-side). * Single volatile write ensures both fields are visible atomically to render thread. */ @Override public void setStruggling(boolean struggling, long currentTick) { this.struggleState = new StruggleSnapshot( struggling, struggling ? currentTick : struggleState.startTick() ); } /** * Set struggle animation flag (client-side only). * Used by network sync - does NOT update timer (server manages timer). */ @Override public void setStrugglingClient(boolean struggling) { StruggleSnapshot current = this.struggleState; this.struggleState = new StruggleSnapshot(struggling, current.startTick()); } /** * Delegated to PlayerEquipment component */ @Override public int getCurrentBindResistance() { return equipment.getCurrentBindResistance(); } /** * Delegated to PlayerEquipment component. * Thread safety: must be called from the server thread. */ @Override public void setCurrentBindResistance(int resistance) { equipment.setCurrentBindResistance(resistance); } /** * Delegated to PlayerEquipment component. * Thread safety: must be called from the server thread. */ @Override public int getCurrentCollarResistance() { return equipment.getCurrentCollarResistance(); } /** * Delegated to PlayerEquipment component. * Thread safety: must be called from the server thread. */ @Override public void setCurrentCollarResistance(int resistance) { equipment.setCurrentCollarResistance(resistance); } /** * Initiates the capture process by a captor. * Uses the proxy-based leash system (player is NOT mounted). * Delegated to PlayerCaptivity component. */ @Override public boolean getCapturedBy(ICaptor newCaptor) { return captivity.getCapturedBy(newCaptor); } @Override public void free() { captivity.free(); } /** Ends captivity and detaches the leash proxy. * Delegated to PlayerCaptivity component. */ @Override public void free(boolean dropLead) { captivity.free(dropLead); } /** * Delegated to PlayerCaptivity component. */ @Override public void transferCaptivityTo(ICaptor newCaptor) { captivity.transferCaptivityTo(newCaptor); } @Override public boolean isEnslavable() { return captivity.isEnslavable(); } /** * Delegated to PlayerCaptivity component. */ @Override public boolean isCaptive() { return captivity.isCaptive(); } /** * Also implements IPlayerBindStateHost. */ @Override public ICaptor getCaptor() { return captor; } @Override public LeashProxyEntity getTransport() { return captivity.getTransport(); } public ItemStack takeCollarOff() { return takeCollarOff(false); } /** * Tries to remove the collar. Fails if locked unless forced. * Delegated to PlayerEquipment component */ public ItemStack takeCollarOff(boolean force) { return equipment.takeCollarOff(force); } /** Checks if the wearer has a collar with the 'locked' NBT flag set. */ @Override public boolean hasLockedCollar() { ItemStack collar = getCurrentCollar(); return ( !collar.isEmpty() && collar.getItem() instanceof ILockable lockable && lockable.isLocked(collar) ); } @Override public UUID getKidnappedUniqueId() { return playerUUID; } @Override public String getKidnappedName() { return player.getName().getString(); } // Delegated to PlayerShockCollar component @Override public void shockKidnapped() { shockCollar.shockKidnapped(); } /** * Triggers a visual and auditory shock effect. * Damage is applied (shock can kill). */ @Override public void shockKidnapped(@Nullable String messageAddon, float damage) { shockCollar.shockKidnapped(messageAddon, damage); } /** * Periodically monitors captivity validity. * Simplified: If any condition is invalid, free the captive immediately. * Delegated to PlayerCaptivity component. */ public void checkStillCaptive() { captivity.checkStillCaptive(); } /** * Periodic check for Auto-Shock intervals and GPS Safe Zones. * Called from RestraintTaskTickHandler every player tick. * Delegated to PlayerShockCollar component. */ public void checkAutoShockCollar() { shockCollar.checkAutoShockCollar(); } /** * Force-stops and clears any active shock timers. * Delegated to PlayerShockCollar component. */ public void resetAutoShockTimer() { shockCollar.resetAutoShockTimer(); } /** * Manager for capturing other entities (acting as captor). * Also implements IPlayerBindStateHost. */ @Override public PlayerCaptorManager getCaptorManager() { return captorManager; } // IRestrainable Missing Methods @Override public void teleportToPosition(Position position) { if (player == null || position == null) return; // Check if dimension change is needed if (!player.level().dimension().equals(position.getDimension())) { // Cross-dimension teleport net.minecraft.server.level.ServerPlayer serverPlayer = (net.minecraft.server.level.ServerPlayer) player; net.minecraft.server.level.ServerLevel targetLevel = serverPlayer.server.getLevel(position.getDimension()); if (targetLevel != null) { serverPlayer.teleportTo( targetLevel, position.getX(), position.getY(), position.getZ(), serverPlayer.getYRot(), serverPlayer.getXRot() ); } } else { // Same dimension teleport player.teleportTo( position.getX(), position.getY(), position.getZ() ); } TiedUpMod.LOGGER.debug( "[PlayerBindState] Teleported {} to {}", player.getName().getString(), position ); } @Override public String getNameFromCollar() { return dataRetrieval.getNameFromCollar(); } // V2 Region-Based Equipment Access @Override public ItemStack getEquipment(BodyRegionV2 region) { return V2EquipmentHelper.getInRegion(player, region); } @Override public void equip(BodyRegionV2 region, ItemStack stack) { switch (region) { case ARMS -> equipment.putBindOn(stack); case MOUTH -> equipment.putGagOn(stack); case EYES -> equipment.putBlindfoldOn(stack); case EARS -> equipment.putEarplugsOn(stack); case NECK -> equipment.putCollarOn(stack); case TORSO -> equipment.putClothesOn(stack); case HANDS -> equipment.putMittensOn(stack); default -> { } } } @Override public ItemStack unequip(BodyRegionV2 region) { return switch (region) { case ARMS -> equipment.takeBindOff(); case MOUTH -> equipment.takeGagOff(); case EYES -> equipment.takeBlindfoldOff(); case EARS -> equipment.takeEarplugsOff(); case NECK -> equipment.takeCollarOff(false); case TORSO -> equipment.takeClothesOff(); case HANDS -> equipment.takeMittensOff(); default -> ItemStack.EMPTY; }; } @Override public ItemStack forceUnequip(BodyRegionV2 region) { if (region == BodyRegionV2.NECK) { return equipment.takeCollarOff(true); } if (region == BodyRegionV2.TORSO) { // takeClothesOff has no lock check but has syncClothesConfig side effect return equipment.takeClothesOff(); } // All other regions: bypass both V1 isLocked and V2 canUnequip checks // by using V2EquipmentHelper directly with force=true return V2EquipmentHelper.unequipFromRegion(player, region, true); } // IRestrainable State Queries // Delegated to PlayerStateQuery component @Override public boolean canBeTiedUp() { return stateQuery.canBeTiedUp(); } @Override public boolean isBoundAndGagged() { return stateQuery.isBoundAndGagged(); } @Override public boolean hasKnives() { return stateQuery.hasKnives(); } // Sale System - Delegated to PlayerSale component @Override public boolean isForSell() { return sale.isForSell(); } @Override public com.tiedup.remake.util.tasks.ItemTask getSalePrice() { return sale.getSalePrice(); } @Override public void putForSale(com.tiedup.remake.util.tasks.ItemTask price) { sale.putForSale(price); } @Override public void cancelSale() { sale.cancelSale(); } @Override public boolean isTiedToPole() { // Check if leash proxy is attached to a fence knot (pole) if (!(player instanceof IPlayerLeashAccess access)) return false; if (!access.tiedup$isLeashed()) return false; net.minecraft.world.entity.Entity leashHolder = access.tiedup$getLeashHolder(); return ( leashHolder instanceof net.minecraft.world.entity.decoration.LeashFenceKnotEntity ); } @Override public boolean tieToClosestPole(int searchRadius) { if (player == null) return false; return RestraintEffectUtils.tieToClosestPole(player, searchRadius); } @Override public boolean canBeKidnappedByEvents() { return stateQuery.canBeKidnappedByEvents(); } @Override public boolean hasNamedCollar() { return dataRetrieval.hasNamedCollar(); } @Override public boolean hasClothesWithSmallArms() { return dataRetrieval.hasClothesWithSmallArms(); } @Override public boolean hasGaggingEffect() { return stateQuery.hasGaggingEffect(); } @Override public boolean hasBlindingEffect() { return stateQuery.hasBlindingEffect(); } // Equipment Take Off (local helpers) // Delegated to PlayerEquipment component public ItemStack takeEarplugsOff() { return equipment.takeEarplugsOff(); } public ItemStack takeClothesOff() { return equipment.takeClothesOff(); } // IRestrainable Equipment Put On // Delegated to PlayerEquipment component /** Equips earplugs (muffles sounds). Issue #14 fix: now calls onEquipped. */ public void putEarplugsOn(ItemStack earplugs) { equipment.putEarplugsOn(earplugs); } // IRestrainable Equipment Replacement (V2 region-based) // Delegated to PlayerEquipment component (except TORSO - handled by PlayerClothesPermission) @Override public ItemStack replaceEquipment( BodyRegionV2 region, ItemStack newStack, boolean force ) { // Thread safety: must be called from the server thread. return switch (region) { case ARMS -> equipment.replaceBind(newStack, force); case MOUTH -> equipment.replaceGag(newStack, force); case EYES -> equipment.replaceBlindfold(newStack, force); case EARS -> equipment.replaceEarplugs(newStack, force); case NECK -> equipment.replaceCollar(newStack, force); case TORSO -> clothesPermission.replaceClothes(newStack, force); case HANDS -> equipment.replaceMittens(newStack, force); default -> ItemStack.EMPTY; }; } // IRestrainable Bulk Operations @Override public void untie(boolean drop) { if (drop) { dropBondageItems(true); } else { // Clear all V2 equipment slots without dropping (no lifecycle hooks) var v2Equip = V2EquipmentHelper.getEquipment(player); if (v2Equip != null) { v2Equip.clearAll(); V2EquipmentHelper.sync(player); } } // V1 speed reduction handled by MovementStyleManager (V2 tick-based). // See H6 fix — removing V1 calls prevents double stacking. if (isCaptive()) { free(); } // Also detach leash if tied to pole (no captor but still leashed) else if ( player instanceof IPlayerLeashAccess access && access.tiedup$isLeashed() ) { access.tiedup$detachLeash(); access.tiedup$dropLeash(); } } @Override public void dropBondageItems(boolean drop) { if (!drop) return; dropBondageItems(true, true, true, true, true, true, true); } @Override public void dropBondageItems(boolean drop, boolean dropBind) { if (!drop) return; if (dropBind) kidnappedDropItem(takeBindOff()); } @Override public void dropBondageItems( boolean drop, boolean dropBind, boolean dropGag, boolean dropBlindfold, boolean dropEarplugs, boolean dropCollar, boolean dropClothes ) { if (!drop) return; if (dropBind) takeBondageItemIfUnlocked( getEquipment(BodyRegionV2.ARMS), this::takeBindOff ); if (dropGag) takeBondageItemIfUnlocked( getEquipment(BodyRegionV2.MOUTH), this::takeGagOff ); if (dropBlindfold) takeBondageItemIfUnlocked( getEquipment(BodyRegionV2.EYES), this::takeBlindfoldOff ); if (dropEarplugs) takeBondageItemIfUnlocked( getEquipment(BodyRegionV2.EARS), this::takeEarplugsOff ); if (dropCollar) takeBondageItemIfUnlocked( getEquipment(BodyRegionV2.NECK), this::takeCollarOff ); if (dropClothes) kidnappedDropItem(takeClothesOff()); } @Override public void dropClothes() { ItemStack clothes = takeClothesOff(); if (!clothes.isEmpty()) { kidnappedDropItem(clothes); } } @Override public void applyBondage( ItemStack bind, ItemStack gag, ItemStack blindfold, ItemStack earplugs, ItemStack collar, ItemStack clothes ) { if (!bind.isEmpty()) putBindOn(bind); if (!gag.isEmpty()) putGagOn(gag); if (!blindfold.isEmpty()) putBlindfoldOn(blindfold); if (!earplugs.isEmpty()) putEarplugsOn(earplugs); if (!collar.isEmpty()) putCollarOn(collar); if (!clothes.isEmpty()) putClothesOn(clothes); } @Override public int getBondageItemsWhichCanBeRemovedCount() { int count = 0; if (!isLocked(getEquipment(BodyRegionV2.ARMS), false)) count++; if (!isLocked(getEquipment(BodyRegionV2.MOUTH), false)) count++; if (!isLocked(getEquipment(BodyRegionV2.EYES), false)) count++; if (!isLocked(getEquipment(BodyRegionV2.EARS), false)) count++; if (!isLocked(getEquipment(BodyRegionV2.NECK), false)) count++; if (!getEquipment(BodyRegionV2.TORSO).isEmpty()) count++; return count; } // IRestrainable Callbacks // Delegated to PlayerEquipment component @Override public void checkGagAfterApply() { equipment.checkGagAfterApply(); } @Override public void checkBlindfoldAfterApply() { equipment.checkBlindfoldAfterApply(); } @Override public void checkEarplugsAfterApply() { equipment.checkEarplugsAfterApply(); } @Override public void checkCollarAfterApply() { equipment.checkCollarAfterApply(); } // IRestrainable Special Interactions // Delegated to PlayerSpecialActions component @Override public void applyChloroform(int duration) { specialActions.applyChloroform(duration); } /** * C6-V2: Narrowed to IRestrainableEntity */ @Override public void takeBondageItemBy(IRestrainableEntity taker, int slotIndex) { specialActions.takeBondageItemBy(taker, slotIndex); } // IRestrainable Clothes Permissions // Delegated to PlayerClothesPermission component @Override public boolean canTakeOffClothes(Player player) { return clothesPermission.canTakeOffClothes(player); } @Override public boolean canChangeClothes(Player player) { return clothesPermission.canChangeClothes(player); } @Override public boolean canChangeClothes() { return clothesPermission.canChangeClothes(); } // IRestrainable Lifecycle @Override public boolean onDeathKidnapped(net.minecraft.world.level.Level world) { // Clear movement style state so onActivate() re-fires on respawn // (PlayerBindState instance survives respawn via computeIfAbsent) clearMovementState(); // Reset shock timers resetAutoShockTimer(); // Unlock all locked items ItemStack collar = getEquipment(BodyRegionV2.NECK); if ( !collar.isEmpty() && collar.getItem() instanceof ILockable lockable ) { lockable.setLocked(collar, false); } // Drop all items dropBondageItems(true); if (isCaptive()) { free(); } // Delegate to lifecycle component for registry cleanup and offline status return lifecycle.onDeathKidnapped(world); } // IRestrainable Entity Communication // Delegated to PlayerDataRetrieval component @Override public net.minecraft.world.entity.LivingEntity asLivingEntity() { return dataRetrieval.asLivingEntity(); } }