Files
TiedUp-/src/main/java/com/tiedup/remake/state/PlayerBindState.java

1075 lines
32 KiB
Java

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<UUID, PlayerBindState> instances =
new ConcurrentHashMap<>();
/** Client-side instances - one per player. Thread-safe map with atomic computeIfAbsent. */
private static final Map<UUID, PlayerBindState> 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<UUID, PlayerBindState> 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<UUID, PlayerBindState> 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();
}
}