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,