From b81d3eed95b2eb58a7fa162ec18befeea92df802 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 15:05:48 +0200 Subject: [PATCH 01/11] feat(D-01/A): config-driven components + tooltip hook (A1, A2, A3) - ResistanceComponent: resistanceId delegates to SettingsAccessor at runtime, fallback to hardcoded base for backward compat - GaggingComponent: material field delegates to GagMaterial enum from ModConfig, explicit comprehension/range overrides take priority - IItemComponent: add default appendTooltip() method - ComponentHolder: iterate components for tooltip contribution - 6 components implement appendTooltip (lockable, resistance, gagging, shock, gps, choking) - DataDrivenBondageItem: call holder.appendTooltip() in appendHoverText() --- .../bondage/component/ChokingComponent.java | 12 +++ .../v2/bondage/component/ComponentHolder.java | 10 +++ .../bondage/component/GaggingComponent.java | 79 +++++++++++++++---- .../v2/bondage/component/GpsComponent.java | 17 ++++ .../v2/bondage/component/IItemComponent.java | 7 ++ .../bondage/component/LockableComponent.java | 16 ++++ .../component/ResistanceComponent.java | 53 +++++++++++-- .../v2/bondage/component/ShockComponent.java | 19 +++++ .../datadriven/DataDrivenBondageItem.java | 6 ++ 9 files changed, 196 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/tiedup/remake/v2/bondage/component/ChokingComponent.java b/src/main/java/com/tiedup/remake/v2/bondage/component/ChokingComponent.java index 5860159..5cc4d74 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/component/ChokingComponent.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/ChokingComponent.java @@ -1,6 +1,13 @@ package com.tiedup.remake.v2.bondage.component; import com.google.gson.JsonObject; +import java.util.List; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; /** * Component: choking effect for data-driven items. @@ -43,4 +50,9 @@ public class ChokingComponent implements IItemComponent { public boolean isNonLethalForMaster() { return nonLethalForMaster; } + + @Override + public void appendTooltip(ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag) { + tooltip.add(Component.translatable("item.tiedup.tooltip.choking").withStyle(ChatFormatting.DARK_PURPLE)); + } } diff --git a/src/main/java/com/tiedup/remake/v2/bondage/component/ComponentHolder.java b/src/main/java/com/tiedup/remake/v2/bondage/component/ComponentHolder.java index 6a465ae..d6fcdfb 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/component/ComponentHolder.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/ComponentHolder.java @@ -2,9 +2,13 @@ package com.tiedup.remake.v2.bondage.component; import java.util.Collections; import java.util.EnumMap; +import java.util.List; import java.util.Map; +import net.minecraft.network.chat.Component; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.Level; import org.jetbrains.annotations.Nullable; public final class ComponentHolder { @@ -58,6 +62,12 @@ public final class ComponentHolder { return false; } + public void appendTooltip(ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag) { + for (IItemComponent c : components.values()) { + c.appendTooltip(stack, level, tooltip, flag); + } + } + public boolean isEmpty() { return components.isEmpty(); } diff --git a/src/main/java/com/tiedup/remake/v2/bondage/component/GaggingComponent.java b/src/main/java/com/tiedup/remake/v2/bondage/component/GaggingComponent.java index 0e401a5..e6af614 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/component/GaggingComponent.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/GaggingComponent.java @@ -1,45 +1,94 @@ package com.tiedup.remake.v2.bondage.component; import com.google.gson.JsonObject; +import com.tiedup.remake.core.TiedUpMod; +import com.tiedup.remake.util.GagMaterial; +import java.util.List; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; /** * Component: gagging behavior for data-driven items. * - * JSON config: {@code "gagging": {"comprehension": 0.2, "range": 10.0}} + *

Config-driven: {@code "gagging": {"material": "ball"}} delegates to + * {@link GagMaterial} for comprehension/range from ModConfig at runtime.

+ * + *

Override: {@code "gagging": {"comprehension": 0.15, "range": 8.0}} uses + * explicit values that take priority over the material lookup.

*/ public class GaggingComponent implements IItemComponent { - private final double comprehension; - private final double range; + private final @Nullable String material; + private final double comprehensionOverride; + private final double rangeOverride; - private GaggingComponent(double comprehension, double range) { - this.comprehension = comprehension; - this.range = range; + private GaggingComponent(@Nullable String material, double comprehensionOverride, double rangeOverride) { + this.material = material; + this.comprehensionOverride = comprehensionOverride; + this.rangeOverride = rangeOverride; } public static IItemComponent fromJson(JsonObject config) { - double comprehension = 0.2; - double range = 10.0; + String material = null; + double comprehension = -1; + double range = -1; if (config != null) { + if (config.has("material")) { + material = config.get("material").getAsString(); + } if (config.has("comprehension")) { - comprehension = config.get("comprehension").getAsDouble(); + comprehension = Math.max(0.0, Math.min(1.0, config.get("comprehension").getAsDouble())); } if (config.has("range")) { - range = config.get("range").getAsDouble(); + range = Math.max(0.0, config.get("range").getAsDouble()); } } - comprehension = Math.max(0.0, Math.min(1.0, comprehension)); - range = Math.max(0.0, range); - return new GaggingComponent(comprehension, range); + return new GaggingComponent(material, comprehension, range); } /** How much of the gagged speech is comprehensible (0.0 = nothing, 1.0 = full). */ public double getComprehension() { - return comprehension; + if (comprehensionOverride >= 0) return comprehensionOverride; + GagMaterial gag = getMaterial(); + if (gag != null) return gag.getComprehension(); + return 0.2; } /** Maximum range in blocks where muffled speech can be heard. */ public double getRange() { - return range; + if (rangeOverride >= 0) return rangeOverride; + GagMaterial gag = getMaterial(); + if (gag != null) return gag.getTalkRange(); + return 10.0; + } + + /** The gag material enum, or null if not configured or invalid. */ + public @Nullable GagMaterial getMaterial() { + if (material == null) return null; + try { + return GagMaterial.valueOf(material.toUpperCase()); + } catch (IllegalArgumentException e) { + TiedUpMod.LOGGER.warn("[GaggingComponent] Unknown gag material: {}", material); + return null; + } + } + + /** The raw material string from JSON, or null. */ + public @Nullable String getMaterialName() { + return material; + } + + @Override + public void appendTooltip(ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag) { + if (material != null) { + tooltip.add(Component.translatable("item.tiedup.tooltip.gag_material", material) + .withStyle(ChatFormatting.RED)); + } else { + tooltip.add(Component.translatable("item.tiedup.tooltip.gagging").withStyle(ChatFormatting.RED)); + } } } diff --git a/src/main/java/com/tiedup/remake/v2/bondage/component/GpsComponent.java b/src/main/java/com/tiedup/remake/v2/bondage/component/GpsComponent.java index 272792a..3eaf778 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/component/GpsComponent.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/GpsComponent.java @@ -1,6 +1,13 @@ package com.tiedup.remake.v2.bondage.component; import com.google.gson.JsonObject; +import java.util.List; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; /** * Component: GPS tracking and safe zone for data-driven items. @@ -42,4 +49,14 @@ public class GpsComponent implements IItemComponent { public boolean isPublicTracking() { return publicTracking; } + + @Override + public void appendTooltip(ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag) { + tooltip.add(Component.translatable("item.tiedup.tooltip.gps_tracking") + .withStyle(ChatFormatting.AQUA)); + if (safeZoneRadius > 0) { + tooltip.add(Component.translatable("item.tiedup.tooltip.gps_zone_radius", safeZoneRadius) + .withStyle(ChatFormatting.DARK_AQUA)); + } + } } diff --git a/src/main/java/com/tiedup/remake/v2/bondage/component/IItemComponent.java b/src/main/java/com/tiedup/remake/v2/bondage/component/IItemComponent.java index 4af99c0..f051abc 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/component/IItemComponent.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/IItemComponent.java @@ -1,7 +1,12 @@ package com.tiedup.remake.v2.bondage.component; +import java.util.List; +import net.minecraft.network.chat.Component; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; /** * A reusable behavior module for data-driven bondage items. @@ -16,4 +21,6 @@ public interface IItemComponent { default boolean blocksUnequip(ItemStack stack, LivingEntity entity) { return false; } + + default void appendTooltip(ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag) {} } diff --git a/src/main/java/com/tiedup/remake/v2/bondage/component/LockableComponent.java b/src/main/java/com/tiedup/remake/v2/bondage/component/LockableComponent.java index 3a57c71..ef8ddcd 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/component/LockableComponent.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/LockableComponent.java @@ -1,6 +1,13 @@ package com.tiedup.remake.v2.bondage.component; import com.google.gson.JsonObject; +import java.util.List; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; /** * Component: lockable behavior for data-driven items. @@ -41,4 +48,13 @@ public class LockableComponent implements IItemComponent { public int getLockResistance() { return lockResistance; } + + @Override + public void appendTooltip(ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag) { + tooltip.add(Component.translatable("item.tiedup.tooltip.lockable").withStyle(ChatFormatting.GOLD)); + if (flag.isAdvanced()) { + tooltip.add(Component.translatable("item.tiedup.tooltip.lock_resistance", lockResistance) + .withStyle(ChatFormatting.DARK_GRAY)); + } + } } diff --git a/src/main/java/com/tiedup/remake/v2/bondage/component/ResistanceComponent.java b/src/main/java/com/tiedup/remake/v2/bondage/component/ResistanceComponent.java index d59f838..4288596 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/component/ResistanceComponent.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/ResistanceComponent.java @@ -1,33 +1,70 @@ package com.tiedup.remake.v2.bondage.component; import com.google.gson.JsonObject; +import com.tiedup.remake.core.SettingsAccessor; +import java.util.List; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; /** * Component: struggle resistance for data-driven items. * - * JSON config: {@code "resistance": {"base": 150}} + *

Config-driven: {@code "resistance": {"id": "rope"}} delegates to + * {@link SettingsAccessor#getBindResistance(String)} at runtime.

+ * + *

Legacy/override: {@code "resistance": {"base": 150}} uses a hardcoded value.

*/ public class ResistanceComponent implements IItemComponent { - private final int baseResistance; + private final @Nullable String resistanceId; + private final int fallbackBase; - private ResistanceComponent(int baseResistance) { - this.baseResistance = baseResistance; + private ResistanceComponent(@Nullable String resistanceId, int fallbackBase) { + this.resistanceId = resistanceId; + this.fallbackBase = fallbackBase; } public static IItemComponent fromJson(JsonObject config) { + String id = null; int base = 100; - if (config != null && config.has("base")) { - base = config.get("base").getAsInt(); + if (config != null) { + if (config.has("id")) { + id = config.get("id").getAsString(); + } + if (config.has("base")) { + base = config.get("base").getAsInt(); + } } base = Math.max(0, base); - return new ResistanceComponent(base); + return new ResistanceComponent(id, base); } /** * Get the base resistance for this item. + * If a {@code resistanceId} is configured, delegates to server config at runtime. + * Otherwise returns the hardcoded fallback value. */ public int getBaseResistance() { - return baseResistance; + if (resistanceId != null) { + return SettingsAccessor.getBindResistance(resistanceId); + } + return fallbackBase; + } + + /** The config key used for runtime resistance lookup, or null if hardcoded. */ + public @Nullable String getResistanceId() { + return resistanceId; + } + + @Override + public void appendTooltip(ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag) { + if (flag.isAdvanced()) { + tooltip.add(Component.translatable("item.tiedup.tooltip.resistance", getBaseResistance()) + .withStyle(ChatFormatting.DARK_GRAY)); + } } } diff --git a/src/main/java/com/tiedup/remake/v2/bondage/component/ShockComponent.java b/src/main/java/com/tiedup/remake/v2/bondage/component/ShockComponent.java index 1c6d867..e54b8de 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/component/ShockComponent.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/ShockComponent.java @@ -1,6 +1,13 @@ package com.tiedup.remake.v2.bondage.component; import com.google.gson.JsonObject; +import java.util.List; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; /** * Component: shock collar behavior for data-driven items. @@ -49,4 +56,16 @@ public class ShockComponent implements IItemComponent { public boolean hasAutoShock() { return autoInterval > 0; } + + @Override + public void appendTooltip(ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag) { + if (hasAutoShock()) { + float seconds = autoInterval / 20.0f; + tooltip.add(Component.translatable("item.tiedup.tooltip.shock_auto", String.format("%.1f", seconds)) + .withStyle(ChatFormatting.DARK_RED)); + } else { + tooltip.add(Component.translatable("item.tiedup.tooltip.shock_manual") + .withStyle(ChatFormatting.DARK_RED)); + } + } } diff --git a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java index 56d6738..b37d114 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java @@ -246,6 +246,12 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem { } } + // Component tooltips + ComponentHolder holder = DataDrivenItemRegistry.getComponents(stack); + if (holder != null) { + holder.appendTooltip(stack, level, tooltip, flag); + } + // Lock status + escape difficulty (from AbstractV2BondageItem) super.appendHoverText(stack, level, tooltip, flag); From 751bad418dc162b43a6dfe22094272257748ec36 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 15:23:08 +0200 Subject: [PATCH 02/11] feat(D-01/A): poseType, helpers, OWNERSHIP ComponentType (A4, A5, A6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DataDrivenItemDefinition: add poseType field, parsed from JSON "pose_type" - PoseTypeHelper: resolves PoseType from V2 definition or V1 ItemBind fallback - BindModeHelper: static bind mode NBT utilities (isBindItem, hasArmsBound, hasLegsBound, cycleBindModeId) — works for V1 and V2 items - CollarHelper: complete static utility class for collar operations with dual-path V2/V1 dispatch (ownership, features, shock, GPS, choke, alert) - ComponentType: add OWNERSHIP enum value - OwnershipComponent: stub class (lifecycle hooks added in next commit) --- .../remake/v2/bondage/BindModeHelper.java | 95 +++++ .../remake/v2/bondage/CollarHelper.java | 371 ++++++++++++++++++ .../remake/v2/bondage/PoseTypeHelper.java | 35 ++ .../v2/bondage/component/ComponentType.java | 3 +- .../bondage/component/OwnershipComponent.java | 20 + .../datadriven/DataDrivenItemDefinition.java | 3 + .../datadriven/DataDrivenItemParser.java | 4 + 7 files changed, 530 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/tiedup/remake/v2/bondage/BindModeHelper.java create mode 100644 src/main/java/com/tiedup/remake/v2/bondage/CollarHelper.java create mode 100644 src/main/java/com/tiedup/remake/v2/bondage/PoseTypeHelper.java create mode 100644 src/main/java/com/tiedup/remake/v2/bondage/component/OwnershipComponent.java diff --git a/src/main/java/com/tiedup/remake/v2/bondage/BindModeHelper.java b/src/main/java/com/tiedup/remake/v2/bondage/BindModeHelper.java new file mode 100644 index 0000000..bbfe851 --- /dev/null +++ b/src/main/java/com/tiedup/remake/v2/bondage/BindModeHelper.java @@ -0,0 +1,95 @@ +package com.tiedup.remake.v2.bondage; + +import com.tiedup.remake.items.base.ItemBind; +import com.tiedup.remake.v2.BodyRegionV2; +import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition; +import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry; +import java.util.Map; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.item.ItemStack; + +/** + * Static utilities for bind mode operations on any bondage item stack (V1 or V2). + * + *

Bind mode determines whether a bind restrains arms, legs, or both. + * The mode is stored in the stack's NBT tag {@code "bindMode"}.

+ */ +public final class BindModeHelper { + + private BindModeHelper() {} + + private static final String NBT_BIND_MODE = "bindMode"; + + public static final String MODE_FULL = "full"; + public static final String MODE_ARMS = "arms"; + public static final String MODE_LEGS = "legs"; + + private static final String[] MODE_CYCLE = { MODE_FULL, MODE_ARMS, MODE_LEGS }; + + private static final Map MODE_TRANSLATION_KEYS = Map.of( + MODE_FULL, "tiedup.bindmode.full", + MODE_ARMS, "tiedup.bindmode.arms", + MODE_LEGS, "tiedup.bindmode.legs" + ); + + /** + * Check if the given stack is a bind item (V2 ARMS-region item or V1 ItemBind). + */ + public static boolean isBindItem(ItemStack stack) { + if (stack.isEmpty()) return false; + // V2: check data-driven definition + DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack); + if (def != null) { + return def.occupiedRegions().contains(BodyRegionV2.ARMS); + } + // V1 fallback + return stack.getItem() instanceof ItemBind; + } + + /** + * Get the bind mode ID from the stack's NBT. + * @return "full", "arms", or "legs" (defaults to "full") + */ + public static String getBindModeId(ItemStack stack) { + if (stack.isEmpty()) return MODE_FULL; + CompoundTag tag = stack.getTag(); + if (tag == null || !tag.contains(NBT_BIND_MODE)) return MODE_FULL; + String value = tag.getString(NBT_BIND_MODE); + if (MODE_ARMS.equals(value) || MODE_LEGS.equals(value)) return value; + return MODE_FULL; + } + + /** True if arms are restrained (mode is "arms" or "full"). */ + public static boolean hasArmsBound(ItemStack stack) { + String mode = getBindModeId(stack); + return MODE_ARMS.equals(mode) || MODE_FULL.equals(mode); + } + + /** True if legs are restrained (mode is "legs" or "full"). */ + public static boolean hasLegsBound(ItemStack stack) { + String mode = getBindModeId(stack); + return MODE_LEGS.equals(mode) || MODE_FULL.equals(mode); + } + + /** + * Cycle bind mode: full → arms → legs → full. + * @return the new mode ID + */ + public static String cycleBindModeId(ItemStack stack) { + String current = getBindModeId(stack); + String next = MODE_FULL; + for (int i = 0; i < MODE_CYCLE.length; i++) { + if (MODE_CYCLE[i].equals(current)) { + next = MODE_CYCLE[(i + 1) % MODE_CYCLE.length]; + break; + } + } + stack.getOrCreateTag().putString(NBT_BIND_MODE, next); + return next; + } + + /** Get the translation key for the current bind mode. */ + public static String getBindModeTranslationKey(ItemStack stack) { + return MODE_TRANSLATION_KEYS.getOrDefault(getBindModeId(stack), "tiedup.bindmode.full"); + } +} diff --git a/src/main/java/com/tiedup/remake/v2/bondage/CollarHelper.java b/src/main/java/com/tiedup/remake/v2/bondage/CollarHelper.java new file mode 100644 index 0000000..c85641d --- /dev/null +++ b/src/main/java/com/tiedup/remake/v2/bondage/CollarHelper.java @@ -0,0 +1,371 @@ +package com.tiedup.remake.v2.bondage; + +import com.tiedup.remake.items.ItemChokeCollar; +import com.tiedup.remake.items.ItemGpsCollar; +import com.tiedup.remake.items.ItemShockCollar; +import com.tiedup.remake.items.ItemShockCollarAuto; +import com.tiedup.remake.items.base.ItemCollar; +import com.tiedup.remake.v2.bondage.component.ChokingComponent; +import com.tiedup.remake.v2.bondage.component.ComponentType; +import com.tiedup.remake.v2.bondage.component.GpsComponent; +import com.tiedup.remake.v2.bondage.component.OwnershipComponent; +import com.tiedup.remake.v2.bondage.component.ShockComponent; +import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +/** + * Static utility for collar operations bridging V1 (ItemCollar subclasses) + * and V2 (data-driven items with OwnershipComponent). + */ +public final class CollarHelper { + + private CollarHelper() {} + + // ===== DETECTION ===== + + // True if the stack is any kind of collar (V2 ownership component or V1 ItemCollar) + public static boolean isCollar(ItemStack stack) { + if (stack.isEmpty()) return false; + if (DataDrivenBondageItem.getComponent(stack, ComponentType.OWNERSHIP, OwnershipComponent.class) != null) { + return true; + } + return stack.getItem() instanceof ItemCollar; + } + + // ===== OWNERSHIP (NBT: "owners") ===== + + // Returns all owner UUIDs stored in the collar's "owners" ListTag + public static List getOwners(ItemStack stack) { + return getListUUIDs(stack, "owners"); + } + + // True if the given UUID is in the owners list + public static boolean isOwner(ItemStack stack, UUID uuid) { + return hasUUIDInList(stack, "owners", uuid); + } + + // True if the given player is an owner + public static boolean isOwner(ItemStack stack, Player player) { + return isOwner(stack, player.getUUID()); + } + + // True if the collar has at least one owner + public static boolean hasOwner(ItemStack stack) { + CompoundTag tag = stack.getTag(); + if (tag == null || !tag.contains("owners", Tag.TAG_LIST)) return false; + return !tag.getList("owners", Tag.TAG_COMPOUND).isEmpty(); + } + + // Adds an owner entry {uuid, name} to the "owners" ListTag + public static void addOwner(ItemStack stack, UUID uuid, String name) { + addToList(stack, "owners", uuid, name); + } + + // Convenience: add a player as owner + public static void addOwner(ItemStack stack, Player player) { + addOwner(stack, player.getUUID(), player.getGameProfile().getName()); + } + + // Removes an owner by UUID + public static void removeOwner(ItemStack stack, UUID uuid) { + removeFromList(stack, "owners", uuid); + } + + // ===== BLACKLIST (NBT: "blacklist") ===== + + public static List getBlacklist(ItemStack stack) { + return getListUUIDs(stack, "blacklist"); + } + + public static boolean isBlacklisted(ItemStack stack, UUID uuid) { + return hasUUIDInList(stack, "blacklist", uuid); + } + + public static void addToBlacklist(ItemStack stack, UUID uuid, String name) { + addToList(stack, "blacklist", uuid, name); + } + + public static void removeFromBlacklist(ItemStack stack, UUID uuid) { + removeFromList(stack, "blacklist", uuid); + } + + // ===== WHITELIST (NBT: "whitelist") ===== + + public static List getWhitelist(ItemStack stack) { + return getListUUIDs(stack, "whitelist"); + } + + public static boolean isWhitelisted(ItemStack stack, UUID uuid) { + return hasUUIDInList(stack, "whitelist", uuid); + } + + public static void addToWhitelist(ItemStack stack, UUID uuid, String name) { + addToList(stack, "whitelist", uuid, name); + } + + public static void removeFromWhitelist(ItemStack stack, UUID uuid) { + removeFromList(stack, "whitelist", uuid); + } + + // ===== LIST INTERNALS ===== + + private static List getListUUIDs(ItemStack stack, String listKey) { + List result = new ArrayList<>(); + CompoundTag tag = stack.getTag(); + if (tag == null || !tag.contains(listKey, Tag.TAG_LIST)) return result; + ListTag list = tag.getList(listKey, Tag.TAG_COMPOUND); + for (int i = 0; i < list.size(); i++) { + CompoundTag entry = list.getCompound(i); + if (entry.contains("uuid")) { + try { + result.add(UUID.fromString(entry.getString("uuid"))); + } catch (IllegalArgumentException ignored) { + // Malformed UUID in NBT, skip + } + } + } + return result; + } + + private static boolean hasUUIDInList(ItemStack stack, String listKey, UUID uuid) { + CompoundTag tag = stack.getTag(); + if (tag == null || !tag.contains(listKey, Tag.TAG_LIST)) return false; + String uuidStr = uuid.toString(); + ListTag list = tag.getList(listKey, Tag.TAG_COMPOUND); + for (int i = 0; i < list.size(); i++) { + if (uuidStr.equals(list.getCompound(i).getString("uuid"))) { + return true; + } + } + return false; + } + + private static void addToList(ItemStack stack, String listKey, UUID uuid, String name) { + CompoundTag tag = stack.getOrCreateTag(); + ListTag list = tag.contains(listKey, Tag.TAG_LIST) + ? tag.getList(listKey, Tag.TAG_COMPOUND) + : new ListTag(); + // Prevent duplicates + String uuidStr = uuid.toString(); + for (int i = 0; i < list.size(); i++) { + if (uuidStr.equals(list.getCompound(i).getString("uuid"))) return; + } + CompoundTag entry = new CompoundTag(); + entry.putString("uuid", uuidStr); + entry.putString("name", name); + list.add(entry); + tag.put(listKey, list); + } + + private static void removeFromList(ItemStack stack, String listKey, UUID uuid) { + CompoundTag tag = stack.getTag(); + if (tag == null || !tag.contains(listKey, Tag.TAG_LIST)) return; + String uuidStr = uuid.toString(); + ListTag list = tag.getList(listKey, Tag.TAG_COMPOUND); + list.removeIf(element -> + uuidStr.equals(((CompoundTag) element).getString("uuid")) + ); + } + + // ===== FEATURES ===== + + @Nullable + public static String getNickname(ItemStack stack) { + CompoundTag tag = stack.getTag(); + if (tag == null || !tag.contains("nickname")) return null; + return tag.getString("nickname"); + } + + public static void setNickname(ItemStack stack, String nickname) { + stack.getOrCreateTag().putString("nickname", nickname); + } + + public static boolean hasNickname(ItemStack stack) { + return stack.hasTag() && stack.getTag().contains("nickname"); + } + + @Nullable + public static UUID getCellId(ItemStack stack) { + CompoundTag tag = stack.getTag(); + if (tag == null || !tag.contains("cellId")) return null; + try { + return UUID.fromString(tag.getString("cellId")); + } catch (IllegalArgumentException e) { + return null; + } + } + + public static void setCellId(ItemStack stack, UUID cellId) { + stack.getOrCreateTag().putString("cellId", cellId.toString()); + } + + public static boolean hasCellAssigned(ItemStack stack) { + return getCellId(stack) != null; + } + + public static boolean isKidnappingModeEnabled(ItemStack stack) { + CompoundTag tag = stack.getTag(); + return tag != null && tag.getBoolean("kidnappingMode"); + } + + public static void setKidnappingModeEnabled(ItemStack stack, boolean enabled) { + stack.getOrCreateTag().putBoolean("kidnappingMode", enabled); + } + + // Kidnapping mode is ready when enabled AND a cell is assigned + public static boolean isKidnappingModeReady(ItemStack stack) { + return isKidnappingModeEnabled(stack) && hasCellAssigned(stack); + } + + public static boolean shouldTieToPole(ItemStack stack) { + CompoundTag tag = stack.getTag(); + return tag != null && tag.getBoolean("tieToPole"); + } + + public static void setShouldTieToPole(ItemStack stack, boolean value) { + stack.getOrCreateTag().putBoolean("tieToPole", value); + } + + // Default true when tag is absent or key is missing + public static boolean shouldWarnMasters(ItemStack stack) { + CompoundTag tag = stack.getTag(); + if (tag == null || !tag.contains("warnMasters")) return true; + return tag.getBoolean("warnMasters"); + } + + public static void setShouldWarnMasters(ItemStack stack, boolean value) { + stack.getOrCreateTag().putBoolean("warnMasters", value); + } + + public static boolean isBondageServiceEnabled(ItemStack stack) { + CompoundTag tag = stack.getTag(); + return tag != null && tag.getBoolean("bondageservice"); + } + + public static void setBondageServiceEnabled(ItemStack stack, boolean enabled) { + stack.getOrCreateTag().putBoolean("bondageservice", enabled); + } + + @Nullable + public static String getServiceSentence(ItemStack stack) { + CompoundTag tag = stack.getTag(); + if (tag == null || !tag.contains("servicesentence")) return null; + return tag.getString("servicesentence"); + } + + public static void setServiceSentence(ItemStack stack, String sentence) { + stack.getOrCreateTag().putString("servicesentence", sentence); + } + + // ===== SHOCK ===== + + // True if the collar can shock (V2 ShockComponent or V1 ItemShockCollar) + public static boolean canShock(ItemStack stack) { + if (stack.isEmpty()) return false; + if (DataDrivenBondageItem.getComponent(stack, ComponentType.SHOCK, ShockComponent.class) != null) { + return true; + } + return stack.getItem() instanceof ItemShockCollar; + } + + public static boolean isPublicShock(ItemStack stack) { + CompoundTag tag = stack.getTag(); + return tag != null && tag.getBoolean("public_mode"); + } + + public static void setPublicShock(ItemStack stack, boolean publicMode) { + stack.getOrCreateTag().putBoolean("public_mode", publicMode); + } + + // V2: from ShockComponent auto interval, V1: from ItemShockCollarAuto field, else 0 + public static int getShockInterval(ItemStack stack) { + ShockComponent comp = DataDrivenBondageItem.getComponent( + stack, ComponentType.SHOCK, ShockComponent.class + ); + if (comp != null) return comp.getAutoInterval(); + if (stack.getItem() instanceof ItemShockCollarAuto auto) { + return auto.getInterval(); + } + return 0; + } + + // ===== GPS ===== + + // True if the collar has GPS capabilities + public static boolean hasGPS(ItemStack stack) { + if (stack.isEmpty()) return false; + if (DataDrivenBondageItem.getComponent(stack, ComponentType.GPS, GpsComponent.class) != null) { + return true; + } + return stack.getItem() instanceof ItemGpsCollar; + } + + public static boolean hasPublicTracking(ItemStack stack) { + CompoundTag tag = stack.getTag(); + return tag != null && tag.getBoolean("publicTracking"); + } + + public static void setPublicTracking(ItemStack stack, boolean publicTracking) { + stack.getOrCreateTag().putBoolean("publicTracking", publicTracking); + } + + // GPS active defaults to true when absent + public static boolean isActive(ItemStack stack) { + CompoundTag tag = stack.getTag(); + if (tag == null || !tag.contains("gpsActive")) return true; + return tag.getBoolean("gpsActive"); + } + + public static void setActive(ItemStack stack, boolean active) { + stack.getOrCreateTag().putBoolean("gpsActive", active); + } + + // ===== CHOKE ===== + + // True if the collar is a choke collar + public static boolean isChokeCollar(ItemStack stack) { + if (stack.isEmpty()) return false; + if (DataDrivenBondageItem.getComponent(stack, ComponentType.CHOKING, ChokingComponent.class) != null) { + return true; + } + return stack.getItem() instanceof ItemChokeCollar; + } + + public static boolean isChoking(ItemStack stack) { + CompoundTag tag = stack.getTag(); + return tag != null && tag.getBoolean("choking"); + } + + public static void setChoking(ItemStack stack, boolean choking) { + stack.getOrCreateTag().putBoolean("choking", choking); + } + + public static boolean isPetPlayMode(ItemStack stack) { + CompoundTag tag = stack.getTag(); + return tag != null && tag.getBoolean("petPlayMode"); + } + + public static void setPetPlayMode(ItemStack stack, boolean petPlay) { + stack.getOrCreateTag().putBoolean("petPlayMode", petPlay); + } + + // ===== ALERT SUPPRESSION ===== + + // Executes the action with collar removal alerts suppressed + public static void runWithSuppressedAlert(Runnable action) { + ItemCollar.runWithSuppressedAlert(action); + } + + // True if removal alerts are currently suppressed (ThreadLocal state) + public static boolean isRemovalAlertSuppressed() { + return ItemCollar.isRemovalAlertSuppressed(); + } +} diff --git a/src/main/java/com/tiedup/remake/v2/bondage/PoseTypeHelper.java b/src/main/java/com/tiedup/remake/v2/bondage/PoseTypeHelper.java new file mode 100644 index 0000000..5804147 --- /dev/null +++ b/src/main/java/com/tiedup/remake/v2/bondage/PoseTypeHelper.java @@ -0,0 +1,35 @@ +package com.tiedup.remake.v2.bondage; + +import com.tiedup.remake.items.base.ItemBind; +import com.tiedup.remake.items.base.PoseType; +import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition; +import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry; +import net.minecraft.world.item.ItemStack; + +/** + * Resolves the {@link PoseType} for any bondage item stack (V1 or V2). + * + *

V2 items read from the data-driven definition's {@code pose_type} field. + * V1 items fall back to {@code ItemBind.getPoseType()}.

+ */ +public final class PoseTypeHelper { + + private PoseTypeHelper() {} + + public static PoseType getPoseType(ItemStack stack) { + // V2: read from data-driven definition + DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack); + if (def != null && def.poseType() != null) { + try { + return PoseType.valueOf(def.poseType().toUpperCase()); + } catch (IllegalArgumentException e) { + return PoseType.STANDARD; + } + } + // V1 fallback + if (stack.getItem() instanceof ItemBind bind) { + return bind.getPoseType(); + } + return PoseType.STANDARD; + } +} diff --git a/src/main/java/com/tiedup/remake/v2/bondage/component/ComponentType.java b/src/main/java/com/tiedup/remake/v2/bondage/component/ComponentType.java index 3964db4..198159e 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/component/ComponentType.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/ComponentType.java @@ -15,7 +15,8 @@ public enum ComponentType { SHOCK("shock", ShockComponent::fromJson), GPS("gps", GpsComponent::fromJson), CHOKING("choking", ChokingComponent::fromJson), - ADJUSTABLE("adjustable", AdjustableComponent::fromJson); + ADJUSTABLE("adjustable", AdjustableComponent::fromJson), + OWNERSHIP("ownership", OwnershipComponent::fromJson); private final String jsonKey; private final Function factory; diff --git a/src/main/java/com/tiedup/remake/v2/bondage/component/OwnershipComponent.java b/src/main/java/com/tiedup/remake/v2/bondage/component/OwnershipComponent.java new file mode 100644 index 0000000..b4846cc --- /dev/null +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/OwnershipComponent.java @@ -0,0 +1,20 @@ +package com.tiedup.remake.v2.bondage.component; + +import com.google.gson.JsonObject; + +/** + * Component: collar ownership behavior for data-driven items. + * + *

Marks an item as a collar with ownership capabilities. + * Lifecycle hooks handle CollarRegistry registration/unregistration.

+ * + *

JSON config: {@code "ownership": {}}

+ */ +public class OwnershipComponent implements IItemComponent { + + private OwnershipComponent() {} + + public static IItemComponent fromJson(JsonObject config) { + return new OwnershipComponent(); + } +} diff --git a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemDefinition.java b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemDefinition.java index 8d3e972..fa5d9e4 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemDefinition.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemDefinition.java @@ -46,6 +46,9 @@ public record DataDrivenItemDefinition( /** Body regions this item blocks. Defaults to occupiedRegions if not specified. */ Set blockedRegions, + /** Optional pose type identifier (e.g., "STANDARD", "STRAITJACKET", "DOG"). */ + @Nullable String poseType, + /** Pose priority for conflict resolution. Higher wins. */ int posePriority, diff --git a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemParser.java b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemParser.java index 221b2ae..9c4dd1c 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemParser.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemParser.java @@ -188,6 +188,9 @@ public final class DataDrivenItemParser { blockedRegions = occupiedRegions; } + // Optional: pose_type (e.g., "STANDARD", "STRAITJACKET", "DOG") + String poseType = getStringOrNull(root, "pose_type"); + // Optional: pose_priority (default 0) int posePriority = getIntOrDefault(root, "pose_priority", 0); @@ -328,6 +331,7 @@ public final class DataDrivenItemParser { animationSource, occupiedRegions, blockedRegions, + poseType, posePriority, escapeDifficulty, lockable, From b79225d6847316611e7cc2c3e546b8947b89c981 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 15:29:31 +0200 Subject: [PATCH 03/11] feat(D-01/A): OwnershipComponent lifecycle hooks (A7) - onEquipped: register collar owners in CollarRegistry (server-side only) - onUnequipped: alert kidnappers + unregister from CollarRegistry - Guards: client-side check, ServerLevel cast, empty owners skip, try-catch - appendTooltip: nickname, owner count, shock/GPS/choke capabilities - Delegates alert suppression to ItemCollar.isRemovalAlertSuppressed() --- .../bondage/component/OwnershipComponent.java | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/src/main/java/com/tiedup/remake/v2/bondage/component/OwnershipComponent.java b/src/main/java/com/tiedup/remake/v2/bondage/component/OwnershipComponent.java index b4846cc..2d6e632 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/component/OwnershipComponent.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/OwnershipComponent.java @@ -1,6 +1,21 @@ package com.tiedup.remake.v2.bondage.component; import com.google.gson.JsonObject; +import com.tiedup.remake.core.TiedUpMod; +import com.tiedup.remake.items.base.ItemCollar; +import com.tiedup.remake.state.CollarRegistry; +import com.tiedup.remake.v2.bondage.CollarHelper; +import java.util.HashSet; +import java.util.List; +import java.util.UUID; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; /** * Component: collar ownership behavior for data-driven items. @@ -17,4 +32,88 @@ public class OwnershipComponent implements IItemComponent { public static IItemComponent fromJson(JsonObject config) { return new OwnershipComponent(); } + + @Override + public void onEquipped(ItemStack stack, LivingEntity entity) { + if (entity.level().isClientSide()) return; + if (!(entity.level() instanceof ServerLevel serverLevel)) return; + + List owners = CollarHelper.getOwners(stack); + if (owners.isEmpty()) { + TiedUpMod.LOGGER.debug( + "[OwnershipComponent] Collar equipped on {} with no owners in NBT — skipping registry", + entity.getName().getString() + ); + return; + } + + try { + CollarRegistry registry = CollarRegistry.get(serverLevel); + registry.registerCollar(entity.getUUID(), new HashSet<>(owners)); + TiedUpMod.LOGGER.debug( + "[OwnershipComponent] Registered collar for {} with {} owner(s)", + entity.getName().getString(), owners.size() + ); + } catch (Exception e) { + TiedUpMod.LOGGER.warn( + "[OwnershipComponent] Failed to register collar for {}: {}", + entity.getName().getString(), e.getMessage() + ); + } + } + + @Override + public void onUnequipped(ItemStack stack, LivingEntity entity) { + if (entity.level().isClientSide()) return; + if (!(entity.level() instanceof ServerLevel serverLevel)) return; + + // Alert kidnappers if removal wasn't suppressed + if (!CollarHelper.isRemovalAlertSuppressed()) { + ItemCollar.onCollarRemoved(entity, true); + } + + try { + CollarRegistry registry = CollarRegistry.get(serverLevel); + registry.unregisterWearer(entity.getUUID()); + TiedUpMod.LOGGER.debug( + "[OwnershipComponent] Unregistered collar for {}", + entity.getName().getString() + ); + } catch (Exception e) { + TiedUpMod.LOGGER.warn( + "[OwnershipComponent] Failed to unregister collar for {}: {}", + entity.getName().getString(), e.getMessage() + ); + } + } + + @Override + public void appendTooltip(ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag) { + String nickname = CollarHelper.getNickname(stack); + if (nickname != null && !nickname.isEmpty()) { + tooltip.add(Component.translatable("item.tiedup.tooltip.nickname", nickname) + .withStyle(ChatFormatting.LIGHT_PURPLE)); + } + + List owners = CollarHelper.getOwners(stack); + if (!owners.isEmpty()) { + tooltip.add(Component.translatable("item.tiedup.tooltip.owners", owners.size()) + .withStyle(ChatFormatting.GOLD)); + } + + if (CollarHelper.canShock(stack)) { + tooltip.add(Component.translatable("item.tiedup.tooltip.shock_capable") + .withStyle(ChatFormatting.DARK_RED)); + } + + if (CollarHelper.hasGPS(stack)) { + tooltip.add(Component.translatable("item.tiedup.tooltip.gps_capable") + .withStyle(ChatFormatting.AQUA)); + } + + if (CollarHelper.isChokeCollar(stack)) { + tooltip.add(Component.translatable("item.tiedup.tooltip.choke_capable") + .withStyle(ChatFormatting.DARK_PURPLE)); + } + } } From 737a4fd59bc233f9595f6774a96ed934493b2fb3 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 15:35:31 +0200 Subject: [PATCH 04/11] feat(D-01/A): interaction routing + TyingInteractionHelper (A8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DataDrivenBondageItem.use(): shift+click cycles bind mode for ARMS items - DataDrivenBondageItem.interactLivingEntity(): region-based routing - ARMS → TyingInteractionHelper (tying task with progress bar) - NECK → deferred to Branch C (no V2 collar JSONs yet) - Other regions → instant equip via parent AbstractV2BondageItem - TyingInteractionHelper: extracted tying flow using V2TyingPlayerTask - Distance/LoS validation, swap if already tied, task lifecycle --- .../v2/bondage/TyingInteractionHelper.java | 107 ++++++++++++++++++ .../datadriven/DataDrivenBondageItem.java | 58 ++++++++++ 2 files changed, 165 insertions(+) create mode 100644 src/main/java/com/tiedup/remake/v2/bondage/TyingInteractionHelper.java diff --git a/src/main/java/com/tiedup/remake/v2/bondage/TyingInteractionHelper.java b/src/main/java/com/tiedup/remake/v2/bondage/TyingInteractionHelper.java new file mode 100644 index 0000000..02e9939 --- /dev/null +++ b/src/main/java/com/tiedup/remake/v2/bondage/TyingInteractionHelper.java @@ -0,0 +1,107 @@ +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 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. + * + *

Extracted from {@code ItemBind.interactLivingEntity()} to support + * the same tying task flow for data-driven items.

+ */ +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 + if (targetState.isTiedUp()) { + if (stack.isEmpty()) return InteractionResult.PASS; + ItemStack oldBind = V2EquipmentHelper.unequipFromRegion(target, BodyRegionV2.ARMS); + if (!oldBind.isEmpty()) { + V2EquipmentHelper.equipItem(target, stack.copy()); + stack.shrink(1); + target.spawnAtLocation(oldBind); + TiedUpMod.LOGGER.debug("[TyingInteraction] Swapped bind on {}", target.getName().getString()); + return InteractionResult.SUCCESS; + } + 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.isSameTarget(target) + || currentTask.isOutdated() + || !ItemStack.matches(currentTask.getBind(), stack)) { + playerState.setCurrentTyingTask(newTask); + newTask.start(); + } else { + newTask = (V2TyingPlayerTask) currentTask; + } + + newTask.update(); + + if (newTask.isStopped()) { + stack.shrink(1); + playerState.setCurrentTyingTask(null); + TiedUpMod.LOGGER.info("[TyingInteraction] {} tied {}", player.getName().getString(), target.getName().getString()); + } + + return InteractionResult.SUCCESS; + } +} diff --git a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java index b37d114..7dd6269 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java @@ -1,7 +1,10 @@ package com.tiedup.remake.v2.bondage.datadriven; +import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.v2.BodyRegionV2; +import com.tiedup.remake.v2.bondage.BindModeHelper; import com.tiedup.remake.v2.bondage.IV2BondageEquipment; +import com.tiedup.remake.v2.bondage.TyingInteractionHelper; import com.tiedup.remake.v2.bondage.V2BondageItems; import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper; import com.tiedup.remake.v2.bondage.component.ComponentHolder; @@ -17,7 +20,13 @@ import java.util.stream.Collectors; import net.minecraft.ChatFormatting; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.InteractionResultHolder; import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.TooltipFlag; import net.minecraft.world.level.Level; @@ -116,6 +125,55 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem { return def != null && def.supportsColor(); } + // ===== INTERACTION ROUTING ===== + + @Override + public InteractionResultHolder use(Level level, Player player, InteractionHand hand) { + ItemStack stack = player.getItemInHand(hand); + Set regions = getOccupiedRegions(stack); + + // ARMS items: shift+click cycles bind mode + if (regions.contains(BodyRegionV2.ARMS) && player.isShiftKeyDown() && !level.isClientSide) { + BindModeHelper.cycleBindModeId(stack); + player.playSound(SoundEvents.CHAIN_STEP, 0.5f, 1.2f); + player.displayClientMessage( + Component.translatable( + "tiedup.message.bindmode_changed", + Component.translatable(BindModeHelper.getBindModeTranslationKey(stack)) + ), + true + ); + return InteractionResultHolder.success(stack); + } + return super.use(level, player, hand); + } + + @Override + public InteractionResult interactLivingEntity( + ItemStack stack, Player player, LivingEntity target, InteractionHand hand + ) { + // Client: arm swing + if (player.level().isClientSide) { + return InteractionResult.SUCCESS; + } + + DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack); + if (def == null) return InteractionResult.PASS; + + Set regions = def.occupiedRegions(); + + // ARMS: tying flow (do NOT call super — avoids double equip) + if (regions.contains(BodyRegionV2.ARMS) && player instanceof ServerPlayer serverPlayer) { + return TyingInteractionHelper.handleTying(serverPlayer, target, stack, hand); + } + + // NECK: collar equip blocked for now — V2 collar JSONs don't exist in Branch A. + // Full collar equip flow (add owner, register, sound) wired in Branch C. + + // All other regions (MOUTH, EYES, EARS, HANDS): instant equip via parent + return super.interactLivingEntity(stack, player, target, hand); + } + // ===== IHasResistance IMPLEMENTATION ===== @Override From c240f48b297c5879fa937dbd805a7c445727b0fe Mon Sep 17 00:00:00 2001 From: Adrien Date: Tue, 14 Apr 2026 15:47:20 +0200 Subject: [PATCH 05/11] feat(D-01/A): V2-aware struggle system (A9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit StruggleBinds: - canStruggle(): instanceof ItemBind → BindModeHelper.isBindItem() - isItemLocked(): instanceof ItemBind → instanceof ILockable (fixes R4) - onAttempt(): instanceof ItemShockCollar → CollarHelper.canShock() (fixes R5) - tighten(): reads ResistanceComponent directly for V2, avoids MAX scan bug StruggleCollar: - getResistanceState/setResistanceState: instanceof ItemCollar → IHasResistance - canStruggle(): instanceof ItemCollar → CollarHelper.isCollar() + ILockable - onAttempt(): shock check via CollarHelper.canShock() - successAction(): unlock via ILockable - tighten(): resistance via IHasResistance All V1 items continue working through the same interfaces they already implement. --- .../remake/state/struggle/StruggleBinds.java | 69 ++++---- .../remake/state/struggle/StruggleCollar.java | 147 ++++++++---------- 2 files changed, 103 insertions(+), 113 deletions(-) diff --git a/src/main/java/com/tiedup/remake/state/struggle/StruggleBinds.java b/src/main/java/com/tiedup/remake/state/struggle/StruggleBinds.java index f32c357..e1bddf6 100644 --- a/src/main/java/com/tiedup/remake/state/struggle/StruggleBinds.java +++ b/src/main/java/com/tiedup/remake/state/struggle/StruggleBinds.java @@ -4,12 +4,18 @@ import com.tiedup.remake.core.SettingsAccessor; 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.IHasResistance; 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.v2.BodyRegionV2; +import com.tiedup.remake.v2.bondage.BindModeHelper; +import com.tiedup.remake.v2.bondage.CollarHelper; import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper; +import com.tiedup.remake.v2.bondage.component.ComponentType; +import com.tiedup.remake.v2.bondage.component.ResistanceComponent; +import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem; import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; @@ -75,16 +81,17 @@ public class StruggleBinds extends StruggleState { player, BodyRegionV2.ARMS ); - if ( - bindStack.isEmpty() || - !(bindStack.getItem() instanceof ItemBind bind) - ) { + if (bindStack.isEmpty() || !BindModeHelper.isBindItem(bindStack)) { return false; } // The locked check has been moved to struggle() where decrease is reduced - return bind.canBeStruggledOut(bindStack); + // Check canBeStruggledOut — works for both V1 and V2 via IHasResistance + if (bindStack.getItem() instanceof IHasResistance resistance) { + return resistance.canBeStruggledOut(bindStack); + } + return true; } /** @@ -103,14 +110,13 @@ public class StruggleBinds extends StruggleState { player, BodyRegionV2.ARMS ); - if ( - bindStack.isEmpty() || - !(bindStack.getItem() instanceof ItemBind bind) - ) { - return false; - } + if (bindStack.isEmpty()) return false; - return bind.isLocked(bindStack); + // Works for both V1 (ItemBind) and V2 (DataDrivenBondageItem) via ILockable + if (bindStack.getItem() instanceof ILockable lockable) { + return lockable.isLocked(bindStack); + } + return false; } /** @@ -148,14 +154,18 @@ public class StruggleBinds extends StruggleState { BodyRegionV2.NECK ); - if ( - !collar.isEmpty() && - collar.getItem() instanceof - com.tiedup.remake.items.ItemShockCollar shockCollar - ) { - return shockCollar.notifyStruggle(player, collar); + if (!collar.isEmpty() && CollarHelper.canShock(collar)) { + // V1 shock collar + if (collar.getItem() instanceof com.tiedup.remake.items.ItemShockCollar shockCollar) { + return shockCollar.notifyStruggle(player, collar); + } + // V2 shock collar — notify via IHasResistance if available + if (collar.getItem() instanceof IHasResistance resistance) { + resistance.notifyStruggle(player); + } + return true; } - return true; // No collar, proceed normally + return true; // No shock collar, proceed normally } /** @@ -317,18 +327,23 @@ public class StruggleBinds extends StruggleState { target, BodyRegionV2.ARMS ); - if ( - bindStack.isEmpty() || - !(bindStack.getItem() instanceof ItemBind bind) - ) { + if (bindStack.isEmpty() || !BindModeHelper.isBindItem(bindStack)) { 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() + // Get base resistance: V2 reads from ResistanceComponent directly, + // V1 reads from SettingsAccessor via item name (BUG-003 fix) + int baseResistance; + ResistanceComponent comp = DataDrivenBondageItem.getComponent( + bindStack, ComponentType.RESISTANCE, ResistanceComponent.class ); + if (comp != null) { + baseResistance = comp.getBaseResistance(); + } else if (bindStack.getItem() instanceof ItemBind bind) { + baseResistance = SettingsAccessor.getBindResistance(bind.getItemName()); + } else { + baseResistance = 100; + } // Set current resistance to base (full restore) setResistanceState(state, baseResistance); diff --git a/src/main/java/com/tiedup/remake/state/struggle/StruggleCollar.java b/src/main/java/com/tiedup/remake/state/struggle/StruggleCollar.java index ec46cad..3432141 100644 --- a/src/main/java/com/tiedup/remake/state/struggle/StruggleCollar.java +++ b/src/main/java/com/tiedup/remake/state/struggle/StruggleCollar.java @@ -2,9 +2,12 @@ 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.IHasResistance; +import com.tiedup.remake.items.base.ILockable; 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.CollarHelper; import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; @@ -44,19 +47,14 @@ public class StruggleCollar extends StruggleState { @Override protected int getResistanceState(PlayerBindState state) { Player player = state.getPlayer(); - ItemStack collar = V2EquipmentHelper.getInRegion( - player, - BodyRegionV2.NECK - ); + ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK); - if ( - collar.isEmpty() || - !(collar.getItem() instanceof ItemCollar collarItem) - ) { - return 0; + if (collar.isEmpty() || !CollarHelper.isCollar(collar)) return 0; + + if (collar.getItem() instanceof IHasResistance resistance) { + return resistance.getCurrentResistance(collar, player); } - - return collarItem.getCurrentResistance(collar, player); + return 0; } /** @@ -68,19 +66,13 @@ public class StruggleCollar extends StruggleState { @Override protected void setResistanceState(PlayerBindState state, int resistance) { Player player = state.getPlayer(); - ItemStack collar = V2EquipmentHelper.getInRegion( - player, - BodyRegionV2.NECK - ); + ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK); - if ( - collar.isEmpty() || - !(collar.getItem() instanceof ItemCollar collarItem) - ) { - return; + if (collar.isEmpty() || !CollarHelper.isCollar(collar)) return; + + if (collar.getItem() instanceof IHasResistance resistanceItem) { + resistanceItem.setCurrentResistance(collar, resistance); } - - collarItem.setCurrentResistance(collar, resistance); } /** @@ -104,31 +96,29 @@ public class StruggleCollar extends StruggleState { return false; } - ItemStack collar = V2EquipmentHelper.getInRegion( - player, - BodyRegionV2.NECK - ); + ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK); - if ( - collar.isEmpty() || - !(collar.getItem() instanceof ItemCollar collarItem) - ) { + if (collar.isEmpty() || !CollarHelper.isCollar(collar)) { TiedUpMod.LOGGER.debug("[StruggleCollar] No collar equipped"); return false; } - // Check if locked - if (!collarItem.isLocked(collar)) { - TiedUpMod.LOGGER.debug("[StruggleCollar] Collar is not locked"); + // Check if locked (works for V1 and V2 via ILockable) + if (collar.getItem() instanceof ILockable lockable) { + if (!lockable.isLocked(collar)) { + TiedUpMod.LOGGER.debug("[StruggleCollar] Collar is not locked"); + return false; + } + } else { return false; } - // Check if struggle is enabled - if (!collarItem.canBeStruggledOut(collar)) { - TiedUpMod.LOGGER.debug( - "[StruggleCollar] Collar struggle is disabled" - ); - return false; + // Check if struggle is enabled (works for V1 and V2 via IHasResistance) + if (collar.getItem() instanceof IHasResistance resistance) { + if (!resistance.canBeStruggledOut(collar)) { + TiedUpMod.LOGGER.debug("[StruggleCollar] Collar struggle is disabled"); + return false; + } } return true; @@ -141,17 +131,17 @@ public class StruggleCollar extends StruggleState { @Override protected boolean onAttempt(PlayerBindState state) { Player player = state.getPlayer(); - ItemStack collar = V2EquipmentHelper.getInRegion( - player, - BodyRegionV2.NECK - ); + ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK); - if ( - !collar.isEmpty() && - collar.getItem() instanceof - com.tiedup.remake.items.ItemShockCollar shockCollar - ) { - return shockCollar.notifyStruggle(player, collar); + if (!collar.isEmpty() && CollarHelper.canShock(collar)) { + // V1 shock collar + if (collar.getItem() instanceof com.tiedup.remake.items.ItemShockCollar shockCollar) { + return shockCollar.notifyStruggle(player, collar); + } + // V2 shock collar — notify via IHasResistance + if (collar.getItem() instanceof IHasResistance resistance) { + resistance.notifyStruggle(player); + } } return true; } @@ -167,31 +157,19 @@ public class StruggleCollar extends StruggleState { @Override protected void successAction(PlayerBindState state) { Player player = state.getPlayer(); - ItemStack collar = V2EquipmentHelper.getInRegion( - player, - BodyRegionV2.NECK - ); + 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" - ); + if (collar.isEmpty() || !CollarHelper.isCollar(collar)) { + TiedUpMod.LOGGER.warn("[StruggleCollar] successAction called but no collar equipped"); return; } - // Unlock the collar - collarItem.setLocked(collar, false); + // Unlock the collar (works for V1 and V2 via ILockable) + if (collar.getItem() instanceof ILockable lockable) { + lockable.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 + TiedUpMod.LOGGER.info("[StruggleCollar] {} unlocked their collar!", player.getName().getString()); } @Override @@ -230,30 +208,27 @@ public class StruggleCollar extends StruggleState { return; } - ItemStack collar = V2EquipmentHelper.getInRegion( - target, - BodyRegionV2.NECK - ); + ItemStack collar = V2EquipmentHelper.getInRegion(target, BodyRegionV2.NECK); - if ( - collar.isEmpty() || - !(collar.getItem() instanceof ItemCollar collarItem) - ) { + if (collar.isEmpty() || !CollarHelper.isCollar(collar)) { 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" - ); + // Check if collar is locked (V1 and V2 via ILockable) + if (collar.getItem() instanceof ILockable lockable) { + if (!lockable.isLocked(collar)) { + TiedUpMod.LOGGER.debug("[StruggleCollar] Collar must be locked to tighten"); + return; + } + } else { return; } - // Get base resistance from GameRules - int baseResistance = collarItem.getBaseResistance(target); - int currentResistance = collarItem.getCurrentResistance(collar, target); + // Get resistance (V1 and V2 via IHasResistance) + if (!(collar.getItem() instanceof IHasResistance resistanceItem)) return; + int baseResistance = resistanceItem.getBaseResistance(target); + int currentResistance = resistanceItem.getCurrentResistance(collar, target); // Only tighten if current resistance is lower than base if (currentResistance >= baseResistance) { @@ -264,7 +239,7 @@ public class StruggleCollar extends StruggleState { } // Restore to base resistance - collarItem.setCurrentResistance(collar, baseResistance); + resistanceItem.setCurrentResistance(collar, baseResistance); TiedUpMod.LOGGER.info( "[StruggleCollar] {} tightened {}'s collar (resistance {} -> {})", From 21c1fc3b572501fe2d0f9dfba76f87090f920354 Mon Sep 17 00:00:00 2001 From: Adrien Date: Tue, 14 Apr 2026 16:06:01 +0200 Subject: [PATCH 06/11] feat(D-01/A): self-bondage region routing (A11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - handleV2SelfBondage: split into region-based routing - NECK → blocked (cannot self-collar) - ARMS → handleV2SelfBind (tying task with progress bar) - Other → handleV2SelfAccessory (instant equip) - handleV2SelfAccessory: arms-bound check via BindModeHelper, locked check for swap, V2EquipmentHelper for conflict resolution --- .../selfbondage/PacketSelfBondage.java | 113 ++++++++++++------ 1 file changed, 78 insertions(+), 35 deletions(-) diff --git a/src/main/java/com/tiedup/remake/network/selfbondage/PacketSelfBondage.java b/src/main/java/com/tiedup/remake/network/selfbondage/PacketSelfBondage.java index 1eb0fed..e65c9d3 100644 --- a/src/main/java/com/tiedup/remake/network/selfbondage/PacketSelfBondage.java +++ b/src/main/java/com/tiedup/remake/network/selfbondage/PacketSelfBondage.java @@ -14,8 +14,10 @@ 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.BindModeHelper; import com.tiedup.remake.v2.bondage.IV2BondageItem; import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper; +import com.tiedup.remake.v2.bondage.V2EquipResult; import java.util.function.Supplier; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.server.level.ServerPlayer; @@ -222,71 +224,112 @@ public class PacketSelfBondage { IV2BondageItem v2Item, IBondageState state ) { + java.util.Set regions = v2Item.getOccupiedRegions(stack); + + // Cannot self-collar + if (regions.contains(BodyRegionV2.NECK)) { + TiedUpMod.LOGGER.debug("[SelfBondage] {} tried to self-collar — blocked", player.getName().getString()); + return; + } + + // ARMS: tying task with progress bar + if (regions.contains(BodyRegionV2.ARMS)) { + handleV2SelfBind(player, stack, v2Item, state); + return; + } + + // Accessories (MOUTH, EYES, EARS, HANDS): instant equip + handleV2SelfAccessory(player, stack, v2Item, state); + } + + private static void handleV2SelfBind( + ServerPlayer player, + ItemStack stack, + IV2BondageItem v2Item, + IBondageState state + ) { + // Can't self-tie if already tied + if (state.isTiedUp()) { + TiedUpMod.LOGGER.debug("[SelfBondage] {} tried V2 self-tie but already tied", player.getName().getString()); + return; + } + // Check if all target regions are already occupied or blocked boolean allBlocked = true; for (BodyRegionV2 region : v2Item.getOccupiedRegions(stack)) { - if ( - !V2EquipmentHelper.isRegionOccupied(player, region) && - !V2EquipmentHelper.isRegionBlocked(player, region) - ) { + if (!V2EquipmentHelper.isRegionOccupied(player, region) + && !V2EquipmentHelper.isRegionBlocked(player, region)) { allBlocked = false; break; } } if (allBlocked) { - TiedUpMod.LOGGER.debug( - "[SelfBondage] {} tried V2 self-equip but all regions occupied", - player.getName().getString() - ); + TiedUpMod.LOGGER.debug("[SelfBondage] {} tried V2 self-equip but all regions occupied", player.getName().getString()); return; } PlayerBindState playerState = PlayerBindState.getInstance(player); if (playerState == null) return; - int tyingSeconds = SettingsAccessor.getTyingPlayerTime( - player.level().getGameRules() - ); + int tyingSeconds = SettingsAccessor.getTyingPlayerTime(player.level().getGameRules()); - // Create V2 tying task (uses V2EquipmentHelper on completion, NOT putBindOn) V2TyingPlayerTask newTask = new V2TyingPlayerTask( - stack.copy(), // copy for display/matching - stack, // live reference for consumption - state, - player, // target is self - tyingSeconds, - player.level(), - player // kidnapper is also self + stack.copy(), stack, state, player, tyingSeconds, player.level(), player ); TyingTask currentTask = playerState.getCurrentTyingTask(); - - if ( - currentTask == null || - !currentTask.isSameTarget(player) || - currentTask.isOutdated() || - !ItemStack.matches(currentTask.getBind(), stack) - ) { - // Start new task + if (currentTask == null + || !currentTask.isSameTarget(player) + || currentTask.isOutdated() + || !ItemStack.matches(currentTask.getBind(), stack)) { playerState.setCurrentTyingTask(newTask); newTask.start(); - - TiedUpMod.LOGGER.debug( - "[SelfBondage] {} started V2 self-tying ({} seconds)", - player.getName().getString(), - tyingSeconds - ); } else { - // Continue existing task — just mark active currentTask.update(); } - // If we started a new task, mark it active too if (playerState.getCurrentTyingTask() == newTask) { newTask.update(); } } + private static void handleV2SelfAccessory( + ServerPlayer player, + ItemStack stack, + IV2BondageItem v2Item, + IBondageState state + ) { + // Can't equip accessories if arms are fully bound + ItemStack currentBind = V2EquipmentHelper.getInRegion(player, BodyRegionV2.ARMS); + if (!currentBind.isEmpty() && BindModeHelper.hasArmsBound(currentBind)) { + TiedUpMod.LOGGER.debug("[SelfBondage] {} can't self-accessory — arms bound", player.getName().getString()); + return; + } + + // Check if region is occupied — try to swap + for (BodyRegionV2 region : v2Item.getOccupiedRegions(stack)) { + if (V2EquipmentHelper.isRegionOccupied(player, region)) { + ItemStack existing = V2EquipmentHelper.getInRegion(player, region); + // Can't swap if locked + if (existing.getItem() instanceof ILockable lockable && lockable.isLocked(existing)) { + TiedUpMod.LOGGER.debug("[SelfBondage] {} can't swap — current is locked", player.getName().getString()); + return; + } + } + } + + // Equip via V2EquipmentHelper (handles conflict resolution, displaced items) + V2EquipResult result = V2EquipmentHelper.equipItem(player, stack.copy()); + if (result.isSuccess()) { + for (ItemStack displaced : result.displaced()) { + player.spawnAtLocation(displaced); + } + stack.shrink(1); + SyncManager.syncInventory(player); + TiedUpMod.LOGGER.info("[SelfBondage] {} self-equipped V2 accessory", player.getName().getString()); + } + } + /** * Handle self-equipping an accessory (gag, blindfold, mittens, earplugs). * Can be used anytime (no need to be tied). From 47fa37282fc723fdc94fa6d1fef8059fed39ab75 Mon Sep 17 00:00:00 2001 From: Adrien Date: Tue, 14 Apr 2026 16:07:41 +0200 Subject: [PATCH 07/11] feat(D-01/A): NPC speed reduction for V2 items (A12) - onEquipped: apply RestraintEffectUtils speed reduction for non-Player entities with ARMS region and legs bound. Full immobilization for WRAP/LATEX_SACK pose types. - onUnequipped: remove speed reduction for non-Player entities - Players use MovementStyleManager (V2 tick-based), not this legacy path --- .../datadriven/DataDrivenBondageItem.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java index 7dd6269..876002a 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java @@ -349,6 +349,17 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem { if (holder != null) { holder.onEquipped(stack, entity); } + + // NPC speed reduction (players use MovementStyleManager, not this legacy path) + if (!(entity instanceof Player)) { + Set regions = getOccupiedRegions(stack); + if (regions.contains(BodyRegionV2.ARMS) && BindModeHelper.hasLegsBound(stack)) { + com.tiedup.remake.items.base.PoseType pose = com.tiedup.remake.v2.bondage.PoseTypeHelper.getPoseType(stack); + boolean fullImmobilization = pose == com.tiedup.remake.items.base.PoseType.WRAP + || pose == com.tiedup.remake.items.base.PoseType.LATEX_SACK; + com.tiedup.remake.util.RestraintEffectUtils.applyBindSpeedReduction(entity, fullImmobilization); + } + } } @Override @@ -357,6 +368,14 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem { if (holder != null) { holder.onUnequipped(stack, entity); } + + // NPC speed cleanup + if (!(entity instanceof Player)) { + Set regions = getOccupiedRegions(stack); + if (regions.contains(BodyRegionV2.ARMS)) { + com.tiedup.remake.util.RestraintEffectUtils.removeBindSpeedReduction(entity); + } + } } @Override From d1b864c4ec58054918de4dffb896e17bfd725400 Mon Sep 17 00:00:00 2001 From: Adrien Date: Tue, 14 Apr 2026 16:35:05 +0200 Subject: [PATCH 08/11] fix(D-01/A): double item consumption + unchecked cast in TyingInteractionHelper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QA-001: add instanceof V2TyingPlayerTask guard before cast to prevent ClassCastException when a V1 TyingPlayerTask was still active QA-002: remove stack.shrink(1) after tying completion — V2TyingPlayerTask .onComplete() already consumes the held item via heldStack.shrink(1) --- .../com/tiedup/remake/v2/bondage/TyingInteractionHelper.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/tiedup/remake/v2/bondage/TyingInteractionHelper.java b/src/main/java/com/tiedup/remake/v2/bondage/TyingInteractionHelper.java index 02e9939..99350de 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/TyingInteractionHelper.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/TyingInteractionHelper.java @@ -85,9 +85,11 @@ public final class TyingInteractionHelper { 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 { @@ -97,7 +99,7 @@ public final class TyingInteractionHelper { newTask.update(); if (newTask.isStopped()) { - stack.shrink(1); + // Item already consumed by V2TyingPlayerTask.onComplete() — don't shrink again playerState.setCurrentTyingTask(null); TiedUpMod.LOGGER.info("[TyingInteraction] {} tied {}", player.getName().getString(), target.getName().getString()); } From fe8ef3d1dc428bde309f903f2be7a9e1e84a39cd Mon Sep 17 00:00:00 2001 From: Adrien Date: Tue, 14 Apr 2026 16:38:09 +0200 Subject: [PATCH 09/11] fix(D-01/A): 3 review bugs + null guards (BUG-001, BUG-002, BUG-003, RISK-003) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BUG-001: TyingInteractionHelper swap now checks V2EquipResult — on failure, rolls back by re-equipping the old bind instead of leaving target untied BUG-002: OwnershipComponent.onUnequipped no longer double-calls unregisterWearer — onCollarRemoved already handles it. Suppressed-alert path calls unregister directly since onCollarRemoved is skipped. BUG-003: PacketSelfBondage handleV2SelfBind now clears completed tying task from PlayerBindState to prevent blocking future tying interactions RISK-003: StruggleCollar get/setResistanceState null-guard on player --- .../selfbondage/PacketSelfBondage.java | 6 ++++ .../remake/state/struggle/StruggleCollar.java | 2 ++ .../v2/bondage/TyingInteractionHelper.java | 18 +++++++--- .../bondage/component/OwnershipComponent.java | 33 ++++++++++--------- 4 files changed, 39 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/tiedup/remake/network/selfbondage/PacketSelfBondage.java b/src/main/java/com/tiedup/remake/network/selfbondage/PacketSelfBondage.java index e65c9d3..51f48ee 100644 --- a/src/main/java/com/tiedup/remake/network/selfbondage/PacketSelfBondage.java +++ b/src/main/java/com/tiedup/remake/network/selfbondage/PacketSelfBondage.java @@ -291,6 +291,12 @@ public class PacketSelfBondage { if (playerState.getCurrentTyingTask() == newTask) { newTask.update(); } + + // Clear completed task to prevent blocking future tying interactions + TyingTask activeTask = playerState.getCurrentTyingTask(); + if (activeTask != null && activeTask.isStopped()) { + playerState.setCurrentTyingTask(null); + } } private static void handleV2SelfAccessory( diff --git a/src/main/java/com/tiedup/remake/state/struggle/StruggleCollar.java b/src/main/java/com/tiedup/remake/state/struggle/StruggleCollar.java index 3432141..b75a5a7 100644 --- a/src/main/java/com/tiedup/remake/state/struggle/StruggleCollar.java +++ b/src/main/java/com/tiedup/remake/state/struggle/StruggleCollar.java @@ -47,6 +47,7 @@ public class StruggleCollar extends StruggleState { @Override protected int getResistanceState(PlayerBindState state) { Player player = state.getPlayer(); + if (player == null) return 0; ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK); if (collar.isEmpty() || !CollarHelper.isCollar(collar)) return 0; @@ -66,6 +67,7 @@ public class StruggleCollar extends StruggleState { @Override protected void setResistanceState(PlayerBindState state, int resistance) { Player player = state.getPlayer(); + if (player == null) return; ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK); if (collar.isEmpty() || !CollarHelper.isCollar(collar)) return; diff --git a/src/main/java/com/tiedup/remake/v2/bondage/TyingInteractionHelper.java b/src/main/java/com/tiedup/remake/v2/bondage/TyingInteractionHelper.java index 99350de..f75a08c 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/TyingInteractionHelper.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/TyingInteractionHelper.java @@ -9,6 +9,7 @@ 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; @@ -50,11 +51,18 @@ public final class TyingInteractionHelper { if (stack.isEmpty()) return InteractionResult.PASS; ItemStack oldBind = V2EquipmentHelper.unequipFromRegion(target, BodyRegionV2.ARMS); if (!oldBind.isEmpty()) { - V2EquipmentHelper.equipItem(target, stack.copy()); - stack.shrink(1); - target.spawnAtLocation(oldBind); - TiedUpMod.LOGGER.debug("[TyingInteraction] Swapped bind on {}", target.getName().getString()); - return InteractionResult.SUCCESS; + 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 + V2EquipmentHelper.equipItem(target, oldBind); + TiedUpMod.LOGGER.debug("[TyingInteraction] Swap failed, rolled back old bind on {}", target.getName().getString()); + return InteractionResult.PASS; + } } return InteractionResult.PASS; } diff --git a/src/main/java/com/tiedup/remake/v2/bondage/component/OwnershipComponent.java b/src/main/java/com/tiedup/remake/v2/bondage/component/OwnershipComponent.java index 2d6e632..07be4a8 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/component/OwnershipComponent.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/OwnershipComponent.java @@ -67,23 +67,26 @@ public class OwnershipComponent implements IItemComponent { if (entity.level().isClientSide()) return; if (!(entity.level() instanceof ServerLevel serverLevel)) return; - // Alert kidnappers if removal wasn't suppressed + // Alert kidnappers + unregister from CollarRegistry + // onCollarRemoved handles both the alert AND the unregister call internally, + // so we do NOT call registry.unregisterWearer() separately to avoid double unregister. if (!CollarHelper.isRemovalAlertSuppressed()) { ItemCollar.onCollarRemoved(entity, true); - } - - try { - CollarRegistry registry = CollarRegistry.get(serverLevel); - registry.unregisterWearer(entity.getUUID()); - TiedUpMod.LOGGER.debug( - "[OwnershipComponent] Unregistered collar for {}", - entity.getName().getString() - ); - } catch (Exception e) { - TiedUpMod.LOGGER.warn( - "[OwnershipComponent] Failed to unregister collar for {}: {}", - entity.getName().getString(), e.getMessage() - ); + } else { + // Suppressed alert path: still need to unregister, just skip the alert + try { + CollarRegistry registry = CollarRegistry.get(serverLevel); + registry.unregisterWearer(entity.getUUID()); + TiedUpMod.LOGGER.debug( + "[OwnershipComponent] Unregistered collar for {} (alert suppressed)", + entity.getName().getString() + ); + } catch (Exception e) { + TiedUpMod.LOGGER.warn( + "[OwnershipComponent] Failed to unregister collar for {}: {}", + entity.getName().getString(), e.getMessage() + ); + } } } From a75b89929f001c4a09ee2136062fe8a0befd77b1 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 16:44:59 +0200 Subject: [PATCH 10/11] fix(D-01/A): V2 bind/collar resistance completely broken (CRITICAL) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PlayerEquipment.getCurrentBindResistance/setCurrentBindResistance and getCurrentCollarResistance/setCurrentCollarResistance all checked instanceof ItemBind/ItemCollar — V2 DataDrivenBondageItem silently returned 0, making V2 items escapable in 1 struggle roll. Fix: use instanceof IHasResistance which both V1 and V2 implement. Also fix StruggleCollar.tighten() to read ResistanceComponent directly for V2 collars instead of IHasResistance.getBaseResistance(entity) which triggers the singleton MAX-scan across all equipped items. Note: isItemLocked() dead code in StruggleState is a PRE-EXISTING bug (x10 locked penalty never applied) — tracked for separate fix. --- .../state/components/PlayerEquipment.java | 38 +++++++++++-------- .../remake/state/struggle/StruggleCollar.java | 16 +++++++- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/tiedup/remake/state/components/PlayerEquipment.java b/src/main/java/com/tiedup/remake/state/components/PlayerEquipment.java index a25bfad..8294d3a 100644 --- a/src/main/java/com/tiedup/remake/state/components/PlayerEquipment.java +++ b/src/main/java/com/tiedup/remake/state/components/PlayerEquipment.java @@ -320,10 +320,12 @@ public class PlayerEquipment { player, BodyRegionV2.ARMS ); - if ( - stack.isEmpty() || !(stack.getItem() instanceof ItemBind bind) - ) return 0; - return bind.getCurrentResistance(stack, player); + if (stack.isEmpty()) return 0; + // V1 and V2 both implement IHasResistance + if (stack.getItem() instanceof com.tiedup.remake.items.base.IHasResistance resistance) { + return resistance.getCurrentResistance(stack, player); + } + return 0; } /** @@ -334,10 +336,11 @@ public class PlayerEquipment { player, BodyRegionV2.ARMS ); - if ( - stack.isEmpty() || !(stack.getItem() instanceof ItemBind bind) - ) return; - bind.setCurrentResistance(stack, resistance); + if (stack.isEmpty()) return; + // V1 and V2 both implement IHasResistance + if (stack.getItem() instanceof com.tiedup.remake.items.base.IHasResistance resistanceItem) { + resistanceItem.setCurrentResistance(stack, resistance); + } } /** @@ -348,10 +351,12 @@ public class PlayerEquipment { player, BodyRegionV2.NECK ); - if ( - stack.isEmpty() || !(stack.getItem() instanceof ItemCollar collar) - ) return 0; - return collar.getCurrentResistance(stack, player); + if (stack.isEmpty()) return 0; + // V1 and V2 both implement IHasResistance + if (stack.getItem() instanceof com.tiedup.remake.items.base.IHasResistance resistance) { + return resistance.getCurrentResistance(stack, player); + } + return 0; } /** @@ -362,10 +367,11 @@ public class PlayerEquipment { player, BodyRegionV2.NECK ); - if ( - stack.isEmpty() || !(stack.getItem() instanceof ItemCollar collar) - ) return; - collar.setCurrentResistance(stack, resistance); + if (stack.isEmpty()) return; + // V1 and V2 both implement IHasResistance + if (stack.getItem() instanceof com.tiedup.remake.items.base.IHasResistance resistanceItem) { + resistanceItem.setCurrentResistance(stack, resistance); + } } // ========== Helper Methods ========== diff --git a/src/main/java/com/tiedup/remake/state/struggle/StruggleCollar.java b/src/main/java/com/tiedup/remake/state/struggle/StruggleCollar.java index b75a5a7..f8a6b9c 100644 --- a/src/main/java/com/tiedup/remake/state/struggle/StruggleCollar.java +++ b/src/main/java/com/tiedup/remake/state/struggle/StruggleCollar.java @@ -9,6 +9,9 @@ import com.tiedup.remake.state.PlayerBindState; import com.tiedup.remake.v2.BodyRegionV2; import com.tiedup.remake.v2.bondage.CollarHelper; import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper; +import com.tiedup.remake.v2.bondage.component.ComponentType; +import com.tiedup.remake.v2.bondage.component.ResistanceComponent; +import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; @@ -227,9 +230,18 @@ public class StruggleCollar extends StruggleState { return; } - // Get resistance (V1 and V2 via IHasResistance) + // Get resistance — V2: read ResistanceComponent directly (avoids MAX-scan bug), + // V1: use IHasResistance.getBaseResistance() if (!(collar.getItem() instanceof IHasResistance resistanceItem)) return; - int baseResistance = resistanceItem.getBaseResistance(target); + int baseResistance; + ResistanceComponent comp = DataDrivenBondageItem.getComponent( + collar, ComponentType.RESISTANCE, ResistanceComponent.class + ); + if (comp != null) { + baseResistance = comp.getBaseResistance(); + } else { + baseResistance = resistanceItem.getBaseResistance(target); + } int currentResistance = resistanceItem.getCurrentResistance(collar, target); // Only tighten if current resistance is lower than base From d2d8df31c5c94d3425468e2c921f0a95e627297f Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 16:48:50 +0200 Subject: [PATCH 11/11] =?UTF-8?q?fix(D-01/A):=20adversarial=20review=20fix?= =?UTF-8?q?es=20=E2=80=94=204=20logic=20bugs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../selfbondage/PacketSelfBondage.java | 4 +-- .../v2/bondage/TyingInteractionHelper.java | 14 +++++++--- .../bondage/component/GaggingComponent.java | 26 ++++++++++++------- .../datadriven/DataDrivenBondageItem.java | 9 +++++-- 4 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/tiedup/remake/network/selfbondage/PacketSelfBondage.java b/src/main/java/com/tiedup/remake/network/selfbondage/PacketSelfBondage.java index 51f48ee..392178f 100644 --- a/src/main/java/com/tiedup/remake/network/selfbondage/PacketSelfBondage.java +++ b/src/main/java/com/tiedup/remake/network/selfbondage/PacketSelfBondage.java @@ -248,8 +248,8 @@ public class PacketSelfBondage { IV2BondageItem v2Item, IBondageState state ) { - // Can't self-tie if already tied - if (state.isTiedUp()) { + // Can't self-tie if already tied (check both V1 state and V2 region to prevent dual-bind) + if (state.isTiedUp() || V2EquipmentHelper.isRegionOccupied(player, BodyRegionV2.ARMS)) { TiedUpMod.LOGGER.debug("[SelfBondage] {} tried V2 self-tie but already tied", player.getName().getString()); return; } diff --git a/src/main/java/com/tiedup/remake/v2/bondage/TyingInteractionHelper.java b/src/main/java/com/tiedup/remake/v2/bondage/TyingInteractionHelper.java index f75a08c..5117419 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/TyingInteractionHelper.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/TyingInteractionHelper.java @@ -46,8 +46,8 @@ public final class TyingInteractionHelper { return InteractionResult.PASS; } - // Already tied — try to swap - if (targetState.isTiedUp()) { + // 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()) { @@ -59,8 +59,14 @@ public final class TyingInteractionHelper { return InteractionResult.SUCCESS; } else { // Equip failed — rollback: re-equip old bind - V2EquipmentHelper.equipItem(target, oldBind); - TiedUpMod.LOGGER.debug("[TyingInteraction] Swap failed, rolled back old bind on {}", target.getName().getString()); + 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; } } diff --git a/src/main/java/com/tiedup/remake/v2/bondage/component/GaggingComponent.java b/src/main/java/com/tiedup/remake/v2/bondage/component/GaggingComponent.java index e6af614..07e664b 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/component/GaggingComponent.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/GaggingComponent.java @@ -23,11 +23,14 @@ import org.jetbrains.annotations.Nullable; public class GaggingComponent implements IItemComponent { private final @Nullable String material; + private final @Nullable GagMaterial cachedMaterial; private final double comprehensionOverride; private final double rangeOverride; - private GaggingComponent(@Nullable String material, double comprehensionOverride, double rangeOverride) { + private GaggingComponent(@Nullable String material, @Nullable GagMaterial cachedMaterial, + double comprehensionOverride, double rangeOverride) { this.material = material; + this.cachedMaterial = cachedMaterial; this.comprehensionOverride = comprehensionOverride; this.rangeOverride = rangeOverride; } @@ -47,7 +50,16 @@ public class GaggingComponent implements IItemComponent { range = Math.max(0.0, config.get("range").getAsDouble()); } } - return new GaggingComponent(material, comprehension, range); + // Resolve and cache GagMaterial at load time to avoid valueOf() on every chat message + GagMaterial resolved = null; + if (material != null) { + try { + resolved = GagMaterial.valueOf(material.toUpperCase()); + } catch (IllegalArgumentException e) { + TiedUpMod.LOGGER.warn("[GaggingComponent] Unknown gag material '{}' — using defaults", material); + } + } + return new GaggingComponent(material, resolved, comprehension, range); } /** How much of the gagged speech is comprehensible (0.0 = nothing, 1.0 = full). */ @@ -66,15 +78,9 @@ public class GaggingComponent implements IItemComponent { return 10.0; } - /** The gag material enum, or null if not configured or invalid. */ + /** The gag material enum, or null if not configured or invalid. Cached at load time. */ public @Nullable GagMaterial getMaterial() { - if (material == null) return null; - try { - return GagMaterial.valueOf(material.toUpperCase()); - } catch (IllegalArgumentException e) { - TiedUpMod.LOGGER.warn("[GaggingComponent] Unknown gag material: {}", material); - return null; - } + return cachedMaterial; } /** The raw material string from JSON, or null. */ diff --git a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java index 876002a..3bf2e04 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java @@ -167,8 +167,13 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem { return TyingInteractionHelper.handleTying(serverPlayer, target, stack, hand); } - // NECK: collar equip blocked for now — V2 collar JSONs don't exist in Branch A. - // Full collar equip flow (add owner, register, sound) wired in Branch C. + // NECK: blocked until Branch C wires the full collar equip flow + // (add owner to NBT, register in CollarRegistry, play sound, sync). + // Without this, V2 collars equip without ownership — breaking GPS, shock, alerts. + if (regions.contains(BodyRegionV2.NECK)) { + TiedUpMod.LOGGER.debug("[DataDrivenBondageItem] NECK equip blocked — collar flow not wired yet"); + return InteractionResult.PASS; + } // All other regions (MOUTH, EYES, EARS, HANDS): instant equip via parent return super.interactLivingEntity(stack, player, target, hand);