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,219 @@
package com.tiedup.remake.tasks;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.action.PacketForceFeeding;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.state.PlayerBindState;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
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;
/**
* Server-side task for force feeding a gagged entity.
* Player must keep looking at the target for 5 seconds to complete.
*/
public class ForceFeedingTask extends TimedInteractTask {
private Player feeder;
private ItemStack foodStack;
private int sourceSlot;
public ForceFeedingTask(
IBondageState targetState,
LivingEntity targetEntity,
int seconds,
Level level,
Player feeder,
ItemStack foodStack,
int sourceSlot
) {
super(targetState, targetEntity, seconds, level);
this.feeder = feeder;
this.foodStack = foodStack.copy();
this.sourceSlot = sourceSlot;
}
public void setFeeder(Player feeder) {
this.feeder = feeder;
}
@Override
public synchronized void update() {
// Cancel if feeder is gone or dead
if (feeder == null || !feeder.isAlive() || feeder.isRemoved()) {
stop();
return;
}
if (targetEntity != null) {
double distance = feeder.distanceTo(targetEntity);
if (distance > 4.0) {
TiedUpMod.LOGGER.debug(
"[ForceFeedingTask] Feeder {} moved too far from target ({} blocks), cancelling",
feeder.getName().getString(),
String.format("%.1f", distance)
);
stop();
return;
}
if (!feeder.hasLineOfSight(targetEntity)) {
TiedUpMod.LOGGER.debug(
"[ForceFeedingTask] Feeder {} lost line of sight to target, cancelling",
feeder.getName().getString()
);
stop();
return;
}
}
super.update();
}
@Override
protected void onComplete() {
if (!isTargetValid()) {
TiedUpMod.LOGGER.warn(
"[ForceFeedingTask] Target entity no longer valid, cancelling task"
);
stop();
return;
}
if (feeder == null || !feeder.isAlive()) {
TiedUpMod.LOGGER.warn(
"[ForceFeedingTask] Feeder no longer valid, cancelling task"
);
stop();
return;
}
// Validate the item in the source slot is still edible
ItemStack slotStack = feeder.getInventory().getItem(sourceSlot);
if (slotStack.isEmpty() || !slotStack.getItem().isEdible()) {
TiedUpMod.LOGGER.warn(
"[ForceFeedingTask] Food item no longer in slot {}, cancelling",
sourceSlot
);
stop();
return;
}
TiedUpMod.LOGGER.info(
"[ForceFeedingTask] Force feeding complete for {}",
targetEntity.getName().getString()
);
if (targetEntity instanceof Player targetPlayer) {
// Feed the player using vanilla eat mechanics
targetPlayer.eat(targetPlayer.level(), slotStack.copy());
slotStack.shrink(1);
} else if (targetEntity instanceof EntityDamsel damsel) {
// Use existing NPC feed method (handles shrink internally)
damsel.feedByPlayer(feeder, slotStack);
}
// Play eating sound at target
targetEntity
.level()
.playSound(
null,
targetEntity.getX(),
targetEntity.getY(),
targetEntity.getZ(),
SoundEvents.GENERIC_EAT,
SoundSource.PLAYERS,
1.0F,
1.0F
);
// Send messages
String targetName = targetEntity.getName().getString();
String feederName = feeder.getName().getString();
if (feeder instanceof ServerPlayer serverFeeder) {
serverFeeder.displayClientMessage(
Component.literal(
"You force fed " + targetName + "."
).withStyle(ChatFormatting.GRAY),
true
);
}
if (targetEntity instanceof ServerPlayer serverTarget) {
serverTarget.displayClientMessage(
Component.literal("You have been force fed.").withStyle(
ChatFormatting.GRAY
),
true
);
}
stop();
// Send completion packets (stateInfo = -1)
if (targetEntity instanceof ServerPlayer serverTarget) {
PacketForceFeeding completionPacket = new PacketForceFeeding(
-1,
this.getMaxSeconds(),
false,
feederName
);
ModNetwork.sendToPlayer(completionPacket, serverTarget);
}
if (feeder instanceof ServerPlayer serverFeeder) {
PacketForceFeeding completionPacket = new PacketForceFeeding(
-1,
this.getMaxSeconds(),
true,
targetName
);
ModNetwork.sendToPlayer(completionPacket, serverFeeder);
}
}
@Override
public void sendProgressPackets() {
if (stopped) return;
String feederName =
feeder != null ? feeder.getName().getString() : "Someone";
String targetName = targetEntity.getName().getString();
// Packet to target (if player): isActiveRole=false, shows feeder's name
if (targetEntity instanceof ServerPlayer serverTarget) {
PacketForceFeeding victimPacket = new PacketForceFeeding(
this.getState(),
this.getMaxSeconds(),
false,
feederName
);
ModNetwork.sendToPlayer(victimPacket, serverTarget);
}
// Packet to feeder: isActiveRole=true, shows target's name
if (feeder instanceof ServerPlayer serverFeeder) {
PacketForceFeeding feederPacket = new PacketForceFeeding(
this.getState(),
this.getMaxSeconds(),
true,
targetName
);
ModNetwork.sendToPlayer(feederPacket, serverFeeder);
}
}
@Override
public void setUpTargetState() {
// Server-side: nothing to do - client handles its own PlayerStateTask via packets
}
}

