package com.tiedup.remake.state; import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.v2.BodyRegionV2; import com.tiedup.remake.items.base.ItemCollar; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; // C6-V2: IRestrainable → IBondageState (narrowed API) /** * Phase 8: Master-Captive Relationships * Phase 17: Renamed from PlayerKidnapperManager, terminology slave → captive * * Manages capture relationships for player captors. * * Terminology (Phase 17): * - "Captive" = Entity attached by leash (active physical control) * - "Slave" = Entity wearing a collar owned by someone (passive ownership via CollarRegistry) * * Features: * - Supports multiple captives simultaneously * - Allows captive transfer between captors * - Tracks captive list persistently * - Handles cleanup on captive logout/escape * * Thread Safety: * - Uses CopyOnWriteArrayList to avoid ConcurrentModificationException * - Safe for iteration during modification * * Design: * - Each PlayerBindState has one PlayerCaptorManager * - Manager tracks all captives owned by that player * - Implements ICaptor interface for polymorphic usage * * @see ICaptor * @see PlayerBindState */ public class PlayerCaptorManager implements ICaptor { /** * The player who owns this manager (the captor). */ private final Player captor; /** * List of all captives currently owned by this captor. * Thread-safe to avoid concurrent modification during iteration. * * Phase 14.1.6: Changed from List to List * Phase 17: Renamed from slaves to captives */ private final List captives; /** * Create a new captor manager for the given player. * * @param captor The player who will be the captor */ public PlayerCaptorManager(Player captor) { this.captor = captor; this.captives = new CopyOnWriteArrayList<>(); } // ======================================== // ICaptor Implementation // ======================================== /** * Phase 14.1.6: Changed parameter from PlayerBindState to IBondageState * Phase 17: Renamed from addSlave to addCaptive */ @Override public synchronized void addCaptive(IBondageState captive) { if (captive == null) { TiedUpMod.LOGGER.warn( "[PlayerCaptorManager] Attempted to add null captive" ); return; } if (!captives.contains(captive)) { captives.add(captive); TiedUpMod.LOGGER.info( "[PlayerCaptorManager] {} captured {} (total captives: {})", captor.getName().getString(), captive.getKidnappedName(), captives.size() ); } } /** * Phase 14.1.6: Changed parameter from PlayerBindState to IBondageState * Phase 17: Renamed from removeSlave to removeCaptive * * Thread Safety: Synchronized on 'this' to match addCaptive and freeAllCaptives. */ @Override public synchronized void removeCaptive( IBondageState captive, boolean transportState ) { if (captive == null) { TiedUpMod.LOGGER.warn( "[PlayerCaptorManager] Attempted to remove null captive" ); return; } if (captives.remove(captive)) { TiedUpMod.LOGGER.info( "[PlayerCaptorManager] {} freed {} (remaining captives: {})", captor.getName().getString(), captive.getKidnappedName(), captives.size() ); // If requested, also despawn the transport entity if (transportState && captive.getTransport() != null) { captive.getTransport().discard(); } } } /** * Phase 14.1.6: Changed parameter from PlayerBindState to IBondageState * Phase 17: Renamed from canEnslave to canCapture */ @Override public boolean canCapture(IBondageState target) { if (target == null) { return false; } // From original code (PlayerBindState.java:195-225): // Can capture if: // - Target is tied up, OR // - Target has collar AND collar has this captor as owner // Phase 14.1.6: Use asLivingEntity() instead of getPlayer() net.minecraft.world.entity.LivingEntity targetEntity = target.asLivingEntity(); if (targetEntity == null) { return false; } // Check if target is tied up if (target.isTiedUp()) { return true; } // Check if target has collar with this captor as owner if (target.hasCollar()) { ItemStack collar = target.getEquipment(BodyRegionV2.NECK); if (collar.getItem() instanceof ItemCollar collarItem) { if ( collarItem.getOwners(collar).contains(this.captor.getUUID()) ) { return true; } } } return false; } /** * Phase 14.1.6: Changed parameter from PlayerBindState to IBondageState * Phase 17: Renamed from canFree to canRelease */ @Override public boolean canRelease(IBondageState captive) { if (captive == null) { return false; } // Can only release if this manager is the captive's captor return captive.getCaptor() == this; } /** * Phase 17: Renamed from allowSlaveTransfer to allowCaptiveTransfer */ @Override public boolean allowCaptiveTransfer() { // Players always allow captive transfer return true; } /** * Phase 17: Renamed from allowMultipleSlaves to allowMultipleCaptives */ @Override public boolean allowMultipleCaptives() { // Players can have multiple captives return true; } /** * Phase 14.1.6: Changed parameter from PlayerBindState to IBondageState * Phase 17: Renamed from onSlaveLogout to onCaptiveLogout * Note: For NPC captives, this is never called (NPCs don't log out) */ @Override public void onCaptiveLogout(IBondageState captive) { if (captive == null) { return; } TiedUpMod.LOGGER.info( "[PlayerCaptorManager] Captive {} logged out while captured by {}", captive.getKidnappedName(), captor.getName().getString() ); // Keep captive in list - they might reconnect // Transport entity will despawn after timeout // On reconnect, checkStillCaptive() will clean up if needed } /** * Phase 14.1.6: Changed parameter from PlayerBindState to IBondageState * Phase 17: Renamed from onSlaveReleased to onCaptiveReleased */ @Override public void onCaptiveReleased(IBondageState captive) { if (captive == null) { return; } TiedUpMod.LOGGER.info( "[PlayerCaptorManager] Captive {} was released from {}", captive.getKidnappedName(), captor.getName().getString() ); // No special action needed - already removed from list by removeCaptive() } /** * Phase 14.1.6: Changed parameter from PlayerBindState to IBondageState * Phase 17: Renamed from onSlaveStruggle to onCaptiveStruggle */ @Override public void onCaptiveStruggle(IBondageState captive) { if (captive == null) { return; } TiedUpMod.LOGGER.debug( "[PlayerCaptorManager] Captive {} struggled (captor: {})", captive.getKidnappedName(), captor.getName().getString() ); // Phase 8: No action for basic struggle // Phase 14: Shock collar would activate here } /** * Phase 17: Renamed from hasSlaves to hasCaptives */ @Override public boolean hasCaptives() { return !captives.isEmpty(); } @Override public Entity getEntity() { return captor; } // ======================================== // Additional Methods // ======================================== /** * Frees all captives currently owned by this captor. * * Phase 17: Renamed from freeAllSlaves to freeAllCaptives * * Thread Safety: Synchronized on 'this' to match addCaptive and removeCaptive. * * @param transportState If true, destroy the transporter entities */ public synchronized void freeAllCaptives(boolean transportState) { // Use a copy to avoid concurrent modification while iterating List copy = new ArrayList<>(captives); for (IBondageState captive : copy) { captive.free(transportState); } captives.clear(); } /** * Frees all captives with default behavior (destroy transport entities). */ public void freeAllCaptives() { freeAllCaptives(true); } /** * Get a copy of the captive list. * Safe for iteration without concurrent modification issues. * * Phase 17: Renamed from getSlaves to getCaptives * * @return Copy of the current captive list */ public List getCaptives() { return new ArrayList<>(captives); } /** * Get the number of captives currently owned. * * Phase 17: Renamed from getSlaveCount to getCaptiveCount * * @return Captive count */ public int getCaptiveCount() { return captives.size(); } /** * Transfer all captives from this captor to a new captor. * Used when this player gets captured themselves. * * From original code: * - When a player gets captured, their captives transfer to new captor * - Prevents circular capture issues * * Phase 17: Renamed from transferAllSlavesTo to transferAllCaptivesTo * * @param newCaptor The new captor to transfer captives to */ public void transferAllCaptivesTo(ICaptor newCaptor) { if (newCaptor == null) { TiedUpMod.LOGGER.warn( "[PlayerCaptorManager] Attempted to transfer captives to null captor" ); return; } if (captives.isEmpty()) { return; } TiedUpMod.LOGGER.info( "[PlayerCaptorManager] Transferring {} captives from {} to {}", captives.size(), captor.getName().getString(), newCaptor.getEntity().getName().getString() ); // Create copy to avoid concurrent modification List captivesToTransfer = new ArrayList<>(captives); for (IBondageState captive : captivesToTransfer) { if (captive != null) { captive.transferCaptivityTo(newCaptor); } } // All captives should now be removed from this manager's list if (!captives.isEmpty()) { TiedUpMod.LOGGER.warn( "[PlayerCaptorManager] {} captives remain after transfer - cleaning up", captives.size() ); captives.clear(); } } /** * Get the captor player. * * @return The player who owns this manager */ public Player getCaptor() { return captor; } /** * Clean up invalid captives from the list. * Removes captives that are no longer valid (offline, transport gone, etc.). * * Phase 17: Renamed from cleanupInvalidSlaves to cleanupInvalidCaptives * * Should be called periodically (e.g., on tick). */ public void cleanupInvalidCaptives() { captives.removeIf(captive -> { if (captive == null) { return true; } // Remove if not actually captured anymore if (!captive.isCaptive()) { TiedUpMod.LOGGER.debug( "[PlayerCaptorManager] Removing invalid captive {}", captive.getKidnappedName() ); return true; } // Remove if captured by different captor if (captive.getCaptor() != this) { TiedUpMod.LOGGER.debug( "[PlayerCaptorManager] Removing captive {} (belongs to different captor)", captive.getKidnappedName() ); return true; } return false; }); } // ======================================== // Backward Compatibility (Phase 17) // ======================================== }