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,386 @@
package com.tiedup.remake.state;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.level.saveddata.SavedData;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Global registry for collar ownership relationships.
*
* This registry tracks which entities are wearing collars and who owns them.
* It persists across server restarts and provides efficient lookups in both directions:
* - Owner UUID → Set of collar-wearer UUIDs (slaves)
* - Wearer UUID → Set of owner UUIDs (masters)
*
* Terminology:
* - "Slave" = Entity wearing a collar owned by a player (passive ownership)
* - "Captive" = Entity attached by leash (active physical control) - managed by PlayerCaptiveManager
*
* Phase 17: Terminology Refactoring
*/
public class CollarRegistry extends SavedData {
private static final String DATA_NAME = "tiedup_collar_registry";
// Owner UUID → Set of wearer UUIDs (a master can own multiple slaves)
private final Map<UUID, Set<UUID>> ownerToWearers =
new ConcurrentHashMap<>();
// Wearer UUID → Set of owner UUIDs (a collar can have multiple owners)
private final Map<UUID, Set<UUID>> wearerToOwners =
new ConcurrentHashMap<>();
// ==================== STATIC ACCESS ====================
/**
* Get the CollarRegistry for a server level.
* Creates a new registry if one doesn't exist.
*/
public static CollarRegistry get(ServerLevel level) {
return level
.getDataStorage()
.computeIfAbsent(
CollarRegistry::load,
CollarRegistry::new,
DATA_NAME
);
}
/**
* Get the CollarRegistry from a MinecraftServer.
* Uses the overworld as the storage dimension.
*/
public static CollarRegistry get(MinecraftServer server) {
ServerLevel overworld = server.overworld();
return get(overworld);
}
/**
* Convenience method to get registry from a ServerPlayer.
*/
@Nullable
public static CollarRegistry get(ServerPlayer player) {
if (player.getServer() == null) return null;
return get(player.getServer());
}
// ==================== REGISTRATION ====================
/**
* Register a collar relationship: owner now owns the collar on wearer.
*
* @param wearerUUID UUID of the entity wearing the collar
* @param ownerUUID UUID of the collar's owner
*/
public void registerCollar(UUID wearerUUID, UUID ownerUUID) {
// Add to owner → wearers map
ownerToWearers
.computeIfAbsent(ownerUUID, k -> ConcurrentHashMap.newKeySet())
.add(wearerUUID);
// Add to wearer → owners map
wearerToOwners
.computeIfAbsent(wearerUUID, k -> ConcurrentHashMap.newKeySet())
.add(ownerUUID);
setDirty();
}
/**
* Register a collar with multiple owners at once.
*
* @param wearerUUID UUID of the entity wearing the collar
* @param ownerUUIDs Set of owner UUIDs
*/
public void registerCollar(UUID wearerUUID, Set<UUID> ownerUUIDs) {
for (UUID ownerUUID : ownerUUIDs) {
registerCollar(wearerUUID, ownerUUID);
}
}
/**
* Unregister a specific owner from a collar wearer.
*
* @param wearerUUID UUID of the entity wearing the collar
* @param ownerUUID UUID of the owner to remove
*/
private void unregisterOwner(UUID wearerUUID, UUID ownerUUID) {
// Remove from owner → wearers map using atomic operation
ownerToWearers.computeIfPresent(ownerUUID, (key, wearers) -> {
wearers.remove(wearerUUID);
return wearers.isEmpty() ? null : wearers;
});
// Remove from wearer → owners map using atomic operation
wearerToOwners.computeIfPresent(wearerUUID, (key, owners) -> {
owners.remove(ownerUUID);
return owners.isEmpty() ? null : owners;
});
setDirty();
}
/**
* Completely unregister a collar wearer (removes all owner relationships).
* Called when a collar is removed from an entity.
*
* @param wearerUUID UUID of the entity whose collar was removed
*/
public void unregisterWearer(UUID wearerUUID) {
Set<UUID> owners = wearerToOwners.remove(wearerUUID);
if (owners != null) {
for (UUID ownerUUID : owners) {
// Use atomic operation for thread-safe removal
ownerToWearers.computeIfPresent(ownerUUID, (key, wearers) -> {
wearers.remove(wearerUUID);
return wearers.isEmpty() ? null : wearers;
});
}
setDirty();
}
}
/**
* Update a wearer's owners completely (replaces all existing owners).
* Useful when collar NBT is the source of truth.
*
* @param wearerUUID UUID of the entity wearing the collar
* @param newOwnerUUIDs New set of owner UUIDs
*/
public void updateWearerOwners(UUID wearerUUID, Set<UUID> newOwnerUUIDs) {
// Get current owners
Set<UUID> currentOwners = wearerToOwners.get(wearerUUID);
if (currentOwners == null) {
currentOwners = Collections.emptySet();
}
// Find owners to remove
Set<UUID> toRemove = new HashSet<>(currentOwners);
toRemove.removeAll(newOwnerUUIDs);
// Find owners to add
Set<UUID> toAdd = new HashSet<>(newOwnerUUIDs);
toAdd.removeAll(currentOwners);
// Apply changes
for (UUID ownerUUID : toRemove) {
unregisterOwner(wearerUUID, ownerUUID);
}
for (UUID ownerUUID : toAdd) {
registerCollar(wearerUUID, ownerUUID);
}
}
// ==================== QUERIES ====================
/**
* Get all slaves (collar wearers) owned by a specific owner.
*
* @param ownerUUID UUID of the owner
* @return Unmodifiable set of wearer UUIDs (never null)
*/
public Set<UUID> getSlaves(UUID ownerUUID) {
Set<UUID> wearers = ownerToWearers.get(ownerUUID);
if (wearers == null) return Collections.emptySet();
return Collections.unmodifiableSet(new HashSet<>(wearers));
}
/**
* Get all owners of a specific collar wearer.
*
* @param wearerUUID UUID of the wearer
* @return Unmodifiable set of owner UUIDs (never null)
*/
public Set<UUID> getOwners(UUID wearerUUID) {
Set<UUID> owners = wearerToOwners.get(wearerUUID);
if (owners == null) return Collections.emptySet();
return Collections.unmodifiableSet(new HashSet<>(owners));
}
/**
* Check if an owner has any slaves.
*/
public boolean hasSlaves(UUID ownerUUID) {
Set<UUID> wearers = ownerToWearers.get(ownerUUID);
return wearers != null && !wearers.isEmpty();
}
/**
* Check if a wearer has any owners.
*/
public boolean hasOwners(UUID wearerUUID) {
Set<UUID> owners = wearerToOwners.get(wearerUUID);
return owners != null && !owners.isEmpty();
}
/**
* Check if a specific owner owns a specific wearer.
*/
public boolean isOwner(UUID ownerUUID, UUID wearerUUID) {
Set<UUID> wearers = ownerToWearers.get(ownerUUID);
return wearers != null && wearers.contains(wearerUUID);
}
/**
* Get the count of slaves for an owner.
*/
public int getSlaveCount(UUID ownerUUID) {
Set<UUID> wearers = ownerToWearers.get(ownerUUID);
return wearers == null ? 0 : wearers.size();
}
/**
* Get all registered wearers (for admin/debug purposes).
*/
public Set<UUID> getAllWearers() {
return Collections.unmodifiableSet(
new HashSet<>(wearerToOwners.keySet())
);
}
/**
* Get all registered owners (for admin/debug purposes).
*/
public Set<UUID> getAllOwners() {
return Collections.unmodifiableSet(
new HashSet<>(ownerToWearers.keySet())
);
}
// ==================== ENTITY RESOLUTION ====================
/**
* Find all slave entities for an owner that are currently loaded.
* This is useful for GUI and proximity-based actions.
*
* @param owner The owner player
* @return List of currently loaded slave entities
*/
public List<LivingEntity> findLoadedSlaves(ServerPlayer owner) {
List<LivingEntity> loadedSlaves = new ArrayList<>();
Set<UUID> slaveUUIDs = getSlaves(owner.getUUID());
MinecraftServer server = owner.getServer();
if (server == null) return loadedSlaves;
for (UUID slaveUUID : slaveUUIDs) {
Entity entity = findEntityByUUID(server, slaveUUID);
if (entity instanceof LivingEntity living) {
loadedSlaves.add(living);
}
}
return loadedSlaves;
}
/**
* Find an entity by UUID across all dimensions.
* Optimized: checks player list first (O(1)) before searching dimensions.
*/
@Nullable
private Entity findEntityByUUID(MinecraftServer server, UUID uuid) {
// Check player first - O(1) lookup
net.minecraft.server.level.ServerPlayer player = server
.getPlayerList()
.getPlayer(uuid);
if (player != null) {
return player;
}
// Fallback: search dimensions for NPCs
for (ServerLevel level : server.getAllLevels()) {
Entity entity = level.getEntity(uuid);
if (entity != null) {
return entity;
}
}
return null;
}
// ==================== PERSISTENCE ====================
@Override
public @NotNull CompoundTag save(@NotNull CompoundTag tag) {
ListTag registryList = new ListTag();
for (Map.Entry<UUID, Set<UUID>> entry : wearerToOwners.entrySet()) {
CompoundTag wearerTag = new CompoundTag();
wearerTag.putUUID("wearer", entry.getKey());
ListTag ownersTag = new ListTag();
for (UUID ownerUUID : entry.getValue()) {
CompoundTag ownerTag = new CompoundTag();
ownerTag.putUUID("uuid", ownerUUID);
ownersTag.add(ownerTag);
}
wearerTag.put("owners", ownersTag);
registryList.add(wearerTag);
}
tag.put("collar_registry", registryList);
return tag;
}
public static CollarRegistry load(CompoundTag tag) {
CollarRegistry registry = new CollarRegistry();
ListTag registryList = tag.getList("collar_registry", Tag.TAG_COMPOUND);
for (int i = 0; i < registryList.size(); i++) {
CompoundTag wearerTag = registryList.getCompound(i);
UUID wearerUUID = wearerTag.getUUID("wearer");
ListTag ownersTag = wearerTag.getList("owners", Tag.TAG_COMPOUND);
for (int j = 0; j < ownersTag.size(); j++) {
CompoundTag ownerTag = ownersTag.getCompound(j);
UUID ownerUUID = ownerTag.getUUID("uuid");
// Register relationship (without marking dirty - we're loading)
registry.ownerToWearers
.computeIfAbsent(ownerUUID, k ->
ConcurrentHashMap.newKeySet()
)
.add(wearerUUID);
registry.wearerToOwners
.computeIfAbsent(wearerUUID, k ->
ConcurrentHashMap.newKeySet()
)
.add(ownerUUID);
}
}
return registry;
}
// ==================== DEBUG ====================
/**
* Get a debug string representation of the registry.
*/
public String toDebugString() {
StringBuilder sb = new StringBuilder();
sb.append("CollarRegistry:\n");
sb.append(" Owners: ").append(ownerToWearers.size()).append("\n");
sb.append(" Wearers: ").append(wearerToOwners.size()).append("\n");
for (Map.Entry<UUID, Set<UUID>> entry : ownerToWearers.entrySet()) {
sb
.append(" Owner ")
.append(entry.getKey().toString().substring(0, 8))
.append("... → ")
.append(entry.getValue().size())
.append(" slaves\n");
}
return sb.toString();
}
}

View File

@@ -0,0 +1,61 @@
package com.tiedup.remake.state;
import com.tiedup.remake.items.base.PoseType;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.item.ItemStack;
/**
* Centralizes human chair NBT tag keys and resolution logic.
*
* <p>The human chair mode is stored as NBT tags on the dog bind item.
* This helper eliminates hardcoded string literals scattered across 7+ files.
*/
public final class HumanChairHelper {
/** NBT key indicating this bind is in human chair mode */
public static final String NBT_KEY = "humanChairMode";
/** NBT key for the locked facing direction (degrees) */
public static final String NBT_FACING_KEY = "humanChairFacing";
private HumanChairHelper() {}
/**
* Check if a bind item is in human chair mode.
*
* @param bind The bind ItemStack to check
* @return true if the bind has humanChairMode NBT set to true
*/
public static boolean isActive(ItemStack bind) {
if (bind.isEmpty()) return false;
CompoundTag tag = bind.getTag();
return tag != null && tag.getBoolean(NBT_KEY);
}
/**
* Get the locked facing direction for human chair mode.
*
* @param bind The bind ItemStack
* @return The facing angle in degrees, or 0 if not set
*/
public static float getFacing(ItemStack bind) {
if (bind.isEmpty()) return 0f;
CompoundTag tag = bind.getTag();
return tag != null ? tag.getFloat(NBT_FACING_KEY) : 0f;
}
/**
* Resolve the effective pose type, overriding DOG to HUMAN_CHAIR
* when the bind has humanChairMode enabled.
*
* @param base The base pose type from the bind item
* @param bind The bind ItemStack (checked for humanChairMode NBT)
* @return HUMAN_CHAIR if base is DOG and humanChairMode is active, otherwise base
*/
public static PoseType resolveEffectivePose(PoseType base, ItemStack bind) {
if (base == PoseType.DOG && isActive(bind)) {
return PoseType.HUMAN_CHAIR;
}
return base;
}
}

View File

