Files
TiedUp-/src/main/java/com/tiedup/remake/v2/bondage/TyingInteractionHelper.java
NotEvil df56ebb6bc fix(D-01/A): adversarial review fixes — 4 logic bugs
1. NECK region explicitly blocked in interactLivingEntity() — prevents
   V2 collars equipping without ownership setup (latent, no JSONs yet)

2. Swap rollback safety: if re-equip fails after swap failure, drop the
   old bind as item entity instead of losing it silently

3. GaggingComponent: cache GagMaterial at construction time — eliminates
   valueOf() log spam on every chat message with misconfigured material

4. Dual-bind prevention: check both V1 isTiedUp() AND V2 region occupied
   in TyingInteractionHelper and PacketSelfBondage to prevent equipping
   V2 bind on top of V1 bind
2026-04-14 16:48:50 +02:00

124 lines
5.1 KiB
Java

package com.tiedup.remake.v2.bondage;
import com.tiedup.remake.core.SettingsAccessor;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.tasks.TyingTask;
import com.tiedup.remake.tasks.V2TyingPlayerTask;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import com.tiedup.remake.v2.bondage.V2EquipResult;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
/**
* Handles the tying interaction flow for V2 data-driven ARMS items.
*
* <p>Extracted from {@code ItemBind.interactLivingEntity()} to support
* the same tying task flow for data-driven items.</p>
*/
public final class TyingInteractionHelper {
private TyingInteractionHelper() {}
/**
* Handle right-click tying of a target entity with a V2 ARMS item.
* Creates/continues a V2TyingPlayerTask.
*/
public static InteractionResult handleTying(
ServerPlayer player,
LivingEntity target,
ItemStack stack,
InteractionHand hand
) {
IBondageState targetState = KidnappedHelper.getKidnappedState(target);
if (targetState == null) return InteractionResult.PASS;
// Kidnapper can't be tied themselves
IBondageState kidnapperState = KidnappedHelper.getKidnappedState(player);
if (kidnapperState != null && kidnapperState.isTiedUp()) {
return InteractionResult.PASS;
}
// Already tied — try to swap (check both V1 state and V2 region to prevent dual-bind)
if (targetState.isTiedUp() || V2EquipmentHelper.isRegionOccupied(target, BodyRegionV2.ARMS)) {
if (stack.isEmpty()) return InteractionResult.PASS;
ItemStack oldBind = V2EquipmentHelper.unequipFromRegion(target, BodyRegionV2.ARMS);
if (!oldBind.isEmpty()) {
V2EquipResult result = V2EquipmentHelper.equipItem(target, stack.copy());
if (result.isSuccess()) {
stack.shrink(1);
target.spawnAtLocation(oldBind);
TiedUpMod.LOGGER.debug("[TyingInteraction] Swapped bind on {}", target.getName().getString());
return InteractionResult.SUCCESS;
} else {
// Equip failed — rollback: re-equip old bind
V2EquipResult rollback = V2EquipmentHelper.equipItem(target, oldBind);
if (!rollback.isSuccess()) {
// Rollback also failed — drop old bind as safety net
target.spawnAtLocation(oldBind);
TiedUpMod.LOGGER.warn("[TyingInteraction] Swap AND rollback failed, dropped old bind for {}", target.getName().getString());
} else {
TiedUpMod.LOGGER.debug("[TyingInteraction] Swap failed, rolled back old bind on {}", target.getName().getString());
}
return InteractionResult.PASS;
}
}
return InteractionResult.PASS;
}
// Distance + line-of-sight (skip for self-tying)
boolean isSelfTying = player.equals(target);
if (!isSelfTying) {
if (player.distanceTo(target) > 4.0 || !player.hasLineOfSight(target)) {
return InteractionResult.PASS;
}
}
// Create/continue tying task
PlayerBindState playerState = PlayerBindState.getInstance(player);
if (playerState == null) return InteractionResult.PASS;
int tyingSeconds = SettingsAccessor.getTyingPlayerTime(player.level().getGameRules());
V2TyingPlayerTask newTask = new V2TyingPlayerTask(
stack.copy(),
stack,
targetState,
target,
tyingSeconds,
player.level(),
player
);
TyingTask currentTask = playerState.getCurrentTyingTask();
if (currentTask == null
|| !(currentTask instanceof V2TyingPlayerTask)
|| !currentTask.isSameTarget(target)
|| currentTask.isOutdated()
|| !ItemStack.matches(currentTask.getBind(), stack)) {
// Start new task (also handles case where existing task is V1 TyingPlayerTask)
playerState.setCurrentTyingTask(newTask);
newTask.start();
} else {
newTask = (V2TyingPlayerTask) currentTask;
}
newTask.update();
if (newTask.isStopped()) {
// Item already consumed by V2TyingPlayerTask.onComplete() — don't shrink again
playerState.setCurrentTyingTask(null);
TiedUpMod.LOGGER.info("[TyingInteraction] {} tied {}", player.getName().getString(), target.getName().getString());
}
return InteractionResult.SUCCESS;
}
}