View File

@@ -0,0 +1,146 @@
package com.tiedup.remake.tasks;
import com.tiedup.remake.util.time.Timer;
import net.minecraft.client.Minecraft;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
/**
* Phase 6: Lightweight client-side task state for displaying progress.
*
* Based on original PlayerStateTask from 1.12.2
*
* This is used on the CLIENT SIDE ONLY to track the progress of
* tying/untying tasks for GUI display (progress bars, messages, etc.).
*
* The server sends progress updates via packets, and this class
* stores them and auto-expires if updates stop coming.
*/
@OnlyIn(Dist.CLIENT)
public class PlayerStateTask {
private final int maxState; // Total time in seconds
private Timer timerOutdating; // Auto-expiration timer (3 seconds)
private int state; // Current elapsed time in seconds
// Role info for progress bar display
private final boolean isKidnapper; // true = doing the tying, false = being tied
private final String otherEntityName; // Name of the other party
// Outdating timeout: 3 seconds (60 ticks)
// Slightly longer than server's 2 seconds to account for network delay
private static final int OUTDATING_TIMEOUT_SECONDS = 3;
/**
* Create a new client-side task state.
*
* @param maxState Total duration in seconds
*/
public PlayerStateTask(int maxState) {
this(maxState, false, null);
}
/**
* Create a new client-side task state with role info.
*
* @param maxState Total duration in seconds
* @param isKidnapper true if this player is doing the tying
* @param otherEntityName Name of the other party
*/
public PlayerStateTask(
int maxState,
boolean isKidnapper,
String otherEntityName
) {
this.maxState = maxState;
this.state = 0;
this.isKidnapper = isKidnapper;
this.otherEntityName = otherEntityName;
this.timerOutdating = new Timer(
OUTDATING_TIMEOUT_SECONDS,
Minecraft.getInstance().level
);
}
/**
* Update the task progress.
* Called when receiving a progress packet from the server.
*
* @param state Current elapsed time in seconds
*/
public synchronized void update(int state) {
this.state = state;
// Reset outdating timer (we received an update from server)
this.timerOutdating = new Timer(
OUTDATING_TIMEOUT_SECONDS,
Minecraft.getInstance().level
);
}
/**
* Check if this task state has become outdated.
* A task is outdated if we haven't received an update for 3 seconds.
*
* @return true if outdated
*/
public boolean isOutdated() {
return timerOutdating != null && timerOutdating.isExpired();
}
/**
* Get the current progress state (elapsed time).
*
* @return Elapsed seconds
*/
public int getState() {
return state;
}
/**
* Get the maximum duration.
*
* @return Total seconds
*/
public int getMaxState() {
return maxState;
}
/**
* Get progress as a percentage (0.0 to 1.0).
*
* @return Progress percentage
*/
public float getProgress() {
if (maxState <= 0) {
return 0.0f;
}
return Math.min(1.0f, (float) state / (float) maxState);
}
/**
* Get remaining seconds.
*
* @return Remaining seconds
*/
public int getRemaining() {
return Math.max(0, maxState - state);
}
/**
* Check if this player is the kidnapper (doing the tying).
*
* @return true if kidnapper, false if victim
*/
public boolean isKidnapper() {
return isKidnapper;
}
/**
* Get the name of the other party (target if kidnapper, kidnapper if victim).
*
* @return Other entity name, or null if unknown
*/
public String getOtherEntityName() {
return otherEntityName;
}
}