@@ -0,0 +1,553 @@
package com.tiedup.remake.state;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.function.Supplier;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
/**
* Equipment CRUD, state checks, bulk operations, resistance, and lock safety
* for bondage items on kidnapped entities.
*
* <p>This is the largest sub-interface, covering:</p>
* <ul>
* <li>State query methods (isTiedUp, isGagged, etc.)</li>
* <li>Bind mode defaults (hasArmsBound, hasLegsBound, getBindModeId)</li>
* <li>Deprecated per-slot put-on / take-off / get-current / replace methods</li>
* <li>Bulk operations (applyBondage, untie, dropBondageItems)</li>
* <li>Post-apply callbacks (checkXAfterApply defaults)</li>
* <li>Lock safety helpers (ifUnlocked, isLocked, ifUnlockedReturn, takeBondageItemIfUnlocked)</li>
* <li>Clothes permissions</li>
* <li>Resistance system defaults</li>
* </ul>
*
* @see ICapturable
* @see IRestrainableEntity
* @see IRestrainable
*/
public interface IBondageState extends ICapturable {
// ========================================
// V2 REGION-BASED EQUIPMENT ACCESS
// ========================================
/**
* Get the item equipped in a V2 body region.
*
* @param region The body region to query
* @return The equipped ItemStack, or {@link ItemStack#EMPTY} if empty
*/
ItemStack getEquipment(BodyRegionV2 region);
/**
* Equip an item in a V2 body region.
* Dispatches to the appropriate slot-specific equip logic (sounds, hooks, sync).
*
* @param region The body region to equip into
* @param stack The ItemStack to equip
*/
void equip(BodyRegionV2 region, ItemStack stack);
/**
* Unequip the item from a V2 body region, respecting locks.
*
* @param region The body region to unequip from
* @return The removed ItemStack, or {@link ItemStack#EMPTY} if locked/empty
*/
ItemStack unequip(BodyRegionV2 region);
/**
* Force-unequip the item from a V2 body region, ignoring locks.
* Used for admin commands, kidnapper theft, kill cleanup, etc.
*
* @param region The body region to force-unequip from
* @return The removed ItemStack, or {@link ItemStack#EMPTY} if empty
*/
ItemStack forceUnequip(BodyRegionV2 region);
/**
* Replace the item in a body region, returning the old item.
*
* @param region The body region
* @param newStack The new ItemStack to equip
* @param force If true, replace even if current item is locked
* @return The old ItemStack, or empty if locked (and !force) or nothing equipped
*/
ItemStack replaceEquipment(BodyRegionV2 region, ItemStack newStack, boolean force);
// ========================================
// STATE QUERIES - BONDAGE EQUIPMENT
// ========================================
/**
* Check if entity has bind/ropes equipped.
* @return true if BIND slot is not empty
*/
boolean isTiedUp();
/**
* Check if entity's arms are bound.
* True if tied with mode ARMS or FULL.
* Leg Binding System.
*
* @return true if arms are bound
*/
default boolean hasArmsBound() {
if (!isTiedUp()) return false;
ItemStack bind = getEquipment(BodyRegionV2.ARMS);
if (bind.isEmpty()) return false;
return ItemBind.hasArmsBound(bind);
}
/**
* Check if entity's legs are bound.
* True if tied with mode LEGS or FULL.
* Leg Binding System.
*
* @return true if legs are bound
*/
default boolean hasLegsBound() {
if (!isTiedUp()) return false;
ItemStack bind = getEquipment(BodyRegionV2.ARMS);
if (bind.isEmpty()) return false;
return ItemBind.hasLegsBound(bind);
}
/**
* Get the bind mode ID string.
* Leg Binding System.
*
* @return "full", "arms", or "legs"
*/
default String getBindModeId() {
if (!isTiedUp()) return ItemBind.BIND_MODE_FULL;
ItemStack bind = getEquipment(BodyRegionV2.ARMS);
if (bind.isEmpty()) return ItemBind.BIND_MODE_FULL;
return ItemBind.getBindModeId(bind);
}
/**
* Check if entity is gagged.
* @return true if GAG slot is not empty
*/
boolean isGagged();
/**
* Check if entity is blindfolded.
* @return true if BLINDFOLD slot is not empty
*/
boolean isBlindfolded();
/**
* Check if entity has earplugs.
* @return true if EARPLUGS slot is not empty
*/
boolean hasEarplugs();
/**
* Check if entity has collar.
* @return true if COLLAR slot is not empty
*/
boolean hasCollar();
/**
* Check if entity is wearing a locked collar.
* @return true if has collar AND collar is locked
*/
boolean hasLockedCollar();
/**
* Check if entity's collar has a custom name/nickname.
* Used for collar ownership display.
*
* @return true if collar has NBT nickname
*/
boolean hasNamedCollar();
/**
* Check if entity has clothes equipped.
* @return true if CLOTHES slot is not empty
*/
boolean hasClothes();
/**
* Check if entity has mittens equipped.
* Phase 14.4: Mittens system
* @return true if MITTENS slot is not empty
*/
boolean hasMittens();
/**
* Check if clothes have "small arms" rendering flag.
* Used by EntityDamsel for custom model rendering.
*
* @return true if clothes enable small arms mode
*/
boolean hasClothesWithSmallArms();
/**
* Check if entity is both tied AND gagged.
* Common check for "fully restrained" state.
*
* @return true if {@code isTiedUp() && isGagged()}
*/
boolean isBoundAndGagged();
/**
* Check if gag item has gagging sound effect.
* @return true if gag implements ItemGaggingEffect
*/
boolean hasGaggingEffect();
/**
* Check if blindfold item has blinding visual effect.
* @return true if blindfold implements IHasBlindingEffect
*/
boolean hasBlindingEffect();
/**
* Check if entity has knives in inventory (for struggling).
* @return true if inventory contains knife items
*/
boolean hasKnives();
// ========================================
// BULK OPERATIONS
// ========================================
/**
* Apply a full set of bondage equipment at once.
*
* <p>Typically used when:</p>
* <ul>
* <li>EntityKidnapper captures a player</li>
* <li>Trapped bed activates</li>
* <li>Rope arrow hits target</li>
* <li>Admin command applies full restraint</li>
* </ul>
*
* @param bind Bind ItemStack (or empty)
* @param gag Gag ItemStack (or empty)
* @param blindfold Blindfold ItemStack (or empty)
* @param earplugs Earplugs ItemStack (or empty)
* @param collar Collar ItemStack (or empty)
* @param clothes Clothes ItemStack (or empty)
*/
void applyBondage(
ItemStack bind,
ItemStack gag,
ItemStack blindfold,
ItemStack earplugs,
ItemStack collar,
ItemStack clothes
);
/**
* Untie this entity, removing all bondage items.
*
* @param drop If true, drop items on ground. If false, items are deleted.
*/
void untie(boolean drop);
/**
* Drop all equipped bondage items on the ground.
* Equivalent to {@code dropBondageItems(true)}.
*/
void dropBondageItems(boolean drop);
/**
* Drop specific bondage items with granular control.
*
* @param drop If false, skip all drops (no-op)
* @param dropBind If true, drop bind item
*/
void dropBondageItems(boolean drop, boolean dropBind);
/**
* Drop bondage items with full granular control.
*
* @param drop If false, skip all drops (no-op)
* @param dropBind If true, drop bind
* @param dropGag If true, drop gag
* @param dropBlindfold If true, drop blindfold
* @param dropEarplugs If true, drop earplugs
* @param dropCollar If true, drop collar
* @param dropClothes If true, drop clothes
*/
void dropBondageItems(
boolean drop,
boolean dropBind,
boolean dropGag,
boolean dropBlindfold,
boolean dropEarplugs,
boolean dropCollar,
boolean dropClothes
);
/**
* Drop only clothes item.
*/
void dropClothes();
/**
* Count how many bondage items can be removed (are not locked).
*
* @return Count of removable items (0-6)
*/
int getBondageItemsWhichCanBeRemovedCount();
// ========================================
// CLOTHES PERMISSION SYSTEM
// ========================================
/**
* Check if a player can remove clothes from this entity.
* Used for permission checks in multiplayer.
*
* @param player The player attempting to remove clothes
* @return true if allowed
*/
boolean canTakeOffClothes(Player player);
/**
* Check if a player can change clothes on this entity.
*
* @param player The player attempting to change clothes
* @return true if allowed
*/
boolean canChangeClothes(Player player);
/**
* Check if clothes can be changed (global permission).
*
* @return true if clothes are changeable
*/
boolean canChangeClothes();
// ========================================
// POST-APPLY CALLBACKS
// ========================================
/**
* Called after a bind is applied.
* Implementations can trigger additional effects (sounds, particles, etc.).
*
* Phase 14.1.7: Added missing callback (was documented but not defined)
*/
default void checkBindAfterApply() {
// Default: no-op
// NPCs don't need post-apply logic by default
}
/**
* Called after a gag is applied.
* Implementations can trigger additional effects (sounds, particles, etc.).
*/
default void checkGagAfterApply() {
// Default: no-op
// NPCs don't need post-apply logic by default
}
/**
* Called after a blindfold is applied.
*/
default void checkBlindfoldAfterApply() {
// Default: no-op
}
/**
* Called after earplugs are applied.
*/
default void checkEarplugsAfterApply() {
// Default: no-op
}
/**
* Called after a collar is applied.
*/
default void checkCollarAfterApply() {
// Default: no-op
}
/**
* Called after mittens are applied.
* Phase 14.4: Mittens system
*/
default void checkMittensAfterApply() {
// Default: no-op
// NPCs don't need post-apply logic by default
}
// ========================================
// LOCK SAFETY HELPERS
// ========================================
/**
* Execute a runnable only if the item is unlocked.
*
* <p><b>Lock Check:</b> If {@code stack} is ILockable and locked, runnable is NOT executed.</p>
*
* <p><b>Usage Example:</b></p>
* <pre>{@code
* ifUnlocked(currentGag, false, () -> {
* // Remove gag code here
* });
* }</pre>
*
* @param stack The item to check
* @param force If true, ignore lock (NOT USED in default implementation - for consistency)
* @param run The code to execute if unlocked
*/
default void ifUnlocked(ItemStack stack, boolean force, Runnable run) {
if (!stack.isEmpty()) {
if (
stack.getItem() instanceof ILockable lockable &&
lockable.isLocked(stack)
) {
return; // Locked - don't run
}
run.run(); // Unlocked or not lockable - safe to run
}
}
/**
* Check if an item is locked.
*
* @param stack The item to check
* @param force If true, returns false (treat as unlocked)
* @return true if item is locked AND force is false
*/
default boolean isLocked(ItemStack stack, boolean force) {
return (
!force &&
stack.getItem() instanceof ILockable lockable &&
lockable.isLocked(stack)
);
}
/**
* Return a value based on item lock state.
*
* <p><b>Usage Example:</b></p>
* <pre>{@code
* return ifUnlockedReturn(currentGag, false,
* () -> removeAndReturnGag(), // If unlocked
* () -> ItemStack.EMPTY // If locked
* );
* }</pre>
*
* @param stack The item to check
* @param force If true, always returns {@code run.get()}
* @param run Supplier to call if unlocked
* @param def Supplier to call if locked or stack is empty
* @param <T> Return type
* @return Result from run or def supplier
*/
default <T> T ifUnlockedReturn(
ItemStack stack,
boolean force,
Supplier<T> run,
Supplier<T> def
) {
if (stack.isEmpty()) {
return def.get();
}
if (
stack.getItem() instanceof ILockable lockable &&
lockable.isLocked(stack) &&
!force
) {
return def.get(); // Locked
}
return run.get(); // Unlocked
}
/**
* Take a bondage item if it's unlocked, then drop it.
*
* <p>Helper method combining lock check + take + drop logic.</p>
*
* @param stack The current item in slot
* @param takeOff Supplier that removes and returns the item
*/
default void takeBondageItemIfUnlocked(
ItemStack stack,
Supplier<ItemStack> takeOff
) {
if (stack.isEmpty()) return;
// Locked ILockable items cannot be removed
if (stack.getItem() instanceof ILockable lockable && lockable.isLocked(stack)) return;
// Non-ILockable items or unlocked items: remove and drop
ItemStack removed = takeOff.get();
if (!removed.isEmpty()) {
kidnappedDropItem(removed);
}
}
// ========================================
// RESISTANCE SYSTEM (Phase 14.1.7)
// ========================================
/**
* Get current bind resistance value.
*
* <p><b>Resistance System:</b></p>
* Higher resistance = harder to struggle out of restraints.
* Resistance decreases with each struggle attempt.
* When resistance reaches 0, the entity escapes.
*
* <p><b>For Players:</b> Stored in NBT, persists across sessions
* <p><b>For NPCs:</b> Default returns 0 (instant escape, can be overridden)
*
* Phase 14.1.7: Added to enable struggle system for NPCs
*
* @return Current resistance value (0 = can escape)
*/
default int getCurrentBindResistance() {
// Default for NPCs: 0 resistance (they escape instantly)
// PlayerBindState overrides this to return NBT-stored value
return 0;
}
/**
* Set current bind resistance.
*
* <p><b>For Players:</b> Updates NBT storage
* <p><b>For NPCs:</b> Default no-op (no persistent resistance)
*
* Phase 14.1.7: Added to enable struggle system for NPCs
*
* @param resistance New resistance value
*/
default void setCurrentBindResistance(int resistance) {
// Default for NPCs: no-op (they don't store resistance)
// PlayerBindState overrides this to update NBT
}
/**
* Get current collar resistance value.
*
* <p>Same as bind resistance but for collars.
*
* Phase 14.1.7: Added for collar struggle system
*
* @return Current collar resistance value
*/
default int getCurrentCollarResistance() {
// Default for NPCs: 0 resistance
return 0;
}
/**
* Set current collar resistance.
*
* Phase 14.1.7: Added for collar struggle system
*
* @param resistance New resistance value
*/
default void setCurrentCollarResistance(int resistance) {
// Default for NPCs: no-op
}
}

View File

@@ -0,0 +1,171 @@
package com.tiedup.remake.state;
import net.minecraft.world.entity.Entity;
/**
* Phase 8: Master-Captive Relationships
* Phase 14.1.6: Refactored to use IRestrainable for NPC capture support
* Phase 17: Terminology refactoring - slave → captive
* C6-V2: Narrowed parameters from IRestrainable to IBondageState (minimum needed type)
*
* Interface for entities that can capture other entities (players or NPCs).
*
* Terminology (Phase 17):
* - "Captive" = Entity attached by leash (active physical control)
* - "Slave" = Entity wearing a collar owned by someone (passive ownership via CollarRegistry)
*
* Design Pattern:
* - Interface-based design allows both players (PlayerCaptorManager)
* and NPCs (EntityKidnapper - Phase 14.2+) to act as captors
* - Separates concerns: ICaptor manages captives, IBondageState is managed
*
* Implementation:
* - PlayerCaptorManager: For player captors (Phase 8)
* - EntityKidnapper: For NPC captors (Phase 14.2+)
*
* @see IBondageState
* @see PlayerCaptorManager
*/
public interface ICaptor {
// ========================================
// Captive Management
// ========================================
/**
* Add a captive to this captor's captive list.
* Called when capture succeeds.
*
* Phase 14.1.6: Changed from PlayerBindState to IRestrainable
* Phase 17: Renamed from addSlave to addCaptive
* C6-V2: Narrowed from IRestrainable to IBondageState
*
* @param captive The IBondageState entity to capture
*/
void addCaptive(IBondageState captive);
/**
* Remove a captive from this captor's captive list.
* Called when freeing a captive or when captive escapes.
*
* Phase 14.1.6: Changed from PlayerBindState to IRestrainable
* Phase 17: Renamed from removeSlave to removeCaptive
* C6-V2: Narrowed from IRestrainable to IBondageState
*
* @param captive The IBondageState captive to remove
* @param transportState If true, also despawn the transport entity
*/
void removeCaptive(IBondageState captive, boolean transportState);
/**
* Check if this captor can capture the given target.
*
* Conditions (from original):
* - Target must be tied up OR have collar with this captor as owner
* - Target must not already be captured
*
* Phase 14.1.6: Changed from PlayerBindState to IRestrainable
* Phase 17: Renamed from canEnslave to canCapture
* C6-V2: Narrowed from IRestrainable to IBondageState
*
* @param target The potential IBondageState captive
* @return true if capture is allowed
*/
boolean canCapture(IBondageState target);
/**
* Check if this captor can release the given captive.
* Only the current captor can release their captive.
*
* Phase 14.1.6: Changed from PlayerBindState to IRestrainable
* Phase 17: Renamed from canFree to canRelease
* C6-V2: Narrowed from IRestrainable to IBondageState
*
* @param captive The IBondageState captive to check
* @return true if this captor is the captive's captor
*/
boolean canRelease(IBondageState captive);
// ========================================
// Configuration
// ========================================
/**
* Whether this captor allows captives to be transferred to other captors.
*
* Phase 17: Renamed from allowSlaveTransfer to allowCaptiveTransfer
*
* @return true if captive transfer is allowed (default for players)
*/
boolean allowCaptiveTransfer();
/**
* Whether this captor can have multiple captives simultaneously.
*
* Phase 17: Renamed from allowMultipleSlaves to allowMultipleCaptives
*
* @return true if multiple captives allowed (default for players)
*/
boolean allowMultipleCaptives();
// ========================================
// Event Callbacks
// ========================================
/**
* Called when a captive logs out while captured.
* Allows the captor to handle cleanup or persistence.
*
* Phase 14.1.6: Changed from PlayerBindState to IRestrainable
* Phase 17: Renamed from onSlaveLogout to onCaptiveLogout
* C6-V2: Narrowed from IRestrainable to IBondageState
* Note: For NPC captives, this may never be called (NPCs don't log out)
*
* @param captive The IBondageState captive that logged out
*/
void onCaptiveLogout(IBondageState captive);
/**
* Called when a captive is released (freed).
* Allows the captor to react to losing a captive.
*
* Phase 14.1.6: Changed from PlayerBindState to IRestrainable
* Phase 17: Renamed from onSlaveReleased to onCaptiveReleased
* C6-V2: Narrowed from IRestrainable to IBondageState
*
* @param captive The IBondageState captive that was released
*/
void onCaptiveReleased(IBondageState captive);
/**
* Called when a captive attempts to struggle.
* Allows the captor to react (e.g., shock collar activation).
*
* Phase 14.1.6: Changed from PlayerBindState to IRestrainable
* Phase 17: Renamed from onSlaveStruggle to onCaptiveStruggle
* C6-V2: Narrowed from IRestrainable to IBondageState
*
* @param captive The IBondageState captive that struggled
*/
void onCaptiveStruggle(IBondageState captive);
// ========================================
// Queries
// ========================================
/**
* Check if this captor currently has any captives.
*
* Phase 17: Renamed from hasSlaves to hasCaptives
*
* @return true if captive list is not empty
*/
boolean hasCaptives();
/**
* Get the entity representing this captor.
* Used for lead attachment and position queries.
*
* @return The entity (Player or custom entity)
*/
Entity getEntity();
}

View File

@@ -0,0 +1,166 @@
package com.tiedup.remake.state;
import org.jetbrains.annotations.Nullable;
import net.minecraft.world.entity.Entity;
/**
* Capture, leash, and transport interface for kidnapped entities.
*
* <p>Covers the capture lifecycle: being captured by a captor, being freed,
* being transferred to another captor, and querying capture state.</p>
*
* @see IRestrainableEntity
* @see IBondageState
* @see IRestrainable
*/
public interface ICapturable extends IRestrainableEntity {
// ========================================
// CAPTURE LIFECYCLE
// ========================================
/**
* Capture this entity by the given captor.
*
* <p><b>Prerequisites:</b></p>
* <ul>
* <li>Must be tied up OR have collar with captor as owner</li>
* <li>Must not already be captured ({@link #isCaptive()} == false)</li>
* <li>Captor must allow capture ({@link ICaptor#canCapture(IRestrainable)})</li>
* </ul>
*
* <p><b>Process for Players:</b></p>
* <ol>
* <li>Validate conditions (tied up or has owner's collar)</li>
* <li>Transfer any existing captives to new captor (if allowed)</li>
* <li>Create LeashProxyEntity that follows the player</li>
* <li>Attach leash from proxy to captor entity</li>
* <li>Add captive to captor's captive list</li>
* </ol>
*
* <p><b>Process for NPCs:</b></p>
* <ol>
* <li>Validate conditions</li>
* <li>Use vanilla setLeashedTo() directly on the NPC</li>
* </ol>
*
* Phase 17: Renamed from getEnslavedBy to getCapturedBy
*
* @param captor The captor attempting to capture
* @return true if capture succeeded, false otherwise
*/
boolean getCapturedBy(ICaptor captor);
/**
* Free this captive from capture.
* Equivalent to {@code free(true)}.
*
* <p>This will:</p>
* <ul>
* <li>Despawn the transport entity</li>
* <li>Drop the lead item</li>
* <li>Remove from captor's captive list</li>
* </ul>
*/
void free();
/**
* Free this captive from capture with transport state option.
*
* @param transportState If true, despawn the transport entity. If false, keep it (for transfer).
*/
void free(boolean transportState);
/**
* Transfer this captive to a new captor.
* Current captor loses the captive, new captor gains it.
*
* <p>Only works if current captor allows captive transfer
* ({@link ICaptor#allowCaptiveTransfer()} == true).</p>
*
* Phase 17: Renamed from transferSlaveryTo to transferCaptivityTo
*
* @param newCaptor The new captor to transfer to
*/
void transferCaptivityTo(ICaptor newCaptor);
// ========================================
// STATE QUERIES - CAPTURE
// ========================================
/**
* Check if this entity can be captured.
*
* <p><b>From original code (PlayerBindState.java:195-225):</b></p>
* <ul>
* <li>If tied up: Always capturable</li>
* <li>If NOT tied up: Only capturable if has collar AND collar has captor as owner</li>
* </ul>
*
* @return true if capturable
*/
boolean isEnslavable();
/**
* Check if this entity is currently captured (attached by leash).
*
* <p><b>For Players:</b> Returns true when LeashProxyEntity is attached and leashed to captor</p>
* <p><b>For NPCs:</b> Returns true when vanilla leash is attached</p>
*
* Phase 17: Renamed from isSlave to isCaptive
*
* @return true if captured (has leash holder)
*/
boolean isCaptive();
/**
* Check if this entity can be tied up (not already restrained).
*
* @return true if entity can accept bind items
*/
boolean canBeTiedUp();
/**
* Check if this entity is tied to a pole (immobilized).
*
* @return true if tied to a static pole entity
*/
boolean isTiedToPole();
/**
* Tie this entity to the closest fence/pole within range.
* Searches for fence blocks near the entity's current position.
*
* @param searchRadius The radius in blocks to search for fences
* @return true if successfully tied to a pole
*/
boolean tieToClosestPole(int searchRadius);
/**
* Check if this entity can be auto-kidnapped by events.
* Used by EntityKidnapper AI to determine valid targets.
*
* @return true if can be targeted by kidnapping events
*/
boolean canBeKidnappedByEvents();
/**
* Get the current captor (the entity holding the leash).
*
* Phase 17: Renamed from getMaster to getCaptor
*
* @return The captor, or null if not captured
*/
ICaptor getCaptor();
/**
* Get the leash proxy or transport entity for this captive.
*
* <p><b>For Players:</b> Returns the LeashProxyEntity following the player</p>
* <p><b>For NPCs:</b> Returns null (NPCs use vanilla leash directly)</p>
*
* @return The proxy/transport entity, or null if not applicable
*/
@Nullable
Entity getTransport();
}

View File

