package com.tiedup.remake.tasks; import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.network.ModNetwork; import com.tiedup.remake.network.action.PacketTying; import com.tiedup.remake.state.IBondageState; import com.tiedup.remake.state.PlayerBindState; import com.tiedup.remake.v2.BodyRegionV2; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; /** * * Based on original TyingPlayerTask from 1.12.2 * * This task: * 1. Tracks progress as the kidnapper repeatedly right-clicks the target * 2. Sends progress updates to kidnapper (and target if it's a player) * 3. Applies the bind/gag when the timer completes * 4. Works with any LivingEntity that has an IBondageState state */ public class TyingPlayerTask extends TyingTask { /** The player performing the tying action. */ protected Player kidnapper; /** FIX: Source inventory slot to consume from when task completes */ private int sourceSlot = -1; /** FIX: Source player whose inventory to consume from */ private Player sourcePlayer; /** * Create a new tying task. * * @param bind The bind/gag item to apply * @param targetState The target's IBondageState state * @param targetEntity The target entity (Player or NPC) * @param seconds Total duration in seconds * @param level The world */ public TyingPlayerTask( ItemStack bind, IBondageState targetState, LivingEntity targetEntity, int seconds, Level level ) { super(bind, targetState, targetEntity, seconds, level); } /** * Create a new tying task with kidnapper reference. * * @param bind The bind/gag item to apply * @param targetState The target's IBondageState state * @param targetEntity The target entity (Player or NPC) * @param seconds Total duration in seconds * @param level The world * @param kidnapper The player performing the tying */ public TyingPlayerTask( ItemStack bind, IBondageState targetState, LivingEntity targetEntity, int seconds, Level level, Player kidnapper ) { super(bind, targetState, targetEntity, seconds, level); this.kidnapper = kidnapper; } /** * Set the kidnapper (the player doing the tying). */ public void setKidnapper(Player kidnapper) { this.kidnapper = kidnapper; } /** * FIX: Set the source inventory slot for item consumption. * Called when task starts to track which slot to consume from. */ public void setSourceSlot(int slot) { this.sourceSlot = slot; } /** * FIX: Set the source player for item consumption. * Called when task starts to track whose inventory to consume from. */ public void setSourcePlayer(Player player) { this.sourcePlayer = player; } /** * Update the tying progress. * Called each time the kidnapper right-clicks the target (or left-clicks for self-tying). * * In the new progress-based system, this method only: * 1. Validates kidnapper is still close to target * 2. Marks this tick as "active" (progress will increase in tick()) * * The actual progress increment and completion check happen in tick(). */ @Override public synchronized void update() { // Check if this is self-tying (target == kidnapper) boolean isSelfTying = kidnapper != null && kidnapper.equals(targetEntity); // SECURITY: Validate kidnapper is still close to target (skip for self-tying) if (!isSelfTying && kidnapper != null && targetEntity != null) { double distance = kidnapper.distanceTo(targetEntity); if (distance > 4.0) { TiedUpMod.LOGGER.debug( "[TyingPlayerTask] Kidnapper {} moved too far from target ({} blocks), cancelling", kidnapper.getName().getString(), String.format("%.1f", distance) ); stop(); return; } // Check line-of-sight if (!kidnapper.hasLineOfSight(targetEntity)) { TiedUpMod.LOGGER.debug( "[TyingPlayerTask] Kidnapper {} lost line of sight to target, cancelling", kidnapper.getName().getString() ); stop(); return; } } // Mark this tick as active (player is clicking on target) super.update(); } /** * Send progress packets to both kidnapper and target (if players). * Called every tick from ItemBind or RestraintTaskTickHandler. */ @Override public void sendProgressPackets() { if (stopped) return; // Check if this is self-tying (target == kidnapper) boolean isSelfTying = kidnapper != null && kidnapper.equals(targetEntity); String kidnapperName = kidnapper != null ? kidnapper.getName().getString() : "Someone"; String targetName = targetEntity.getName().getString(); if (isSelfTying) { // Self-tying: Send single packet with self-tying message if (kidnapper instanceof ServerPlayer serverPlayer) { PacketTying selfPacket = new PacketTying( this.getState(), this.getMaxSeconds(), true, // isKidnapper (shows "Tying..." message) "yourself" // Special indicator for self-tying ); ModNetwork.sendToPlayer(selfPacket, serverPlayer); } } else { // Normal tying: Send packets to both parties // Packet to victim: isKidnapper=false, shows kidnapper's name if (targetEntity instanceof ServerPlayer serverTarget) { PacketTying victimPacket = new PacketTying( this.getState(), this.getMaxSeconds(), false, kidnapperName ); ModNetwork.sendToPlayer(victimPacket, serverTarget); } // Packet to kidnapper: isKidnapper=true, shows target's name if (kidnapper instanceof ServerPlayer serverKidnapper) { PacketTying kidnapperPacket = new PacketTying( this.getState(), this.getMaxSeconds(), true, targetName ); ModNetwork.sendToPlayer(kidnapperPacket, serverKidnapper); } } } /** * Called when the task completes successfully. * Applies the bind, consumes the item, and sends completion packets. */ @Override protected void onComplete() { // Verify target entity still exists and is alive if (!isTargetValid()) { TiedUpMod.LOGGER.warn( "[TyingPlayerTask] Target entity no longer valid, cancelling task" ); stop(); return; } TiedUpMod.LOGGER.info( "[TyingPlayerTask] Tying complete for {}", targetEntity.getName().getString() ); // Apply the bind/gag to the target targetState.equip(BodyRegionV2.ARMS, bind); // FIX: Consume the item from the stored inventory slot // This prevents duplication by consuming from the exact slot used to start the task if (sourcePlayer != null && sourceSlot >= 0) { ItemStack slotStack = sourcePlayer .getInventory() .getItem(sourceSlot); if ( !slotStack.isEmpty() && ItemStack.isSameItemSameTags(slotStack, bind) ) { slotStack.shrink(1); TiedUpMod.LOGGER.debug( "[TyingPlayerTask] Consumed bind item from slot {}", sourceSlot ); } else { // Slot changed - try to find and consume from any matching slot for ( int i = 0; i < sourcePlayer.getInventory().getContainerSize(); i++ ) { ItemStack checkStack = sourcePlayer .getInventory() .getItem(i); if ( !checkStack.isEmpty() && ItemStack.isSameItemSameTags(checkStack, bind) ) { checkStack.shrink(1); TiedUpMod.LOGGER.debug( "[TyingPlayerTask] Consumed bind item from fallback slot {}", i ); break; } } } } // Track who tied this entity (for reward anti-abuse) if ( targetEntity instanceof com.tiedup.remake.entities.EntityDamsel damsel ) { damsel.setTiedBy(kidnapper); } // Mark task as stopped stop(); sendCompletionPackets(); } /** * Send completion packets to kidnapper and/or target. * Handles both self-tying and normal tying cases. */ protected void sendCompletionPackets() { boolean isSelfTying = kidnapper != null && kidnapper.equals(targetEntity); String kidnapperName = kidnapper != null ? kidnapper.getName().getString() : "Someone"; String targetName = targetEntity.getName().getString(); if (isSelfTying) { if (kidnapper instanceof ServerPlayer serverPlayer) { PacketTying completionPacket = new PacketTying( -1, this.getMaxSeconds(), true, "yourself" ); ModNetwork.sendToPlayer(completionPacket, serverPlayer); PlayerBindState playerState = PlayerBindState.getInstance( serverPlayer ); if (playerState != null) { playerState.setRestrainedState(null); } } } else { if (targetEntity instanceof ServerPlayer serverTarget) { PacketTying completionPacket = new PacketTying( -1, this.getMaxSeconds(), false, kidnapperName ); ModNetwork.sendToPlayer(completionPacket, serverTarget); PlayerBindState playerState = PlayerBindState.getInstance( serverTarget ); if (playerState != null) { playerState.setRestrainedState(null); } } if (kidnapper instanceof ServerPlayer serverKidnapper) { PacketTying completionPacket = new PacketTying( -1, this.getMaxSeconds(), true, targetName ); ModNetwork.sendToPlayer(completionPacket, serverKidnapper); } } } /** * Set up the target's restraint state for client-side progress tracking. * Only applies to player targets (NPCs don't need client-side state). * * Note: On dedicated servers, this is a no-op. The client receives * progress packets (PacketTying) which create the PlayerStateTask locally. */ @Override public void setUpTargetState() { // Only set up state for player targets if (!(targetEntity instanceof Player targetPlayer)) { return; } // Server-side: nothing to do - client handles its own PlayerStateTask via packets if (!targetPlayer.level().isClientSide) { return; } // Client-side: set up local progress tracking (only reached in single-player/integrated server) // Note: This code path is rarely used since packets handle client-side state } }