View File

@@ -0,0 +1,121 @@
package com.tiedup.remake.tasks;
import com.tiedup.remake.state.IBondageState;
import java.util.UUID;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.level.Level;
/**
* Phase 6: Timed task that involves interacting with a target entity.
* Phase 14.2.6: Refactored to support any IBondageState entity (Players + NPCs)
*
* Based on original TimedInteractTask from 1.12.2
*
* Extends TimedTask with target tracking:
* - Tracks the target entity's IBondageState state
* - Can check if the same target is being interacted with (via UUID)
* - Abstract method to set up target state
*/
public abstract class TimedInteractTask extends TimedTask {
protected final IBondageState targetState; // The target's kidnapped state
protected final LivingEntity targetEntity; // The target entity
protected final UUID targetUUID; // Target's UUID for comparison
/**
* Create a new timed interaction task.
*
* @param targetState The target's IBondageState state
* @param targetEntity The target entity
* @param seconds Total duration in seconds
* @param level The world
*/
public TimedInteractTask(
IBondageState targetState,
LivingEntity targetEntity,
int seconds,
Level level
) {
super(seconds, level);
this.targetState = targetState;
this.targetEntity = targetEntity;
this.targetUUID = targetEntity.getUUID();
}
/**
* Check if this task is targeting the same entity.
* Uses UUID comparison to ensure uniqueness.
*
* @param entity The entity to compare with
* @return true if same target
*/
public boolean isSameTarget(LivingEntity entity) {
if (entity == null) {
return false;
}
return targetUUID.equals(entity.getUUID());
}
/**
* Get the target's IBondageState state.
*
* @return The target's kidnapped state
*/
public IBondageState getTargetState() {
return targetState;
}
/**
* Get the target entity.
*
* @return The target entity
*/
public LivingEntity getTargetEntity() {
return targetEntity;
}
/**
* Get the target's UUID.
*
* @return The target's UUID
*/
public UUID getTargetUUID() {
return targetUUID;
}
/**
* Check if the target entity is still valid (alive and exists).
*
* @return true if target is valid
*/
public boolean isTargetValid() {
return targetEntity != null && targetEntity.isAlive();
}
/**
* Set up the target's state for this task.
* This should be called when the task starts to initialize
* any necessary state on the target.
*
* Implementation is task-specific (tying vs untying).
*/
public abstract void setUpTargetState();
/**
* Called when the task completes successfully.
* Default implementation does nothing - subclasses should override.
*/
@Override
protected void onComplete() {
// Default: no-op, subclasses implement specific completion logic
}
/**
* Send progress packets to relevant players.
* Default implementation does nothing - subclasses should override.
*/
@Override
public void sendProgressPackets() {
// Default: no-op, subclasses implement packet sending
}
}

View File