@@ -0,0 +1,102 @@
package com.tiedup.remake.state;
import net.minecraft.world.entity.player.Player;
/**
* Active coercion interface for kidnapped entities.
*
* <p>Covers tightening binds, chloroform, electric shocks,
* and forceful item removal by another entity.</p>
*
* @see IRestrainableEntity
* @see IRestrainable
*/
// C6-V2: takeBondageItemBy narrowed from IRestrainable to IRestrainableEntity
public interface ICoercible extends IRestrainableEntity {
// ========================================
// SPECIAL INTERACTIONS
// ========================================
/**
* Tighten binds on this entity (increase resistance).
* Called when master uses paddle/whip on slave.
*
* <p><b>Effects:</b></p>
* <ul>
* <li>Resets bind resistance to maximum</li>
* <li>Plays slap/whip sound</li>
* <li>Shows message to entity</li>
* </ul>
*
* @param tightener The player tightening the binds
*/
void tighten(Player tightener);
/**
* Apply chloroform effect to this entity.
*
* <p><b>Effects (from original):</b></p>
* <ul>
* <li>Slowness effect (duration from parameter)</li>
* <li>Weakness effect</li>
* <li>Prevents movement/interaction</li>
* </ul>
*
* @param duration Effect duration in ticks
*/
void applyChloroform(int duration);
/**
* Shock this kidnapped entity.
* Uses default damage and no message.
*
* <p><b>Effects:</b></p>
* <ul>
* <li>Plays electric shock sound</li>
* <li>Applies damage (default: 1.0F)</li>
* <li>Shows shock particles (client-side)</li>
* </ul>
*/
void shockKidnapped();
/**
* Shock this kidnapped entity with custom message and damage.
*
* @param messageAddon Additional message to send to the entity
* @param damage Damage amount to apply
*/
void shockKidnapped(String messageAddon, float damage);
/**
* Another entity takes a bondage item from this entity.
* Used when master removes items from slave.
*
* Phase 14.1.7: Changed from PlayerBindState to IRestrainable for polymorphism
* C6-V2: Narrowed from IRestrainable to IRestrainableEntity (only uses identity methods)
* This allows NPCs to take items from Players or other NPCs
*
* @param taker The IRestrainableEntity taking the item
* @param slotIndex The slot index (0-5: bind, gag, blindfold, earplugs, collar, clothes)
*/
void takeBondageItemBy(IRestrainableEntity taker, int slotIndex);
// ========================================
// COLLAR TIMERS (Phase 14.1.4)
// ========================================
/**
* Force-stops and clears any active auto-shock collar timer.
*
* <p>Called when:</p>
* <ul>
* <li>GPS collar is removed</li>
* <li>Auto-shock collar is removed</li>
* <li>Entity is freed from slavery</li>
* </ul>
*/
default void resetAutoShockTimer() {
// Default no-op (NPCs don't have timers by default)
// PlayerBindState overrides this to clear timer
}
}

View File

@@ -0,0 +1,81 @@
package com.tiedup.remake.state;
import com.tiedup.remake.entities.LeashProxyEntity;
import net.minecraft.world.entity.Entity;
/**
* Interface for accessing leash-related fields injected into ServerPlayer via mixin.
* Cast any ServerPlayer to this interface to access leash functionality.
*
* NOTE: This interface is in a separate package from mixins because mixin packages
* cannot be referenced directly from regular code.
*
* Usage:
* <pre>
* if (player instanceof IPlayerLeashAccess access) {
* access.tiedup$attachLeash(masterEntity);
* }
* </pre>
*/
public interface IPlayerLeashAccess {
/**
* Attach a leash from this player to a holder entity.
* Creates a LeashProxyEntity if not already present.
*
* @param holder The entity holding the leash (master player, NPC, or fence knot)
*/
void tiedup$attachLeash(Entity holder);
/**
* Detach the leash from this player.
* Discards the LeashProxyEntity.
*/
void tiedup$detachLeash();
/**
* Drop a lead item when detaching.
*/
void tiedup$dropLeash();
/**
* Check if this player is currently leashed.
*
* @return true if leashed to an entity
*/
boolean tiedup$isLeashed();
/**
* Get the entity holding this player's leash.
*
* @return The leash holder, or null if not leashed
*/
Entity tiedup$getLeashHolder();
/**
* Get the proxy entity used for leash rendering.
*
* @return The LeashProxyEntity, or null if not leashed
*/
LeashProxyEntity tiedup$getLeashProxy();
/**
* Tick the leash system - check validity and apply traction.
* Called from Forge TickEvent.PlayerTickEvent.
*/
void tiedup$tickLeash();
/**
* Set extra slack on the leash (increases pull start and max distances).
* Used during "pet leads" dogwalk so the player can walk ahead without being yanked back.
*
* @param slack Extra distance in blocks (0.0 = no slack)
*/
void tiedup$setLeashSlack(double slack);
/**
* Get the current leash slack value.
*
* @return Extra slack distance in blocks
*/
double tiedup$getLeashSlack();
}

View File

@@ -0,0 +1,40 @@
package com.tiedup.remake.state;
/**
* Union interface for entities that participate in the full restraint system.
*
* <p>Prefer using the narrowest sub-interface for your needs:</p>
* <ul>
* <li>{@link IBondageState} — equipment, state checks, capture, identity (most common)</li>
* <li>{@link ICoercible} — shock, chloroform, tighten</li>
* <li>{@link ISaleable} — sale system</li>
* </ul>
*
* <p>Use {@code IRestrainable} only when you genuinely need methods from multiple
* branches (e.g., both bondage state AND coercion, or both bondage AND sale).</p>
*
* <p><b>Inheritance hierarchy:</b></p>
* <pre>
* IRestrainableEntity (base)
* +-- ICapturable extends IRestrainableEntity
* | +-- IBondageState extends ICapturable
* +-- ICoercible extends IRestrainableEntity
*
* ISaleable (standalone)
*
* IRestrainable extends IBondageState, ICoercible, ISaleable
* </pre>
*
* <p><b>Implementors:</b> PlayerBindState, DamselBondageManager,
* MCAKidnappedAdapter</p>
*
* @see IRestrainableEntity
* @see ICapturable
* @see IBondageState
* @see ICoercible
* @see ISaleable
* @see PlayerBindState
*/
public interface IRestrainable extends IBondageState, ICoercible, ISaleable {
// Empty union body — all methods inherited from sub-interfaces.
}

View File

@@ -0,0 +1,207 @@
package com.tiedup.remake.state;
import java.util.UUID;
import com.tiedup.remake.v2.BodyRegionV2;
import net.minecraft.network.chat.Component;
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;
/**
* Base identity, lifecycle, and utility interface for kidnapped entities.
*
* <p>Provides the fundamental identity methods ({@link #asLivingEntity()},
* {@link #getKidnappedUniqueId()}, etc.), death handling, and default
* helper methods for communication and movement queries.</p>
*
* <p>Also hosts the V2 region-based equipment API defaults, which delegate
* to {@code V2EquipmentHelper} and work automatically for Players.</p>
*
* @see ICapturable
* @see IBondageState
* @see ICoercible
* @see IRestrainable
*/
public interface IRestrainableEntity {
// ========================================
// ENTITY IDENTITY
// ========================================
/**
* Get the underlying LivingEntity.
*
* <p>For Players: return the player
* <p>For NPCs: return this (cast to LivingEntity)
*
* @return The LivingEntity backing this IRestrainable instance
*/
LivingEntity asLivingEntity();
/**
* Get the unique ID of this kidnapped entity.
*
* @return The entity's UUID
*/
UUID getKidnappedUniqueId();
/**
* Get the display name of this kidnapped entity.
* For players: player name. For NPCs: entity custom name or default name.
*
* @return The entity's name as string
*/
String getKidnappedName();
/**
* Get the name from the collar's NBT tag (nickname).
* If collar has no nickname, returns empty string or entity name.
*
* @return The collar nickname, or fallback name
*/
String getNameFromCollar();
// ========================================
// LIFECYCLE
// ========================================
/**
* Called when this entity dies while kidnapped.
*
* <p><b>From original (PlayerBindState.java:1395-1461):</b></p>
* <ul>
* <li>Unlock all locked items</li>
* <li>Drop bondage items (if configured)</li>
* <li>Free from slavery</li>
* <li>Reset state</li>
* </ul>
*
* @param world The world the entity died in
* @return true if death was handled as kidnapped entity
*/
boolean onDeathKidnapped(Level world);
// ========================================
// UTILITY
// ========================================
/**
* Drop an item at this entity's position.
* Helper method for dropping bondage items.
*
* @param stack The ItemStack to drop
*/
void kidnappedDropItem(ItemStack stack);
/**
* Teleport this entity to a specific position.
* Used by collar teleport commands and warp points.
*
* @param position The target position (x, y, z, dimension)
*/
void teleportToPosition(com.tiedup.remake.util.teleport.Position position);
// ========================================
// DEFAULT HELPER METHODS - COMMUNICATION & STATE
// ========================================
/**
* Send a message to this entity (if it can receive messages).
*
* <p>For Players: Uses displayClientMessage()
* <p>For NPCs: No-op (NPCs don't need visual feedback)
*
* @param message The message component to send
* @param actionBar If true, shows message in action bar; if false, shows in chat
*/
default void sendMessage(Component message, boolean actionBar) {
LivingEntity entity = asLivingEntity();
if (entity instanceof Player player) {
player.displayClientMessage(message, actionBar);
}
// NPCs: silent no-op (acceptable degradation)
}
/**
* Check if entity is currently sprinting.
*
* <p>For Players: player.isSprinting()
* <p>For NPCs: Checks movement speed
*
* @return true if entity is sprinting
*/
default boolean isSprinting() {
LivingEntity entity = asLivingEntity();
if (entity instanceof Player player) {
return player.isSprinting();
}
// For NPCs: check movement speed
return entity.getDeltaMovement().horizontalDistanceSqr() > 0.1;
}
/**
* Check if entity is currently swimming.
*
* @return true if entity is swimming
*/
default boolean isSwimming() {
return asLivingEntity().isSwimming();
}
// ==================== V2 REGION-BASED API ====================
// These default methods delegate to V2EquipmentHelper.
// They work automatically for Players (V2 capability attached since Epic 0).
// For Damsels, requires IV2EquipmentHolder implementation (Epic 4B).
/**
* Get all V2 equipped items (de-duplicated).
* @return Unmodifiable map of region to ItemStack, or empty map if no V2 support.
*/
default java.util.Map<com.tiedup.remake.v2.BodyRegionV2, ItemStack> getAllEquippedV2() {
com.tiedup.remake.v2.bondage.IV2BondageEquipment equip =
com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.getEquipment(asLivingEntity());
return equip != null ? equip.getAllEquipped() : java.util.Map.of();
}
/**
* Get V2 item in a specific body region.
*/
default ItemStack getItemInRegion(com.tiedup.remake.v2.BodyRegionV2 region) {
return com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper
.getInRegion(asLivingEntity(), region);
}
/**
* Equip a V2 item to its declared regions. Server-only.
* @return The equip result.
*/
default com.tiedup.remake.v2.bondage.V2EquipResult equipToRegion(ItemStack stack) {
return com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper
.equipItem(asLivingEntity(), stack);
}
/**
* Unequip V2 item from a region. Server-only.
*/
default ItemStack unequipFromRegion(com.tiedup.remake.v2.BodyRegionV2 region) {
return com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper
.unequipFromRegion(asLivingEntity(), region);
}
/**
* Unequip V2 item from a region with force option. Server-only.
*/
default ItemStack unequipFromRegion(com.tiedup.remake.v2.BodyRegionV2 region, boolean force) {
return com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper
.unequipFromRegion(asLivingEntity(), region, force);
}
/**
* Whether this entity has V2 equipment support.
*/
default boolean hasV2Support() {
return com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper
.getEquipment(asLivingEntity()) != null;
}
}

View File

