Clean repo for open source release

Remove build artifacts, dev tool configs, unused dependencies,
and third-party source dumps. Add proper README, update .gitignore,
clean up Makefile.
This commit is contained in:
NotEvil
2026-04-12 00:51:22 +02:00
parent 2e7a1d403b
commit f6466360b6
1947 changed files with 238025 additions and 1 deletions

View File

@@ -0,0 +1,548 @@
package com.tiedup.remake.events.restriction;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.items.ItemLockpick;
import com.tiedup.remake.items.base.IKnife;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.dialogue.EntityDialogueManager;
import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.SystemMessageManager.MessageCategory;
import com.tiedup.remake.util.GameConstants;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.ButtonBlock;
import net.minecraft.world.level.block.DoorBlock;
import net.minecraft.world.level.block.FenceGateBlock;
import net.minecraft.world.level.block.LeverBlock;
import net.minecraft.world.level.block.TrapDoorBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.entity.living.LivingEvent;
import net.minecraftforge.event.entity.living.LivingHurtEvent;
import net.minecraftforge.event.entity.living.LivingEntityUseItemEvent;
import net.minecraftforge.event.entity.player.AttackEntityEvent;
import net.minecraftforge.event.entity.player.PlayerEvent;
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
import net.minecraftforge.event.level.BlockEvent;
import net.minecraftforge.eventbus.api.EventPriority;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
/**
* Unified event handler for all bondage item restrictions.
*
* Phase 14.4+: Centralized restriction system
* Leg Binding: Separate arm/leg restrictions
*
* This handler manages restrictions based on equipped bondage items:
*
* === LEGS BOUND (hasLegsBound) ===
* - No sprinting
* - No climbing ladders (can descend)
* - No elytra flying
* - Reduced swim speed (50%)
*
* === ARMS BOUND (hasArmsBound) ===
* - No block breaking
* - No block placing
* - No attacking
* - No item usage
* - No block interaction (except allowed blocks)
*
* === MITTENS ===
* - Additional hand restrictions
* - Allowed: buttons, levers, doors, trapdoors, fence gates (when arms bound only)
*
* === BLINDFOLDED ===
* - Vision effects (handled client-side)
*
* === GAGGED ===
* - Chat muffling (handled in ChatEventHandler)
*
* @see RestraintTaskTickHandler for task tick progression (untying, tying, force feeding)
*/
@Mod.EventBusSubscriber(
modid = TiedUpMod.MOD_ID,
bus = Mod.EventBusSubscriber.Bus.FORGE
)
public class BondageItemRestrictionHandler {
/** Cooldown between restriction messages (in milliseconds) */
private static final long MESSAGE_COOLDOWN_MS = 2000; // 2 seconds
/** Swim speed multiplier when tied (from config) */
/** Per-player, per-category message cooldowns */
private static final Map<
UUID,
Map<MessageCategory, Long>
> messageCooldowns = new HashMap<>();
// ========================================
// MOVEMENT RESTRICTIONS
// ========================================
/**
* Movement restrictions per tick (throttled to every 5 ticks).
* - Prevent sprinting when tied
* - Prevent ladder climbing when tied (can descend)
* - Reduce swim speed when tied
*/
@SubscribeEvent
public static void onPlayerTick(TickEvent.PlayerTickEvent event) {
if (event.side.isClient() || event.phase != TickEvent.Phase.END) {
return;
}
Player player = event.player;
// Throttle: only check every 5 ticks (0.25 seconds) - per-player timing
if (player.tickCount % 5 != 0) {
return;
}
IRestrainable state = KidnappedHelper.getKidnappedState(player);
if (state == null) return;
// Movement restrictions only apply when LEGS are bound
if (!state.hasLegsBound()) return;
// === SPRINT RESTRICTION ===
if (player.isSprinting()) {
player.setSprinting(false);
}
// === LADDER RESTRICTION ===
// Can descend but not climb
if (player.onClimbable()) {
Vec3 motion = player.getDeltaMovement();
if (motion.y > 0) {
player.setDeltaMovement(motion.x, 0, motion.z);
}
}
// === SWIM SPEED RESTRICTION ===
if (player.isInWater() && player.isSwimming()) {
Vec3 motion = player.getDeltaMovement();
player.setDeltaMovement(motion.scale(
com.tiedup.remake.core.ModConfig.SERVER.tiedSwimSpeedMultiplier.get()
));
}
}
/**
* Prevent elytra flying when tied.
*/
@SubscribeEvent
public static void onLivingTick(LivingEvent.LivingTickEvent event) {
if (!(event.getEntity() instanceof Player player)) return;
if (player.level().isClientSide) return;
// Cheap check FIRST: only proceed if player is flying
if (!player.isFallFlying()) return;
IRestrainable state = KidnappedHelper.getKidnappedState(player);
if (state == null || !state.hasLegsBound()) return;
// Elytra restriction only applies when LEGS are bound
player.stopFallFlying();
sendRestrictionMessage(player, MessageCategory.NO_ELYTRA);
}
// ========================================
// INTERACTION RESTRICTIONS
// ========================================
/**
* Block breaking restrictions.
* - Tied: Cannot break blocks
* - Mittens: Cannot break blocks (hands covered)
*/
@SubscribeEvent(priority = EventPriority.HIGH)
public static void onBlockBreak(BlockEvent.BreakEvent event) {
Player player = event.getPlayer();
if (player == null) return;
IRestrainable state = KidnappedHelper.getKidnappedState(player);
if (state == null) return;
// Block breaking requires ARMS to be free
if (state.hasArmsBound()) {
event.setCanceled(true);
sendRestrictionMessage(player, MessageCategory.CANT_BREAK_TIED);
} else if (state.hasMittens()) {
event.setCanceled(true);
sendRestrictionMessage(player, MessageCategory.CANT_BREAK_MITTENS);
}
}
/**
* Block placing restrictions.
* - Arms bound: Cannot place blocks
* - Mittens: Cannot place blocks (hands covered)
*/
@SubscribeEvent(priority = EventPriority.HIGH)
public static void onBlockPlace(BlockEvent.EntityPlaceEvent event) {
if (!(event.getEntity() instanceof Player player)) return;
IRestrainable state = KidnappedHelper.getKidnappedState(player);
if (state == null) return;
// Block placing requires ARMS to be free
if (state.hasArmsBound()) {
event.setCanceled(true);
sendRestrictionMessage(player, MessageCategory.CANT_PLACE_TIED);
} else if (state.hasMittens()) {
event.setCanceled(true);
sendRestrictionMessage(player, MessageCategory.CANT_PLACE_MITTENS);
}
}
/**
* Block interaction restrictions.
* - Arms bound: Cannot interact with most blocks
* - Mittens: Cannot interact (hands covered)
* - Exception: Buttons, levers, doors (can be pressed/opened with body when arms bound, but not with mittens)
*/
@SubscribeEvent(priority = EventPriority.HIGH)
public static void onRightClickBlock(
PlayerInteractEvent.RightClickBlock event
) {
Player player = event.getEntity();
IRestrainable state = KidnappedHelper.getKidnappedState(player);
if (state == null) return;
boolean hasArmsBound = state.hasArmsBound();
boolean hasMittens = state.hasMittens();
if (!hasArmsBound && !hasMittens) return;
// Get the block being interacted with
BlockState blockState = event.getLevel().getBlockState(event.getPos());
// Allow specific block interactions (can press with body/head) - only when arms bound WITHOUT mittens
// Mittens block ALL interactions since you can't use buttons with covered hands
if (
hasArmsBound && !hasMittens && isAllowedTiedInteraction(blockState)
) {
return; // Allow this interaction
}
// Block all other interactions
event.setCanceled(true);
if (hasArmsBound) {
sendRestrictionMessage(player, MessageCategory.CANT_INTERACT_TIED);
} else {
sendRestrictionMessage(
player,
MessageCategory.CANT_INTERACT_MITTENS
);
}
}
/**
* Item usage restrictions.
* - Arms bound: Cannot use items (except knife/lockpick without mittens)
* - Mittens: Cannot use items (hands covered)
*
* v2.5: Allow knife and lockpick usage when tied (but NOT with mittens).
* This enables the player to cut/lockpick their binds while restrained.
*/
@SubscribeEvent(priority = EventPriority.HIGH)
public static void onRightClickItem(
PlayerInteractEvent.RightClickItem event
) {
Player player = event.getEntity();
ItemStack heldItem = event.getItemStack();
IRestrainable state = KidnappedHelper.getKidnappedState(player);
if (state == null) return;
// v2.5: Allow knife and lockpick usage when tied (but NOT with mittens)
if (state.hasArmsBound() && !state.hasMittens()) {
// Allow knives - they have special cutting logic
if (heldItem.getItem() instanceof IKnife) {
return; // Don't block
}
// Allow lockpicks - they open the struggle choice screen
if (heldItem.getItem() instanceof ItemLockpick) {
return; // Don't block
}
}
// Item usage requires ARMS to be free (except above exceptions)
if (state.hasArmsBound()) {
event.setCanceled(true);
sendRestrictionMessage(player, MessageCategory.CANT_USE_ITEM_TIED);
} else if (state.hasMittens()) {
event.setCanceled(true);
sendRestrictionMessage(
player,
MessageCategory.CANT_USE_ITEM_MITTENS
);
}
}
/**
* Attack restrictions.
* - Captive/slave attacking their kidnapper: Cannot attack, gets shocked
* - Arms bound: Cannot attack at all
* - Mittens only: Can punch but damage is zeroed in onLivingHurt()
*/
@SubscribeEvent(priority = EventPriority.HIGH)
public static void onAttack(AttackEntityEvent event) {
Player player = event.getEntity();
Entity target = event.getTarget();
// Check if player is attacking a kidnapper
if (target instanceof EntityKidnapper kidnapper) {
// Check if the attacker is this kidnapper's captive or job worker
boolean isThisCaptive = false;
boolean isThisJobWorker = false;
// Check if player is the current captive
IRestrainable captive = kidnapper.getCaptive();
if (captive != null) {
UUID captiveUUID = captive.getKidnappedUniqueId();
if (
captiveUUID != null && captiveUUID.equals(player.getUUID())
) {
isThisCaptive = true;
}
}
// Check if player is the job worker
UUID workerUUID = kidnapper.getJobWorkerUUID();
if (workerUUID != null && workerUUID.equals(player.getUUID())) {
isThisJobWorker = true;
}
// Only block if player is THIS kidnapper's captive or job worker
if (isThisCaptive || isThisJobWorker) {
event.setCanceled(true);
// Kidnapper responds with dialogue
kidnapper.talkTo(
player,
DialogueCategory.ATTACK_SLAVE
);
// Shock the captive/worker
IRestrainable playerState = KidnappedHelper.getKidnappedState(
player
);
if (playerState != null) {
playerState.shockKidnapped(
" (You cannot attack your master!)",
2.0f
);
}
TiedUpMod.LOGGER.debug(
"[RESTRICTION] {} tried to attack master {} - shocked (captive={}, worker={})",
player.getName().getString(),
kidnapper.getName().getString(),
isThisCaptive,
isThisJobWorker
);
return;
}
// Other players CAN attack this kidnapper - don't block
}
IRestrainable state = KidnappedHelper.getKidnappedState(player);
if (state == null) return;
// Arms bound: cannot attack at all
if (state.hasArmsBound()) {
event.setCanceled(true);
return;
}
// Mittens only (not arms bound): allow punch animation
// Damage will be zeroed in onLivingHurt()
}
/**
* Damage reduction for mittens.
* When a player with mittens (but not arms bound) attacks, damage is reduced to 0.
* The punch animation still plays, but no damage is dealt.
*/
@SubscribeEvent(priority = EventPriority.HIGHEST)
public static void onLivingHurt(LivingHurtEvent event) {
// Check if the attacker is a player with mittens
if (!(event.getSource().getEntity() instanceof Player player)) return;
IRestrainable state = KidnappedHelper.getKidnappedState(player);
if (state == null) return;
// Mittens only (not arms bound): zero damage
if (state.hasMittens() && !state.hasArmsBound()) {
event.setAmount(0);
}
}
/**
* Fall damage protection for captive players on a leash.
* When being led by a kidnapper/master, the player has no control over movement
* and should not take fall damage from terrain the captor drags them through.
*/
@SubscribeEvent(priority = EventPriority.HIGHEST)
public static void onCaptiveFallDamage(LivingHurtEvent event) {
if (!(event.getEntity() instanceof Player player)) return;
if (
!event
.getSource()
.is(net.minecraft.world.damagesource.DamageTypes.FALL)
) return;
IRestrainable state = KidnappedHelper.getKidnappedState(player);
if (state == null) return;
if (state.isCaptive()) {
event.setCanceled(true);
}
}
/**
* Notify the Master when their pet takes damage.
* This allows the Master to react (e.g., get up from human chair).
*/
@SubscribeEvent
public static void onPetHurt(LivingHurtEvent event) {
if (!(event.getEntity() instanceof ServerPlayer pet)) return;
if (event.getAmount() <= 0) return;
// Find if this player has a nearby Master who owns them
var masters = pet.level().getEntitiesOfClass(
com.tiedup.remake.entities.EntityMaster.class,
pet.getBoundingBox().inflate(32.0),
m -> m.isAlive() && m.hasPet()
&& pet.getUUID().equals(m.getStateManager().getPetPlayerUUID())
);
for (var master : masters) {
master.onPetHurt(event.getSource(), event.getAmount());
}
}
// ========== BREAK SPEED REDUCTION ==========
/**
* Apply break speed reduction when tied up.
* This makes mining extremely slow (10% speed) when restrained.
*/
@SubscribeEvent
public static void onCalculateSpeed(PlayerEvent.BreakSpeed event) {
Player player = event.getEntity();
IBondageState state = KidnappedHelper.getKidnappedState(player);
if (state != null && state.isTiedUp()) {
event.setNewSpeed(
event.getNewSpeed() * GameConstants.TIED_BREAK_SPEED_MULTIPLIER
);
}
}
// ========== BLOCK SELF-EATING WHEN GAGGED ==========
/**
* Block gagged players from eating food themselves.
*/
@SubscribeEvent
public static void onGaggedPlayerEat(LivingEntityUseItemEvent.Start event) {
if (!(event.getEntity() instanceof Player player)) {
return;
}
if (!event.getItem().getItem().isEdible()) {
return;
}
IBondageState state = KidnappedHelper.getKidnappedState(player);
if (state != null && state.isGagged()) {
event.setCanceled(true);
player.displayClientMessage(
Component.literal("You can't eat with a gag on.").withStyle(
ChatFormatting.RED
),
true
);
}
}
// ========================================
// HELPER METHODS
// ========================================
/**
* Check if a block interaction should be allowed when tied.
* These blocks can be activated with body/head, not hands.
*/
private static boolean isAllowedTiedInteraction(BlockState blockState) {
// Buttons - can press with head/body
if (blockState.getBlock() instanceof ButtonBlock) return true;
// Levers - can push with body
if (blockState.getBlock() instanceof LeverBlock) return true;
// Doors - can push open with body
if (blockState.getBlock() instanceof DoorBlock) return true;
// Trapdoors - debatable, but allow for now
if (blockState.getBlock() instanceof TrapDoorBlock) return true;
// Fence gates - can push open
if (blockState.getBlock() instanceof FenceGateBlock) return true;
return false;
}
/**
* Send a restriction message to the player.
* Uses per-category cooldown to prevent spam.
*/
private static void sendRestrictionMessage(
Player player,
MessageCategory category
) {
// Only send on server side
if (player.level().isClientSide) return;
UUID playerId = player.getUUID();
long now = System.currentTimeMillis();
// Get or create player's cooldown map
Map<MessageCategory, Long> playerCooldowns =
messageCooldowns.computeIfAbsent(playerId, k -> new HashMap<>());
// Check cooldown for this category
Long lastSent = playerCooldowns.get(category);
if (lastSent != null && (now - lastSent) < MESSAGE_COOLDOWN_MS) {
return; // Still on cooldown
}
// Update cooldown and send message
playerCooldowns.put(category, now);
SystemMessageManager.sendRestriction(player, category);
}
/**
* Clean up cooldowns for a player (call when they disconnect).
*/
public static void clearCooldowns(UUID playerId) {
messageCooldowns.remove(playerId);
}
}

