From edfc3c650677a7a4e5b5bbc7b2ced39f01162737 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 01:29:24 +0200 Subject: [PATCH 01/20] feat(D-01): add IItemComponent interface for data-driven item behaviors --- .../v2/bondage/component/IItemComponent.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/main/java/com/tiedup/remake/v2/bondage/component/IItemComponent.java 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 new file mode 100644 index 0000000..9f43772 --- /dev/null +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/IItemComponent.java @@ -0,0 +1,21 @@ +package com.tiedup.remake.v2.bondage.component; + +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.ItemStack; + +/** + * A reusable behavior module for data-driven bondage items. + * Components are declared in JSON and instantiated per item definition. + */ +public interface IItemComponent { + + default void onEquipped(ItemStack stack, LivingEntity entity) {} + + default void onUnequipped(ItemStack stack, LivingEntity entity) {} + + default void onWornTick(ItemStack stack, LivingEntity entity) {} + + default boolean blocksUnequip(ItemStack stack, LivingEntity entity) { + return false; + } +} From b8a0d839f55537a8000483bc707656b586f378a1 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 01:33:37 +0200 Subject: [PATCH 02/20] feat(D-01): add ComponentType enum with stub component classes --- .../v2/bondage/component/ComponentType.java | 38 +++++++++++++++++++ .../bondage/component/GaggingComponent.java | 12 ++++++ .../bondage/component/LockableComponent.java | 12 ++++++ .../component/ResistanceComponent.java | 12 ++++++ 4 files changed, 74 insertions(+) create mode 100644 src/main/java/com/tiedup/remake/v2/bondage/component/ComponentType.java create mode 100644 src/main/java/com/tiedup/remake/v2/bondage/component/GaggingComponent.java create mode 100644 src/main/java/com/tiedup/remake/v2/bondage/component/LockableComponent.java create mode 100644 src/main/java/com/tiedup/remake/v2/bondage/component/ResistanceComponent.java 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 new file mode 100644 index 0000000..7a26186 --- /dev/null +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/ComponentType.java @@ -0,0 +1,38 @@ +package com.tiedup.remake.v2.bondage.component; + +import com.google.gson.JsonObject; +import java.util.function.Function; +import org.jetbrains.annotations.Nullable; + +public enum ComponentType { + LOCKABLE("lockable", LockableComponent::fromJson), + RESISTANCE("resistance", ResistanceComponent::fromJson), + GAGGING("gagging", GaggingComponent::fromJson); + + private final String jsonKey; + private final Function factory; + + ComponentType( + String jsonKey, + Function factory + ) { + this.jsonKey = jsonKey; + this.factory = factory; + } + + public String getJsonKey() { + return jsonKey; + } + + public IItemComponent create(JsonObject config) { + return factory.apply(config); + } + + @Nullable + public static ComponentType fromKey(String key) { + for (ComponentType type : values()) { + if (type.jsonKey.equals(key)) return type; + } + return null; + } +} 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 new file mode 100644 index 0000000..cb434f1 --- /dev/null +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/GaggingComponent.java @@ -0,0 +1,12 @@ +package com.tiedup.remake.v2.bondage.component; + +import com.google.gson.JsonObject; + +public class GaggingComponent implements IItemComponent { + + private GaggingComponent() {} + + public static IItemComponent fromJson(JsonObject config) { + return new GaggingComponent(); + } +} 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 new file mode 100644 index 0000000..4c383d3 --- /dev/null +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/LockableComponent.java @@ -0,0 +1,12 @@ +package com.tiedup.remake.v2.bondage.component; + +import com.google.gson.JsonObject; + +public class LockableComponent implements IItemComponent { + + private LockableComponent() {} + + public static IItemComponent fromJson(JsonObject config) { + return new LockableComponent(); + } +} 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 new file mode 100644 index 0000000..ab8f741 --- /dev/null +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/ResistanceComponent.java @@ -0,0 +1,12 @@ +package com.tiedup.remake.v2.bondage.component; + +import com.google.gson.JsonObject; + +public class ResistanceComponent implements IItemComponent { + + private ResistanceComponent() {} + + public static IItemComponent fromJson(JsonObject config) { + return new ResistanceComponent(); + } +} From 1b70041c36f59c0eef5ccd0e222902606aa40870 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 01:38:09 +0200 Subject: [PATCH 03/20] feat(D-01): add ComponentHolder container for item components --- .../v2/bondage/component/ComponentHolder.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/main/java/com/tiedup/remake/v2/bondage/component/ComponentHolder.java 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 new file mode 100644 index 0000000..982841e --- /dev/null +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/ComponentHolder.java @@ -0,0 +1,70 @@ +package com.tiedup.remake.v2.bondage.component; + +import java.util.Collections; +import java.util.EnumMap; +import java.util.Map; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +public final class ComponentHolder { + + public static final ComponentHolder EMPTY = new ComponentHolder(Map.of()); + + private final Map components; + + public ComponentHolder(Map components) { + this.components = components.isEmpty() + ? Map.of() + : Collections.unmodifiableMap(new EnumMap<>(components)); + } + + @Nullable + public IItemComponent get(ComponentType type) { + return components.get(type); + } + + @Nullable + @SuppressWarnings("unchecked") + public T get( + ComponentType type, + Class clazz + ) { + IItemComponent component = components.get(type); + if (clazz.isInstance(component)) return (T) component; + return null; + } + + public boolean has(ComponentType type) { + return components.containsKey(type); + } + + public void onEquipped(ItemStack stack, LivingEntity entity) { + for (IItemComponent c : components.values()) { + c.onEquipped(stack, entity); + } + } + + public void onUnequipped(ItemStack stack, LivingEntity entity) { + for (IItemComponent c : components.values()) { + c.onUnequipped(stack, entity); + } + } + + public void onWornTick(ItemStack stack, LivingEntity entity) { + for (IItemComponent c : components.values()) { + c.onWornTick(stack, entity); + } + } + + public boolean blocksUnequip(ItemStack stack, LivingEntity entity) { + for (IItemComponent c : components.values()) { + if (c.blocksUnequip(stack, entity)) return true; + } + return false; + } + + public boolean isEmpty() { + return components.isEmpty(); + } +} From 750be66d80f1d490a70f821acb87e7769003c800 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 01:44:19 +0200 Subject: [PATCH 04/20] feat(D-01): parse component configs from item JSON definitions Add componentConfigs field (Map) to DataDrivenItemDefinition record. The parser now reads an optional "components" JSON block, resolves each key via ComponentType.fromKey(), and stores the raw JsonObject configs for later instantiation. --- .../datadriven/DataDrivenItemDefinition.java | 15 ++++++++-- .../datadriven/DataDrivenItemParser.java | 30 ++++++++++++++++++- 2 files changed, 42 insertions(+), 3 deletions(-) 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 0c5f5f4..a9af8af 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 @@ -1,6 +1,8 @@ package com.tiedup.remake.v2.bondage.datadriven; +import com.google.gson.JsonObject; import com.tiedup.remake.v2.BodyRegionV2; +import com.tiedup.remake.v2.bondage.component.ComponentType; import java.util.Map; import java.util.Set; import net.minecraft.resources.ResourceLocation; @@ -97,5 +99,14 @@ public record DataDrivenItemDefinition( * *

This field is required in the JSON definition. Never null, never empty.

*/ - Map> animationBones -) {} + Map> animationBones, + + /** Raw component configs from JSON, keyed by ComponentType. */ + Map componentConfigs +) { + + /** Check whether this definition declares a given component type. */ + public boolean hasComponent(ComponentType type) { + return componentConfigs != null && componentConfigs.containsKey(type); + } +} 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 ff9e9fb..f0da6eb 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 @@ -5,12 +5,14 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.tiedup.remake.v2.BodyRegionV2; +import com.tiedup.remake.v2.bondage.component.ComponentType; import com.tiedup.remake.v2.bondage.movement.MovementModifier; import com.tiedup.remake.v2.bondage.movement.MovementStyle; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.Collections; +import java.util.EnumMap; import java.util.EnumSet; import java.util.HashSet; import java.util.LinkedHashMap; @@ -266,6 +268,31 @@ public final class DataDrivenItemParser { return null; } + // Optional: components (per-component JSON configs) + Map componentConfigs = + new EnumMap<>(ComponentType.class); + if (root.has("components")) { + JsonObject componentsObj = root.getAsJsonObject("components"); + for (Map.Entry entry : + componentsObj.entrySet()) { + ComponentType compType = ComponentType.fromKey( + entry.getKey() + ); + if (compType != null) { + JsonObject config = entry.getValue().isJsonObject() + ? entry.getValue().getAsJsonObject() + : new JsonObject(); + componentConfigs.put(compType, config); + } else { + LOGGER.warn( + "[DataDrivenItemParser] Unknown component type '{}' in item '{}'", + entry.getKey(), + fileId + ); + } + } + } + // Build the item ID from the file path // fileId is like "tiedup:tiedup_items/leather_armbinder.json" // We want "tiedup:leather_armbinder" @@ -302,7 +329,8 @@ public final class DataDrivenItemParser { movementStyle, movementModifier, creator, - animationBones + animationBones, + componentConfigs ); } From a781dad597c3a92549b4b74888498e280d368b82 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 01:46:19 +0200 Subject: [PATCH 05/20] feat(D-01): instantiate ComponentHolder per item definition on reload Add a parallel COMPONENT_HOLDERS volatile cache to DataDrivenItemRegistry, rebuilt from raw componentConfigs every time definitions are loaded via reload() or mergeAll(). Cleared alongside DEFINITIONS in clear(). Two accessor methods allow looking up a ComponentHolder by ItemStack (reads tiedup_item_id NBT) or by ResourceLocation directly. --- .../datadriven/DataDrivenItemRegistry.java | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemRegistry.java b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemRegistry.java index b721b0f..070ef7f 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemRegistry.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemRegistry.java @@ -1,7 +1,12 @@ package com.tiedup.remake.v2.bondage.datadriven; +import com.google.gson.JsonObject; +import com.tiedup.remake.v2.bondage.component.ComponentHolder; +import com.tiedup.remake.v2.bondage.component.ComponentType; +import com.tiedup.remake.v2.bondage.component.IItemComponent; import java.util.Collection; import java.util.Collections; +import java.util.EnumMap; import java.util.HashMap; import java.util.Map; import net.minecraft.nbt.CompoundTag; @@ -34,6 +39,13 @@ public final class DataDrivenItemRegistry { DataDrivenItemDefinition > DEFINITIONS = Map.of(); + /** + * Parallel cache of instantiated component holders, keyed by item definition ID. + * Rebuilt every time DEFINITIONS changes. + */ + private static volatile Map + COMPONENT_HOLDERS = Map.of(); + private DataDrivenItemRegistry() {} /** @@ -46,6 +58,7 @@ public final class DataDrivenItemRegistry { Map newDefs ) { DEFINITIONS = Collections.unmodifiableMap(new HashMap<>(newDefs)); + COMPONENT_HOLDERS = buildComponentHolders(DEFINITIONS); } /** @@ -66,6 +79,7 @@ public final class DataDrivenItemRegistry { ); merged.putAll(newDefs); DEFINITIONS = Collections.unmodifiableMap(merged); + COMPONENT_HOLDERS = buildComponentHolders(DEFINITIONS); } /** @@ -111,5 +125,65 @@ public final class DataDrivenItemRegistry { */ public static void clear() { DEFINITIONS = Map.of(); + COMPONENT_HOLDERS = Map.of(); + } + + // ===== COMPONENT HOLDERS ===== + + /** + * Get the ComponentHolder for a data-driven item stack. + * + * @param stack the ItemStack to inspect + * @return the holder, or null if the stack is not data-driven or has no definition + */ + @Nullable + public static ComponentHolder getComponents(ItemStack stack) { + DataDrivenItemDefinition def = get(stack); + if (def == null) return null; + return COMPONENT_HOLDERS.get(def.id()); + } + + /** + * Get the ComponentHolder for a data-driven item by its definition ID. + * + * @param id the definition ID + * @return the holder, or null if not found + */ + @Nullable + public static ComponentHolder getComponents(ResourceLocation id) { + return COMPONENT_HOLDERS.get(id); + } + + /** + * Build component holders from a definitions map. + * Each definition's raw componentConfigs are instantiated via + * {@link ComponentType#create(JsonObject)}. + */ + private static Map buildComponentHolders( + Map definitions + ) { + Map holders = new HashMap<>(); + for (Map.Entry entry : + definitions.entrySet()) { + DataDrivenItemDefinition def = entry.getValue(); + Map components = + new EnumMap<>(ComponentType.class); + if (def.componentConfigs() != null) { + for (Map.Entry compEntry : + def.componentConfigs().entrySet()) { + components.put( + compEntry.getKey(), + compEntry.getKey().create(compEntry.getValue()) + ); + } + } + holders.put( + entry.getKey(), + components.isEmpty() + ? ComponentHolder.EMPTY + : new ComponentHolder(components) + ); + } + return Collections.unmodifiableMap(holders); } } From 3a1f401ccfaa29c3ca60c7dae979daeb0046dd82 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 01:47:19 +0200 Subject: [PATCH 06/20] feat(D-01): delegate DataDrivenBondageItem lifecycle to components Override onEquipped(), onUnequipped(), and canUnequip() in DataDrivenBondageItem to delegate to the item's ComponentHolder. The canUnequip() override preserves the existing lock check from AbstractV2BondageItem via super.canUnequip(). Add a static getComponent() helper for external code to retrieve a typed component from any data-driven item stack. --- .../datadriven/DataDrivenBondageItem.java | 50 +++++++++++++++++++ 1 file changed, 50 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 9f215a2..743bd8e 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 @@ -4,6 +4,9 @@ import com.tiedup.remake.v2.BodyRegionV2; import com.tiedup.remake.v2.bondage.IV2BondageEquipment; import com.tiedup.remake.v2.bondage.V2BondageItems; import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper; +import com.tiedup.remake.v2.bondage.component.ComponentHolder; +import com.tiedup.remake.v2.bondage.component.ComponentType; +import com.tiedup.remake.v2.bondage.component.IItemComponent; import com.tiedup.remake.v2.bondage.items.AbstractV2BondageItem; import java.util.List; import java.util.Map; @@ -257,6 +260,53 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem { return Component.literal(def.displayName()); } + // ===== COMPONENT LIFECYCLE DELEGATION ===== + + @Override + public void onEquipped(ItemStack stack, LivingEntity entity) { + ComponentHolder holder = DataDrivenItemRegistry.getComponents(stack); + if (holder != null) { + holder.onEquipped(stack, entity); + } + } + + @Override + public void onUnequipped(ItemStack stack, LivingEntity entity) { + ComponentHolder holder = DataDrivenItemRegistry.getComponents(stack); + if (holder != null) { + holder.onUnequipped(stack, entity); + } + } + + @Override + public boolean canUnequip(ItemStack stack, LivingEntity entity) { + ComponentHolder holder = DataDrivenItemRegistry.getComponents(stack); + if (holder != null && holder.blocksUnequip(stack, entity)) { + return false; + } + return super.canUnequip(stack, entity); + } + + /** + * Get a specific component from a data-driven item stack. + * + * @param stack the ItemStack to inspect + * @param type the component type to look up + * @param clazz the expected component class + * @param the component type + * @return the component, or null if the item is not data-driven or lacks this component + */ + @Nullable + public static T getComponent( + ItemStack stack, + ComponentType type, + Class clazz + ) { + ComponentHolder holder = DataDrivenItemRegistry.getComponents(stack); + if (holder == null) return null; + return holder.get(type, clazz); + } + // ===== FACTORY ===== /** From caeb4469b137e398793427955e93c1f3435bf2b2 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 02:01:41 +0200 Subject: [PATCH 07/20] feat(D-01): implement LockableComponent with configurable lock resistance --- .../bondage/component/LockableComponent.java | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) 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 4c383d3..6c8131c 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,12 +1,42 @@ package com.tiedup.remake.v2.bondage.component; import com.google.gson.JsonObject; +import com.tiedup.remake.items.base.ILockable; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.ItemStack; +/** + * Component: lockable behavior for data-driven items. + * Delegates lock checks to ILockable on the item. + * + * JSON config: + * {@code "lockable": true} or {@code "lockable": {"lock_resistance": 300}} + */ public class LockableComponent implements IItemComponent { - private LockableComponent() {} + private final int lockResistance; + + private LockableComponent(int lockResistance) { + this.lockResistance = lockResistance; + } public static IItemComponent fromJson(JsonObject config) { - return new LockableComponent(); + int resistance = 250; // default + if (config != null && config.has("lock_resistance")) { + resistance = config.get("lock_resistance").getAsInt(); + } + return new LockableComponent(resistance); + } + + public int getLockResistance() { + return lockResistance; + } + + @Override + public boolean blocksUnequip(ItemStack stack, LivingEntity entity) { + if (stack.getItem() instanceof ILockable lockable) { + return lockable.isLocked(stack); + } + return false; } } From 84f4c3a53f8554d86d531b399403c7436e746a95 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 02:01:46 +0200 Subject: [PATCH 08/20] feat(D-01): implement ResistanceComponent, improve stack-aware resistance lookup --- .../component/ResistanceComponent.java | 24 +++++++++++++++++-- .../datadriven/DataDrivenBondageItem.java | 16 +++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) 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 ab8f741..e7abad7 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 @@ -2,11 +2,31 @@ package com.tiedup.remake.v2.bondage.component; import com.google.gson.JsonObject; +/** + * Component: struggle resistance for data-driven items. + * + * JSON config: {@code "resistance": {"base": 150}} + */ public class ResistanceComponent implements IItemComponent { - private ResistanceComponent() {} + private final int baseResistance; + + private ResistanceComponent(int baseResistance) { + this.baseResistance = baseResistance; + } public static IItemComponent fromJson(JsonObject config) { - return new ResistanceComponent(); + int base = 100; + if (config != null && config.has("base")) { + base = config.get("base").getAsInt(); + } + return new ResistanceComponent(base); + } + + /** + * Get the base resistance for this item. + */ + public int getBaseResistance() { + return baseResistance; } } 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 743bd8e..4d65f10 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 @@ -7,6 +7,7 @@ import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper; import com.tiedup.remake.v2.bondage.component.ComponentHolder; import com.tiedup.remake.v2.bondage.component.ComponentType; import com.tiedup.remake.v2.bondage.component.IItemComponent; +import com.tiedup.remake.v2.bondage.component.ResistanceComponent; import com.tiedup.remake.v2.bondage.items.AbstractV2BondageItem; import java.util.List; import java.util.Map; @@ -150,6 +151,21 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem { .entrySet()) { ItemStack stack = entry.getValue(); if (stack.getItem() == this) { + // Try component first (stack-aware, fixes I-03) + ResistanceComponent comp = DataDrivenBondageItem + .getComponent( + stack, + ComponentType.RESISTANCE, + ResistanceComponent.class + ); + if (comp != null) { + maxDifficulty = Math.max( + maxDifficulty, + comp.getBaseResistance() + ); + continue; + } + // Fallback: read from definition directly DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack); if (def != null) { From 231522c68ea61208f633fd6e195f4fdaba905068 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 02:01:50 +0200 Subject: [PATCH 09/20] feat(D-01): implement GaggingComponent with comprehension and range --- .../bondage/component/GaggingComponent.java | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) 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 cb434f1..1f09123 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 @@ -2,11 +2,42 @@ package com.tiedup.remake.v2.bondage.component; import com.google.gson.JsonObject; +/** + * Component: gagging behavior for data-driven items. + * + * JSON config: {@code "gagging": {"comprehension": 0.2, "range": 10.0}} + */ public class GaggingComponent implements IItemComponent { - private GaggingComponent() {} + private final double comprehension; + private final double range; + + private GaggingComponent(double comprehension, double range) { + this.comprehension = comprehension; + this.range = range; + } public static IItemComponent fromJson(JsonObject config) { - return new GaggingComponent(); + double comprehension = 0.2; + double range = 10.0; + if (config != null) { + if (config.has("comprehension")) { + comprehension = config.get("comprehension").getAsDouble(); + } + if (config.has("range")) { + range = config.get("range").getAsDouble(); + } + } + return new GaggingComponent(comprehension, range); + } + + /** How much of the gagged speech is comprehensible (0.0 = nothing, 1.0 = full). */ + public double getComprehension() { + return comprehension; + } + + /** Maximum range in blocks where muffled speech can be heard. */ + public double getRange() { + return range; } } From dbacef66d5a2db62bca0158cb069dae72875b4d3 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 02:03:50 +0200 Subject: [PATCH 10/20] feat(D-01): add test_component_gag.json demonstrating component system JSON item using all 3 implemented components: lockable (lock_resistance: 200), resistance (base: 80), and gagging (comprehension: 0.15, range: 8.0). --- .../tiedup_items/test_component_gag.json | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/main/resources/data/tiedup/tiedup_items/test_component_gag.json diff --git a/src/main/resources/data/tiedup/tiedup_items/test_component_gag.json b/src/main/resources/data/tiedup/tiedup_items/test_component_gag.json new file mode 100644 index 0000000..b7adc12 --- /dev/null +++ b/src/main/resources/data/tiedup/tiedup_items/test_component_gag.json @@ -0,0 +1,24 @@ +{ + "type": "tiedup:bondage_item", + "display_name": "Component Test Gag", + "model": "tiedup:models/gltf/v2/handcuffs/cuffs_prototype.glb", + "regions": ["MOUTH"], + "pose_priority": 10, + "escape_difficulty": 3, + "lockable": true, + "animation_bones": { + "idle": [] + }, + "components": { + "lockable": { + "lock_resistance": 200 + }, + "resistance": { + "base": 80 + }, + "gagging": { + "comprehension": 0.15, + "range": 8.0 + } + } +} From 1327e3bfc3b0edc8bd0b1fcbe2c9e260682d0fc9 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 02:25:57 +0200 Subject: [PATCH 11/20] fix(D-01): atomic snapshot for registry to prevent torn reads (BUG-001) Replace two separate volatile fields (DEFINITIONS, COMPONENT_HOLDERS) with a single RegistrySnapshot record swapped atomically. This prevents race conditions where a reader thread could see new definitions paired with stale/empty component holders between the two volatile writes. --- .../datadriven/DataDrivenItemRegistry.java | 71 ++++++++++++------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemRegistry.java b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemRegistry.java index 070ef7f..e0e25ec 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemRegistry.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemRegistry.java @@ -18,9 +18,9 @@ import org.jetbrains.annotations.Nullable; * Thread-safe registry for data-driven bondage item definitions. * *