@@ -0,0 +1,39 @@
package com.tiedup.remake.state;
import com.tiedup.remake.util.tasks.ItemTask;
import org.jetbrains.annotations.Nullable;
/**
* Standalone sale state interface for entities that can be put up for sale.
*
* @see IRestrainable
*/
public interface ISaleable {
/**
* Check if this entity is marked for sale by a captor.
*
* @return true if captive is being sold
*/
boolean isForSell();
/**
* Get the sale price for this captive.
*
* @return The sale price ItemTask, or null if not for sale
*/
@Nullable
ItemTask getSalePrice();
/**
* Mark this captive as for sale with the given price.
*
* @param price The sale price
*/
void putForSale(ItemTask price);
/**
* Cancel the sale and reset sale state.
*/
void cancelSale();
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,427 @@
package com.tiedup.remake.state;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.items.base.ItemCollar;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
// C6-V2: IRestrainable → IBondageState (narrowed API)
/**
* Phase 8: Master-Captive Relationships
* Phase 17: Renamed from PlayerKidnapperManager, terminology slave → captive
*
* Manages capture relationships for player captors.
*
* Terminology (Phase 17):
* - "Captive" = Entity attached by leash (active physical control)
* - "Slave" = Entity wearing a collar owned by someone (passive ownership via CollarRegistry)
*
* Features:
* - Supports multiple captives simultaneously
* - Allows captive transfer between captors
* - Tracks captive list persistently
* - Handles cleanup on captive logout/escape
*
* Thread Safety:
* - Uses CopyOnWriteArrayList to avoid ConcurrentModificationException
* - Safe for iteration during modification
*
* Design:
* - Each PlayerBindState has one PlayerCaptorManager
* - Manager tracks all captives owned by that player
* - Implements ICaptor interface for polymorphic usage
*
* @see ICaptor
* @see PlayerBindState
*/
public class PlayerCaptorManager implements ICaptor {
/**
* The player who owns this manager (the captor).
*/
private final Player captor;
/**
* List of all captives currently owned by this captor.
* Thread-safe to avoid concurrent modification during iteration.
*
* Phase 14.1.6: Changed from List<PlayerBindState> to List<IBondageState>
* Phase 17: Renamed from slaves to captives
*/
private final List<IBondageState> captives;
/**
* Create a new captor manager for the given player.
*
* @param captor The player who will be the captor
*/
public PlayerCaptorManager(Player captor) {
this.captor = captor;
this.captives = new CopyOnWriteArrayList<>();
}
// ========================================
// ICaptor Implementation
// ========================================
/**
* Phase 14.1.6: Changed parameter from PlayerBindState to IBondageState
* Phase 17: Renamed from addSlave to addCaptive
*/
@Override
public synchronized void addCaptive(IBondageState captive) {
if (captive == null) {
TiedUpMod.LOGGER.warn(
"[PlayerCaptorManager] Attempted to add null captive"
);
return;
}
if (!captives.contains(captive)) {
captives.add(captive);
TiedUpMod.LOGGER.info(
"[PlayerCaptorManager] {} captured {} (total captives: {})",
captor.getName().getString(),
captive.getKidnappedName(),
captives.size()
);
}
}
/**
* Phase 14.1.6: Changed parameter from PlayerBindState to IBondageState
* Phase 17: Renamed from removeSlave to removeCaptive
*
* Thread Safety: Synchronized on 'this' to match addCaptive and freeAllCaptives.
*/
@Override
public synchronized void removeCaptive(
IBondageState captive,
boolean transportState
) {
if (captive == null) {
TiedUpMod.LOGGER.warn(
"[PlayerCaptorManager] Attempted to remove null captive"
);
return;
}
if (captives.remove(captive)) {
TiedUpMod.LOGGER.info(
"[PlayerCaptorManager] {} freed {} (remaining captives: {})",
captor.getName().getString(),
captive.getKidnappedName(),
captives.size()
);
// If requested, also despawn the transport entity
if (transportState && captive.getTransport() != null) {
captive.getTransport().discard();
}
}
}
/**
* Phase 14.1.6: Changed parameter from PlayerBindState to IBondageState
* Phase 17: Renamed from canEnslave to canCapture
*/
@Override
public boolean canCapture(IBondageState target) {
if (target == null) {
return false;
}
// From original code (PlayerBindState.java:195-225):
// Can capture if:
// - Target is tied up, OR
// - Target has collar AND collar has this captor as owner
// Phase 14.1.6: Use asLivingEntity() instead of getPlayer()
net.minecraft.world.entity.LivingEntity targetEntity =
target.asLivingEntity();
if (targetEntity == null) {
return false;
}
// Check if target is tied up
if (target.isTiedUp()) {
return true;
}
// Check if target has collar with this captor as owner
if (target.hasCollar()) {
ItemStack collar = target.getEquipment(BodyRegionV2.NECK);
if (collar.getItem() instanceof ItemCollar collarItem) {
if (
collarItem.getOwners(collar).contains(this.captor.getUUID())
) {
return true;
}
}
}
return false;
}
/**
* Phase 14.1.6: Changed parameter from PlayerBindState to IBondageState
* Phase 17: Renamed from canFree to canRelease
*/
@Override
public boolean canRelease(IBondageState captive) {
if (captive == null) {
return false;
}
// Can only release if this manager is the captive's captor
return captive.getCaptor() == this;
}
/**
* Phase 17: Renamed from allowSlaveTransfer to allowCaptiveTransfer
*/
@Override
public boolean allowCaptiveTransfer() {
// Players always allow captive transfer
return true;
}
/**
* Phase 17: Renamed from allowMultipleSlaves to allowMultipleCaptives
*/
@Override
public boolean allowMultipleCaptives() {
// Players can have multiple captives
return true;
}
/**
* Phase 14.1.6: Changed parameter from PlayerBindState to IBondageState
* Phase 17: Renamed from onSlaveLogout to onCaptiveLogout
* Note: For NPC captives, this is never called (NPCs don't log out)
*/
@Override
public void onCaptiveLogout(IBondageState captive) {
if (captive == null) {
return;
}
TiedUpMod.LOGGER.info(
"[PlayerCaptorManager] Captive {} logged out while captured by {}",
captive.getKidnappedName(),
captor.getName().getString()
);
// Keep captive in list - they might reconnect
// Transport entity will despawn after timeout
// On reconnect, checkStillCaptive() will clean up if needed
}
/**
* Phase 14.1.6: Changed parameter from PlayerBindState to IBondageState
* Phase 17: Renamed from onSlaveReleased to onCaptiveReleased
*/
@Override
public void onCaptiveReleased(IBondageState captive) {
if (captive == null) {
return;
}
TiedUpMod.LOGGER.info(
"[PlayerCaptorManager] Captive {} was released from {}",
captive.getKidnappedName(),
captor.getName().getString()
);
// No special action needed - already removed from list by removeCaptive()
}
/**
* Phase 14.1.6: Changed parameter from PlayerBindState to IBondageState
* Phase 17: Renamed from onSlaveStruggle to onCaptiveStruggle
*/
@Override
public void onCaptiveStruggle(IBondageState captive) {
if (captive == null) {
return;
}
TiedUpMod.LOGGER.debug(
"[PlayerCaptorManager] Captive {} struggled (captor: {})",
captive.getKidnappedName(),
captor.getName().getString()
);
// Phase 8: No action for basic struggle
// Phase 14: Shock collar would activate here
}
/**
* Phase 17: Renamed from hasSlaves to hasCaptives
*/
@Override
public boolean hasCaptives() {
return !captives.isEmpty();
}
@Override
public Entity getEntity() {
return captor;
}
// ========================================
// Additional Methods
// ========================================
/**
* Frees all captives currently owned by this captor.
*
* Phase 17: Renamed from freeAllSlaves to freeAllCaptives
*
* Thread Safety: Synchronized on 'this' to match addCaptive and removeCaptive.
*
* @param transportState If true, destroy the transporter entities
*/
public synchronized void freeAllCaptives(boolean transportState) {
// Use a copy to avoid concurrent modification while iterating
List<IBondageState> copy = new ArrayList<>(captives);
for (IBondageState captive : copy) {
captive.free(transportState);
}
captives.clear();
}
/**
* Frees all captives with default behavior (destroy transport entities).
*/
public void freeAllCaptives() {
freeAllCaptives(true);
}
/**
* Get a copy of the captive list.
* Safe for iteration without concurrent modification issues.
*
* Phase 17: Renamed from getSlaves to getCaptives
*
* @return Copy of the current captive list
*/
public List<IBondageState> getCaptives() {
return new ArrayList<>(captives);
}
/**
* Get the number of captives currently owned.
*
* Phase 17: Renamed from getSlaveCount to getCaptiveCount
*
* @return Captive count
*/
public int getCaptiveCount() {
return captives.size();
}
/**
* Transfer all captives from this captor to a new captor.
* Used when this player gets captured themselves.
*
* From original code:
* - When a player gets captured, their captives transfer to new captor
* - Prevents circular capture issues
*
* Phase 17: Renamed from transferAllSlavesTo to transferAllCaptivesTo
*
* @param newCaptor The new captor to transfer captives to
*/
public void transferAllCaptivesTo(ICaptor newCaptor) {
if (newCaptor == null) {
TiedUpMod.LOGGER.warn(
"[PlayerCaptorManager] Attempted to transfer captives to null captor"
);
return;
}
if (captives.isEmpty()) {
return;
}
TiedUpMod.LOGGER.info(
"[PlayerCaptorManager] Transferring {} captives from {} to {}",
captives.size(),
captor.getName().getString(),
newCaptor.getEntity().getName().getString()
);
// Create copy to avoid concurrent modification
List<IBondageState> captivesToTransfer = new ArrayList<>(captives);
for (IBondageState captive : captivesToTransfer) {
if (captive != null) {
captive.transferCaptivityTo(newCaptor);
}
}
// All captives should now be removed from this manager's list
if (!captives.isEmpty()) {
TiedUpMod.LOGGER.warn(
"[PlayerCaptorManager] {} captives remain after transfer - cleaning up",
captives.size()
);
captives.clear();
}
}
/**
* Get the captor player.
*
* @return The player who owns this manager
*/
public Player getCaptor() {
return captor;
}
/**
* Clean up invalid captives from the list.
* Removes captives that are no longer valid (offline, transport gone, etc.).
*
* Phase 17: Renamed from cleanupInvalidSlaves to cleanupInvalidCaptives
*
* Should be called periodically (e.g., on tick).
*/
public void cleanupInvalidCaptives() {
captives.removeIf(captive -> {
if (captive == null) {
return true;
}
// Remove if not actually captured anymore
if (!captive.isCaptive()) {
TiedUpMod.LOGGER.debug(
"[PlayerCaptorManager] Removing invalid captive {}",
captive.getKidnappedName()
);
return true;
}
// Remove if captured by different captor
if (captive.getCaptor() != this) {
TiedUpMod.LOGGER.debug(
"[PlayerCaptorManager] Removing captive {} (belongs to different captor)",
captive.getKidnappedName()
);
return true;
}
return false;
});
}
// ========================================
// Backward Compatibility (Phase 17)
// ========================================
}

View File

@@ -0,0 +1,187 @@
package com.tiedup.remake.state;
import com.tiedup.remake.core.TiedUpMod;
import java.util.*;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.saveddata.SavedData;
/**
* Persistent storage for social command data.
*
* Stores:
* - Block lists (which players have blocked which other players)
* - Talk area settings (local chat distance per player)
*
* Phase 18: Social Commands persistence.
*/
public class SocialData extends SavedData {
private static final String DATA_NAME = TiedUpMod.MOD_ID + "_social";
// Block lists per player UUID
private final Map<UUID, Set<UUID>> blockedPlayers = new HashMap<>();
// Talk area settings per player UUID
private final Map<UUID, Integer> talkAreas = new HashMap<>();
public SocialData() {}
/**
* Load from NBT.
*/
public static SocialData load(CompoundTag tag) {
SocialData data = new SocialData();
// Load blocked players
if (tag.contains("blocked")) {
ListTag blockedList = tag.getList("blocked", Tag.TAG_COMPOUND);
for (int i = 0; i < blockedList.size(); i++) {
CompoundTag entry = blockedList.getCompound(i);
UUID blocker = entry.getUUID("blocker");
Set<UUID> blocked = new HashSet<>();
ListTag blockedUUIDs = entry.getList(
"blocked_uuids",
Tag.TAG_COMPOUND
);
for (int j = 0; j < blockedUUIDs.size(); j++) {
CompoundTag uuidTag = blockedUUIDs.getCompound(j);
blocked.add(uuidTag.getUUID("uuid"));
}
if (!blocked.isEmpty()) {
data.blockedPlayers.put(blocker, blocked);
}
}
}
// Load talk areas
if (tag.contains("talk_areas")) {
ListTag talkList = tag.getList("talk_areas", Tag.TAG_COMPOUND);
for (int i = 0; i < talkList.size(); i++) {
CompoundTag entry = talkList.getCompound(i);
UUID player = entry.getUUID("player");
int distance = entry.getInt("distance");
data.talkAreas.put(player, distance);
}
}
TiedUpMod.LOGGER.debug(
"[SocialData] Loaded {} block lists, {} talk areas",
data.blockedPlayers.size(),
data.talkAreas.size()
);
return data;
}
@Override
public CompoundTag save(CompoundTag tag) {
// Save blocked players
ListTag blockedList = new ListTag();
for (Map.Entry<UUID, Set<UUID>> entry : blockedPlayers.entrySet()) {
if (entry.getValue().isEmpty()) continue;
CompoundTag blockerTag = new CompoundTag();
blockerTag.putUUID("blocker", entry.getKey());
ListTag blockedUUIDs = new ListTag();
for (UUID blocked : entry.getValue()) {
CompoundTag uuidTag = new CompoundTag();
uuidTag.putUUID("uuid", blocked);
blockedUUIDs.add(uuidTag);
}
blockerTag.put("blocked_uuids", blockedUUIDs);
blockedList.add(blockerTag);
}
tag.put("blocked", blockedList);
// Save talk areas
ListTag talkList = new ListTag();
for (Map.Entry<UUID, Integer> entry : talkAreas.entrySet()) {
CompoundTag talkTag = new CompoundTag();
talkTag.putUUID("player", entry.getKey());
talkTag.putInt("distance", entry.getValue());
talkList.add(talkTag);
}
tag.put("talk_areas", talkList);
return tag;
}
// ==================== Block List Methods ====================
/**
* Add a player to another player's block list.
*/
public void addBlock(UUID blocker, UUID blocked) {
blockedPlayers
.computeIfAbsent(blocker, k -> new HashSet<>())
.add(blocked);
setDirty();
}
/**
* Remove a player from another player's block list.
*/
public void removeBlock(UUID blocker, UUID blocked) {
Set<UUID> set = blockedPlayers.get(blocker);
if (set != null) {
set.remove(blocked);
if (set.isEmpty()) {
blockedPlayers.remove(blocker);
}
setDirty();
}
}
/**
* Check if a player has blocked another.
*/
public boolean isBlocked(UUID blocker, UUID blocked) {
Set<UUID> set = blockedPlayers.get(blocker);
return set != null && set.contains(blocked);
}
/**
* Get the set of players blocked by a player.
*/
public Set<UUID> getBlockedPlayers(UUID blocker) {
return blockedPlayers.getOrDefault(blocker, Collections.emptySet());
}
// ==================== Talk Area Methods ====================
/**
* Set a player's talk area distance.
*/
public void setTalkArea(UUID player, int distance) {
if (distance <= 0) {
talkAreas.remove(player);
} else {
talkAreas.put(player, distance);
}
setDirty();
}
/**
* Get a player's talk area distance (0 = global chat).
*/
public int getTalkArea(UUID player) {
return talkAreas.getOrDefault(player, 0);
}
// ==================== Static Access ====================
/**
* Get the SocialData for a server level.
*/
public static SocialData get(ServerLevel level) {
return level
.getDataStorage()
.computeIfAbsent(SocialData::load, SocialData::new, DATA_NAME);
}
}

View File

@@ -0,0 +1,295 @@
package com.tiedup.remake.state.components;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.LeashProxyEntity;
import com.tiedup.remake.state.IPlayerLeashAccess;
import com.tiedup.remake.state.ICaptor;
import com.tiedup.remake.state.hosts.IPlayerBindStateHost;
import net.minecraft.world.entity.player.Player;
import org.jetbrains.annotations.Nullable;
/**
* Component responsible for captivity mechanics and leash proxy management.
* Phase 17: Advanced capture system (proxy-based leashing)
*
* Single Responsibility: Captivity lifecycle and transport management
* Complexity: VERY HIGH (mixin coupling, network sync, leash proxy coordination)
* Risk: VERY HIGH (critical path, mixin dependency, 4 network sync points)
*
* Mixin Dependency: IPlayerLeashAccess for leash proxy system
* Network Sync: 4 syncEnslavement() calls coordinated via host
*
* Captivity States:
* - Not Captive: No captor, no leash
* - Captive (by entity): Has captor, leashed to entity
* - Pole Binding: No captor, leashed to pole (LeashFenceKnotEntity)
*/
public class PlayerCaptivity {
private final IPlayerBindStateHost host;
public PlayerCaptivity(IPlayerBindStateHost host) {
this.host = host;
}
// ========== Captivity Initiation ==========
/**
* Phase 17: Renamed from getEnslavedBy to getCapturedBy
* Initiates the capture process by a captor.
* Uses the proxy-based leash system (player is NOT mounted).
*
* Thread Safety: Synchronized to prevent race condition where two kidnappers
* could both pass the isCaptive() check and attempt to capture simultaneously.
*
* @param newCaptor The entity attempting to capture this player
* @return true if capture succeeded
*/
public synchronized boolean getCapturedBy(ICaptor newCaptor) {
Player player = host.getPlayer();
if (player == null || newCaptor == null) return false;
// Must be enslavable (tied up) OR captor can capture (includes collar owner exception)
if (
!isEnslavable() && !newCaptor.canCapture(host.getKidnapped())
) return false;
// Check if already captured (atomic check under synchronization)
if (isCaptive()) return false;
// Free all captives instead of transferring (until multi-captive is implemented)
if (host.getCaptorManager().hasCaptives()) {
TiedUpMod.LOGGER.debug(
"[PlayerCaptivity] {} is being captured - freeing their {} captives",
player.getName().getString(),
host.getCaptorManager().getCaptiveCount()
);
host.getCaptorManager().freeAllCaptives(true);
}
// Use new proxy-based leash system
if (player instanceof IPlayerLeashAccess access) {
access.tiedup$attachLeash(newCaptor.getEntity());
newCaptor.addCaptive(host.getKidnapped());
host.setCaptor(newCaptor);
TiedUpMod.LOGGER.debug(
"[PlayerCaptivity] {} captured by {} (proxy leash)",
player.getName().getString(),
newCaptor.getEntity().getName().getString()
);
// Sync enslavement state to all clients
host.syncEnslavement();
return true;
}
TiedUpMod.LOGGER.error(
"[PlayerCaptivity] Player {} does not implement IPlayerLeashAccess!",
player.getName().getString()
);
return false;
}
// ========== Captivity Release ==========
/**
* Ends captivity with default behavior (drops leash item).
*/
public void free() {
free(true);
}
/**
* Phase 17: Ends captivity and detaches the leash proxy.
*
* @param dropLead Whether to drop the leash item
*/
public void free(boolean dropLead) {
Player player = host.getPlayer();
if (player == null) return;
if (!(player instanceof IPlayerLeashAccess access)) {
TiedUpMod.LOGGER.error(
"[PlayerCaptivity] Player {} does not implement IPlayerLeashAccess!",
player.getName().getString()
);
return;
}
ICaptor captor = host.getCaptor();
// Handle pole binding (no captor) - just detach leash
if (captor == null) {
if (access.tiedup$isLeashed()) {
TiedUpMod.LOGGER.debug(
"[PlayerCaptivity] Freeing {} from pole binding",
player.getName().getString()
);
if (dropLead) {
access.tiedup$dropLeash();
}
access.tiedup$detachLeash();
host.syncEnslavement();
}
return;
}
TiedUpMod.LOGGER.debug(
"[PlayerCaptivity] Freeing {} from captivity",
player.getName().getString()
);
// 1. Remove from captor's tracking list
captor.removeCaptive(host.getKidnapped(), false);
// 2. Detach leash proxy
if (dropLead) {
access.tiedup$dropLeash();
}
access.tiedup$detachLeash();
// 3. Reset state
host.setCaptor(null);
// 4. Sync freed state to all clients
host.syncEnslavement();
}
// ========== Captivity Transfer ==========
/**
* Phase 17: Renamed from transferSlaveryTo to transferCaptivityTo
* Transfers captivity from current captor to a new captor.
*
* Thread Safety: Synchronized to prevent concurrent transfer attempts.
*
* @param newCaptor The new captor entity
*/
public synchronized void transferCaptivityTo(ICaptor newCaptor) {
Player player = host.getPlayer();
ICaptor currentCaptor = host.getCaptor();
if (
player == null ||
newCaptor == null ||
currentCaptor == null ||
!currentCaptor.allowCaptiveTransfer()
) return;
currentCaptor.removeCaptive(host.getKidnapped(), false);
// Re-attach leash to new captor
if (player instanceof IPlayerLeashAccess access) {
access.tiedup$detachLeash();
access.tiedup$attachLeash(newCaptor.getEntity());
}
newCaptor.addCaptive(host.getKidnapped());
host.setCaptor(newCaptor);
// Sync new captor to all clients
host.syncEnslavement();
}
// ========== State Queries ==========
/**
* Check if this player can be captured (leashed).
* Must be tied up to be leashed.
* Collar alone is NOT enough - collar owner exception is handled in canCapture().
*
* @return true if player is tied up and can be leashed
*/
public boolean isEnslavable() {
return host.isTiedUp();
}
/**
* Phase 17: Renamed from isSlave to isCaptive
* Check if player is currently captured by an entity.
*
* @return true if player has a captor and is leashed
*/
public boolean isCaptive() {
Player player = host.getPlayer();
if (host.getCaptor() == null) return false;
if (player instanceof IPlayerLeashAccess access) {
return access.tiedup$isLeashed();
}
return false;
}
/**
* Get the leash proxy entity (transport system).
* Phase 17: Proxy-based leashing (no mounting).
*
* @return The leash proxy, or null if not leashed
*/
@Nullable
public LeashProxyEntity getTransport() {
Player player = host.getPlayer();
if (player instanceof IPlayerLeashAccess access) {
return access.tiedup$getLeashProxy();
}
return null;
}
// ========== Captivity Monitoring ==========
/**
* Phase 17: Renamed from checkStillSlave to checkStillCaptive
* Periodically monitors captivity validity.
* Simplified: If any condition is invalid, free the captive immediately.
*
* Called from RestraintTaskTickHandler every player tick.
*/
public void checkStillCaptive() {
if (!isCaptive()) return;
Player player = host.getPlayer();
if (player == null) return;
// Check if no longer tied/collared
if (!host.isTiedUp() && !host.hasCollar()) {
TiedUpMod.LOGGER.debug(
"[PlayerCaptivity] Auto-freeing {} - no restraints",
player.getName().getString()
);
free();
return;
}
// Check leash proxy status
if (player instanceof IPlayerLeashAccess access) {
LeashProxyEntity proxy = access.tiedup$getLeashProxy();
if (proxy == null || proxy.proxyIsRemoved()) {
TiedUpMod.LOGGER.debug(
"[PlayerCaptivity] Auto-freeing {} - proxy invalid",
player.getName().getString()
);
// Notify captor BEFORE freeing (triggers retrieval behavior)
ICaptor captor = host.getCaptor();
if (captor != null) {
captor.onCaptiveReleased(host.getKidnapped());
}
free();
return;
}
// Check if leash holder is still valid
if (proxy.getLeashHolder() == null) {
TiedUpMod.LOGGER.debug(
"[PlayerCaptivity] Auto-freeing {} - leash holder gone",
player.getName().getString()
);
// Notify captor BEFORE freeing (triggers retrieval behavior)
ICaptor captor = host.getCaptor();
if (captor != null) {
captor.onCaptiveReleased(host.getKidnapped());
}
free();
}
}
}
}

View File

@@ -0,0 +1,149 @@
package com.tiedup.remake.state.components;
import com.tiedup.remake.v2.bondage.IV2BondageItem;
import com.tiedup.remake.state.hosts.IPlayerBindStateHost;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageEquipment;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
/**
* Component responsible for clothes permission and management.
* Handles who can remove/change clothes and manages clothes replacement with network sync.
*
* Single Responsibility: Clothes permissions and synchronization
* Complexity: MEDIUM (network sync coordination)
* Risk: MEDIUM (must coordinate SyncManager calls)
*
* Epic 5F: Uses V2EquipmentHelper/BodyRegionV2.
*/
public class PlayerClothesPermission {
private final IPlayerBindStateHost host;
public PlayerClothesPermission(IPlayerBindStateHost host) {
this.host = host;
}
// ========== Permission Checks ==========
/**
* Check if a specific player can take off this player's clothes.
* Currently permissive - anyone can take off clothes.
* Future: Could check if 'player' is owner/master when tied up.
*
* @param player The player attempting to remove clothes
* @return true if allowed
*/
public boolean canTakeOffClothes(Player player) {
// Currently permissive - anyone can take off clothes
// Future: Could check if 'player' is owner/master when tied up
return true;
}
/**
* Check if a specific player can change this player's clothes.
* Currently permissive - anyone can change clothes.
* Future: Could check if 'player' is owner/master when tied up.
*
* @param player The player attempting to change clothes
* @return true if allowed
*/
public boolean canChangeClothes(Player player) {
// Currently permissive - anyone can change clothes
// Future: Could check if 'player' is owner/master when tied up
return true;
}
/**
* Check if clothes can be changed (no specific player context).
* Checks if no clothes are equipped, or if clothes are not locked.
*
* @return true if clothes can be changed
*/
public boolean canChangeClothes() {
ItemStack clothes = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.TORSO);
if (clothes.isEmpty()) return true;
// Check if clothes are locked
return !isLocked(clothes, false);
}
// ========== Clothes Management ==========
/**
* Replace current clothes with new clothes.
* Removes old clothes and equips new ones.
* Syncs clothes config to all clients.
*
* @param newClothes The new clothes to equip
* @return The old clothes, or empty if none
*/
public ItemStack replaceClothes(ItemStack newClothes) {
return replaceClothes(newClothes, false);
}
/**
* Replace current clothes with new clothes, with optional force.
* If force is true, bypasses lock checks.
* Syncs clothes config to all clients.
*
* @param newClothes The new clothes to equip
* @param force true to bypass lock checks
* @return The old clothes, or empty if none
*/
public ItemStack replaceClothes(ItemStack newClothes, boolean force) {
Player player = host.getPlayer();
if (player == null || player.level().isClientSide) return ItemStack.EMPTY;
IV2BondageEquipment equip = V2EquipmentHelper.getEquipment(player);
if (equip == null) return ItemStack.EMPTY;
// Take off old clothes
ItemStack old = V2EquipmentHelper.unequipFromRegion(player, BodyRegionV2.TORSO);
// Equip new clothes if we successfully removed old ones
if (!old.isEmpty()) {
equip.setInRegion(BodyRegionV2.TORSO, newClothes.copy());
// Fire lifecycle hook for new item
if (!newClothes.isEmpty() && newClothes.getItem() instanceof IV2BondageItem newItem) {
newItem.onEquipped(newClothes, player);
}
V2EquipmentHelper.sync(player);
// CRITICAL: Sync clothes config to all tracking clients
// This ensures dynamic textures and other clothes properties are synced
host.syncClothesConfig();
}
return old;
}
// ========== Helper Methods ==========
/**
* Check if an item is locked.
* Helper method for lock checking.
*
* @param stack The item to check
* @param force If true, returns false (bypasses lock)
* @return true if locked and not forced
*/
private boolean isLocked(ItemStack stack, boolean force) {
if (force) return false;
if (stack.isEmpty()) return false;
// Check if item has locked property
if (
stack.getItem() instanceof
com.tiedup.remake.items.base.ILockable lockable
) {
return lockable.isLocked(stack);
}
return false;
}
}