View File

@@ -0,0 +1,154 @@
package com.tiedup.remake.events.restriction;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityLaborGuard;
import com.tiedup.remake.prison.LaborRecord;
import com.tiedup.remake.prison.PrisonerManager;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.jetbrains.annotations.Nullable;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.ChestBlock;
import net.minecraft.world.level.block.ShulkerBoxBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraftforge.event.entity.item.ItemTossEvent;
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
/**
* Event handler for labor tool protection.
*
* Prevents prisoners from:
* - Dropping labor tools
* - Storing labor tools in containers
*
* Note: Death handling removed - if prisoner dies, they're no longer working anyway.
*
* This is extracted from CampLaborEventHandler after the refactoring to remove event-driven task tracking.
* Only the essential protection mechanisms remain.
*/
@Mod.EventBusSubscriber(modid = TiedUpMod.MOD_ID)
public class LaborToolProtectionHandler {
private static final Map<UUID, Long> lastDropWarningTick = new HashMap<>();
private static final long DROP_WARNING_COOLDOWN = 60; // 3 seconds
/** Remove player entry on disconnect to prevent memory leak. */
public static void cleanupPlayer(java.util.UUID playerId) {
lastDropWarningTick.remove(playerId);
}
/**
* Prevent dropping labor tools.
* Forge removes the item from inventory before firing ItemTossEvent,
* so we must restore it after canceling.
*/
@SubscribeEvent
public static void onItemToss(ItemTossEvent event) {
ItemStack stack = event.getEntity().getItem();
if (!isLaborTool(stack)) return;
// Cancel + restore (Forge removes from inventory before event fires)
event.setCanceled(true);
event.getPlayer().getInventory().add(stack);
Player player = event.getPlayer();
if (!(player instanceof ServerPlayer serverPlayer)) return;
if (!(player.level() instanceof ServerLevel level)) return;
// Cooldown
long tick = level.getGameTime();
Long last = lastDropWarningTick.get(player.getUUID());
if (last != null && (tick - last) < DROP_WARNING_COOLDOWN) return;
lastDropWarningTick.put(player.getUUID(), tick);
// Guard reaction
EntityLaborGuard guard = findGuardForPlayer(level, player.getUUID());
if (guard != null && guard.isAlive()) {
guard.getLookControl().setLookAt(player);
guard.guardSay(
serverPlayer,
"guard.labor.drop_tool",
"Stupid! Don't drop your tools!"
);
} else {
player.displayClientMessage(
Component.literal("You cannot drop labor tools!"),
true
);
}
}
/**
* Prevent storing labor tools in containers.
*/
@SubscribeEvent
public static void onRightClickBlock(
PlayerInteractEvent.RightClickBlock event
) {
Player player = event.getEntity();
ItemStack held = player.getMainHandItem();
if (!isLaborTool(held)) return;
BlockState state = event.getLevel().getBlockState(event.getPos());
if (
!(state.getBlock() instanceof ChestBlock) &&
!(state.getBlock() instanceof ShulkerBoxBlock)
) return;
event.setCanceled(true);
if (
player instanceof ServerPlayer sp &&
player.level() instanceof ServerLevel sl
) {
// Reuse same cooldown
long tick = sl.getGameTime();
Long last = lastDropWarningTick.get(player.getUUID());
if (last != null && (tick - last) < DROP_WARNING_COOLDOWN) return;
lastDropWarningTick.put(player.getUUID(), tick);
EntityLaborGuard guard = findGuardForPlayer(sl, player.getUUID());
if (guard != null && guard.isAlive()) {
guard.getLookControl().setLookAt(player);
guard.guardSay(
sp,
"guard.labor.hide_tool",
"Don't try to hide your tools!"
);
} else {
player.displayClientMessage(
Component.literal("You cannot store labor tools!"),
true
);
}
}
}
@Nullable
private static EntityLaborGuard findGuardForPlayer(
ServerLevel level,
UUID playerUUID
) {
PrisonerManager manager = PrisonerManager.get(level);
LaborRecord labor = manager.getLaborRecord(playerUUID);
UUID guardId = labor.getGuardId();
if (guardId == null) return null;
net.minecraft.world.entity.Entity entity = level.getEntity(guardId);
if (entity instanceof EntityLaborGuard guard) return guard;
return null;
}
/**
* Check if an item is a labor tool (tagged as LaborTool).
*/
private static boolean isLaborTool(ItemStack stack) {
return stack.hasTag() && stack.getTag().getBoolean("LaborTool");
}
}

