From 751bad418dc162b43a6dfe22094272257748ec36 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 15:23:08 +0200 Subject: [PATCH] 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,