@@ -0,0 +1,205 @@
package com.tiedup.remake.tasks;
import net.minecraft.world.level.Level;
/**
* Phase 6: Base class for all progress-based tasks (tying, untying, etc.)
*
* Based on original TimedTask from 1.12.2, refactored for continuous click requirement.
*
* A TimedTask represents a progressive action that requires continuous clicking:
* - Progress increases when player clicks on target (update() called)
* - Progress decreases when player is not clicking (activeThisTick = false)
* - Task completes when progress reaches maxProgress
* - Task cancels when progress drops to 0
*
* This ensures players must HOLD click on the target to complete the action.
*/
public abstract class TimedTask {
protected int progress = 0; // Current progress (ticks)
protected int maxProgress; // Target progress (seconds * 20 ticks)
protected boolean activeThisTick = false; // Was update() called this tick?
protected boolean stopped = false; // Task has been stopped/completed
protected final Level level; // World reference
protected final int seconds; // Total duration in seconds (for display)
/**
* Create a new timed task.
*
* @param seconds Total duration in seconds
* @param level The world (for game time)
*/
public TimedTask(int seconds, Level level) {
this.seconds = seconds;
this.maxProgress = seconds * 20; // Convert seconds to ticks
this.level = level;
this.stopped = false;
}
/**
* Start the task.
* Resets progress and stopped state.
*/
public void start() {
this.progress = 0;
this.stopped = false;
this.activeThisTick = false;
}
/**
* Called when player clicks on target.
* Marks this tick as "active" - progress will increase.
*/
public synchronized void update() {
activeThisTick = true;
}
/**
* Called every tick to process progress.
* Increments if active (clicking), decrements if not.
*/
public void tick() {
if (stopped) return;
if (activeThisTick) {
// Player is clicking on target - progress up
progress++;
if (progress >= maxProgress) {
// Task complete!
onComplete();
}
} else {
// Player is not clicking - progress down
progress--;
if (progress <= 0) {
progress = 0;
stop();
}
}
// Reset for next tick
activeThisTick = false;
}
/**
* Reset the task to its initial state.
*/
public void reset() {
this.progress = 0;
this.stopped = false;
this.activeThisTick = false;
}
/**
* Stop/cancel the task.
* Marks the task as stopped.
*/
public void stop() {
this.stopped = true;
}
/**
* Check if the task has been stopped.
*
* @return true if stopped
*/
public boolean isStopped() {
return stopped;
}
/**
* Check if the task has become outdated.
* In the new progress-based system, a task is outdated if progress is 0.
*
* @return true if outdated (no progress)
*/
public boolean isOutdated() {
return progress <= 0;
}
/**
* Get the current progress in ticks.
*
* @return Current progress ticks
*/
public int getProgress() {
return progress;
}
/**
* Get the max progress in ticks.
*
* @return Max progress ticks
*/
public int getMaxProgress() {
return maxProgress;
}
/**
* Get the current state (elapsed time in seconds).
* Calculates from progress ticks for display compatibility.
*
* @return Elapsed seconds (progress / 20)
*/
public int getState() {
return progress / 20;
}
/**
* Get progress as a percentage (0.0 to 1.0).
*
* @return Progress percentage
*/
public float getProgressPercent() {
return (float) progress / maxProgress;
}
/**
* Check if the task is complete (progress >= maxProgress).
*
* @return true if task is done
*/
public boolean isOver() {
return progress >= maxProgress;
}
/**
* Check if the task is complete.
*
* @return true if complete
*/
public boolean isComplete() {
return progress >= maxProgress;
}
/**
* Get the total duration in seconds.
*
* @return Total seconds
*/
public int getMaxSeconds() {
return seconds;
}
/**
* Get the remaining seconds.
*
* @return Remaining seconds
*/
public int getSecondsRemaining() {
return Math.max(0, (maxProgress - progress) / 20);
}
/**
* Called when the task completes successfully.
* Subclasses should override to implement completion logic.
*/
protected abstract void onComplete();
/**
* Send progress packets to relevant players.
* Subclasses should override to send UI updates.
*/
public abstract void sendProgressPackets();
}