View File

@@ -0,0 +1,113 @@
package com.tiedup.remake.state.components;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.state.hosts.IPlayerBindStateHost;
import com.tiedup.remake.v2.BodyRegionV2;
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;
/**
* Component responsible for retrieving player data and equipment.
* Provides access to current bondage items and player information.
*
* Single Responsibility: Data retrieval
* Complexity: LOW (simple getters)
* Risk: LOW (read-only access)
*
* Epic 5F: Uses V2EquipmentHelper/BodyRegionV2.
*/
public class PlayerDataRetrieval {
private final IPlayerBindStateHost host;
public PlayerDataRetrieval(IPlayerBindStateHost host) {
this.host = host;
}
// ========== Equipment Getters ==========
public ItemStack getCurrentBind() {
return V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.ARMS);
}
public ItemStack getCurrentGag() {
return V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.MOUTH);
}
public ItemStack getCurrentBlindfold() {
return V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.EYES);
}
public ItemStack getCurrentEarplugs() {
return V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.EARS);
}
public ItemStack getCurrentClothes() {
return V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.TORSO);
}
public ItemStack getCurrentMittens() {
return V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.HANDS);
}
public ItemStack getCurrentCollar() {
return V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.NECK);
}
// ========== Player Information ==========
/**
* Get the player's display name, checking collar for nickname first.
* If collar has a nickname, returns that; otherwise returns player name.
*/
public String getNameFromCollar() {
Player player = host.getPlayer();
ItemStack collar = getCurrentCollar();
if (
!collar.isEmpty() &&
collar.getItem() instanceof ItemCollar collarItem
) {
// Try to get nickname from collar NBT
String nickname = collarItem.getNickname(collar);
if (nickname != null && !nickname.isEmpty()) {
return nickname;
}
}
// Fallback to player name
return player.getName().getString();
}
/**
* Check if player has a named collar (collar with nickname).
*/
public boolean hasNamedCollar() {
ItemStack collar = getCurrentCollar();
if (collar.isEmpty()) return false;
if (collar.getItem() instanceof ItemCollar collarItem) {
String nickname = collarItem.getNickname(collar);
return nickname != null && !nickname.isEmpty();
}
return false;
}
/**
* Check if player has clothes with small arms flag.
* TODO Phase 14+: Check clothes NBT for small arms flag
*/
public boolean hasClothesWithSmallArms() {
return false;
}
/**
* Get the player as a LivingEntity.
* Used for generic entity operations.
*/
public LivingEntity asLivingEntity() {
return host.getPlayer();
}
}

View File

@@ -0,0 +1,429 @@
package com.tiedup.remake.state.components;
import com.tiedup.remake.v2.bondage.IV2BondageItem;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.state.hosts.IPlayerBindStateHost;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageEquipment;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import java.util.function.Supplier;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
/**
* Component responsible for bondage equipment management.
* Handles putting on, taking off, and replacing all bondage items.
*
* Single Responsibility: Equipment lifecycle management
* Complexity: MEDIUM (V2 equipment coupling)
* Risk: MEDIUM (core equipment system)
*
* Epic 5F: Uses V2EquipmentHelper.
* Uses low-level setInRegion for equips because V1 items (IBondageItem) do not
* implement IV2BondageItem, so V2EquipmentHelper.equipItem() would reject them.
*/
public class PlayerEquipment {
private final IPlayerBindStateHost host;
public PlayerEquipment(IPlayerBindStateHost host) {
this.host = host;
}
// ========== Put On Methods ==========
/** Equips a bind item and applies speed reduction. */
public void putBindOn(ItemStack bind) {
equipInRegion(BodyRegionV2.ARMS, bind);
}
/** Equips a gag (enables gag talk if implemented). Issue #14 fix: now calls onEquipped. */
public void putGagOn(ItemStack gag) {
equipInRegion(BodyRegionV2.MOUTH, gag);
}
/** Equips a blindfold (restricts vision). Issue #14 fix: now calls onEquipped. */
public void putBlindfoldOn(ItemStack blindfold) {
equipInRegion(BodyRegionV2.EYES, blindfold);
}
/** Equips a collar (starts GPS/Shock monitoring). Issue #14 fix: now calls onEquipped. */
public void putCollarOn(ItemStack collar) {
equipInRegion(BodyRegionV2.NECK, collar);
}
/** Issue #14 fix: now calls onEquipped. Syncs clothes config to all clients. */
public void putClothesOn(ItemStack clothes) {
equipInRegion(BodyRegionV2.TORSO, clothes);
// Sync clothes config (dynamic textures, etc.) to all tracking clients
host.syncClothesConfig();
}
/** Equips mittens (blocks hand interactions). Phase 14.4: Mittens system. Issue #14 fix: now calls onEquipped. */
public void putMittensOn(ItemStack mittens) {
equipInRegion(BodyRegionV2.HANDS, mittens);
checkMittensAfterApply();
}
/** Equips earplugs (muffles sounds). Issue #14 fix: now calls onEquipped. */
public void putEarplugsOn(ItemStack earplugs) {
equipInRegion(BodyRegionV2.EARS, earplugs);
checkEarplugsAfterApply();
}
// ========== Take Off Methods ==========
/** Removes binds and restores speed. */
public ItemStack takeBindOff() {
return V2EquipmentHelper.unequipFromRegion(host.getPlayer(), BodyRegionV2.ARMS);
}
public ItemStack takeGagOff() {
return V2EquipmentHelper.unequipFromRegion(host.getPlayer(), BodyRegionV2.MOUTH);
}
public ItemStack takeBlindfoldOff() {
return V2EquipmentHelper.unequipFromRegion(host.getPlayer(), BodyRegionV2.EYES);
}
public ItemStack takeCollarOff() {
return V2EquipmentHelper.unequipFromRegion(host.getPlayer(), BodyRegionV2.NECK);
}
/** Removes mittens. Phase 14.4: Mittens system */
public ItemStack takeMittensOff() {
ItemStack mittens = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.HANDS);
if (isLocked(mittens, false)) {
return ItemStack.EMPTY;
}
return V2EquipmentHelper.unequipFromRegion(host.getPlayer(), BodyRegionV2.HANDS);
}
public ItemStack takeEarplugsOff() {
ItemStack earplugs = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.EARS);
if (isLocked(earplugs, false)) {
return ItemStack.EMPTY;
}
return V2EquipmentHelper.unequipFromRegion(host.getPlayer(), BodyRegionV2.EARS);
}
public ItemStack takeClothesOff() {
ItemStack clothes = V2EquipmentHelper.unequipFromRegion(host.getPlayer(), BodyRegionV2.TORSO);
// Sync clothes removal to all tracking clients
host.syncClothesConfig();
return clothes;
}
/**
* Tries to remove the collar. Fails if locked unless forced.
*/
public ItemStack takeCollarOff(boolean force) {
Player player = host.getPlayer();
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
if (collar.isEmpty()) return ItemStack.EMPTY;
if (collar.getItem() instanceof ItemCollar collarItem) {
if (!force && collarItem.isLocked(collar)) return ItemStack.EMPTY;
collarItem.setLocked(collar, false);
}
return V2EquipmentHelper.unequipFromRegion(player, BodyRegionV2.NECK);
}
// ========== Replacement Methods ==========
/** Replaces the blindfold and returns the old one. Issue #14 fix: now calls lifecycle hooks. */
public ItemStack replaceBlindfold(ItemStack newBlindfold) {
ItemStack current = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.EYES);
if (current.isEmpty()) return ItemStack.EMPTY;
return replaceInRegion(BodyRegionV2.EYES, newBlindfold);
}
/** Replaces the gag and returns the old one. Issue #14 fix: now calls lifecycle hooks. */
public ItemStack replaceGag(ItemStack newGag) {
ItemStack current = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.MOUTH);
if (current.isEmpty()) return ItemStack.EMPTY;
return replaceInRegion(BodyRegionV2.MOUTH, newGag);
}
/** Replaces the collar and returns the old one. Issue #14 fix: now calls lifecycle hooks. */
public ItemStack replaceCollar(ItemStack newCollar) {
ItemStack current = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.NECK);
if (current.isEmpty()) return ItemStack.EMPTY;
return replaceInRegion(BodyRegionV2.NECK, newCollar);
}
/**
* Thread Safety: Synchronized to prevent inventory tearing (gap between remove and add).
*/
public synchronized ItemStack replaceBind(ItemStack newBind) {
ItemStack old = takeBindOff();
if (!old.isEmpty()) {
putBindOn(newBind);
}
return old;
}
/**
* Thread Safety: Synchronized to prevent inventory tearing (gap between remove and add).
*/
public synchronized ItemStack replaceBind(
ItemStack newBind,
boolean force
) {
// Safety: Don't remove current bind if newBind is empty
if (newBind.isEmpty()) {
return ItemStack.EMPTY;
}
ItemStack current = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.ARMS);
if (isLocked(current, force)) {
return ItemStack.EMPTY;
}
ItemStack old = takeBindOff();
if (!old.isEmpty()) {
putBindOn(newBind);
}
return old;
}
public ItemStack replaceGag(ItemStack newGag, boolean force) {
ItemStack current = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.MOUTH);
if (isLocked(current, force)) {
return ItemStack.EMPTY;
}
ItemStack old = V2EquipmentHelper.unequipFromRegion(host.getPlayer(), BodyRegionV2.MOUTH);
putGagOn(newGag);
return old;
}
public ItemStack replaceBlindfold(ItemStack newBlindfold, boolean force) {
ItemStack current = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.EYES);
if (isLocked(current, force)) {
return ItemStack.EMPTY;
}
ItemStack old = V2EquipmentHelper.unequipFromRegion(host.getPlayer(), BodyRegionV2.EYES);
putBlindfoldOn(newBlindfold);
return old;
}
public ItemStack replaceCollar(ItemStack newCollar, boolean force) {
ItemStack current = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.NECK);
if (isLocked(current, force)) {
return ItemStack.EMPTY;
}
ItemStack old = takeCollarOff(force);
putCollarOn(newCollar);
return old;
}
public ItemStack replaceEarplugs(ItemStack newEarplugs) {
return replaceEarplugs(newEarplugs, false);
}
public ItemStack replaceEarplugs(ItemStack newEarplugs, boolean force) {
ItemStack current = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.EARS);
if (isLocked(current, force)) {
return ItemStack.EMPTY;
}
ItemStack old = takeEarplugsOff();
putEarplugsOn(newEarplugs);
return old;
}
public ItemStack replaceMittens(ItemStack newMittens) {
return replaceMittens(newMittens, false);
}
public ItemStack replaceMittens(ItemStack newMittens, boolean force) {
ItemStack current = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.HANDS);
if (isLocked(current, force)) {
return ItemStack.EMPTY;
}
ItemStack old = takeMittensOff();
if (!old.isEmpty() || !hasMittens()) {
putMittensOn(newMittens);
}
return old;
}
// ========== Resistance Methods ==========
/**
* Phase 14.1.7: Now part of IRestrainable interface
*/
public synchronized int getCurrentBindResistance() {
Player player = host.getPlayer();
ItemStack stack = V2EquipmentHelper.getInRegion(player, BodyRegionV2.ARMS);
if (
stack.isEmpty() || !(stack.getItem() instanceof ItemBind bind)
) return 0;
return bind.getCurrentResistance(stack, player);
}
/**
* Phase 14.1.7: Now part of IRestrainable interface
*/
public synchronized void setCurrentBindResistance(int resistance) {
Player player = host.getPlayer();
ItemStack stack = V2EquipmentHelper.getInRegion(player, BodyRegionV2.ARMS);
if (
stack.isEmpty() || !(stack.getItem() instanceof ItemBind bind)
) return;
bind.setCurrentResistance(stack, resistance);
}
/**
* Phase 14.1.7: Added for IRestrainable interface
*/
public synchronized int getCurrentCollarResistance() {
Player player = host.getPlayer();
ItemStack stack = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
if (
stack.isEmpty() || !(stack.getItem() instanceof ItemCollar collar)
) return 0;
return collar.getCurrentResistance(stack, player);
}
/**
* Phase 14.1.7: Added for IRestrainable interface
*/
public synchronized void setCurrentCollarResistance(int resistance) {
Player player = host.getPlayer();
ItemStack stack = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
if (
stack.isEmpty() || !(stack.getItem() instanceof ItemCollar collar)
) return;
collar.setCurrentResistance(stack, resistance);
}
// ========== Helper Methods ==========
/** Helper to drop an item at the kidnapped player's feet. */
public void kidnappedDropItem(ItemStack stack) {
if (stack.isEmpty() || host.getPlayer() == null) return;
host.getPlayer().drop(stack, false);
}
/**
* Helper to take a bondage item if it's unlocked, and drop it.
*/
public void takeBondageItemIfUnlocked(
ItemStack item,
Supplier<ItemStack> takeOffMethod
) {
if (isLocked(item, false)) {
return; // Item is locked, cannot remove
}
ItemStack removed = takeOffMethod.get();
if (!removed.isEmpty()) {
kidnappedDropItem(removed);
}
}
/**
* Check if an item is locked.
* @param stack The item to check
* @param force If true, returns false (bypasses lock)
* @return true if locked and not forced
*/
public boolean isLocked(ItemStack stack, boolean force) {
if (force) return false;
if (stack.isEmpty()) return false;
if (stack.getItem() instanceof ILockable lockable) {
return lockable.isLocked(stack);
}
return false;
}
private boolean hasMittens() {
return V2EquipmentHelper.isRegionOccupied(host.getPlayer(), BodyRegionV2.HANDS);
}
// ========== Low-level V2 equipment operations ==========
/**
* Low-level equip: writes to V2 capability, calls IBondageItem lifecycle hooks, syncs.
* Uses setInRegion directly because V1 items do not implement IV2BondageItem.
*/
private void equipInRegion(BodyRegionV2 region, ItemStack stack) {
Player player = host.getPlayer();
if (player == null || player.level().isClientSide) return;
if (stack.isEmpty()) return;
IV2BondageEquipment equip = V2EquipmentHelper.getEquipment(player);
if (equip == null) return;
// Check if already occupied
if (equip.isRegionOccupied(region)) return;
// Check canEquip via V1 IBondageItem interface
if (stack.getItem() instanceof IV2BondageItem bondageItem) {
if (!bondageItem.canEquip(stack, player)) return;
}
equip.setInRegion(region, stack.copy());
// Fire lifecycle hook
if (stack.getItem() instanceof IV2BondageItem bondageItem) {
bondageItem.onEquipped(stack, player);
}
V2EquipmentHelper.sync(player);
}
/**
* Low-level replace: unequips old item with lifecycle hooks, equips new item.
* Unequips old item (with onUnequipped hook), sets new item, fires onEquipped.
*/
private ItemStack replaceInRegion(BodyRegionV2 region, ItemStack newStack) {
Player player = host.getPlayer();
if (player == null || player.level().isClientSide) return ItemStack.EMPTY;
IV2BondageEquipment equip = V2EquipmentHelper.getEquipment(player);
if (equip == null) return ItemStack.EMPTY;
ItemStack oldStack = equip.getInRegion(region);
// Call onUnequipped for the old item
if (!oldStack.isEmpty() && oldStack.getItem() instanceof IV2BondageItem oldItem) {
oldItem.onUnequipped(oldStack, player);
}
// Set the new item
equip.setInRegion(region, newStack.copy());
// Call onEquipped for the new item
if (!newStack.isEmpty() && newStack.getItem() instanceof IV2BondageItem newItem) {
newItem.onEquipped(newStack, player);
}
V2EquipmentHelper.sync(player);
return oldStack;
}
// ========== Callbacks ==========
public void checkGagAfterApply() {
// Gag talk handled via ChatEventHandler + GagTalkManager (event-based)
// No initialization needed here - effects apply automatically on chat
}
public void checkBlindfoldAfterApply() {
// Effects applied client-side via rendering
}
public void checkEarplugsAfterApply() {
// Effects applied client-side via sound system
}
public void checkCollarAfterApply() {
// Collar timers/GPS handled elsewhere
}
public void checkMittensAfterApply() {
// Mittens effects handled elsewhere
}
}

