package com.tiedup.remake.state.components; import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.entities.LeashProxyEntity; import com.tiedup.remake.state.ICaptor; import com.tiedup.remake.state.IPlayerLeashAccess; import com.tiedup.remake.state.hosts.IPlayerBindStateHost; import net.minecraft.world.entity.player.Player; import org.jetbrains.annotations.Nullable; /** * Component responsible for captivity mechanics and leash proxy management. * * Single Responsibility: Captivity lifecycle and transport management * Complexity: VERY HIGH (mixin coupling, network sync, leash proxy coordination) * Risk: VERY HIGH (critical path, mixin dependency, 4 network sync points) * * Mixin Dependency: IPlayerLeashAccess for leash proxy system * Network Sync: 4 syncEnslavement() calls coordinated via host * * Captivity States: * - Not Captive: No captor, no leash * - Captive (by entity): Has captor, leashed to entity * - Pole Binding: No captor, leashed to pole (LeashFenceKnotEntity) */ public class PlayerCaptivity { private final IPlayerBindStateHost host; public PlayerCaptivity(IPlayerBindStateHost host) { this.host = host; } // ========== Captivity Initiation ========== /** * Initiates the capture process by a captor. * Uses the proxy-based leash system (player is NOT mounted). * * Thread Safety: Synchronized to prevent race condition where two kidnappers * could both pass the isCaptive() check and attempt to capture simultaneously. * * @param newCaptor The entity attempting to capture this player * @return true if capture succeeded */ public synchronized boolean getCapturedBy(ICaptor newCaptor) { Player player = host.getPlayer(); if (player == null || newCaptor == null) return false; // Must be enslavable (tied up) OR captor can capture (includes collar owner exception) if ( !isEnslavable() && !newCaptor.canCapture(host.getKidnapped()) ) return false; // Check if already captured (atomic check under synchronization) if (isCaptive()) return false; // Free all captives instead of transferring (until multi-captive is implemented) if (host.getCaptorManager().hasCaptives()) { TiedUpMod.LOGGER.debug( "[PlayerCaptivity] {} is being captured - freeing their {} captives", player.getName().getString(), host.getCaptorManager().getCaptiveCount() ); host.getCaptorManager().freeAllCaptives(true); } // Use new proxy-based leash system if (player instanceof IPlayerLeashAccess access) { access.tiedup$attachLeash(newCaptor.getEntity()); newCaptor.addCaptive(host.getKidnapped()); host.setCaptor(newCaptor); TiedUpMod.LOGGER.debug( "[PlayerCaptivity] {} captured by {} (proxy leash)", player.getName().getString(), newCaptor.getEntity().getName().getString() ); // Sync enslavement state to all clients host.syncEnslavement(); return true; } TiedUpMod.LOGGER.error( "[PlayerCaptivity] Player {} does not implement IPlayerLeashAccess!", player.getName().getString() ); return false; } // ========== Captivity Release ========== /** * Ends captivity with default behavior (drops leash item). */ public void free() { free(true); } /** * * @param dropLead Whether to drop the leash item */ public void free(boolean dropLead) { Player player = host.getPlayer(); if (player == null) return; if (!(player instanceof IPlayerLeashAccess access)) { TiedUpMod.LOGGER.error( "[PlayerCaptivity] Player {} does not implement IPlayerLeashAccess!", player.getName().getString() ); return; } ICaptor captor = host.getCaptor(); // Handle pole binding (no captor) - just detach leash if (captor == null) { if (access.tiedup$isLeashed()) { TiedUpMod.LOGGER.debug( "[PlayerCaptivity] Freeing {} from pole binding", player.getName().getString() ); if (dropLead) { access.tiedup$dropLeash(); } access.tiedup$detachLeash(); host.syncEnslavement(); } return; } TiedUpMod.LOGGER.debug( "[PlayerCaptivity] Freeing {} from captivity", player.getName().getString() ); // 1. Remove from captor's tracking list captor.removeCaptive(host.getKidnapped(), false); // 2. Detach leash proxy if (dropLead) { access.tiedup$dropLeash(); } access.tiedup$detachLeash(); // 3. Reset state host.setCaptor(null); // 4. Sync freed state to all clients host.syncEnslavement(); } // ========== Captivity Transfer ========== /** * Transfers captivity from current captor to a new captor. * * Thread Safety: Synchronized to prevent concurrent transfer attempts. * * @param newCaptor The new captor entity */ public synchronized void transferCaptivityTo(ICaptor newCaptor) { Player player = host.getPlayer(); ICaptor currentCaptor = host.getCaptor(); if ( player == null || newCaptor == null || currentCaptor == null || !currentCaptor.allowCaptiveTransfer() ) return; currentCaptor.removeCaptive(host.getKidnapped(), false); // Re-attach leash to new captor if (player instanceof IPlayerLeashAccess access) { access.tiedup$detachLeash(); access.tiedup$attachLeash(newCaptor.getEntity()); } newCaptor.addCaptive(host.getKidnapped()); host.setCaptor(newCaptor); // Sync new captor to all clients host.syncEnslavement(); } // ========== State Queries ========== /** * Check if this player can be captured (leashed). * Must be tied up to be leashed. * Collar alone is NOT enough - collar owner exception is handled in canCapture(). * * @return true if player is tied up and can be leashed */ public boolean isEnslavable() { return host.isTiedUp(); } /** * Check if player is currently captured by an entity. * * @return true if player has a captor and is leashed */ public boolean isCaptive() { Player player = host.getPlayer(); if (host.getCaptor() == null) return false; if (player instanceof IPlayerLeashAccess access) { return access.tiedup$isLeashed(); } return false; } /** * Get the leash proxy entity (transport system). * * @return The leash proxy, or null if not leashed */ @Nullable public LeashProxyEntity getTransport() { Player player = host.getPlayer(); if (player instanceof IPlayerLeashAccess access) { return access.tiedup$getLeashProxy(); } return null; } // ========== Captivity Monitoring ========== /** * Periodically monitors captivity validity. * Simplified: If any condition is invalid, free the captive immediately. * * Called from RestraintTaskTickHandler every player tick. */ public void checkStillCaptive() { if (!isCaptive()) return; Player player = host.getPlayer(); if (player == null) return; // Check if no longer tied/collared if (!host.isTiedUp() && !host.hasCollar()) { TiedUpMod.LOGGER.debug( "[PlayerCaptivity] Auto-freeing {} - no restraints", player.getName().getString() ); free(); return; } // Check leash proxy status if (player instanceof IPlayerLeashAccess access) { LeashProxyEntity proxy = access.tiedup$getLeashProxy(); if (proxy == null || proxy.proxyIsRemoved()) { TiedUpMod.LOGGER.debug( "[PlayerCaptivity] Auto-freeing {} - proxy invalid", player.getName().getString() ); // Notify captor BEFORE freeing (triggers retrieval behavior) ICaptor captor = host.getCaptor(); if (captor != null) { captor.onCaptiveReleased(host.getKidnapped()); } free(); return; } // Check if leash holder is still valid if (proxy.getLeashHolder() == null) { TiedUpMod.LOGGER.debug( "[PlayerCaptivity] Auto-freeing {} - leash holder gone", player.getName().getString() ); // Notify captor BEFORE freeing (triggers retrieval behavior) ICaptor captor = host.getCaptor(); if (captor != null) { captor.onCaptiveReleased(host.getKidnapped()); } free(); } } } }