Populated by the reload listener that scans {@code tiedup_items/} JSON files. - * Uses volatile atomic swap (same pattern as {@link - * com.tiedup.remake.client.animation.context.ContextGlbRegistry}) to ensure - * the render thread always sees a consistent snapshot.

+ * Uses a single volatile snapshot reference to ensure readers always see a + * consistent pair of definitions + component holders. This prevents torn reads + * where one map is updated but the other is stale.

* *

Lookup methods accept either a {@link ResourceLocation} ID directly * or an {@link ItemStack} (reads the {@code tiedup_item_id} NBT tag).

@@ -31,20 +31,22 @@ public final class DataDrivenItemRegistry { public static final String NBT_ITEM_ID = "tiedup_item_id"; /** - * Volatile reference to an unmodifiable map. Reload builds a new map - * and swaps atomically; consumer threads always see a consistent snapshot. + * Immutable snapshot combining definitions and their component holders. + * Swapped atomically via a single volatile write to prevent torn reads. */ - private static volatile Map< - ResourceLocation, - DataDrivenItemDefinition - > DEFINITIONS = Map.of(); + private record RegistrySnapshot( + Map definitions, + Map holders + ) { + static final RegistrySnapshot EMPTY = new RegistrySnapshot(Map.of(), Map.of()); + } /** - * Parallel cache of instantiated component holders, keyed by item definition ID. - * Rebuilt every time DEFINITIONS changes. + * Single volatile reference to the current registry state. + * All read methods capture this reference ONCE at the start to ensure + * consistency within a single call. */ - private static volatile Map - COMPONENT_HOLDERS = Map.of(); + private static volatile RegistrySnapshot SNAPSHOT = RegistrySnapshot.EMPTY; private DataDrivenItemRegistry() {} @@ -57,8 +59,9 @@ public final class DataDrivenItemRegistry { public static void reload( Map newDefs ) { - DEFINITIONS = Collections.unmodifiableMap(new HashMap<>(newDefs)); - COMPONENT_HOLDERS = buildComponentHolders(DEFINITIONS); + Map defs = + Collections.unmodifiableMap(new HashMap<>(newDefs)); + SNAPSHOT = new RegistrySnapshot(defs, buildComponentHolders(defs)); } /** @@ -67,7 +70,8 @@ public final class DataDrivenItemRegistry { *

On an integrated server, both the client (assets/) and server (data/) reload * listeners populate this registry. Using {@link #reload} would cause the second * listener to overwrite the first's definitions. This method builds a new map - * from the existing snapshot + the new entries, then swaps atomically.

+ * from the existing snapshot + the new entries, then swaps atomically as a + * single snapshot to prevent torn reads.

* * @param newDefs the definitions to merge (will overwrite existing entries with same key) */ @@ -75,11 +79,12 @@ public final class DataDrivenItemRegistry { Map newDefs ) { Map merged = new HashMap<>( - DEFINITIONS + SNAPSHOT.definitions ); merged.putAll(newDefs); - DEFINITIONS = Collections.unmodifiableMap(merged); - COMPONENT_HOLDERS = buildComponentHolders(DEFINITIONS); + Map defs = + Collections.unmodifiableMap(merged); + SNAPSHOT = new RegistrySnapshot(defs, buildComponentHolders(defs)); } /** @@ -90,7 +95,7 @@ public final class DataDrivenItemRegistry { */ @Nullable public static DataDrivenItemDefinition get(ResourceLocation id) { - return DEFINITIONS.get(id); + return SNAPSHOT.definitions.get(id); } /** @@ -108,7 +113,7 @@ public final class DataDrivenItemRegistry { tag.getString(NBT_ITEM_ID) ); if (id == null) return null; - return DEFINITIONS.get(id); + return SNAPSHOT.definitions.get(id); } /** @@ -117,15 +122,14 @@ public final class DataDrivenItemRegistry { * @return unmodifiable collection of all definitions */ public static Collection getAll() { - return DEFINITIONS.values(); + return SNAPSHOT.definitions.values(); } /** * Clear all definitions. Called on world unload or for testing. */ public static void clear() { - DEFINITIONS = Map.of(); - COMPONENT_HOLDERS = Map.of(); + SNAPSHOT = RegistrySnapshot.EMPTY; } // ===== COMPONENT HOLDERS ===== @@ -133,14 +137,27 @@ public final class DataDrivenItemRegistry { /** * Get the ComponentHolder for a data-driven item stack. * + *

Captures the snapshot reference once to ensure consistent reads + * between the definition lookup and the holder lookup.

+ * * @param stack the ItemStack to inspect * @return the holder, or null if the stack is not data-driven or has no definition */ @Nullable public static ComponentHolder getComponents(ItemStack stack) { - DataDrivenItemDefinition def = get(stack); + if (stack.isEmpty()) return null; + CompoundTag tag = stack.getTag(); + if (tag == null || !tag.contains(NBT_ITEM_ID)) return null; + ResourceLocation id = ResourceLocation.tryParse( + tag.getString(NBT_ITEM_ID) + ); + if (id == null) return null; + // Capture snapshot once to ensure definition and holder come from + // the same atomic snapshot, preventing torn reads. + RegistrySnapshot snap = SNAPSHOT; + DataDrivenItemDefinition def = snap.definitions.get(id); if (def == null) return null; - return COMPONENT_HOLDERS.get(def.id()); + return snap.holders.get(def.id()); } /** @@ -151,7 +168,7 @@ public final class DataDrivenItemRegistry { */ @Nullable public static ComponentHolder getComponents(ResourceLocation id) { - return COMPONENT_HOLDERS.get(id); + return SNAPSHOT.holders.get(id); } /** From bb209bcd8e108b04d8040b592433302e7cd7c3db Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 02:26:31 +0200 Subject: [PATCH 12/20] fix(D-01): remove dead onWornTick() until V2 tick mechanism exists (BUG-002) Remove onWornTick() from IItemComponent (default method) and ComponentHolder (aggregate method). No V2 tick caller invokes these, so they create a broken contract. Can be re-added when a tick mechanism is implemented. --- .../tiedup/remake/v2/bondage/component/ComponentHolder.java | 6 ------ .../tiedup/remake/v2/bondage/component/IItemComponent.java | 2 -- 2 files changed, 8 deletions(-) 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 982841e..6a465ae 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 @@ -51,12 +51,6 @@ public final class ComponentHolder { } } - public void onWornTick(ItemStack stack, LivingEntity entity) { - for (IItemComponent c : components.values()) { - c.onWornTick(stack, entity); - } - } - public boolean blocksUnequip(ItemStack stack, LivingEntity entity) { for (IItemComponent c : components.values()) { if (c.blocksUnequip(stack, entity)) return true; 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 9f43772..4af99c0 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 @@ -13,8 +13,6 @@ public interface IItemComponent { default void onUnequipped(ItemStack stack, LivingEntity entity) {} - default void onWornTick(ItemStack stack, LivingEntity entity) {} - default boolean blocksUnequip(ItemStack stack, LivingEntity entity) { return false; } From 456335e0dd4f176e41abfd52e4a012232e86197e Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 02:27:37 +0200 Subject: [PATCH 13/20] fix(D-01): wire LockableComponent.lockResistance via getItemLockResistance() (BUG-003) - Remove redundant blocksUnequip() from LockableComponent since AbstractV2BondageItem.canUnequip() already checks ILockable.isLocked() - Add DataDrivenBondageItem.getItemLockResistance(ItemStack) that reads the per-item lock resistance from the LockableComponent, falling back to the global config value when absent --- .../bondage/component/LockableComponent.java | 27 ++++++++++--------- .../datadriven/DataDrivenBondageItem.java | 25 +++++++++++++++++ 2 files changed, 39 insertions(+), 13 deletions(-) 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 6c8131c..c2f4722 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,16 +1,20 @@ package com.tiedup.remake.v2.bondage.component; import com.google.gson.JsonObject; -import com.tiedup.remake.items.base.ILockable; -import net.minecraft.world.entity.LivingEntity; -import net.minecraft.world.item.ItemStack; /** * Component: lockable behavior for data-driven items. - * Delegates lock checks to ILockable on the item. + * + *

Stores the per-item lock resistance parsed from JSON. The lock check + * itself is NOT done here because {@code AbstractV2BondageItem.canUnequip()} + * already delegates to {@code ILockable.isLocked()} -- duplicating it in + * {@code blocksUnequip()} would be redundant.

+ * + *

Consumers retrieve the per-item lock resistance via + * {@link com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem#getItemLockResistance(net.minecraft.world.item.ItemStack)}.

* * JSON config: - * {@code "lockable": true} or {@code "lockable": {"lock_resistance": 300}} + * {@code "lockable": {}} or {@code "lockable": {"lock_resistance": 300}} */ public class LockableComponent implements IItemComponent { @@ -28,15 +32,12 @@ public class LockableComponent implements IItemComponent { return new LockableComponent(resistance); } + /** + * Get the per-item lock resistance value parsed from JSON. + * + * @return lock resistance (always >= 0 after RISK-003 clamping) + */ public int getLockResistance() { return lockResistance; } - - @Override - public boolean blocksUnequip(ItemStack stack, LivingEntity entity) { - if (stack.getItem() instanceof ILockable lockable) { - return lockable.isLocked(stack); - } - return false; - } } 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 4d65f10..56d6738 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 @@ -7,6 +7,7 @@ import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper; import com.tiedup.remake.v2.bondage.component.ComponentHolder; import com.tiedup.remake.v2.bondage.component.ComponentType; import com.tiedup.remake.v2.bondage.component.IItemComponent; +import com.tiedup.remake.v2.bondage.component.LockableComponent; import com.tiedup.remake.v2.bondage.component.ResistanceComponent; import com.tiedup.remake.v2.bondage.items.AbstractV2BondageItem; import java.util.List; @@ -323,6 +324,30 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem { return holder.get(type, clazz); } + /** + * Get per-item lock resistance from the LockableComponent, if present. + * + *

Returns the component's JSON-configured value if the stack has a + * LockableComponent, otherwise falls back to the global config value + * from {@link com.tiedup.remake.core.SettingsAccessor#getPadlockResistance}.

+ * + * @param stack the ItemStack to inspect + * @return lock resistance value for this specific item + */ + public static int getItemLockResistance(ItemStack stack) { + LockableComponent comp = getComponent( + stack, + ComponentType.LOCKABLE, + LockableComponent.class + ); + if (comp != null) { + return comp.getLockResistance(); + } + return com.tiedup.remake.core.SettingsAccessor.getPadlockResistance( + null + ); + } + // ===== FACTORY ===== /** From bb589d44f822cf11dd51f00a39f8836b35298de8 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 02:27:59 +0200 Subject: [PATCH 14/20] fix(D-01): warn on non-object component config, deep-copy configs (RISK-001, RISK-002) - Deep-copy JsonObject configs via deepCopy() before storing in the definition to prevent external mutation of the parsed JSON tree - Log a warning when a component config value is not a JsonObject, making misconfigured JSON easier to diagnose --- .../bondage/datadriven/DataDrivenItemParser.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) 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 f0da6eb..221b2ae 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 @@ -279,9 +279,17 @@ public final class DataDrivenItemParser { entry.getKey() ); if (compType != null) { - JsonObject config = entry.getValue().isJsonObject() - ? entry.getValue().getAsJsonObject() - : new JsonObject(); + JsonObject config; + if (entry.getValue().isJsonObject()) { + config = entry.getValue().getAsJsonObject().deepCopy(); + } else { + LOGGER.warn( + "[DataDrivenItemParser] Component '{}' in item '{}' has non-object config, using defaults", + entry.getKey(), + fileId + ); + config = new JsonObject(); + } componentConfigs.put(compType, config); } else { LOGGER.warn( From 3a81bb6e122a4d51a8cb77e84475a6d9777cd547 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 02:28:42 +0200 Subject: [PATCH 15/20] fix(D-01): clamp component config values to valid ranges (RISK-003) - LockableComponent: lock_resistance clamped to >= 0 - ResistanceComponent: base resistance clamped to >= 0 - GaggingComponent: comprehension clamped to [0.0, 1.0], range to >= 0.0 Prevents nonsensical negative values from malformed JSON configs. --- .../tiedup/remake/v2/bondage/component/GaggingComponent.java | 2 ++ .../tiedup/remake/v2/bondage/component/LockableComponent.java | 1 + .../tiedup/remake/v2/bondage/component/ResistanceComponent.java | 1 + 3 files changed, 4 insertions(+) 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 1f09123..0e401a5 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 @@ -28,6 +28,8 @@ public class GaggingComponent implements IItemComponent { range = 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); } 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 c2f4722..3a57c71 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 @@ -29,6 +29,7 @@ public class LockableComponent implements IItemComponent { if (config != null && config.has("lock_resistance")) { resistance = config.get("lock_resistance").getAsInt(); } + resistance = Math.max(0, resistance); return new LockableComponent(resistance); } 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 e7abad7..d59f838 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 @@ -20,6 +20,7 @@ public class ResistanceComponent implements IItemComponent { if (config != null && config.has("base")) { base = config.get("base").getAsInt(); } + base = Math.max(0, base); return new ResistanceComponent(base); } From bfcc20d242501e55dea0acf431789d794829c457 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 02:29:25 +0200 Subject: [PATCH 16/20] fix(D-01): compact constructor defaults null componentConfigs to empty (RISK-004) Add compact constructor to DataDrivenItemDefinition that defaults null componentConfigs to Map.of(). This makes the field guaranteed non-null, allowing removal of null checks in hasComponent() and DataDrivenItemRegistry.buildComponentHolders(). --- .../datadriven/DataDrivenItemDefinition.java | 7 ++++++- .../bondage/datadriven/DataDrivenItemRegistry.java | 14 ++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) 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 a9af8af..8d3e972 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 @@ -105,8 +105,13 @@ public record DataDrivenItemDefinition( Map componentConfigs ) { + /** Compact constructor: default null componentConfigs to empty immutable map. */ + public DataDrivenItemDefinition { + if (componentConfigs == null) componentConfigs = Map.of(); + } + /** Check whether this definition declares a given component type. */ public boolean hasComponent(ComponentType type) { - return componentConfigs != null && componentConfigs.containsKey(type); + return componentConfigs.containsKey(type); } } diff --git a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemRegistry.java b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemRegistry.java index e0e25ec..5e726e2 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemRegistry.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemRegistry.java @@ -185,14 +185,12 @@ public final class DataDrivenItemRegistry { DataDrivenItemDefinition def = entry.getValue(); Map components = new EnumMap<>(ComponentType.class); - if (def.componentConfigs() != null) { - for (Map.Entry compEntry : - def.componentConfigs().entrySet()) { - components.put( - compEntry.getKey(), - compEntry.getKey().create(compEntry.getValue()) - ); - } + for (Map.Entry compEntry : + def.componentConfigs().entrySet()) { + components.put( + compEntry.getKey(), + compEntry.getKey().create(compEntry.getValue()) + ); } holders.put( entry.getKey(), From dcc8493e5e680e66cb6dd3310287a001c0604909 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 02:29:46 +0200 Subject: [PATCH 17/20] fix(D-01): pre-built map for O(1) ComponentType.fromKey() lookup (RISK-005) Replace linear values() scan with a static unmodifiable HashMap lookup. While only 3 entries currently exist, this establishes the correct pattern for when more component types are added. --- .../v2/bondage/component/ComponentType.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) 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 7a26186..ca16f26 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 @@ -1,6 +1,9 @@ package com.tiedup.remake.v2.bondage.component; import com.google.gson.JsonObject; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.function.Function; import org.jetbrains.annotations.Nullable; @@ -12,6 +15,16 @@ public enum ComponentType { private final String jsonKey; private final Function factory; + /** Pre-built lookup map for O(1) fromKey() instead of linear scan. */ + private static final Map BY_KEY; + static { + Map map = new HashMap<>(); + for (ComponentType type : values()) { + map.put(type.jsonKey, type); + } + BY_KEY = Collections.unmodifiableMap(map); + } + ComponentType( String jsonKey, Function factory @@ -30,9 +43,6 @@ public enum ComponentType { @Nullable public static ComponentType fromKey(String key) { - for (ComponentType type : values()) { - if (type.jsonKey.equals(key)) return type; - } - return null; + return BY_KEY.get(key); } } From 185ac63a445e40cfef5567b2ef161b48ee98ba8f Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 02:37:14 +0200 Subject: [PATCH 18/20] feat(D-01): implement 5 remaining components (blinding, shock, gps, choking, adjustable) --- .../component/AdjustableComponent.java | 67 +++++++++++++++++++ .../bondage/component/BlindingComponent.java | 36 ++++++++++ .../bondage/component/ChokingComponent.java | 46 +++++++++++++ .../v2/bondage/component/ComponentType.java | 7 +- .../v2/bondage/component/GpsComponent.java | 45 +++++++++++++ .../v2/bondage/component/ShockComponent.java | 52 ++++++++++++++ 6 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/tiedup/remake/v2/bondage/component/AdjustableComponent.java create mode 100644 src/main/java/com/tiedup/remake/v2/bondage/component/BlindingComponent.java create mode 100644 src/main/java/com/tiedup/remake/v2/bondage/component/ChokingComponent.java create mode 100644 src/main/java/com/tiedup/remake/v2/bondage/component/GpsComponent.java create mode 100644 src/main/java/com/tiedup/remake/v2/bondage/component/ShockComponent.java diff --git a/src/main/java/com/tiedup/remake/v2/bondage/component/AdjustableComponent.java b/src/main/java/com/tiedup/remake/v2/bondage/component/AdjustableComponent.java new file mode 100644 index 0000000..63238e7 --- /dev/null +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/AdjustableComponent.java @@ -0,0 +1,67 @@ +package com.tiedup.remake.v2.bondage.component; + +import com.google.gson.JsonObject; + +/** + * Component: positional adjustment for data-driven items. + * Allows Y offset adjustment via GUI slider. + * + * JSON config: + * {@code "adjustable": {"default": 0.0, "min": -4.0, "max": 4.0, "step": 0.25}} + */ +public class AdjustableComponent implements IItemComponent { + + private final float defaultValue; + private final float min; + private final float max; + private final float step; + + private AdjustableComponent(float defaultValue, float min, float max, float step) { + this.defaultValue = defaultValue; + this.min = min; + this.max = max; + this.step = step; + } + + public static IItemComponent fromJson(JsonObject config) { + float defaultVal = 0.0f; + float min = -4.0f; + float max = 4.0f; + float step = 0.25f; + if (config != null) { + if (config.has("default")) defaultVal = config.get("default").getAsFloat(); + if (config.has("min")) min = config.get("min").getAsFloat(); + if (config.has("max")) max = config.get("max").getAsFloat(); + if (config.has("step")) step = config.get("step").getAsFloat(); + } + // Ensure min <= max + if (min > max) { + float tmp = min; + min = max; + max = tmp; + } + step = Math.max(0.01f, step); + defaultVal = Math.max(min, Math.min(max, defaultVal)); + return new AdjustableComponent(defaultVal, min, max, step); + } + + /** Default Y offset value. */ + public float getDefaultValue() { + return defaultValue; + } + + /** Minimum Y offset. */ + public float getMin() { + return min; + } + + /** Maximum Y offset. */ + public float getMax() { + return max; + } + + /** Step increment for the adjustment slider. */ + public float getStep() { + return step; + } +} diff --git a/src/main/java/com/tiedup/remake/v2/bondage/component/BlindingComponent.java b/src/main/java/com/tiedup/remake/v2/bondage/component/BlindingComponent.java new file mode 100644 index 0000000..d9142f6 --- /dev/null +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/BlindingComponent.java @@ -0,0 +1,36 @@ +package com.tiedup.remake.v2.bondage.component; + +import com.google.gson.JsonObject; +import org.jetbrains.annotations.Nullable; + +/** + * Component: blinding effect for data-driven items. + * Replaces IHasBlindingEffect marker interface. + * + * JSON config: + * {@code "blinding": {}} or {@code "blinding": {"overlay": "tiedup:textures/overlay/custom.png"}} + */ +public class BlindingComponent implements IItemComponent { + + private final String overlay; // nullable — null means default overlay + + private BlindingComponent(String overlay) { + this.overlay = overlay; + } + + public static IItemComponent fromJson(JsonObject config) { + String overlay = null; + if (config != null && config.has("overlay")) { + overlay = config.get("overlay").getAsString(); + } + return new BlindingComponent(overlay); + } + + /** + * Custom overlay texture path, or null for the mod's default blindfold overlay. + */ + @Nullable + public String getOverlay() { + return overlay; + } +} 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 new file mode 100644 index 0000000..5860159 --- /dev/null +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/ChokingComponent.java @@ -0,0 +1,46 @@ +package com.tiedup.remake.v2.bondage.component; + +import com.google.gson.JsonObject; + +/** + * Component: choking effect for data-driven items. + * When active, drains air, applies darkness/slowness, deals damage. + * + * JSON config: + * {@code "choking": {"air_drain_per_tick": 8, "non_lethal_for_master": true}} + */ +public class ChokingComponent implements IItemComponent { + + private final int airDrainPerTick; + private final boolean nonLethalForMaster; + + private ChokingComponent(int airDrainPerTick, boolean nonLethalForMaster) { + this.airDrainPerTick = airDrainPerTick; + this.nonLethalForMaster = nonLethalForMaster; + } + + public static IItemComponent fromJson(JsonObject config) { + int drain = 8; + boolean nonLethal = true; + if (config != null) { + if (config.has("air_drain_per_tick")) { + drain = config.get("air_drain_per_tick").getAsInt(); + } + if (config.has("non_lethal_for_master")) { + nonLethal = config.get("non_lethal_for_master").getAsBoolean(); + } + } + drain = Math.max(1, drain); + return new ChokingComponent(drain, nonLethal); + } + + /** Air drained per tick (net after vanilla +4 restoration). */ + public int getAirDrainPerTick() { + return airDrainPerTick; + } + + /** Whether the choke is non-lethal when used by a master on their pet. */ + public boolean isNonLethalForMaster() { + return nonLethalForMaster; + } +} 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 ca16f26..3964db4 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 @@ -10,7 +10,12 @@ import org.jetbrains.annotations.Nullable; public enum ComponentType { LOCKABLE("lockable", LockableComponent::fromJson), RESISTANCE("resistance", ResistanceComponent::fromJson), - GAGGING("gagging", GaggingComponent::fromJson); + GAGGING("gagging", GaggingComponent::fromJson), + BLINDING("blinding", BlindingComponent::fromJson), + SHOCK("shock", ShockComponent::fromJson), + GPS("gps", GpsComponent::fromJson), + CHOKING("choking", ChokingComponent::fromJson), + ADJUSTABLE("adjustable", AdjustableComponent::fromJson); private final String jsonKey; private final Function factory; 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 new file mode 100644 index 0000000..272792a --- /dev/null +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/GpsComponent.java @@ -0,0 +1,45 @@ +package com.tiedup.remake.v2.bondage.component; + +import com.google.gson.JsonObject; + +/** + * Component: GPS tracking and safe zone for data-driven items. + * + * JSON config: + * {@code "gps": {"safe_zone_radius": 50, "public_tracking": false}} + */ +public class GpsComponent implements IItemComponent { + + private final int safeZoneRadius; + private final boolean publicTracking; + + private GpsComponent(int safeZoneRadius, boolean publicTracking) { + this.safeZoneRadius = safeZoneRadius; + this.publicTracking = publicTracking; + } + + public static IItemComponent fromJson(JsonObject config) { + int radius = 50; + boolean publicTracking = false; + if (config != null) { + if (config.has("safe_zone_radius")) { + radius = config.get("safe_zone_radius").getAsInt(); + } + if (config.has("public_tracking")) { + publicTracking = config.get("public_tracking").getAsBoolean(); + } + } + radius = Math.max(0, radius); + return new GpsComponent(radius, publicTracking); + } + + /** Safe zone radius in blocks. 0 = no safe zone (tracking only). */ + public int getSafeZoneRadius() { + return safeZoneRadius; + } + + /** Whether any player can track (not just the owner). */ + public boolean isPublicTracking() { + return publicTracking; + } +} 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 new file mode 100644 index 0000000..1c6d867 --- /dev/null +++ b/src/main/java/com/tiedup/remake/v2/bondage/component/ShockComponent.java @@ -0,0 +1,52 @@ +package com.tiedup.remake.v2.bondage.component; + +import com.google.gson.JsonObject; + +/** + * Component: shock collar behavior for data-driven items. + * + * JSON config: + * {@code "shock": {"damage": 2.0, "auto_interval": 0}} + * auto_interval: ticks between auto-shocks (0 = no auto-shock, manual only) + */ +public class ShockComponent implements IItemComponent { + + private final float damage; + private final int autoInterval; // 0 = manual only + + private ShockComponent(float damage, int autoInterval) { + this.damage = damage; + this.autoInterval = autoInterval; + } + + public static IItemComponent fromJson(JsonObject config) { + float damage = 2.0f; + int autoInterval = 0; + if (config != null) { + if (config.has("damage")) { + damage = config.get("damage").getAsFloat(); + } + if (config.has("auto_interval")) { + autoInterval = config.get("auto_interval").getAsInt(); + } + } + damage = Math.max(0.0f, damage); + autoInterval = Math.max(0, autoInterval); + return new ShockComponent(damage, autoInterval); + } + + /** Damage dealt per shock. */ + public float getDamage() { + return damage; + } + + /** Ticks between auto-shocks. 0 = manual shock only (via controller). */ + public int getAutoInterval() { + return autoInterval; + } + + /** Whether this item has auto-shock capability. */ + public boolean hasAutoShock() { + return autoInterval > 0; + } +} From 90bc890b95479d7603e0a60ed992032a001bb6ba Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 02:46:09 +0200 Subject: [PATCH 19/20] fix(D-01): synchronize reload paths and capture snapshot locally (RISK-001, RISK-002) --- .../datadriven/DataDrivenItemRegistry.java | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemRegistry.java b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemRegistry.java index 5e726e2..1c72af3 100644 --- a/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemRegistry.java +++ b/src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemRegistry.java @@ -48,6 +48,9 @@ public final class DataDrivenItemRegistry { */ private static volatile RegistrySnapshot SNAPSHOT = RegistrySnapshot.EMPTY; + /** Guards read-then-write sequences in {@link #reload} and {@link #mergeAll}. */ + private static final Object RELOAD_LOCK = new Object(); + private DataDrivenItemRegistry() {} /** @@ -59,9 +62,11 @@ public final class DataDrivenItemRegistry { public static void reload( Map newDefs ) { - Map defs = - Collections.unmodifiableMap(new HashMap<>(newDefs)); - SNAPSHOT = new RegistrySnapshot(defs, buildComponentHolders(defs)); + synchronized (RELOAD_LOCK) { + Map defs = + Collections.unmodifiableMap(new HashMap<>(newDefs)); + SNAPSHOT = new RegistrySnapshot(defs, buildComponentHolders(defs)); + } } /** @@ -78,13 +83,15 @@ public final class DataDrivenItemRegistry { public static void mergeAll( Map newDefs ) { - Map merged = new HashMap<>( - SNAPSHOT.definitions - ); - merged.putAll(newDefs); - Map defs = - Collections.unmodifiableMap(merged); - SNAPSHOT = new RegistrySnapshot(defs, buildComponentHolders(defs)); + synchronized (RELOAD_LOCK) { + Map merged = new HashMap<>( + SNAPSHOT.definitions + ); + merged.putAll(newDefs); + Map defs = + Collections.unmodifiableMap(merged); + SNAPSHOT = new RegistrySnapshot(defs, buildComponentHolders(defs)); + } } /** @@ -95,7 +102,8 @@ public final class DataDrivenItemRegistry { */ @Nullable public static DataDrivenItemDefinition get(ResourceLocation id) { - return SNAPSHOT.definitions.get(id); + RegistrySnapshot snap = SNAPSHOT; + return snap.definitions.get(id); } /** @@ -113,7 +121,8 @@ public final class DataDrivenItemRegistry { tag.getString(NBT_ITEM_ID) ); if (id == null) return null; - return SNAPSHOT.definitions.get(id); + RegistrySnapshot snap = SNAPSHOT; + return snap.definitions.get(id); } /** @@ -122,7 +131,8 @@ public final class DataDrivenItemRegistry { * @return unmodifiable collection of all definitions */ public static Collection getAll() { - return SNAPSHOT.definitions.values(); + RegistrySnapshot snap = SNAPSHOT; + return snap.definitions.values(); } /** @@ -168,7 +178,8 @@ public final class DataDrivenItemRegistry { */ @Nullable public static ComponentHolder getComponents(ResourceLocation id) { - return SNAPSHOT.holders.get(id); + RegistrySnapshot snap = SNAPSHOT; + return snap.holders.get(id); } /** From 7bd840705a4dd355ec0e4ec288b17d5774aa37c2 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Tue, 14 Apr 2026 02:51:37 +0200 Subject: [PATCH 20/20] docs: add components section to ARTIST_GUIDE.md Documents the 8 available gameplay components (lockable, resistance, gagging, blinding, shock, gps, choking, adjustable) with config fields, examples (GPS shock collar, adjustable blindfold), and usage tips. --- docs/ARTIST_GUIDE.md | 84 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/docs/ARTIST_GUIDE.md b/docs/ARTIST_GUIDE.md index 3b72aeb..aa1eb79 100644 --- a/docs/ARTIST_GUIDE.md +++ b/docs/ARTIST_GUIDE.md @@ -16,7 +16,7 @@ 7. [Animations](#animations) — item poses, fallback chain, variants, context animations 8. [Animation Templates](#animation-templates) 9. [Exporting from Blender](#exporting-from-blender) -10. [The JSON Definition](#the-json-definition) +10. [The JSON Definition](#the-json-definition) — field reference, components, pose priority, movement styles 11. [Packaging as a Resource Pack](#packaging-as-a-resource-pack) 12. [Common Mistakes](#common-mistakes) 13. [Examples](#examples) @@ -764,6 +764,7 @@ The `movement_style` changes how the player physically moves — slower speed, d | `movement_modifier` | object | No | Override speed/jump for the movement style (requires `movement_style`) | | `creator` | string | No | Author/creator name, shown in the item tooltip | | `animation_bones` | object | Yes | Per-animation bone whitelist (see below) | +| `components` | object | No | Gameplay behavior components (see [Components](#components-gameplay-behaviors) below) | ### animation_bones (required) @@ -785,6 +786,87 @@ At runtime, the effective bones for a given animation clip are computed as the * This field is **required**. Items without `animation_bones` will be rejected by the parser. +### Components (Gameplay Behaviors) + +Components add gameplay behaviors to your item without requiring Java code. Each component is a self-contained module you declare in the `"components"` block of your JSON definition. + +**Format:** A JSON object where each key is a component name and each value is the component's configuration (an object, or `true` for defaults). + +```json +"components": { + "lockable": { "lock_resistance": 200 }, + "resistance": { "base": 150 }, + "gagging": { "comprehension": 0.2, "range": 10.0 } +} +``` + +Items without the `"components"` field work normally — components are entirely optional. + +#### Available Components + +| Component | Description | Config Fields | +|-----------|-------------|---------------| +| `lockable` | Item can be locked with a padlock. Locked items cannot be unequipped. | `lock_resistance` (int, default: 250) — resistance added by the lock for struggle mechanics | +| `resistance` | Struggle resistance. Higher = harder to escape. | `base` (int, default: 100) — base resistance value | +| `gagging` | Muffles the wearer's speech. | `comprehension` (0.0–1.0, default: 0.2) — how much speech is understandable. `range` (float, default: 10.0) — max hearing distance in blocks | +| `blinding` | Applies a blindfold overlay to the wearer's screen. | `overlay` (string, optional) — custom overlay texture path. Omit for default | +| `shock` | Item can shock the wearer (manually or automatically). | `damage` (float, default: 2.0) — damage per shock. `auto_interval` (int, default: 0) — ticks between auto-shocks (0 = manual only) | +| `gps` | GPS tracking and safe zone enforcement. | `safe_zone_radius` (int, default: 50) — safe zone in blocks (0 = tracking only). `public_tracking` (bool, default: false) — anyone can track, not just owner | +| `choking` | Drains air, applies darkness/slowness, deals damage when activated. | `air_drain_per_tick` (int, default: 8) — air drained per tick. `non_lethal_for_master` (bool, default: true) — won't kill if worn by a master's pet | +| `adjustable` | Allows Y-offset adjustment via GUI slider. | `default` (float, default: 0.0), `min` (float, default: -4.0), `max` (float, default: 4.0), `step` (float, default: 0.25) — all in pixels (1px = 1/16 block) | + +#### Example: Shock Collar with GPS + +```json +{ + "type": "tiedup:bondage_item", + "display_name": "GPS Shock Collar", + "model": "mycreator:models/gltf/gps_shock_collar.glb", + "regions": ["NECK"], + "animation_bones": { + "idle": [] + }, + "pose_priority": 10, + "escape_difficulty": 5, + "components": { + "lockable": { "lock_resistance": 300 }, + "resistance": { "base": 150 }, + "shock": { "damage": 3.0, "auto_interval": 200 }, + "gps": { "safe_zone_radius": 50 } + } +} +``` + +This collar can be locked (300 resistance to break the lock), has 150 base struggle resistance, shocks every 200 ticks (10 seconds) automatically, and enforces a 50-block safe zone. + +#### Example: Adjustable Blindfold + +```json +{ + "type": "tiedup:bondage_item", + "display_name": "Leather Blindfold", + "model": "mycreator:models/gltf/blindfold.glb", + "regions": ["EYES"], + "animation_bones": { + "idle": ["head"] + }, + "pose_priority": 10, + "escape_difficulty": 2, + "components": { + "blinding": {}, + "resistance": { "base": 80 }, + "adjustable": { "min": -2.0, "max": 2.0, "step": 0.5 } + } +} +``` + +#### Component Tips + +- **You can combine any components.** A gag with `gagging` + `lockable` + `resistance` + `adjustable` is perfectly valid. +- **Omit components you don't need.** A decorative collar with no shock/GPS just omits those components entirely. +- **Default values are sensible.** `"lockable": {}` gives you standard lock behavior with default resistance. You only need to specify fields you want to customize. +- **Components don't affect rendering.** They are purely gameplay — your GLB model and animations are independent of which components you use. + ### Pose Priority When multiple items affect the same bones, the highest `pose_priority` wins.