View File

@@ -0,0 +1,155 @@
package com.tiedup.remake.state.components;
import com.tiedup.remake.cells.CampOwnership;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.cells.CellRegistryV2;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.state.hosts.IPlayerBindStateHost;
import java.util.UUID;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
/**
* Component responsible for player lifecycle management.
* Handles connection, death, respawn scenarios.
*
* Single Responsibility: Lifecycle events and registry coordination
* Complexity: HIGH (coordinates 4+ registries on death)
* Risk: HIGH (critical path, must maintain registry cleanup order)
*
* Registry Cleanup Order (on death):
* 1. CellRegistryV2 - remove from cells
* 2. PrisonerManager - release prisoner (via PrisonerService)
*/
public class PlayerLifecycle {
private final IPlayerBindStateHost host;
public PlayerLifecycle(IPlayerBindStateHost host) {
this.host = host;
}
// ========== Lifecycle Methods ==========
/**
* Resets the player instance upon reconnection or respawn.
*
* IMPORTANT: Handle transport restoration for pole binding.
* With proxy-based leash system, leashes are not persisted through disconnection.
* The leash proxy is ephemeral and will be recreated if needed.
*
* Leg Binding: Speed reduction based on hasLegsBound(), not isTiedUp().
*/
public void resetNewConnection(Player player) {
// Update player reference
host.setOnline(true);
// Phase 17: Clear captor reference (enslavement ends on disconnect)
host.setCaptor(null);
// Reset struggle animation state (prevent stuck animations)
host.setStrugglingClient(false);
// Leash proxy doesn't persist through disconnection
// If player was leashed, they are now freed
// H6 fix: V1 speed reduction re-application is no longer needed for players.
// MovementStyleManager (V2 tick-based system) re-resolves the active movement
// style on the first tick after login (clearMovementState() resets activeMovementStyle
// to null, triggering a fresh activation). The V1 RestraintEffectUtils call here would
// cause double stacking with the V2 MULTIPLY_BASE modifier.
}
/**
* Called when the kidnapped player dies.
* Comprehensive cleanup: unlock items, drop items, free captivity, cleanup registries.
*
* @param world The world/level where death occurred
* @return true if death was handled
*/
public boolean onDeathKidnapped(Level world) {
Player player = host.getPlayer();
// Mark player as offline
host.setOnline(false);
// Clean up all registries on death (server-side only)
if (world instanceof ServerLevel serverLevel) {
UUID playerId = player.getUUID();
cleanupRegistries(serverLevel, playerId);
}
TiedUpMod.LOGGER.debug(
"[PlayerLifecycle] {} died while kidnapped",
player.getName().getString()
);
return true;
}
/**
* Coordinates cleanup of all registries when player dies.
* Order matters: Cell → Camp → Ransom → Prisoner
*
* @param serverLevel The server level
* @param playerId The player's UUID
*/
private void cleanupRegistries(ServerLevel serverLevel, UUID playerId) {
String playerName = host.getPlayer().getName().getString();
// 1. Clean up CellRegistryV2 - remove from any cells
CellRegistryV2 cellRegistry = CellRegistryV2.get(serverLevel);
int cellsRemoved = cellRegistry.releasePrisonerFromAllCells(playerId);
if (cellsRemoved > 0) {
TiedUpMod.LOGGER.debug(
"[PlayerLifecycle] Removed {} from {} cells on death",
playerName,
cellsRemoved
);
}
// 2. Clean up prisoner state - release from imprisonment
com.tiedup.remake.prison.PrisonerManager manager =
com.tiedup.remake.prison.PrisonerManager.get(serverLevel);
com.tiedup.remake.prison.PrisonerState state = manager.getState(
playerId
);
// Release if imprisoned or working (player died)
if (
state == com.tiedup.remake.prison.PrisonerState.IMPRISONED ||
state == com.tiedup.remake.prison.PrisonerState.WORKING
) {
// Use centralized escape service for complete cleanup
com.tiedup.remake.prison.service.PrisonerService.get().escape(
serverLevel,
playerId,
"player_death"
);
}
}
// ========== Helper Methods ==========
/**
* Get the current bind ItemStack.
* Epic 5F: Migrated to V2EquipmentHelper.
*/
private ItemStack getCurrentBind() {
return com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.getInRegion(
host.getPlayer(),
com.tiedup.remake.v2.BodyRegionV2.ARMS
);
}
/**
* Check if player has legs bound.
*/
private boolean hasLegsBound(ItemStack bind) {
if (bind.isEmpty()) return false;
if (!(bind.getItem() instanceof ItemBind)) return false;
return ItemBind.hasLegsBound(bind);
}
}

View File

@@ -0,0 +1,54 @@
package com.tiedup.remake.state.components;
import com.tiedup.remake.util.tasks.ItemTask;
/**
* Component responsible for player sale system.
* Phase 14.3.5: Sale system fields
*
* Single Responsibility: Sale state management
* Complexity: LOW (simple state tracking)
* Risk: LOW (isolated system)
*/
public class PlayerSale {
// ========== Sale Fields ==========
private boolean forSale = false;
private ItemTask salePrice = null;
// ========== Sale Methods ==========
/**
* Check if player is currently for sale.
*/
public boolean isForSell() {
return this.forSale && this.salePrice != null;
}
/**
* Get the sale price.
* @return The price, or null if not for sale
*/
public ItemTask getSalePrice() {
return this.salePrice;
}
/**
* Put player up for sale with the given price.
* @param price The sale price task
*/
public void putForSale(ItemTask price) {
if (price == null) return;
this.forSale = true;
this.salePrice = price;
}
/**
* Cancel the sale and clear price.
*/
public void cancelSale() {
this.forSale = false;
this.salePrice = null;
}
}

View File

@@ -0,0 +1,260 @@
package com.tiedup.remake.state.components;
import com.tiedup.remake.core.ModSounds;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.SystemMessageManager.MessageCategory;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.ItemGpsCollar;
import com.tiedup.remake.items.ItemShockCollarAuto;
import com.tiedup.remake.state.hosts.IPlayerBindStateHost;
import com.tiedup.remake.util.GameConstants;
import com.tiedup.remake.util.time.Timer;
import java.util.List;
import java.util.UUID;
import net.minecraft.ChatFormatting;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.Nullable;
/**
* Component responsible for shock collar mechanics and GPS tracking.
* Phase 13: Advanced collar features (shocks, GPS tracking)
*
* Single Responsibility: Collar automation and GPS monitoring
* Complexity: HIGH (synchronized timer, GPS zone checks, network messages)
* Risk: HIGH (timer thread management)
*
* Threading: Uses synchronized blocks for timer access (volatile Timer field)
*/
public class PlayerShockCollar {
private final IPlayerBindStateHost host;
// Phase 13: Collar automation fields
// volatile: accessed from synchronized blocks across multiple threads
private volatile Timer timerAutoShockCollar;
private final Object lockTimerAutoShock = new Object();
public PlayerShockCollar(IPlayerBindStateHost host) {
this.host = host;
}
// ========== Shock Functionality ==========
/**
* Triggers a shock with default damage.
*/
public void shockKidnapped() {
this.shockKidnapped(null, GameConstants.DEFAULT_SHOCK_DAMAGE);
}
/**
* Triggers a visual and auditory shock effect.
* Damage is applied (shock can kill).
*
* @param messageAddon Optional message addon for HUD (e.g., GPS violation)
* @param damage Shock damage amount
*/
public void shockKidnapped(@Nullable String messageAddon, float damage) {
Player player = host.getPlayer();
if (player == null || !player.isAlive()) return;
// Sound effects
player
.level()
.playSound(
null,
player.blockPosition(),
ModSounds.ELECTRIC_SHOCK.get(),
net.minecraft.sounds.SoundSource.PLAYERS,
GameConstants.SHOCK_SOUND_VOLUME,
GameConstants.SHOCK_SOUND_PITCH
);
// Particle effects
if (
player.level() instanceof
net.minecraft.server.level.ServerLevel serverLevel
) {
serverLevel.sendParticles(
net.minecraft.core.particles.ParticleTypes.ELECTRIC_SPARK,
player.getX(),
player.getY() + GameConstants.SHOCK_PARTICLE_Y_OFFSET,
player.getZ(),
GameConstants.SHOCK_PARTICLE_COUNT,
GameConstants.SHOCK_PARTICLE_X_SPREAD,
GameConstants.SHOCK_PARTICLE_Y_SPREAD,
GameConstants.SHOCK_PARTICLE_Z_SPREAD,
GameConstants.SHOCK_PARTICLE_SPEED
);
}
// Damage logic - shock can kill
if (damage > 0) {
player.hurt(player.damageSources().magic(), damage);
}
// HUD Message via SystemMessageManager
if (messageAddon != null) {
// Custom message with addon (e.g., GPS violation)
SystemMessageManager.sendToPlayer(
player,
MessageCategory.SLAVE_SHOCK,
SystemMessageManager.getTemplate(MessageCategory.SLAVE_SHOCK) +
messageAddon
);
} else {
SystemMessageManager.sendToPlayer(
player,
MessageCategory.SLAVE_SHOCK
);
}
}
// ========== Auto-Shock & GPS Monitoring ==========
/**
* Periodic check for Auto-Shock intervals and GPS Safe Zones.
* Called from RestraintTaskTickHandler every player tick.
*
* Thread Safety: Refactored to avoid alien method calls under lock.
* We compute shock decisions inside synchronized block, then execute
* outside the lock to prevent deadlock risk.
*/
public void checkAutoShockCollar() {
Player player = host.getPlayer();
if (player == null || !player.isAlive()) return;
// Flags set inside lock, actions performed outside
boolean shouldShockAuto = false;
boolean shouldShockGPS = false;
ItemGpsCollar gpsCollar = null;
ItemStack gpsStack = null;
synchronized (lockTimerAutoShock) {
ItemStack collarStack = getCurrentCollar();
if (collarStack.isEmpty()) return;
// Auto-Shock Collar handling
if (
collarStack.getItem() instanceof ItemShockCollarAuto collarShock
) {
if (
timerAutoShockCollar != null &&
timerAutoShockCollar.isExpired()
) {
shouldShockAuto = true;
}
if (
timerAutoShockCollar == null ||
timerAutoShockCollar.isExpired()
) {
timerAutoShockCollar = new Timer(
collarShock.getInterval() /
GameConstants.TICKS_PER_SECOND,
player.level()
);
}
}
// GPS Collar handling
else if (collarStack.getItem() instanceof ItemGpsCollar gps) {
if (
gps.isActive(collarStack) &&
(timerAutoShockCollar == null ||
timerAutoShockCollar.isExpired())
) {
List<ItemGpsCollar.SafeSpot> safeSpots = gps.getSafeSpots(
collarStack
);
if (safeSpots != null && !safeSpots.isEmpty()) {
boolean isSafe = false;
for (ItemGpsCollar.SafeSpot spot : safeSpots) {
if (spot.isInside(player)) {
isSafe = true;
break;
}
}
if (!isSafe) {
timerAutoShockCollar = new Timer(
gps.getShockInterval(collarStack) /
GameConstants.TICKS_PER_SECOND,
player.level()
);
shouldShockGPS = true;
gpsCollar = gps;
gpsStack = collarStack.copy();
}
}
}
}
}
// Execute shock actions OUTSIDE the lock (avoid alien method call under lock)
if (shouldShockAuto) {
this.shockKidnapped();
}
if (shouldShockGPS && gpsCollar != null) {
this.shockKidnapped(
" Return back to your allowed area!",
GameConstants.DEFAULT_SHOCK_DAMAGE
);
warnOwnersGPSViolation(gpsCollar, gpsStack);
}
}
/**
* Sends a global alert to masters when a slave violates their GPS zone.
* Private helper method.
*/
private void warnOwnersGPSViolation(ItemGpsCollar gps, ItemStack stack) {
Player player = host.getPlayer();
if (player.getServer() == null) return;
// Format: "ALERT: <player name> is outside the safe zone!"
String alertMessage = String.format(
SystemMessageManager.getTemplate(MessageCategory.GPS_OWNER_ALERT),
player.getName().getString()
);
for (UUID ownerId : gps.getOwners(stack)) {
ServerPlayer owner = player
.getServer()
.getPlayerList()
.getPlayer(ownerId);
if (owner != null) {
SystemMessageManager.sendChatToPlayer(
owner,
alertMessage,
ChatFormatting.RED
);
}
}
}
/**
* Force-stops and clears any active shock timers.
* Threading: Synchronized block protects timer access
*/
public void resetAutoShockTimer() {
synchronized (lockTimerAutoShock) {
this.timerAutoShockCollar = null;
}
}
// ========== Helper Methods ==========
/**
* Get the current collar ItemStack.
* Epic 5F: Migrated to V2EquipmentHelper.
*/
private ItemStack getCurrentCollar() {
return com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.getInRegion(
host.getPlayer(),
com.tiedup.remake.v2.BodyRegionV2.NECK
);
}
}

View File

@@ -0,0 +1,85 @@
package com.tiedup.remake.state.components;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.state.IRestrainableEntity;
import com.tiedup.remake.state.hosts.IPlayerBindStateHost;
import com.tiedup.remake.util.RestraintEffectUtils;
import com.tiedup.remake.v2.BodyRegionV2;
/**
* Component responsible for special player interactions.
* v2.5: Knife cut target for accessory cutting
* Phase 14.1.7: Item transfers between players
*
* Single Responsibility: Special action management
* Complexity: MEDIUM (external dependencies)
* Risk: LOW (well-defined interactions)
*/
public class PlayerSpecialActions {
private final IPlayerBindStateHost host;
// v2.5: Knife cut target region for accessory cutting (migrated to V2)
private BodyRegionV2 knifeCutRegion = null;
public PlayerSpecialActions(IPlayerBindStateHost host) {
this.host = host;
}
// ========== Knife Cut Target ==========
/**
* Set the body region target for knife cutting.
* Used when player selects "Cut" from StruggleChoiceScreen.
*
* @param region The body region to cut (NECK, MOUTH, etc.)
*/
public void setKnifeCutTarget(BodyRegionV2 region) {
this.knifeCutRegion = region;
}
/**
* Get the current knife cut target region.
*
* @return The target region, or null if none
*/
public BodyRegionV2 getKnifeCutTarget() {
return knifeCutRegion;
}
/**
* Clear the knife cut target.
*/
public void clearKnifeCutTarget() {
this.knifeCutRegion = null;
}
// ========== Special Interactions ==========
/**
* Apply chloroform effects to the player.
*
* @param duration Duration in seconds
*/
public void applyChloroform(int duration) {
if (host.getPlayer() == null) return;
RestraintEffectUtils.applyChloroformEffects(host.getPlayer(), duration);
}
/**
* Phase 14.1.7: Transfer bondage item from this player to another.
* Updated to use IRestrainable parameter (was PlayerBindState)
*
* @param taker The entity taking the item
* @param slotIndex The slot index to take from
*/
public void takeBondageItemBy(IRestrainableEntity taker, int slotIndex) {
// TODO Phase 14+: Transfer item from this player to taker
TiedUpMod.LOGGER.debug(
"[PlayerSpecialActions] {} taking bondage item from {} (slot {})",
taker.getKidnappedName(),
host.getPlayer().getName().getString(),
slotIndex
);
}
}

View File

@@ -0,0 +1,148 @@
package com.tiedup.remake.state.components;
import com.tiedup.remake.state.hosts.IPlayerBindStateHost;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
/**
* Component responsible for querying player restraint state.
* Provides read-only checks for equipment status.
*
* Single Responsibility: State queries
* Complexity: LOW (read-only delegation)
* Risk: LOW (no state modification)
*
* Epic 5F: Uses V2EquipmentHelper/BodyRegionV2.
*/
public class PlayerStateQuery {
private final IPlayerBindStateHost host;
public PlayerStateQuery(IPlayerBindStateHost host) {
this.host = host;
}
// ========== State Query Methods ==========
/** Check if player has ropes/ties equipped. */
public boolean isTiedUp() {
return V2EquipmentHelper.isRegionOccupied(host.getPlayer(), BodyRegionV2.ARMS);
}
/** Check if player is currently gagged. */
public boolean isGagged() {
return V2EquipmentHelper.isRegionOccupied(host.getPlayer(), BodyRegionV2.MOUTH);
}
/** Check if player is blindfolded. */
public boolean isBlindfolded() {
return V2EquipmentHelper.isRegionOccupied(host.getPlayer(), BodyRegionV2.EYES);
}
/** Check if player has earplugs. */
public boolean hasEarplugs() {
return V2EquipmentHelper.isRegionOccupied(host.getPlayer(), BodyRegionV2.EARS);
}
public boolean isEarplugged() {
return hasEarplugs();
}
/** Check if player is wearing a collar. */
public boolean hasCollar() {
return V2EquipmentHelper.isRegionOccupied(host.getPlayer(), BodyRegionV2.NECK);
}
/** Returns the current collar ItemStack, or empty if none. */
public ItemStack getCurrentCollar() {
if (!hasCollar()) return ItemStack.EMPTY;
return V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.NECK);
}
public boolean hasClothes() {
return V2EquipmentHelper.isRegionOccupied(host.getPlayer(), BodyRegionV2.TORSO);
}
/** Check if player has mittens equipped. Phase 14.4: Mittens system */
public boolean hasMittens() {
return V2EquipmentHelper.isRegionOccupied(host.getPlayer(), BodyRegionV2.HANDS);
}
/** Check if player can be tied up (not already tied). */
public boolean canBeTiedUp() {
return !isTiedUp();
}
/** Check if player is both tied and gagged. */
public boolean isBoundAndGagged() {
return isTiedUp() && isGagged();
}
/** Check if player has knives in inventory. */
public boolean hasKnives() {
Player player = host.getPlayer();
if (player == null) return false;
// Check main inventory for knife items
for (int i = 0; i < player.getInventory().getContainerSize(); i++) {
ItemStack stack = player.getInventory().getItem(i);
if (
!stack.isEmpty() &&
stack.getItem() instanceof com.tiedup.remake.items.base.IKnife
) {
return true;
}
}
return false;
}
/**
* Check if player has a gagging effect enabled.
* Checks both if gagged AND if the gag item implements the effect.
*/
public boolean hasGaggingEffect() {
if (!isGagged()) return false;
ItemStack gag = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.MOUTH);
if (gag.isEmpty()) return false;
return (
gag.getItem() instanceof
com.tiedup.remake.items.base.IHasGaggingEffect
);
}
/**
* Check if player has a blinding effect enabled.
* Checks both if blindfolded AND if the blindfold item implements the effect.
*/
public boolean hasBlindingEffect() {
if (!isBlindfolded()) return false;
ItemStack blindfold = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.EYES);
if (blindfold.isEmpty()) return false;
return (
blindfold.getItem() instanceof
com.tiedup.remake.items.base.IHasBlindingEffect
);
}
/**
* Check if player can be kidnapped by random events.
* Checks game rules and current state.
*/
public boolean canBeKidnappedByEvents() {
Player player = host.getPlayer();
// Check if kidnapper spawning is enabled
if (player != null && player.level() != null) {
if (
!com.tiedup.remake.core.SettingsAccessor.doKidnappersSpawn(
player.level().getGameRules()
)
) {
return false;
}
}
// Can't be kidnapped if already tied up or captive (grace period protection)
return !isTiedUp() && !host.getKidnapped().isCaptive();
}
}