View File

@@ -0,0 +1,346 @@
package com.tiedup.remake.tasks;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.v2.BodyRegionV2;
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 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;
/**
* Phase 6: Concrete tying task for tying up any IBondageState entity.
* Phase 14.2.6: Unified to work with both Players and NPCs.
*
* 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
}
}

View File

@@ -0,0 +1,59 @@
package com.tiedup.remake.tasks;
import com.tiedup.remake.state.IBondageState;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
/**
* Phase 6: Abstract tying task (binding/gagging an entity).
* Phase 14.2.6: Refactored to support any IBondageState entity (Players + NPCs)
*
* Based on original TyingTask from 1.12.2
*
* Extends TimedInteractTask with item tracking:
* - Holds a reference to the bind/gag item being applied
* - The item will be consumed when tying completes successfully
*/
public abstract class TyingTask extends TimedInteractTask {
protected ItemStack bind; // The bind/gag item being applied
/**
* 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
* @param seconds Total duration in seconds
* @param level The world
*/
public TyingTask(
ItemStack bind,
IBondageState targetState,
LivingEntity targetEntity,
int seconds,
Level level
) {
super(targetState, targetEntity, seconds, level);
this.bind = bind;
}
/**
* Get the bind/gag item being applied.
*
* @return The item stack
*/
public ItemStack getBind() {
return bind;
}
/**
* Set the bind/gag item.
*
* @param bind The item stack
*/
public void setBind(ItemStack bind) {
this.bind = bind;
}
}

View File

