feat(D-01/A): poseType, helpers, OWNERSHIP ComponentType (A4, A5, A6)

- 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)
This commit is contained in:
NotEvil
2026-04-14 15:23:08 +02:00
parent b81d3eed95
commit 751bad418d
7 changed files with 530 additions and 1 deletions

View File

@@ -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).
*
* <p>Bind mode determines whether a bind restrains arms, legs, or both.
* The mode is stored in the stack's NBT tag {@code "bindMode"}.</p>
*/
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<String, String> 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");
}
}

View File

@@ -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<UUID> 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<UUID> 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<UUID> 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<UUID> getListUUIDs(ItemStack stack, String listKey) {
List<UUID> 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();
}
}

View File

@@ -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).
*
* <p>V2 items read from the data-driven definition's {@code pose_type} field.
* V1 items fall back to {@code ItemBind.getPoseType()}.</p>
*/
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;
}
}

View File

@@ -15,7 +15,8 @@ public enum ComponentType {
SHOCK("shock", ShockComponent::fromJson), SHOCK("shock", ShockComponent::fromJson),
GPS("gps", GpsComponent::fromJson), GPS("gps", GpsComponent::fromJson),
CHOKING("choking", ChokingComponent::fromJson), CHOKING("choking", ChokingComponent::fromJson),
ADJUSTABLE("adjustable", AdjustableComponent::fromJson); ADJUSTABLE("adjustable", AdjustableComponent::fromJson),
OWNERSHIP("ownership", OwnershipComponent::fromJson);
private final String jsonKey; private final String jsonKey;
private final Function<JsonObject, IItemComponent> factory; private final Function<JsonObject, IItemComponent> factory;

View File

@@ -0,0 +1,20 @@
package com.tiedup.remake.v2.bondage.component;
import com.google.gson.JsonObject;
/**
* Component: collar ownership behavior for data-driven items.
*
* <p>Marks an item as a collar with ownership capabilities.
* Lifecycle hooks handle CollarRegistry registration/unregistration.</p>
*
* <p>JSON config: {@code "ownership": {}}</p>
*/
public class OwnershipComponent implements IItemComponent {
private OwnershipComponent() {}
public static IItemComponent fromJson(JsonObject config) {
return new OwnershipComponent();
}
}

View File

@@ -46,6 +46,9 @@ public record DataDrivenItemDefinition(
/** Body regions this item blocks. Defaults to occupiedRegions if not specified. */ /** Body regions this item blocks. Defaults to occupiedRegions if not specified. */
Set<BodyRegionV2> blockedRegions, Set<BodyRegionV2> blockedRegions,
/** Optional pose type identifier (e.g., "STANDARD", "STRAITJACKET", "DOG"). */
@Nullable String poseType,
/** Pose priority for conflict resolution. Higher wins. */ /** Pose priority for conflict resolution. Higher wins. */
int posePriority, int posePriority,

View File

@@ -188,6 +188,9 @@ public final class DataDrivenItemParser {
blockedRegions = occupiedRegions; blockedRegions = occupiedRegions;
} }
// Optional: pose_type (e.g., "STANDARD", "STRAITJACKET", "DOG")
String poseType = getStringOrNull(root, "pose_type");
// Optional: pose_priority (default 0) // Optional: pose_priority (default 0)
int posePriority = getIntOrDefault(root, "pose_priority", 0); int posePriority = getIntOrDefault(root, "pose_priority", 0);
@@ -328,6 +331,7 @@ public final class DataDrivenItemParser {
animationSource, animationSource,
occupiedRegions, occupiedRegions,
blockedRegions, blockedRegions,
poseType,
posePriority, posePriority,
escapeDifficulty, escapeDifficulty,
lockable, lockable,