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.
673 lines
25 KiB
Java
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);
|
|
}
|
|
}
|
|
}
|
|
}
|