@@ -0,0 +1,397 @@
package com.tiedup.remake.tasks;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.entities.NpcTypeHelper;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.action.PacketUntying;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import java.util.Map;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
/**
* Phase 6: Concrete untying task for freeing a tied entity.
* Phase 14.2.6: Unified to work with both Players and NPCs.
*
* Based on original UntyingPlayerTask from 1.12.2
*
* This task:
* 1. Tracks progress as the helper repeatedly right-clicks the tied target
* 2. Sends progress updates to both helper and target clients (if player)
* 3. Removes all restraints and drops items when the timer completes
* 4. Sets up target's restraint state for client-side visualization (if player)
*
* Epic 5F: Uses V2EquipmentHelper/BodyRegionV2.
*/
public class UntyingPlayerTask extends UntyingTask {
/** The player performing the untying action. */
private Player helper;
/**
* Create a new untying task.
*
* @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 UntyingPlayerTask(
IBondageState targetState,
LivingEntity targetEntity,
int seconds,
Level level
) {
super(targetState, targetEntity, seconds, level);
}
/**
* Create a new untying task with helper reference.
*
* @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 helper The player performing the untying
*/
public UntyingPlayerTask(
IBondageState targetState,
LivingEntity targetEntity,
int seconds,
Level level,
Player helper
) {
super(targetState, targetEntity, seconds, level);
this.helper = helper;
}
/**
* Set the helper (the player doing the untying).
*/
public void setHelper(Player helper) {
this.helper = helper;
}
/**
* Update the untying progress.
* Called each time the helper right-clicks the tied target.
*
* In the new progress-based system, this method only:
* 1. Marks this tick as "active" (progress will increase in tick())
* 2. Validates helper is still close to target
*
* The actual progress increment and completion check happen in tick().
*/
@Override
public synchronized void update() {
// ========================================
// SECURITY: Validate helper is still close to target
// ========================================
if (helper != null && targetEntity != null) {
double distance = helper.distanceTo(targetEntity);
if (distance > 4.0) {
TiedUpMod.LOGGER.debug(
"[UntyingPlayerTask] Helper {} moved too far from target ({} blocks), cancelling",
helper.getName().getString(),
String.format("%.1f", distance)
);
stop();
return;
}
// Check line-of-sight
if (!helper.hasLineOfSight(targetEntity)) {
TiedUpMod.LOGGER.debug(
"[UntyingPlayerTask] Helper {} lost line of sight to target, cancelling",
helper.getName().getString()
);
stop();
return;
}
}
// Mark this tick as active (player is clicking on target)
super.update();
}
/**
* Send progress packets to both helper and target (if players).
* Called every tick from RestraintTaskTickHandler.onPlayerTick().
*/
@Override
public void sendProgressPackets() {
if (stopped) return;
String helperName =
helper != null ? helper.getName().getString() : "Someone";
String targetName = targetEntity.getName().getString();
// Packet to victim: isHelper=false, shows helper's name
if (targetEntity instanceof ServerPlayer serverTarget) {
PacketUntying victimPacket = new PacketUntying(
this.getState(),
this.getMaxSeconds(),
false,
helperName
);
ModNetwork.sendToPlayer(victimPacket, serverTarget);
}
// Packet to helper: isHelper=true, shows target's name
if (helper instanceof ServerPlayer serverHelper) {
PacketUntying helperPacket = new PacketUntying(
this.getState(),
this.getMaxSeconds(),
true,
targetName
);
ModNetwork.sendToPlayer(helperPacket, serverHelper);
}
}
/**
* Called when the task completes successfully.
* Drops bondage items, frees the target, and sends completion packets.
*/
@Override
protected void onComplete() {
// Verify target entity still exists and is alive
if (!isTargetValid()) {
TiedUpMod.LOGGER.warn(
"[UntyingPlayerTask] Target entity no longer valid, cancelling task"
);
stop();
return;
}
TiedUpMod.LOGGER.info(
"[UntyingPlayerTask] Untying complete for {}",
targetEntity.getName().getString()
);
// Drop all bondage items on the ground
dropBondageItems();
// Remove all restraints from target
untieTarget();
// Handle Damsel-specific rewards
if (targetEntity instanceof EntityDamsel damsel && NpcTypeHelper.isDamselOnly(targetEntity) && helper != null) {
// Reward the savior (gives emeralds and marks player as savior)
damsel.rewardSavior(helper);
}
// Mark task as stopped
stop();
// Send completion packets to both parties
String helperName =
helper != null ? helper.getName().getString() : "Someone";
String targetName = targetEntity.getName().getString();
if (targetEntity instanceof ServerPlayer serverTarget) {
PacketUntying completionPacket = new PacketUntying(
-1,
this.getMaxSeconds(),
false,
helperName
);
ModNetwork.sendToPlayer(completionPacket, serverTarget);
PlayerBindState playerState = PlayerBindState.getInstance(
serverTarget
);
if (playerState != null) {
playerState.setRestrainedState(null);
}
}
if (helper instanceof ServerPlayer serverHelper) {
PacketUntying completionPacket = new PacketUntying(
-1,
this.getMaxSeconds(),
true,
targetName
);
ModNetwork.sendToPlayer(completionPacket, serverHelper);
}
}
/**
* 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 (PacketUntying) 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
}
/**
* Drop all bondage items on the ground.
* Works for both players (via V2EquipmentHelper) and NPCs (via IBondageState).
*/
private void dropBondageItems() {
// For player targets: use V2EquipmentHelper to get all equipped items
if (targetEntity instanceof Player player) {
Map<BodyRegionV2, ItemStack> equipped = V2EquipmentHelper.getAllEquipped(player);
for (Map.Entry<BodyRegionV2, ItemStack> entry : equipped.entrySet()) {
ItemStack stack = entry.getValue();
if (!stack.isEmpty()) {
// Drop item at player's position
ItemEntity itemEntity = new ItemEntity(
targetEntity.level(),
targetEntity.getX(),
targetEntity.getY(),
targetEntity.getZ(),
stack.copy()
);
targetEntity.level().addFreshEntity(itemEntity);
TiedUpMod.LOGGER.debug(
"[UntyingPlayerTask] Dropped {} from region {}",
stack.getHoverName().getString(),
entry.getKey().name()
);
}
}
} else {
// For NPC targets: use IBondageState interface
// Drop bind if present
ItemStack bind = targetState.getEquipment(BodyRegionV2.ARMS);
if (bind != null && !bind.isEmpty()) {
ItemEntity itemEntity = new ItemEntity(
targetEntity.level(),
targetEntity.getX(),
targetEntity.getY(),
targetEntity.getZ(),
bind.copy()
);
targetEntity.level().addFreshEntity(itemEntity);
TiedUpMod.LOGGER.debug(
"[UntyingPlayerTask] Dropped bind: {}",
bind.getHoverName().getString()
);
}
// Drop gag if present
ItemStack gag = targetState.getEquipment(BodyRegionV2.MOUTH);
if (gag != null && !gag.isEmpty()) {
ItemEntity itemEntity = new ItemEntity(
targetEntity.level(),
targetEntity.getX(),
targetEntity.getY(),
targetEntity.getZ(),
gag.copy()
);
targetEntity.level().addFreshEntity(itemEntity);
TiedUpMod.LOGGER.debug(
"[UntyingPlayerTask] Dropped gag: {}",
gag.getHoverName().getString()
);
}
// Drop blindfold if present
ItemStack blindfold = targetState.getEquipment(BodyRegionV2.EYES);
if (blindfold != null && !blindfold.isEmpty()) {
ItemEntity itemEntity = new ItemEntity(
targetEntity.level(),
targetEntity.getX(),
targetEntity.getY(),
targetEntity.getZ(),
blindfold.copy()
);
targetEntity.level().addFreshEntity(itemEntity);
TiedUpMod.LOGGER.debug(
"[UntyingPlayerTask] Dropped blindfold: {}",
blindfold.getHoverName().getString()
);
}
// Drop earplugs if present
ItemStack earplugs = targetState.getEquipment(BodyRegionV2.EARS);
if (earplugs != null && !earplugs.isEmpty()) {
ItemEntity itemEntity = new ItemEntity(
targetEntity.level(),
targetEntity.getX(),
targetEntity.getY(),
targetEntity.getZ(),
earplugs.copy()
);
targetEntity.level().addFreshEntity(itemEntity);
TiedUpMod.LOGGER.debug(
"[UntyingPlayerTask] Dropped earplugs: {}",
earplugs.getHoverName().getString()
);
}
// Drop mittens if present
ItemStack mittens = targetState.getEquipment(BodyRegionV2.HANDS);
if (mittens != null && !mittens.isEmpty()) {
ItemEntity itemEntity = new ItemEntity(
targetEntity.level(),
targetEntity.getX(),
targetEntity.getY(),
targetEntity.getZ(),
mittens.copy()
);
targetEntity.level().addFreshEntity(itemEntity);
TiedUpMod.LOGGER.debug(
"[UntyingPlayerTask] Dropped mittens: {}",
mittens.getHoverName().getString()
);
}
}
}
/**
* Remove all restraints from the target entity.
* Works for both players and NPCs via IBondageState interface.
*/
private void untieTarget() {
// For player targets: clear all V2 equipment (fires onUnequipped callbacks)
if (targetEntity instanceof Player player) {
V2EquipmentHelper.clearAll(player);
// Phase 17: Free from captivity/leash if applicable (player-specific)
PlayerBindState playerState = PlayerBindState.getInstance(player);
if (playerState != null && playerState.isCaptive()) {
playerState.free();
}
} else {
// For NPC targets: use IBondageState interface directly
targetState.unequip(BodyRegionV2.ARMS);
targetState.unequip(BodyRegionV2.MOUTH);
targetState.unequip(BodyRegionV2.EYES);
targetState.unequip(BodyRegionV2.EARS);
targetState.unequip(BodyRegionV2.HANDS);
}
TiedUpMod.LOGGER.info(
"[UntyingPlayerTask] Fully untied {}",
targetEntity.getName().getString()
);
}
}