View File

@@ -0,0 +1,128 @@
package com.tiedup.remake.state.components;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.state.hosts.IPlayerBindStateHost;
import com.tiedup.remake.state.struggle.StruggleBinds;
import com.tiedup.remake.state.struggle.StruggleCollar;
import net.minecraft.world.entity.player.Player;
/**
* Component responsible for struggle mechanics and resistance management.
* Phase 7: Struggle & Resistance Methods
*
* Single Responsibility: Struggle state and resistance tracking
* Complexity: MEDIUM (volatile fields, animation coordination)
* Risk: MEDIUM (thread-safety requirements)
*
* Threading: Uses host's volatile fields for animation state (isStruggling, struggleStartTick)
*
* Note: StruggleBinds and StruggleCollar require PlayerBindState parameter.
* Since PlayerBindState implements IPlayerBindStateHost, we can safely cast.
*/
public class PlayerStruggle {
private final IPlayerBindStateHost host;
private final PlayerBindState state; // Cast reference for struggle system
// Phase 7 & 8: Struggle state tracking
private final StruggleBinds struggleBindState;
private final StruggleCollar struggleCollarState;
public PlayerStruggle(IPlayerBindStateHost host) {
this.host = host;
// Safe cast: PlayerBindState implements IPlayerBindStateHost
this.state = (PlayerBindState) host;
// Initialize sub-states for struggling
this.struggleBindState = new StruggleBinds();
this.struggleCollarState = new StruggleCollar();
}
// ========== Struggle Methods ==========
/**
* Entry point for the Struggle logic (Key R).
* Distributes effort between Binds and Collar.
*
* Thread Safety: Synchronized to prevent lost updates when multiple struggle
* packets arrive simultaneously (e.g., from macro/rapid keypresses).
*/
public synchronized void struggle() {
if (struggleBindState != null) struggleBindState.struggle(state);
if (struggleCollarState != null) struggleCollarState.struggle(state);
}
/**
* Restores resistance to base values when a master tightens the ties.
*
* Thread Safety: Synchronized to prevent race with struggle operations.
*/
public synchronized void tighten(Player tightener) {
if (struggleBindState != null) struggleBindState.tighten(
tightener,
state
);
if (struggleCollarState != null) struggleCollarState.tighten(
tightener,
state
);
}
/**
* Get the StruggleBinds instance for external access (mini-game system).
*/
public StruggleBinds getStruggleBinds() {
return struggleBindState;
}
/**
* Set a cooldown on struggle attempts (used after mini-game exhaustion).
* @param seconds Cooldown duration in seconds
*/
public void setStruggleCooldown(int seconds) {
if (
struggleBindState != null &&
host.getPlayer() != null &&
host.getLevel() != null
) {
struggleBindState.setExternalCooldown(seconds, host.getLevel());
}
}
/**
* v2.5: Check if struggle cooldown is active.
* @return true if cooldown is active (cannot struggle yet)
*/
public boolean isStruggleCooldownActive() {
return (
struggleBindState != null && struggleBindState.isCooldownActive()
);
}
/**
* v2.5: Get remaining struggle cooldown in seconds.
* @return Remaining seconds, or 0 if no cooldown
*/
public int getStruggleCooldownRemaining() {
return struggleBindState != null
? struggleBindState.getRemainingCooldownSeconds()
: 0;
}
// ========== Animation Control ==========
// Note: Animation state (isStruggling, struggleStartTick) is managed by host
// via IPlayerBindStateHost interface (volatile fields for thread-safety)
/**
* Check if struggle animation should stop (duration expired).
* @param currentTick Current game time tick
* @return True if animation has been playing for >= 80 ticks
*/
public boolean shouldStopStruggling(long currentTick) {
if (!host.isStruggling()) return false;
return (
(currentTick - host.getStruggleStartTick()) >=
com.tiedup.remake.util.GameConstants.STRUGGLE_ANIMATION_DURATION_TICKS
);
}
}

View File

@@ -0,0 +1,85 @@
package com.tiedup.remake.state.components;
import com.tiedup.remake.tasks.PlayerStateTask;
import com.tiedup.remake.tasks.TimedInteractTask;
import com.tiedup.remake.tasks.TyingTask;
import com.tiedup.remake.tasks.UntyingTask;
/**
* Component responsible for tracking tying/untying tasks.
* Phase 6: Tying/Untying task tracking (Phase 14.2.6: unified for Players + NPCs)
*
* Single Responsibility: Task state management
* Complexity: LOW (simple getters/setters)
* Risk: LOW (isolated, no external dependencies)
*/
public class PlayerTaskManagement {
// ========== Task Fields ==========
private TyingTask currentTyingTask;
private UntyingTask currentUntyingTask;
private PlayerStateTask clientTyingTask;
private PlayerStateTask clientUntyingTask;
private TimedInteractTask currentFeedingTask;
private PlayerStateTask clientFeedingTask;
private PlayerStateTask restrainedState;
// ========== Task Getters/Setters ==========
public TyingTask getCurrentTyingTask() {
return currentTyingTask;
}
public void setCurrentTyingTask(TyingTask task) {
this.currentTyingTask = task;
}
public UntyingTask getCurrentUntyingTask() {
return currentUntyingTask;
}
public void setCurrentUntyingTask(UntyingTask task) {
this.currentUntyingTask = task;
}
public PlayerStateTask getClientTyingTask() {
return clientTyingTask;
}
public void setClientTyingTask(PlayerStateTask task) {
this.clientTyingTask = task;
}
public PlayerStateTask getClientUntyingTask() {
return clientUntyingTask;
}
public void setClientUntyingTask(PlayerStateTask task) {
this.clientUntyingTask = task;
}
public TimedInteractTask getCurrentFeedingTask() {
return currentFeedingTask;
}
public void setCurrentFeedingTask(TimedInteractTask task) {
this.currentFeedingTask = task;
}
public PlayerStateTask getClientFeedingTask() {
return clientFeedingTask;
}
public void setClientFeedingTask(PlayerStateTask task) {
this.clientFeedingTask = task;
}
public PlayerStateTask getRestrainedState() {
return restrainedState;
}
public void setRestrainedState(PlayerStateTask state) {
this.restrainedState = state;
}
}

View File

@@ -0,0 +1,143 @@
package com.tiedup.remake.state.hosts;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.state.ICaptor;
import com.tiedup.remake.state.PlayerCaptorManager;
import java.util.UUID;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
/**
* Core host interface for PlayerBindState components.
* Provides access to player data and coordination methods.
*
* Thread Safety: Methods accessing volatile fields are thread-safe.
*/
public interface IPlayerBindStateHost {
// ========== Entity Access ==========
/**
* Get the player entity associated with this state.
* @return The player entity
*/
Player getPlayer();
/**
* Get the player's UUID.
* @return The player's unique identifier
*/
UUID getPlayerUUID();
/**
* Get the current level/world the player is in.
* @return The player's current level
*/
Level getLevel();
/**
* Check if this instance is on the client side.
* @return true if client-side, false if server-side
*/
boolean isClientSide();
/**
* Get the IBondageState interface for this player state.
* Used when components need to pass the player state to other systems.
* @return This instance cast to IBondageState
*/
IBondageState getKidnapped();
// ========== Lifecycle ==========
/**
* Check if the player is currently online.
* @return true if player is online
*/
boolean isOnline();
/**
* Set the player's online status.
* @param online true if player is online
*/
void setOnline(boolean online);
// ========== Network Sync (Centralized) ==========
/**
* Sync clothes configuration to all tracking clients.
* Used when clothes are equipped/changed.
*/
void syncClothesConfig();
/**
* Sync enslavement/captivity state to all clients.
* Used when capture/free state changes.
*/
void syncEnslavement();
// ========== System Access ==========
/**
* Get the current captor (master) of this player.
* @return The captor, or null if not captive
*/
ICaptor getCaptor();
/**
* Set the captor for this player.
* @param captor The new captor, or null to clear
*/
void setCaptor(ICaptor captor);
/**
* Get the captor manager (for when player acts as captor).
* @return The captor manager
*/
PlayerCaptorManager getCaptorManager();
// ========== State Queries ==========
/**
* Check if player is tied up (has bind equipment).
* @return true if player has bind
*/
boolean isTiedUp();
/**
* Check if player has a collar equipped.
* @return true if player has collar
*/
boolean hasCollar();
// ========== Struggle State (Volatile Wrapper) ==========
/**
* Set struggle animation state (server-side).
* Thread-safe (volatile field).
* @param struggling True to start animation, false to stop
* @param tick Current game time tick
*/
void setStruggling(boolean struggling, long tick);
/**
* Set struggle animation flag (client-side only).
* Thread-safe (volatile field).
* Does NOT update timer - server manages timer.
* @param struggling True if struggling
*/
void setStrugglingClient(boolean struggling);
/**
* Check if player is currently playing struggle animation.
* Thread-safe (volatile field).
* @return true if struggling
*/
boolean isStruggling();
/**
* Get the tick when struggle animation started.
* Thread-safe (volatile field).
* @return Start tick
*/
long getStruggleStartTick();
}

View File

@@ -0,0 +1,224 @@
package com.tiedup.remake.state.struggle;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.SystemMessageManager.MessageCategory;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
/**
* Phase 21: Struggle implementation for accessory items (gag, blindfold, earplugs, collar).
*
* Accessories have NO base resistance - only locked items can be struggled.
* Lock adds 250 resistance. Struggle success destroys the padlock (lockable=false).
*
* Unlike binds, accessories are NOT automatically removed on struggle success.
* The item remains equipped but unlocked, allowing manual removal.
*/
public class StruggleAccessory extends StruggleState {
/** The V2 body region of the accessory being struggled against. */
private final BodyRegionV2 accessoryRegion;
public StruggleAccessory(BodyRegionV2 accessoryRegion) {
if (accessoryRegion == BodyRegionV2.ARMS) {
throw new IllegalArgumentException(
"Use StruggleBinds for ARMS region, not StruggleAccessory"
);
}
this.accessoryRegion = accessoryRegion;
}
/**
* Get the current resistance for this accessory.
* Accessories have 0 base resistance - only lock resistance exists.
* Resistance is stored in the item's NBT to persist between attempts.
*
* @param state The player's bind state
* @return Current resistance value (0 if not locked, 250 if locked and not yet struggled)
*/
@Override
protected int getResistanceState(PlayerBindState state) {
ItemStack stack = getAccessoryStack(state);
if (stack.isEmpty()) return 0;
if (stack.getItem() instanceof ILockable lockable) {
return lockable.getCurrentLockResistance(stack);
}
return 0;
}
/**
* Set the current resistance for this accessory struggle.
* Stored in the item's NBT to persist between attempts.
*
* @param state The player's bind state
* @param resistance The new resistance value
*/
@Override
protected void setResistanceState(PlayerBindState state, int resistance) {
ItemStack stack = getAccessoryStack(state);
if (stack.isEmpty()) return;
if (stack.getItem() instanceof ILockable lockable) {
lockable.setCurrentLockResistance(stack, resistance);
}
}
/**
* Check if the player can struggle against this accessory.
* Only locked accessories can be struggled against.
*
* @param state The player's bind state
* @return True if the accessory is equipped and locked
*/
@Override
protected boolean canStruggle(PlayerBindState state) {
Player player = state.getPlayer();
if (player == null) {
return false;
}
ItemStack stack = getAccessoryStack(state);
if (stack.isEmpty()) {
return false;
}
// Only locked accessories can be struggled against
if (!(stack.getItem() instanceof ILockable lockable)) {
return false;
}
return lockable.isLocked(stack);
}
/**
* Check if the accessory is locked.
*
* @param state The player's bind state
* @return true if the accessory is locked
*/
@Override
protected boolean isItemLocked(PlayerBindState state) {
ItemStack stack = getAccessoryStack(state);
if (
stack.isEmpty() || !(stack.getItem() instanceof ILockable lockable)
) {
return false;
}
return lockable.isLocked(stack);
}
/**
* Called when the player successfully struggles against the accessory.
* Unlocks the item and DESTROYS the padlock (lockable=false).
* The item remains equipped but can now be removed manually.
*
* @param state The player's bind state
*/
@Override
protected void successAction(PlayerBindState state) {
Player player = state.getPlayer();
if (player == null) return;
ItemStack stack = getAccessoryStack(state);
if (stack.isEmpty()) return;
// Destroy the padlock (unlock + lockable=false)
if (stack.getItem() instanceof ILockable lockable) {
lockable.breakLock(stack);
TiedUpMod.LOGGER.info(
"[STRUGGLE] {} destroyed padlock on {} (region: {})",
player.getName().getString(),
stack.getDisplayName().getString(),
accessoryRegion
);
}
}
@Override
protected MessageCategory getSuccessCategory() {
return MessageCategory.STRUGGLE_SUCCESS; // Used for color (green)
}
@Override
protected MessageCategory getFailCategory() {
return MessageCategory.STRUGGLE_FAIL; // Used for color (gray)
}
/**
* Override to send custom success message per accessory type.
*/
@Override
protected void notifySuccess(Player player, int newResistance) {
String message = switch (accessoryRegion) {
case MOUTH -> "You manage to loosen the gag's lock...";
case EYES -> "The blindfold's lock gives way...";
case EARS -> "You feel the earplug lock breaking...";
case NECK -> "The collar's lock snaps open...";
case TORSO -> "The clothing lock breaks...";
case HANDS -> "The mitten lock comes undone...";
default -> "The lock breaks!";
};
SystemMessageManager.sendToPlayer(
player,
getSuccessCategory(),
message + " (Resistance: " + newResistance + ")"
);
}
/**
* Override to send custom failure message per accessory type.
*/
@Override
protected void notifyFailure(Player player) {
String message = switch (accessoryRegion) {
case MOUTH -> "You struggle against the gag, but the lock holds.";
case EYES -> "The blindfold's lock refuses to budge.";
case EARS -> "The earplug lock stays firmly shut.";
case NECK -> "The collar's lock resists your efforts.";
case TORSO -> "The clothing lock holds tight.";
case HANDS -> "The mitten lock won't give in.";
default -> "The lock holds firm.";
};
SystemMessageManager.sendToPlayer(player, getFailCategory(), message);
}
/**
* Phase 13: Trigger shock collar check when struggling against accessories.
*/
@Override
protected boolean onAttempt(PlayerBindState state) {
Player player = state.getPlayer();
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
if (
!collar.isEmpty() &&
collar.getItem() instanceof
com.tiedup.remake.items.ItemShockCollar shockCollar
) {
return shockCollar.notifyStruggle(player, collar);
}
return true; // No collar, proceed normally
}
/**
* Get the accessory ItemStack from the player.
*/
private ItemStack getAccessoryStack(PlayerBindState state) {
Player player = state.getPlayer();
if (player == null) return ItemStack.EMPTY;
return V2EquipmentHelper.getInRegion(player, accessoryRegion);
}
/**
* Get the V2 body region of the accessory this struggle targets.
*/
public BodyRegionV2 getAccessoryRegion() {
return accessoryRegion;
}
}

View File