View File

@@ -0,0 +1,349 @@
package com.tiedup.remake.events.restriction;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.entities.EntityMaster;
import com.tiedup.remake.items.ItemChokeCollar;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.blocks.PetBedManager;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.Style;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.BedBlock;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
import net.minecraftforge.event.entity.player.PlayerSleepInBedEvent;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.eventbus.api.EventPriority;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
/**
* Event handler for pet play restrictions.
*
* When a player has a pet play collar (from EntityMaster):
* - Cannot eat food from hand (must use Bowl block)
* - Cannot sleep in normal beds (must use Pet Bed block)
*
* Placeholder blocks:
* - Bowl: Cauldron (shift+right-click with food to eat)
* - Pet Bed: White carpet (shift+right-click to sleep)
*/
@Mod.EventBusSubscriber(
modid = TiedUpMod.MOD_ID,
bus = Mod.EventBusSubscriber.Bus.FORGE
)
public class PetPlayRestrictionHandler {
/** Message cooldown in milliseconds */
private static final long MESSAGE_COOLDOWN_MS = 3000;
/** Last message time per player */
private static final java.util.Map<java.util.UUID, Long> lastMessageTime =
new java.util.HashMap<>();
// ========================================
// EATING RESTRICTION
// ========================================
/**
* Prevent pet play players from eating food from hand.
* They must use a Bowl block (cauldron placeholder).
*/
@SubscribeEvent(priority = EventPriority.HIGH)
public static void onRightClickItem(
PlayerInteractEvent.RightClickItem event
) {
Player player = event.getEntity();
if (player.level().isClientSide) return;
// Check for pet play collar
if (!EntityMaster.hasPetCollar(player)) return;
ItemStack heldItem = event.getItemStack();
// Check if trying to eat food
if (heldItem.isEdible()) {
event.setCanceled(true);
event.setCancellationResult(
net.minecraft.world.InteractionResult.FAIL
);
sendThrottledMessage(
player,
"You cannot eat from your hand! Use a bowl."
);
TiedUpMod.LOGGER.debug(
"[PetPlayRestrictionHandler] Blocked {} from eating food directly",
player.getName().getString()
);
}
}
/**
* Allow eating from Bowl block (cauldron placeholder).
* Shift+right-click with food on cauldron to eat.
*/
@SubscribeEvent(priority = EventPriority.HIGH)
public static void onRightClickBlock(
PlayerInteractEvent.RightClickBlock event
) {
Player player = event.getEntity();
if (player.level().isClientSide) return;
// Check for pet play collar
if (!EntityMaster.hasPetCollar(player)) return;
BlockPos pos = event.getPos();
BlockState state = player.level().getBlockState(pos);
ItemStack heldItem = event.getItemStack();
// Bowl interaction (cauldron placeholder)
if (
state.getBlock() == Blocks.CAULDRON &&
player.isShiftKeyDown() &&
heldItem.isEdible()
) {
// Allow eating from bowl
// Consume the food
if (player instanceof ServerPlayer serverPlayer) {
net.minecraft.world.food.FoodProperties food = heldItem
.getItem()
.getFoodProperties();
if (food != null) {
serverPlayer
.getFoodData()
.eat(food.getNutrition(), food.getSaturationModifier());
// Shrink the item
heldItem.shrink(1);
// Play eating sound
player
.level()
.playSound(
null,
pos,
net.minecraft.sounds.SoundEvents.GENERIC_EAT,
net.minecraft.sounds.SoundSource.PLAYERS,
1.0f,
1.0f
);
TiedUpMod.LOGGER.debug(
"[PetPlayRestrictionHandler] {} ate from bowl",
player.getName().getString()
);
// Cancel to prevent normal cauldron interaction
event.setCanceled(true);
event.setCancellationResult(
net.minecraft.world.InteractionResult.SUCCESS
);
}
}
}
// Pet bed interaction (carpet placeholder) - handled in sleep event
}
// ========================================
// SLEEPING RESTRICTION
// ========================================
/**
* Prevent pet play players from sleeping in normal beds.
* They must use a Pet Bed block (carpet placeholder).
*/
@SubscribeEvent(priority = EventPriority.HIGH)
public static void onSleepInBed(PlayerSleepInBedEvent event) {
Player player = event.getEntity();
if (player.level().isClientSide) return;
// Check for pet play collar
if (!EntityMaster.hasPetCollar(player)) return;
// Block sleeping in beds
BlockState state = player.level().getBlockState(event.getPos());
if (state.getBlock() instanceof BedBlock) {
event.setResult(Player.BedSleepingProblem.OTHER_PROBLEM);
sendThrottledMessage(
player,
"You cannot sleep in a bed! Use your pet bed."
);
TiedUpMod.LOGGER.debug(
"[PetPlayRestrictionHandler] Blocked {} from sleeping in bed",
player.getName().getString()
);
}
}
/**
* Allow sleeping on Pet Bed block (carpet placeholder).
* Shift+right-click on white carpet to sleep.
*/
@SubscribeEvent(priority = EventPriority.NORMAL)
public static void onInteractForSleep(
PlayerInteractEvent.RightClickBlock event
) {
Player player = event.getEntity();
if (player.level().isClientSide) return;
// Check for pet play collar
if (!EntityMaster.hasPetCollar(player)) return;
BlockPos pos = event.getPos();
BlockState state = player.level().getBlockState(pos);
// Pet bed interaction (white carpet placeholder)
if (
state.getBlock() == Blocks.WHITE_CARPET && player.isShiftKeyDown()
) {
if (player instanceof ServerPlayer serverPlayer) {
// Try to sleep
Player.BedSleepingProblem problem = canSleepNow(serverPlayer);
if (problem == null) {
// Set spawn point to pet bed location
serverPlayer.setRespawnPosition(
serverPlayer.level().dimension(),
pos,
serverPlayer.getYRot(),
false,
true
);
// Start sleeping (simplified - real implementation would need proper sleep mechanics)
serverPlayer.sendSystemMessage(
Component.literal(
"You curl up in your pet bed..."
).withStyle(Style.EMPTY.withColor(0x888888))
);
// Apply sleep effects (heal, skip night handled elsewhere)
serverPlayer.heal(2.0f);
TiedUpMod.LOGGER.debug(
"[PetPlayRestrictionHandler] {} slept in pet bed",
player.getName().getString()
);
event.setCanceled(true);
event.setCancellationResult(
net.minecraft.world.InteractionResult.SUCCESS
);
} else {
sendThrottledMessage(
player,
"You can only sleep at night or during thunderstorms."
);
}
}
}
}
/**
* Check if player can sleep now (time/weather check).
*/
private static Player.BedSleepingProblem canSleepNow(ServerPlayer player) {
// Check time of day
if (player.level().isDay() && !player.level().isThundering()) {
return Player.BedSleepingProblem.NOT_POSSIBLE_NOW;
}
return null;
}
// ========================================
// CHOKE COLLAR EFFECT
// ========================================
/**
* Apply choke collar effect on player tick.
* When choking is active, rapidly reduces air supply to cause drowning damage.
*/
@SubscribeEvent
public static void onPlayerTick(TickEvent.PlayerTickEvent event) {
// Only process on server side, at END phase
if (event.phase != TickEvent.Phase.END) return;
if (event.player.level().isClientSide) return;
if (!(event.player instanceof ServerPlayer player)) return;
// Check pet bed sit cancellation (movement detection)
PetBedManager.tickPlayer(player);
// Check pet cage validity
com.tiedup.remake.v2.blocks.PetCageManager.tickPlayer(player);
// Get player's collar
PlayerBindState bindState = PlayerBindState.getInstance(player);
if (bindState == null || !bindState.hasCollar()) return;
ItemStack collar = bindState.getEquipment(BodyRegionV2.NECK);
if (collar.getItem() instanceof ItemChokeCollar chokeCollar) {
if (chokeCollar.isChoking(collar)) {
// Apply ChokeEffect (short duration, re-applied each active tick)
if (
!player.hasEffect(
com.tiedup.remake.core.ModEffects.CHOKE.get()
)
) {
player.addEffect(
new MobEffectInstance(
com.tiedup.remake.core.ModEffects.CHOKE.get(),
40,
0,
false,
false,
false
)
);
}
} else {
// Remove effect if choke is deactivated
player.removeEffect(
com.tiedup.remake.core.ModEffects.CHOKE.get()
);
}
}
}
// ========================================
// UTILITY
// ========================================
/**
* BUG FIX: Clean up player data to prevent memory leak.
* Called on player logout.
*/
public static void clearPlayer(java.util.UUID playerId) {
lastMessageTime.remove(playerId);
}
/**
* Send a message with cooldown to prevent spam.
*/
private static void sendThrottledMessage(Player player, String message) {
long now = System.currentTimeMillis();
Long lastTime = lastMessageTime.get(player.getUUID());
if (lastTime == null || now - lastTime > MESSAGE_COOLDOWN_MS) {
lastMessageTime.put(player.getUUID(), now);
player.sendSystemMessage(
Component.literal(message).withStyle(
Style.EMPTY.withColor(EntityMaster.MASTER_NAME_COLOR)
)
);
}
}
}

View File

@@ -0,0 +1,672 @@
package com.tiedup.remake.events.restriction;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.minigame.StruggleSessionManager;
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.core.SettingsAccessor;
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;
}
// Phase 13: Auto-shock collar logic (Player-specific feature)
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);
// Phase 11: Check collar ownership for TiedUp NPCs
// 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.
* Phase 11: Multiplayer protection system
*
* @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() ||
!(collar.getItem() instanceof ItemCollar collarItem)
) {
return;
}
List<UUID> owners = collarItem.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);
}
}
}
}