Files
TiedUp-/src/main/java/com/tiedup/remake/events/restriction/RestraintTaskTickHandler.java
NotEvil 3d61c9e9e6 feat(D-01/C): consumer migration — 85 files migrated to V2 helpers
Phase 1 (state): PlayerBindState, PlayerCaptorManager, PlayerEquipment,
  PlayerDataRetrieval, PlayerLifecycle, PlayerShockCollar, StruggleAccessory
Phase 2 (client): AnimationTickHandler, NpcAnimationTickHandler, 5 render
  handlers, DamselModel, 3 client mixins, SelfBondageInputHandler,
  SlaveManagementScreen, ActionPanel, SlaveEntryWidget, ModKeybindings
Phase 3 (entities): 28 entity/AI files migrated to CollarHelper,
  BindModeHelper, PoseTypeHelper, createStack()
Phase 4 (network): PacketSlaveAction, PacketMasterEquip,
  PacketAssignCellToCollar, PacketNpcCommand, PacketFurnitureForcemount
Phase 5 (events): RestraintTaskTickHandler, PetPlayRestrictionHandler,
  PlayerEnslavementHandler, ChatEventHandler, LaborAttackPunishmentHandler
Phase 6 (commands): BondageSubCommand, CollarCommand, NPCCommand,
  KidnapSetCommand
Phase 7 (compat): MCAKidnappedAdapter, MCA mixins
Phase 8 (misc): GagTalkManager, PetRequestManager, HangingCagePiece,
  BondageItemBlockEntity, TrappedChestBlockEntity, DispenserBehaviors,
  BondageItemLoaderUtility, RestraintApplicator, StruggleSessionManager,
  MovementStyleResolver, CampLifecycleManager

Some files retain dual V1/V2 checks (instanceof V1 || V2Helper) for
coexistence — V1-only branches removed in Branch D.
2026-04-15 00:16:50 +02:00

673 lines
25 KiB
Java