@@ -0,0 +1,347 @@
package com.tiedup.remake.state.struggle;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.SystemMessageManager.MessageCategory;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.state.IPlayerLeashAccess;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.core.SettingsAccessor;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
/**
* Phase 7: Struggle implementation for bind restraints (ropes).
*
* Handles struggling against binds:
* - Gets/sets resistance from equipped bind item
* - Drops bondage items when escaping
* - Unties the player when resistance reaches 0
*
* Based on original StruggleBinds from 1.12.2 (lines 17-117)
*/
public class StruggleBinds extends StruggleState {
/**
* Get the current bind resistance from the equipped bind item.
*
* Based on original StruggleBinds.getResistanceState() (lines 23-25)
*
* @param state The player's bind state
* @return Current resistance value
*/
@Override
protected int getResistanceState(PlayerBindState state) {
return state.getCurrentBindResistance();
}
/**
* Set the current bind resistance on the equipped bind item.
*
* Based on original StruggleBinds.setResistanceState() (lines 28-30)
*
* @param state The player's bind state
* @param resistance The new resistance value
*/
@Override
protected void setResistanceState(PlayerBindState state, int resistance) {
state.setCurrentBindResistance(resistance);
}
/**
* Check if the player can struggle against their binds.
* Returns false if:
* - No bind item equipped
* - Bind item has struggle disabled
*
* Phase 20: Locked items can now be struggled, but with x10 resistance.
* The resistance penalty is applied in StruggleState.struggle().
*
* Based on original StruggleBinds.canStruggle() (lines 33-41)
*
* @param state The player's bind state
* @return True if struggling is allowed
*/
@Override
protected boolean canStruggle(PlayerBindState state) {
Player player = state.getPlayer();
if (player == null) {
return false;
}
ItemStack bindStack = V2EquipmentHelper.getInRegion(player, BodyRegionV2.ARMS);
if (
bindStack.isEmpty() ||
!(bindStack.getItem() instanceof ItemBind bind)
) {
return false;
}
// Phase 20: Locked items can now be struggled (with x10 resistance)
// The locked check has been moved to struggle() where decrease is reduced
return bind.canBeStruggledOut(bindStack);
}
/**
* Check if the bind item is locked.
* Used by StruggleState to apply x10 resistance penalty.
*
* @param state The player's bind state
* @return true if the bind is locked
*/
@Override
protected boolean isItemLocked(PlayerBindState state) {
Player player = state.getPlayer();
if (player == null) return false;
ItemStack bindStack = V2EquipmentHelper.getInRegion(player, BodyRegionV2.ARMS);
if (
bindStack.isEmpty() ||
!(bindStack.getItem() instanceof ItemBind bind)
) {
return false;
}
return bind.isLocked(bindStack);
}
/**
* Called when the player successfully escapes from their binds.
* Drops all bondage items and completely unties the player.
*
* Based on original StruggleBinds.successAction() (lines 44-47)
*
* @param state The player's bind state
*/
@Override
protected void successAction(PlayerBindState state) {
dropBondageItems(state);
untieTarget(state);
}
@Override
protected MessageCategory getSuccessCategory() {
return MessageCategory.STRUGGLE_SUCCESS;
}
@Override
protected MessageCategory getFailCategory() {
return MessageCategory.STRUGGLE_FAIL;
}
/**
* Phase 13: Trigger shock collar check even when struggling against binds.
* If shocked, the attempt is missed.
*/
@Override
protected boolean onAttempt(PlayerBindState state) {
Player player = state.getPlayer();
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
if (
!collar.isEmpty() &&
collar.getItem() instanceof
com.tiedup.remake.items.ItemShockCollar shockCollar
) {
return shockCollar.notifyStruggle(player, collar);
}
return true; // No collar, proceed normally
}
/**
* Drop the bind items to the ground.
* Accessories are NOT dropped during bind struggle.
*
* Phase 21: Struggle success destroys the padlock (lockable=false).
*
* Based on original StruggleBinds.dropBondageItems() (lines 50-69)
*
* @param state The player's bind state
*/
private void dropBondageItems(PlayerBindState state) {
Player player = state.getPlayer();
if (player == null || player.level() == null) {
return;
}
// ONLY drop the BIND slot
ItemStack stack = V2EquipmentHelper.getInRegion(player, BodyRegionV2.ARMS);
if (!stack.isEmpty()) {
// Phase 21: Struggle success DESTROYS the padlock
// The bind drops without its padlock (lockable=false)
ItemStack toDrop = stack.copy();
if (
toDrop.getItem() instanceof ILockable lockable &&
lockable.isLockable(toDrop)
) {
lockable.breakLock(toDrop);
TiedUpMod.LOGGER.info(
"[STRUGGLE] Padlock destroyed on {} during struggle",
toDrop.getDisplayName().getString()
);
}
ItemEntity itemEntity = new ItemEntity(
player.level(),
player.getX(),
player.getY(),
player.getZ(),
toDrop
);
player.level().addFreshEntity(itemEntity);
TiedUpMod.LOGGER.debug(
"[STRUGGLE] Dropped {} from {}",
toDrop.getDisplayName().getString(),
player.getName().getString()
);
}
}
/**
* Remove only the bind items from the player (untie).
* Accessories like gags, collars, and blindfolds remain equipped.
*
* Based on original StruggleBinds.untieTarget() (lines 72-78)
*
* @param state The player's bind state
*/
private void untieTarget(PlayerBindState state) {
Player player = state.getPlayer();
if (player == null) {
return;
}
// Phase 17: 1. Remove from captivity/leash
if (state.isCaptive()) {
state.free();
}
// Also detach leash if tied to pole (no captor but still leashed)
else if (
player instanceof IPlayerLeashAccess access &&
access.tiedup$isLeashed()
) {
access.tiedup$detachLeash();
access.tiedup$dropLeash();
}
// 2. ONLY unequip the BIND slot (ropes, etc.)
V2EquipmentHelper.unequipFromRegion(player, BodyRegionV2.ARMS);
TiedUpMod.LOGGER.info(
"[STRUGGLE] {} has struggled out of their binds and leash",
player.getName().getString()
);
}
/**
* Phase 2: External success action for mini-game system.
* Called when player completes struggle mini-game successfully.
*
* @param state The player's bind state
*/
public void successActionExternal(PlayerBindState state) {
successAction(state);
}
/**
* Phase 2: Set external cooldown timer (e.g., from mini-game exhaustion).
*
* @param seconds Cooldown duration in seconds
* @param level The level for timer
*/
public void setExternalCooldown(
int seconds,
net.minecraft.world.level.Level level
) {
this.struggleCooldownTimer = new com.tiedup.remake.util.time.Timer(
seconds,
level
);
}
/**
* v2.5: Check if struggle cooldown is active.
* Used by QTE mini-game to prevent starting a new session during cooldown.
*
* @return true if cooldown is active (cannot struggle yet)
*/
public boolean isCooldownActive() {
return (
struggleCooldownTimer != null && !struggleCooldownTimer.isExpired()
);
}
/**
* v2.5: Get remaining cooldown time in seconds.
*
* @return Remaining seconds, or 0 if no cooldown
*/
public int getRemainingCooldownSeconds() {
if (
struggleCooldownTimer == null || struggleCooldownTimer.isExpired()
) {
return 0;
}
return struggleCooldownTimer.getSecondsRemaining();
}
/**
* Tighten the player's binds (restore resistance to base value).
* Called when another player right-clicks with a paddle/whip.
*
* Based on original StruggleBinds.tighten() (lines 81-101)
*
* @param tightener The player performing the tightening
* @param state The target player's bind state
*/
public void tighten(Player tightener, PlayerBindState state) {
if (state == null || !state.isTiedUp()) {
return;
}
Player target = state.getPlayer();
if (target == null || target.level() == null) {
return;
}
ItemStack bindStack = V2EquipmentHelper.getInRegion(target, BodyRegionV2.ARMS);
if (
bindStack.isEmpty() ||
!(bindStack.getItem() instanceof ItemBind bind)
) {
return;
}
// Get base resistance from config (BUG-003 fix: was using ModGameRules which
// only knew 4 types and returned hardcoded 100 for the other 10)
int baseResistance = SettingsAccessor.getBindResistance(
bind.getItemName()
);
// Set current resistance to base (full restore)
setResistanceState(state, baseResistance);
TiedUpMod.LOGGER.info(
"[STRUGGLE] {} tightened {}'s binds (resistance restored to {})",
tightener.getName().getString(),
target.getName().getString(),
baseResistance
);
// Send message to target: "X tightened your binds!"
SystemMessageManager.sendBindsTightened(tightener, target);
// Send confirmation to tightener
SystemMessageManager.sendToPlayer(
tightener,
"You tightened " + target.getName().getString() + "'s binds!",
net.minecraft.ChatFormatting.YELLOW
);
}
}

View File

@@ -0,0 +1,261 @@
package com.tiedup.remake.state.struggle;
import com.tiedup.remake.core.SystemMessageManager.MessageCategory;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
/**
* Phase 8: Master-Slave Relationships
*
* Struggle mechanics specifically for locked collars.
*
* How it Works:
* - Only locked collars can be struggled against
* - Success: Collar becomes UNLOCKED (not removed)
* - No items are dropped (unlike bind struggle)
* - Uses collar-specific GameRules for probability and resistance
*
* Differences from StruggleBinds:
* - Condition: hasLockedCollar() instead of isTiedUp()
* - Success: unlock collar instead of untie + drop items
* - Resistance source: collar NBT instead of bind NBT
* - GameRules: STRUGGLE_COLLAR, PROBABILITY_STRUGGLE_COLLAR, etc.
*
* Usage:
* - Slave wears locked collar
* - Presses R (struggle key)
* - Eventually unlocks collar (can then remove it)
*
* @see StruggleBinds
* @see StruggleState
*/
public class StruggleCollar extends StruggleState {
/**
* Get the current resistance from the equipped collar.
*
* @param state The PlayerBindState
* @return Current collar resistance, or 0 if no collar
*/
@Override
protected int getResistanceState(PlayerBindState state) {
Player player = state.getPlayer();
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
if (
collar.isEmpty() ||
!(collar.getItem() instanceof ItemCollar collarItem)
) {
return 0;
}
return collarItem.getCurrentResistance(collar, player);
}
/**
* Set the current resistance on the equipped collar.
*
* @param state The PlayerBindState
* @param resistance The new resistance value
*/
@Override
protected void setResistanceState(PlayerBindState state, int resistance) {
Player player = state.getPlayer();
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
if (
collar.isEmpty() ||
!(collar.getItem() instanceof ItemCollar collarItem)
) {
return;
}
collarItem.setCurrentResistance(collar, resistance);
}
/**
* Check if the collar can be struggled against.
*
* Conditions:
* - Must have collar equipped
* - Collar must be LOCKED
* - Collar must allow struggle (canBeStruggledOut)
*
* Note: Can struggle collar even if NOT tied up.
*
* @param state The PlayerBindState
* @return true if can struggle collar
*/
@Override
protected boolean canStruggle(PlayerBindState state) {
Player player = state.getPlayer();
// Phase 13 logic: Can only struggle against collar if NOT tied up
if (state.isTiedUp()) {
return false;
}
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
if (
collar.isEmpty() ||
!(collar.getItem() instanceof ItemCollar collarItem)
) {
TiedUpMod.LOGGER.debug("[StruggleCollar] No collar equipped");
return false;
}
// Check if locked
if (!collarItem.isLocked(collar)) {
TiedUpMod.LOGGER.debug("[StruggleCollar] Collar is not locked");
return false;
}
// Check if struggle is enabled
if (!collarItem.canBeStruggledOut(collar)) {
TiedUpMod.LOGGER.debug(
"[StruggleCollar] Collar struggle is disabled"
);
return false;
}
return true;
}
/**
* Action to perform when struggle is attempted.
* Used for random shock chance on shock collars.
*/
@Override
protected boolean onAttempt(PlayerBindState state) {
Player player = state.getPlayer();
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
if (
!collar.isEmpty() &&
collar.getItem() instanceof
com.tiedup.remake.items.ItemShockCollar shockCollar
) {
return shockCollar.notifyStruggle(player, collar);
}
return true;
}
/**
* Action to perform when struggle succeeds (resistance = 0).
*
* For collars: Unlock the collar.
* The player can then manually remove it.
*
* @param state The PlayerBindState
*/
@Override
protected void successAction(PlayerBindState state) {
Player player = state.getPlayer();
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
if (
collar.isEmpty() ||
!(collar.getItem() instanceof ItemCollar collarItem)
) {
TiedUpMod.LOGGER.warn(
"[StruggleCollar] successAction called but no collar equipped"
);
return;
}
// Unlock the collar
collarItem.setLocked(collar, false);
TiedUpMod.LOGGER.info(
"[StruggleCollar] {} unlocked their collar!",
player.getName().getString()
);
// Note: Collar is NOT removed, just unlocked
// Player can now manually remove it
}
@Override
protected MessageCategory getSuccessCategory() {
return MessageCategory.STRUGGLE_COLLAR_SUCCESS;
}
@Override
protected MessageCategory getFailCategory() {
return MessageCategory.STRUGGLE_COLLAR_FAIL;
}
/**
* Tighten the collar (restore resistance to base value).
* Called when a master tightens the slave's collar.
*
* Conditions:
* - Collar must be locked
* - Tightener must be different from wearer
*
* @param tightener The player tightening the collar
* @param state The PlayerBindState of the collar wearer
*/
public void tighten(Player tightener, PlayerBindState state) {
Player target = state.getPlayer();
if (tightener == null || target == null) {
return;
}
// Can't tighten your own collar
if (tightener.getUUID().equals(target.getUUID())) {
TiedUpMod.LOGGER.debug(
"[StruggleCollar] Cannot tighten own collar"
);
return;
}
ItemStack collar = V2EquipmentHelper.getInRegion(target, BodyRegionV2.NECK);
if (
collar.isEmpty() ||
!(collar.getItem() instanceof ItemCollar collarItem)
) {
TiedUpMod.LOGGER.debug("[StruggleCollar] No collar to tighten");
return;
}
// Check if collar is locked
if (!collarItem.isLocked(collar)) {
TiedUpMod.LOGGER.debug(
"[StruggleCollar] Collar must be locked to tighten"
);
return;
}
// Get base resistance from GameRules
int baseResistance = collarItem.getBaseResistance(target);
int currentResistance = collarItem.getCurrentResistance(collar, target);
// Only tighten if current resistance is lower than base
if (currentResistance >= baseResistance) {
TiedUpMod.LOGGER.debug(
"[StruggleCollar] Collar already at max resistance"
);
return;
}
// Restore to base resistance
collarItem.setCurrentResistance(collar, baseResistance);
TiedUpMod.LOGGER.info(
"[StruggleCollar] {} tightened {}'s collar (resistance {} -> {})",
tightener.getName().getString(),
target.getName().getString(),
currentResistance,
baseResistance
);
}
}

View File

@@ -0,0 +1,231 @@
package com.tiedup.remake.state.struggle;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.SystemMessageManager.MessageCategory;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.core.SettingsAccessor;
import com.tiedup.remake.util.time.Timer;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.GameRules;
/**
* Phase 7: Base class for struggle mechanics.
*
* Handles the logic for players/NPCs struggling against restraints:
* - Cooldown timer between attempts
* - Probability-based success (dice roll)
* - Resistance decrease on success
* - Automatic removal when resistance reaches 0
*
* v2.5: This is now used for NPC struggle only.
* Players use the continuous struggle mini-game (ContinuousStruggleMiniGameState).
* Knife bonuses have been removed - knives now work by active cutting.
*
* Architecture:
* - StruggleState (abstract base)
* ├─ StruggleBinds (struggle against bind restraints)
* └─ StruggleCollar (struggle against collar/ownership)
*/
public abstract class StruggleState {
/**
* Cooldown timer for struggle attempts.
* Reset after each struggle attempt.
* Volatile for thread safety across network/tick threads.
*/
protected volatile Timer struggleCooldownTimer;
/**
* Main struggle logic (dice roll based).
* Used for NPC struggle - players use QTE mini-game instead.
*
* Flow:
* 1. Check if struggle is enabled (GameRule)
* 2. Check if item can be struggled out of
* 3. Check cooldown timer
* 4. Roll dice for success (probability %)
* 5. If success: decrease resistance by random amount
* 6. If resistance <= 0: call successAction()
* 7. Set cooldown for next attempt
*
* @param state The player's bind state
*/
public synchronized void struggle(PlayerBindState state) {
Player player = state.getPlayer();
if (player == null || player.level() == null) {
return;
}
GameRules gameRules = player.level().getGameRules();
// 1. Check if struggle system is enabled
if (!SettingsAccessor.isStruggleEnabled(gameRules)) {
return;
}
// 2. Check if item can be struggled out of (not locked or forced)
if (!canStruggle(state)) {
return;
}
// 3. Check cooldown timer (prevent spamming)
if (
struggleCooldownTimer != null && !struggleCooldownTimer.isExpired()
) {
TiedUpMod.LOGGER.debug(
"[STRUGGLE] Cooldown not expired yet - ignoring struggle attempt"
);
return;
}
// Start struggle animation (after cooldown check passes)
if (!state.isStruggling()) {
state.setStruggling(true, player.level().getGameTime());
com.tiedup.remake.network.sync.SyncManager.syncStruggleState(
player
);
}
// Phase 13: Trigger attempt effects (shock collar check)
if (!onAttempt(state)) {
return; // Interrupted by pain
}
// 4. Roll for success
int probability = SettingsAccessor.getProbabilityStruggle(gameRules);
int roll = player.getRandom().nextInt(100) + 1;
boolean success = roll <= probability;
if (success) {
// Calculate resistance decrease
int currentResistance = getResistanceState(state);
int minDecrease = SettingsAccessor.getStruggleMinDecrease(gameRules);
int maxDecrease = SettingsAccessor.getStruggleMaxDecrease(gameRules);
int decrease =
minDecrease +
player.getRandom().nextInt(maxDecrease - minDecrease + 1);
int newResistance = Math.max(0, currentResistance - decrease);
setResistanceState(state, newResistance);
// Feedback
notifySuccess(player, newResistance);
// Check for escape
if (newResistance <= 0) {
TiedUpMod.LOGGER.debug(
"[STRUGGLE] {} broke free! (resistance reached 0)",
player.getName().getString()
);
successAction(state);
}
} else {
notifyFailure(player);
}
// 5. Set cooldown
int cooldownTicks = SettingsAccessor.getStruggleTimer(gameRules);
struggleCooldownTimer = new Timer(cooldownTicks / 20, player.level());
}
/**
* Get the message category for successful struggle attempts.
*/
protected abstract MessageCategory getSuccessCategory();
/**
* Get the message category for failed struggle attempts.
*/
protected abstract MessageCategory getFailCategory();
/**
* Send success notification to the player.
*
* @param player The player to notify
* @param newResistance The new resistance value after decrease
*/
protected void notifySuccess(Player player, int newResistance) {
SystemMessageManager.sendWithResistance(
player,
getSuccessCategory(),
newResistance
);
}
/**
* Send failure notification to the player.
*
* @param player The player to notify
*/
protected void notifyFailure(Player player) {
SystemMessageManager.sendToPlayer(player, getFailCategory());
}
/**
* Called when the player successfully struggles free (resistance reaches 0).
* Subclasses implement this to handle the escape action.
*
* @param state The player's bind state
*/
protected abstract void successAction(PlayerBindState state);
/**
* Called when a struggle attempt is performed (regardless of success/fail).
* Can be used for side effects like random shocks.
*
* @param state The player's bind state
* @return true to proceed with struggle, false to interrupt
*/
protected boolean onAttempt(PlayerBindState state) {
return true; // Default: proceed
}
/**
* Get the current resistance value from the player's state.
*
* @param state The player's bind state
* @return Current resistance value
*/
protected abstract int getResistanceState(PlayerBindState state);
/**
* Set the current resistance value in the player's state.
*
* @param state The player's bind state
* @param resistance The new resistance value
*/
protected abstract void setResistanceState(
PlayerBindState state,
int resistance
);
/**
* Check if the player can struggle.
* Returns false if the item cannot be struggled out of.
*
* @param state The player's bind state
* @return True if struggling is allowed
*/
protected abstract boolean canStruggle(PlayerBindState state);
/**
* Check if the item being struggled against is locked.
*
* @param state The player's bind state
* @return true if the item is locked
*/
protected boolean isItemLocked(PlayerBindState state) {
return false; // Default: not locked, subclasses override
}
/**
* Check if debug logging is enabled.
*
* @return True if debug logging should be printed
*/
protected boolean isDebugEnabled() {
return true;
}
}