1075 lines
32 KiB
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();
|
|
}
|
|
}
|