package com.tiedup.remake.events.restriction;
import com.tiedup.remake.core.SettingsAccessor;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.minigame.StruggleSessionManager;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.personality.PacketSlaveBeingFreed;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.tasks.ForceFeedingTask;
import com.tiedup.remake.tasks.TimedInteractTask;
import com.tiedup.remake.tasks.UntyingPlayerTask;
import com.tiedup.remake.tasks.UntyingTask;
import com.tiedup.remake.util.GameConstants;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.List;
import java.util.UUID;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
/**
* Tick handler for restraint-related tasks (untying, tying, force feeding).
*
* Manages progress-based interaction tasks that span multiple ticks:
* - Untying mechanic (empty hand right-click on tied entity)
* - Tying mechanic (tick progression)
* - Force feeding mechanic (food right-click on gagged entity)
* - Auto-shock collar checks
* - Struggle auto-stop (legacy QTE fallback)
*
* @see BondageItemRestrictionHandler for movement, interaction, and eating restrictions
*/
@Mod.EventBusSubscriber(
modid = TiedUpMod.MOD_ID,
bus = Mod.EventBusSubscriber.Bus.FORGE
)
public class RestraintTaskTickHandler {
// ========== PLAYER-SPECIFIC TICK ==========
/**
* Handle player tick event for player-specific features.
* - Auto-shock collar check (throttled to every N ticks)
*/
@SubscribeEvent
public static void onPlayerTick(TickEvent.PlayerTickEvent event) {
if (event.side.isClient() || event.phase != TickEvent.Phase.END) {
return;
}
Player player = event.player;
PlayerBindState playerState = PlayerBindState.getInstance(player);
// Check if struggle animation should stop
// For continuous struggle: animation is managed by MiniGameSessionManager
// Only auto-stop if NO active continuous session (legacy QTE fallback)
if (playerState != null && playerState.isStruggling()) {
// Don't auto-stop if there's an active continuous struggle session
StruggleSessionManager mgr = StruggleSessionManager.getInstance();
if (mgr.getContinuousStruggleSession(player.getUUID()) == null) {
// Legacy behavior: stop after 80 ticks (no active continuous session)
if (
playerState.shouldStopStruggling(
player.level().getGameTime()
)
) {
playerState.setStruggling(false, 0);
com.tiedup.remake.network.sync.SyncManager.syncStruggleState(
player
);
}
}
}
// Process untying task tick (progress-based system)
// tick() increments/decrements progress based on whether update() was called this tick
// sendProgressPackets() updates the UI for both players
if (playerState != null) {
com.tiedup.remake.tasks.UntyingTask currentUntyingTask =
playerState.getCurrentUntyingTask();
if (currentUntyingTask != null && !currentUntyingTask.isStopped()) {
// AUTO-UPDATE: Check if player is still targeting the same entity
// This allows "hold click" behavior without needing repeated interactLivingEntity calls
if (
currentUntyingTask instanceof
com.tiedup.remake.tasks.UntyingPlayerTask untyingPlayerTask
) {
net.minecraft.world.entity.LivingEntity target =
untyingPlayerTask.getTargetEntity();
if (target != null && target.isAlive()) {
// Check if player is looking at target and close enough
double distance = player.distanceTo(target);
boolean isLookingAtTarget = isPlayerLookingAtEntity(
player,
target,
4.0
);
if (
distance <= 4.0 &&
isLookingAtTarget &&
player.hasLineOfSight(target)
) {
// Player is still targeting - auto-update the task
currentUntyingTask.update();
}
}
}
// Process tick (increment if active, decrement if not)
currentUntyingTask.tick();
// Send progress packets to update UI
currentUntyingTask.sendProgressPackets();
// Check if task stopped (completed or cancelled due to no progress)
if (currentUntyingTask.isStopped()) {
playerState.setCurrentUntyingTask(null);
TiedUpMod.LOGGER.debug(
"[RESTRAINT] {} untying task ended (tick update)",
player.getName().getString()
);
}
}
// Process tying task tick (same progress-based system)
com.tiedup.remake.tasks.TyingTask currentTyingTask =
playerState.getCurrentTyingTask();
if (currentTyingTask != null && !currentTyingTask.isStopped()) {
// AUTO-UPDATE: Check if player is still targeting the same entity
// This allows "hold click" behavior without needing repeated interactLivingEntity calls
if (
currentTyingTask instanceof
com.tiedup.remake.tasks.TyingPlayerTask tyingPlayerTask
) {
net.minecraft.world.entity.LivingEntity target =
tyingPlayerTask.getTargetEntity();
boolean isSelfTying =
target != null && target.equals(player);
if (isSelfTying) {
// Self-tying: skip look-at/distance checks (player can't raycast to own hitbox)
// Progress is driven by continuous PacketSelfBondage packets from client
currentTyingTask.update();
} else if (target != null && target.isAlive()) {
// Tying another player: check distance + line of sight
double distance = player.distanceTo(target);
boolean isLookingAtTarget = isPlayerLookingAtEntity(
player,
target,
4.0
);
if (
distance <= 4.0 &&
isLookingAtTarget &&
player.hasLineOfSight(target)
) {
currentTyingTask.update();
}
}
}
// Process tick (increment if active, decrement if not)
currentTyingTask.tick();
// Send progress packets to update UI
currentTyingTask.sendProgressPackets();
// Check if task stopped (completed or cancelled due to no progress)
if (currentTyingTask.isStopped()) {
playerState.setCurrentTyingTask(null);
TiedUpMod.LOGGER.debug(
"[RESTRAINT] {} tying task ended (tick update)",
player.getName().getString()
);
}
}
// Process force feeding task tick
TimedInteractTask feedingTask = playerState.getCurrentFeedingTask();
if (feedingTask != null && !feedingTask.isStopped()) {
LivingEntity target = feedingTask.getTargetEntity();
if (target != null && target.isAlive()) {
double distance = player.distanceTo(target);
boolean isLookingAtTarget = isPlayerLookingAtEntity(
player,
target,
4.0
);
if (
distance <= 4.0 &&
isLookingAtTarget &&
player.hasLineOfSight(target)
) {
feedingTask.update();
}
}
feedingTask.tick();
feedingTask.sendProgressPackets();
if (feedingTask.isStopped()) {
playerState.setCurrentFeedingTask(null);
TiedUpMod.LOGGER.debug(
"[RESTRAINT] {} feeding task ended (tick update)",
player.getName().getString()
);
}
}
}
// Throttle: only check every N ticks (configurable via GameConstants) - per-player timing
if (player.tickCount % GameConstants.SHOCK_COLLAR_CHECK_INTERVAL != 0) {
return;
}
if (playerState != null) {
playerState.checkAutoShockCollar();
}
}
// ========== UNTYING MECHANIC ==========
/**
* Handle untying a tied entity (right-click with empty hand).
*
* Based on original PlayerKidnapActionsHandler.onUntyingTarget() (1.12.2)
*
* When a player right-clicks a tied entity (player or NPC) with an empty hand,
* starts or continues an untying task to free them.
*/
@SubscribeEvent
public static void onUntyingTarget(
PlayerInteractEvent.EntityInteract event
) {
// Only run on server side
if (event.getLevel().isClientSide) {
return;
}
Entity target = event.getTarget();
Player helper = event.getEntity();
// Must be targeting a LivingEntity, using main hand, and have empty hand
if (
!(target instanceof LivingEntity targetEntity) ||
event.getHand() != InteractionHand.MAIN_HAND ||
!helper.getMainHandItem().isEmpty()
) {
return;
}
// MCA villagers require Shift+click to untie (prevents conflict with MCA menu)
if (
com.tiedup.remake.compat.mca.MCACompat.isMCALoaded() &&
com.tiedup.remake.compat.mca.MCACompat.isMCAVillager(
targetEntity
) &&
!helper.isShiftKeyDown()
) {
return;
}
// Check if target is tied using IBondageState interface
IBondageState targetState = KidnappedHelper.getKidnappedState(
targetEntity
);
if (targetState == null || !targetState.isTiedUp()) {
return;
}
// SECURITY: Distance and line-of-sight validation
double maxUntieDistance = 4.0; // Max distance to untie (blocks)
double distance = helper.distanceTo(targetEntity);
if (distance > maxUntieDistance) {
TiedUpMod.LOGGER.warn(
"[RESTRAINT] {} tried to untie {} from too far away ({} blocks)",
helper.getName().getString(),
targetEntity.getName().getString(),
String.format("%.1f", distance)
);
return;
}
// Check line-of-sight (helper must be able to see target)
if (!helper.hasLineOfSight(targetEntity)) {
TiedUpMod.LOGGER.warn(
"[RESTRAINT] {} tried to untie {} without line of sight",
helper.getName().getString(),
targetEntity.getName().getString()
);
return;
}
// Check for Kidnapper fight back - block untying if Kidnapper is nearby
if (
targetEntity instanceof
com.tiedup.remake.entities.AbstractTiedUpNpc npc
) {
if (
npc.getCaptor() instanceof EntityKidnapper kidnapper &&
kidnapper.isAlive()
) {
double distanceToKidnapper = helper.distanceTo(kidnapper);
double fightBackRange = 16.0; // Kidnapper notices within 16 blocks
if (distanceToKidnapper <= fightBackRange) {
// Trigger Kidnapper fight back by setting helper as "attacker"
// This activates KidnapperFightBackGoal which handles pursuit and attack
kidnapper.setLastAttacker(helper);
TiedUpMod.LOGGER.debug(
"[RESTRAINT] {} tried to untie {}, {} fights back!",
helper.getName().getString(),
npc.getName().getString(),
kidnapper.getName().getString()
);
// Block untying - send message to player
helper.displayClientMessage(
Component.translatable(
"tiedup.message.kidnapper_guards_captive"
),
true
);
return;
}
}
}
// Check for Kidnapper fight back - block untying if player is captive or job worker
if (targetEntity instanceof Player targetPlayer) {
List<EntityKidnapper> nearbyKidnappers = helper
.level()
.getEntitiesOfClass(
EntityKidnapper.class,
helper.getBoundingBox().inflate(16.0)
);
for (EntityKidnapper kidnapper : nearbyKidnappers) {
if (!kidnapper.isAlive()) continue;
// Check if player is kidnapper's current captive (held by leash)
IBondageState captive = kidnapper.getCaptive();
boolean isCaptive =
captive != null && captive.asLivingEntity() == targetPlayer;
// Check if player is kidnapper's job worker
UUID workerUUID = kidnapper.getJobWorkerUUID();
boolean isJobWorker =
workerUUID != null &&
workerUUID.equals(targetPlayer.getUUID());
if (isCaptive || isJobWorker) {
// Trigger Kidnapper fight back
kidnapper.setLastAttacker(helper);
TiedUpMod.LOGGER.debug(
"[RESTRAINT] {} tried to untie {} (captive={}, worker={}), {} fights back!",
helper.getName().getString(),
targetPlayer.getName().getString(),
isCaptive,
isJobWorker,
kidnapper.getNpcName()
);
// Block untying - send message to player
helper.displayClientMessage(
Component.translatable(
"tiedup.message.kidnapper_guards_captive"
),
true
);
return;
}
}
}
// Check if helper is tied using IBondageState interface
IBondageState helperKidnappedState = KidnappedHelper.getKidnappedState(
helper
);
if (helperKidnappedState == null || helperKidnappedState.isTiedUp()) {
return;
}
// Get PlayerBindState for task management (helper only)
PlayerBindState helperState = PlayerBindState.getInstance(helper);
if (helperState == null) {
return;
}
// Block untying while force feeding
TimedInteractTask activeFeedTask = helperState.getCurrentFeedingTask();
if (activeFeedTask != null && !activeFeedTask.isStopped()) {
return;
}
// Get untying duration (default: 10 seconds)
int untyingSeconds = getUntyingDuration(helper);
// Non-owners take 3x longer and trigger alert to owners
if (
targetEntity instanceof
com.tiedup.remake.entities.AbstractTiedUpNpc npc
) {
if (!npc.isCollarOwner(helper)) {
// Non-owner: triple the untying time
untyingSeconds *= 3;
// Alert all collar owners
alertCollarOwners(npc, helper);
TiedUpMod.LOGGER.debug(
"[RESTRAINT] Non-owner {} trying to free {} ({}s)",
helper.getName().getString(),
npc.getNpcName(),
untyingSeconds
);
}
}
// Get current untying task (if any)
UntyingTask currentTask = helperState.getCurrentUntyingTask();
// Check if we should start a new task or continue existing one
if (
currentTask == null ||
!currentTask.isSameTarget(targetEntity) ||
currentTask.isStopped()
) {
// Create new untying task (unified for Players and NPCs)
UntyingPlayerTask newTask = new UntyingPlayerTask(
targetState,
targetEntity,
untyingSeconds,
helper.level(),
helper
);
// Start new task
helperState.setCurrentUntyingTask(newTask);
newTask.setUpTargetState(); // Initialize target's restraint state
newTask.start();
currentTask = newTask;
TiedUpMod.LOGGER.debug(
"[RESTRAINT] {} started untying {} ({} seconds)",
helper.getName().getString(),
targetEntity.getName().getString(),
untyingSeconds
);
} else {
// Continue existing task - ensure helper is set
if (currentTask instanceof UntyingPlayerTask playerTask) {
playerTask.setHelper(helper);
}
}
// Mark this tick as active (progress will increase in onPlayerTick)
// The tick() method in onPlayerTick handles progress increment/decrement
currentTask.update();
}
/**
* Check if a player is looking at a specific entity (raycast).
*
* @param player The player
* @param target The target entity
* @param maxDistance Maximum distance to check
* @return true if player is looking at the target entity
*/
private static boolean isPlayerLookingAtEntity(
Player player,
net.minecraft.world.entity.LivingEntity target,
double maxDistance
) {
// Get player's look vector
net.minecraft.world.phys.Vec3 eyePos = player.getEyePosition(1.0F);
net.minecraft.world.phys.Vec3 lookVec = player.getLookAngle();
net.minecraft.world.phys.Vec3 endPos = eyePos.add(
lookVec.x * maxDistance,
lookVec.y * maxDistance,
lookVec.z * maxDistance
);
// Check if raycast hits the target entity
net.minecraft.world.phys.AABB targetBounds = target
.getBoundingBox()
.inflate(0.3);
java.util.Optional<net.minecraft.world.phys.Vec3> hit =
targetBounds.clip(eyePos, endPos);
return hit.isPresent();
}
/**
* Get the untying duration in seconds from GameRule.
*
* @param player The player (for accessing world/GameRules)
* @return Duration in seconds (default: 10)
*/
private static int getUntyingDuration(Player player) {
return SettingsAccessor.getUntyingPlayerTime(
player.level().getGameRules()
);
}
// ========== FORCE FEEDING MECHANIC ==========
/**
* Handle force feeding a gagged entity (right-click with food).
*
* When a player right-clicks a gagged entity (player or NPC) while holding food,
* starts or continues a force feeding task.
*/
@SubscribeEvent
public static void onForceFeedingTarget(
PlayerInteractEvent.EntityInteract event
) {
if (event.getLevel().isClientSide) {
return;
}
Entity target = event.getTarget();
Player feeder = event.getEntity();
if (
!(target instanceof LivingEntity targetEntity) ||
event.getHand() != InteractionHand.MAIN_HAND
) {
return;
}
ItemStack heldItem = feeder.getMainHandItem();
if (heldItem.isEmpty() || !heldItem.getItem().isEdible()) {
return;
}
// Target must have IBondageState state and be gagged
IBondageState targetState = KidnappedHelper.getKidnappedState(
targetEntity
);
if (targetState == null || !targetState.isGagged()) {
return;
}
// Feeder must not be tied up
IBondageState feederState = KidnappedHelper.getKidnappedState(feeder);
if (feederState != null && feederState.isTiedUp()) {
return;
}
// Distance and line-of-sight validation
double distance = feeder.distanceTo(targetEntity);
if (distance > 4.0) {
return;
}
if (!feeder.hasLineOfSight(targetEntity)) {
return;
}
// Get feeder's PlayerBindState for task management
PlayerBindState feederBindState = PlayerBindState.getInstance(feeder);
if (feederBindState == null) {
return;
}
// Block feeding while untying
UntyingTask activeUntieTask = feederBindState.getCurrentUntyingTask();
if (activeUntieTask != null && !activeUntieTask.isStopped()) {
return;
}
// Get current feeding task (if any)
TimedInteractTask currentTask = feederBindState.getCurrentFeedingTask();
if (
currentTask == null ||
!currentTask.isSameTarget(targetEntity) ||
currentTask.isStopped()
) {
// Create new force feeding task (5 seconds)
ForceFeedingTask newTask = new ForceFeedingTask(
targetState,
targetEntity,
5,
feeder.level(),
feeder,
heldItem,
feeder.getInventory().selected
);
feederBindState.setCurrentFeedingTask(newTask);
newTask.setUpTargetState();
newTask.start();
currentTask = newTask;
TiedUpMod.LOGGER.debug(
"[RESTRAINT] {} started force feeding {} (5 seconds)",
feeder.getName().getString(),
targetEntity.getName().getString()
);
} else {
// Continue existing task - ensure feeder is set
if (currentTask instanceof ForceFeedingTask feedTask) {
feedTask.setFeeder(feeder);
}
}
currentTask.update();
// Cancel to prevent mobInteract (avoids instant NPC feed)
event.setCancellationResult(InteractionResult.SUCCESS);
event.setCanceled(true);
}
/**
* Alert all collar owners that someone is trying to free their slave.
*
* @param slave The slave being freed
* @param liberator The player trying to free them
*/
private static void alertCollarOwners(
com.tiedup.remake.entities.AbstractTiedUpNpc slave,
Player liberator
) {
if (!(slave.level() instanceof ServerLevel serverLevel)) return;
ItemStack collar = slave.getEquipment(BodyRegionV2.NECK);
if (collar.isEmpty() || !CollarHelper.isCollar(collar)) {
return;
}
List<UUID> owners = CollarHelper.getOwners(collar);
if (owners.isEmpty()) return;
// Create alert packet
PacketSlaveBeingFreed alertPacket = new PacketSlaveBeingFreed(
slave.getNpcName(),
liberator.getName().getString(),
slave.blockPosition().getX(),
slave.blockPosition().getY(),
slave.blockPosition().getZ()
);
// Send to all online owners
for (UUID ownerUUID : owners) {
ServerPlayer owner = serverLevel
.getServer()
.getPlayerList()
.getPlayer(ownerUUID);
if (owner != null && owner != liberator) {
ModNetwork.sendToPlayer(alertPacket, owner);
}
}
}
}