View File

@@ -0,0 +1,35 @@
package com.tiedup.remake.tasks;
import com.tiedup.remake.state.IBondageState;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.level.Level;
/**
* Phase 6: Abstract untying task (freeing a tied entity).
* Phase 14.2.6: Refactored to support any IBondageState entity (Players + NPCs)
*
* Based on original UntyingTask from 1.12.2
*
* Extends TimedInteractTask for untying operations:
* - Frees a tied entity over time
* - Drops bondage items on the ground when complete
*/
public abstract class UntyingTask extends TimedInteractTask {
/**
* Create a new untying task.
*
* @param targetState The target's IBondageState state
* @param targetEntity The target entity
* @param seconds Total duration in seconds
* @param level The world
*/
public UntyingTask(
IBondageState targetState,
LivingEntity targetEntity,
int seconds,
Level level
) {
super(targetState, targetEntity, seconds, level);
}
}

View File

@@ -0,0 +1,94 @@
package com.tiedup.remake.tasks;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.v2.bondage.V2EquipResult;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
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;
/**
* Tying task for V2 bondage items.
*
* Unlike {@link TyingPlayerTask} which calls {@code targetState.equip(BodyRegionV2.ARMS, bind)} (V1 equip),
* this task uses {@link V2EquipmentHelper#equipItem(LivingEntity, ItemStack)} on completion.
*
* Progress bar, duration, and packet flow are identical to V1 tying.
*/
public class V2TyingPlayerTask extends TyingPlayerTask {
/**
* The live reference to the player's held ItemStack (for consumption on completion).
* The parent class's {@code bind} field holds a copy for display/matching.
*/
private final ItemStack heldStack;
/**
* Create a V2 tying task.
*
* @param bind Copy of the item being equipped (for display/matching)
* @param heldStack Live reference to the player's held ItemStack (for consumption)
* @param targetState The target's IBondageState state
* @param targetEntity The target entity
* @param seconds Duration in seconds
* @param level The world
* @param kidnapper The player performing the tying (self for self-bondage)
*/
public V2TyingPlayerTask(
ItemStack bind,
ItemStack heldStack,
IBondageState targetState,
LivingEntity targetEntity,
int seconds,
Level level,
Player kidnapper
) {
super(bind, targetState, targetEntity, seconds, level, kidnapper);
this.heldStack = heldStack;
}
/**
* V2 completion: equip via V2EquipmentHelper instead of V1 putBindOn.
*
* This REPLACES the parent's onComplete entirely. The parent would call
* targetState.equip(BodyRegionV2.ARMS, bind) which is V1-only.
*/
@Override
protected void onComplete() {
if (!isTargetValid()) {
TiedUpMod.LOGGER.warn(
"[V2TyingPlayerTask] Target entity no longer valid, cancelling"
);
stop();
return;
}
TiedUpMod.LOGGER.info(
"[V2TyingPlayerTask] Tying complete for {}",
targetEntity.getName().getString()
);
// Equip via V2 system
V2EquipResult result = V2EquipmentHelper.equipItem(targetEntity, bind);
if (result.isSuccess()) {
for (ItemStack displaced : result.displaced()) {
targetEntity.spawnAtLocation(displaced);
}
// Consume the held item
heldStack.shrink(1);
TiedUpMod.LOGGER.debug("[V2TyingPlayerTask] V2 equip succeeded, item consumed");
} else {
TiedUpMod.LOGGER.warn(
"[V2TyingPlayerTask] V2 equip BLOCKED after tying — regions may have changed"
);
}
// Mark task as stopped
stop();
// Send completion packets (shared with V1 via parent)
sendCompletionPackets();
}
}