# D-01 Phase 1: Data-Driven Item Component System > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Create a reusable component system so data-driven bondage items can declare gameplay behaviors (lockable, shock, GPS, gagging, etc.) in their JSON definition instead of requiring per-item Java classes. **Architecture:** Each component is a self-contained behavior module implementing `IItemComponent`. Components are declared in item JSON (`"components": {"shock": {...}}`), parsed by an extended `DataDrivenItemParser`, stored on `DataDrivenItemDefinition`, and ticked/queried via `DataDrivenBondageItem` delegation. The existing `ILockable` and `IHasResistance` interfaces are preserved as shared contracts — components implement them. **Tech Stack:** Java 17, Forge 1.20.1, existing V2 data-driven infrastructure (`DataDrivenItemRegistry`, `DataDrivenItemParser`, `DataDrivenItemDefinition`, `DataDrivenBondageItem`) **Scope:** This plan builds ONLY the component infrastructure + 3 core components (lockable, resistance, gagging). The remaining 5 components (shock, GPS, blinding, choking, adjustable) follow the same pattern and will be added in subsequent tasks or a follow-up plan. --- ## File Structure ### New files | File | Responsibility | |------|---------------| | `v2/bondage/component/IItemComponent.java` | Component interface: lifecycle hooks, tick, query | | `v2/bondage/component/ComponentType.java` | Enum of all component types with factory methods | | `v2/bondage/component/ComponentHolder.java` | Container: holds instantiated components for an item stack | | `v2/bondage/component/LockableComponent.java` | Lock/unlock, padlock, key matching, jam, lock resistance | | `v2/bondage/component/ResistanceComponent.java` | Struggle resistance with configurable base value | | `v2/bondage/component/GaggingComponent.java` | Muffled speech, comprehension %, range limit | ### Modified files | File | Changes | |------|---------| | `v2/bondage/datadriven/DataDrivenItemDefinition.java` | Add `Map componentConfigs` field | | `v2/bondage/datadriven/DataDrivenItemParser.java` | Parse `"components"` JSON block | | `v2/bondage/datadriven/DataDrivenBondageItem.java` | Delegate lifecycle hooks to components, expose `getComponent()` | | `v2/bondage/datadriven/DataDrivenItemRegistry.java` | Instantiate `ComponentHolder` per definition | --- ## Tasks ### Task 1: IItemComponent interface **Files:** - Create: `src/main/java/com/tiedup/remake/v2/bondage/component/IItemComponent.java` - [ ] **Step 1: Create the component interface** ```java package com.tiedup.remake.v2.bondage.component; import com.google.gson.JsonObject; 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. * *

Lifecycle: parse config once (from JSON), then tick/query per equipped entity.

*/ public interface IItemComponent { /** * Called when the item is equipped on an entity. * @param stack The equipped item stack * @param entity The entity wearing the item */ default void onEquipped(ItemStack stack, LivingEntity entity) {} /** * Called when the item is unequipped from an entity. * @param stack The unequipped item stack * @param entity The entity that was wearing the item */ default void onUnequipped(ItemStack stack, LivingEntity entity) {} /** * Called every tick while the item is equipped. * @param stack The equipped item stack * @param entity The entity wearing the item */ default void onWornTick(ItemStack stack, LivingEntity entity) {} /** * Whether this component prevents the item from being unequipped. * @param stack The equipped item stack * @param entity The entity wearing the item * @return true if unequip should be blocked */ default boolean blocksUnequip(ItemStack stack, LivingEntity entity) { return false; } } ``` - [ ] **Step 2: Verify file compiles** Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5` Expected: BUILD SUCCESSFUL - [ ] **Step 3: Commit** ```bash git add src/main/java/com/tiedup/remake/v2/bondage/component/IItemComponent.java git commit -m "feat(D-01): add IItemComponent interface for data-driven item behaviors" ``` --- ### Task 2: ComponentType enum **Files:** - Create: `src/main/java/com/tiedup/remake/v2/bondage/component/ComponentType.java` - [ ] **Step 1: Create the component type registry** ```java package com.tiedup.remake.v2.bondage.component; import com.google.gson.JsonObject; import javax.annotation.Nullable; import java.util.function.Function; /** * All known component types. Each type knows how to instantiate itself from JSON config. */ public enum ComponentType { LOCKABLE("lockable", LockableComponent::fromJson), RESISTANCE("resistance", ResistanceComponent::fromJson), GAGGING("gagging", GaggingComponent::fromJson); // Future: SHOCK, GPS, BLINDING, CHOKING, ADJUSTABLE 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); } /** * Look up a ComponentType by its JSON key. Returns null if unknown. */ @Nullable public static ComponentType fromKey(String key) { for (ComponentType type : values()) { if (type.jsonKey.equals(key)) { return type; } } return null; } } ``` Note: This file will not compile yet because `LockableComponent`, `ResistanceComponent`, and `GaggingComponent` don't exist. We'll create stub classes first, then implement them. - [ ] **Step 2: Create stub classes so the enum compiles** Create three empty stubs (they will be fully implemented in Tasks 4-6): `src/main/java/com/tiedup/remake/v2/bondage/component/LockableComponent.java`: ```java 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(); } } ``` `src/main/java/com/tiedup/remake/v2/bondage/component/ResistanceComponent.java`: ```java 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(); } } ``` `src/main/java/com/tiedup/remake/v2/bondage/component/GaggingComponent.java`: ```java 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(); } } ``` - [ ] **Step 3: Verify all files compile** Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5` Expected: BUILD SUCCESSFUL - [ ] **Step 4: Commit** ```bash git add src/main/java/com/tiedup/remake/v2/bondage/component/ git commit -m "feat(D-01): add ComponentType enum with stub component classes" ``` --- ### Task 3: ComponentHolder container **Files:** - Create: `src/main/java/com/tiedup/remake/v2/bondage/component/ComponentHolder.java` - [ ] **Step 1: Create the component container** ```java package com.tiedup.remake.v2.bondage.component; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.item.ItemStack; import javax.annotation.Nullable; import java.util.Collections; import java.util.EnumMap; import java.util.Map; /** * Holds instantiated components for an item definition. * Immutable after construction. One per DataDrivenItemDefinition. */ 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)); } /** * Get a component by type, or null if not present. */ @Nullable public IItemComponent get(ComponentType type) { return components.get(type); } /** * Get a component by type, cast to the expected class. * Returns null if not present or wrong 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; } /** * Check if a component type is present. */ public boolean has(ComponentType type) { return components.containsKey(type); } /** * Fire onEquipped for all components. */ public void onEquipped(ItemStack stack, LivingEntity entity) { for (IItemComponent component : components.values()) { component.onEquipped(stack, entity); } } /** * Fire onUnequipped for all components. */ public void onUnequipped(ItemStack stack, LivingEntity entity) { for (IItemComponent component : components.values()) { component.onUnequipped(stack, entity); } } /** * Fire onWornTick for all components. */ public void onWornTick(ItemStack stack, LivingEntity entity) { for (IItemComponent component : components.values()) { component.onWornTick(stack, entity); } } /** * Check if any component blocks unequip. */ public boolean blocksUnequip(ItemStack stack, LivingEntity entity) { for (IItemComponent component : components.values()) { if (component.blocksUnequip(stack, entity)) { return true; } } return false; } /** * Whether this holder has any components. */ public boolean isEmpty() { return components.isEmpty(); } } ``` - [ ] **Step 2: Verify compilation** Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5` Expected: BUILD SUCCESSFUL - [ ] **Step 3: Commit** ```bash git add src/main/java/com/tiedup/remake/v2/bondage/component/ComponentHolder.java git commit -m "feat(D-01): add ComponentHolder container for item components" ``` --- ### Task 4: Integrate components into DataDrivenItemDefinition + Parser **Files:** - Modify: `src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemDefinition.java` - Modify: `src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemParser.java` - [ ] **Step 1: Add componentConfigs field to DataDrivenItemDefinition** Read the current record definition, then add a new field. The record should get a new parameter: ```java /** Raw component configs from JSON, keyed by ComponentType. */ Map componentConfigs ``` Add after the last existing field in the record. Also add a convenience method: ```java /** * Whether this definition declares a specific component. */ public boolean hasComponent(ComponentType type) { return componentConfigs != null && componentConfigs.containsKey(type); } ``` - [ ] **Step 2: Parse "components" block in DataDrivenItemParser** Read `DataDrivenItemParser.java` and add parsing for the `"components"` JSON field. After parsing all existing fields, add: ```java // Parse components Map componentConfigs = new EnumMap<>(ComponentType.class); if (json.has("components")) { JsonObject componentsObj = json.getAsJsonObject("components"); for (Map.Entry entry : componentsObj.entrySet()) { ComponentType type = ComponentType.fromKey(entry.getKey()); if (type != null) { JsonObject config = entry.getValue().isJsonObject() ? entry.getValue().getAsJsonObject() : new JsonObject(); componentConfigs.put(type, config); } else { LOGGER.warn("[DataDrivenItemParser] Unknown component type '{}' in item '{}'", entry.getKey(), id); } } } ``` Pass `componentConfigs` to the `DataDrivenItemDefinition` record constructor. - [ ] **Step 3: Update all existing call sites that construct DataDrivenItemDefinition** Search for all `new DataDrivenItemDefinition(` calls and add `Map.of()` for the new parameter (for the network sync deserialization path, etc.). - [ ] **Step 4: Verify compilation** Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5` Expected: BUILD SUCCESSFUL - [ ] **Step 5: Commit** ```bash git add src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemDefinition.java git add src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemParser.java git commit -m "feat(D-01): parse component configs from item JSON definitions" ``` --- ### Task 5: Instantiate ComponentHolder in DataDrivenItemRegistry **Files:** - Modify: `src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemRegistry.java` - [ ] **Step 1: Add ComponentHolder cache** Read `DataDrivenItemRegistry.java`. Add a parallel cache that maps `ResourceLocation` to `ComponentHolder`. When definitions are loaded/reloaded, instantiate components from their `componentConfigs`. Add field: ```java private static volatile Map COMPONENT_HOLDERS = Map.of(); ``` In the reload/register method, after storing definitions, build component holders: ```java Map holders = new HashMap<>(); for (Map.Entry entry : newDefinitions.entrySet()) { DataDrivenItemDefinition def = entry.getValue(); Map components = new EnumMap<>(ComponentType.class); for (Map.Entry compEntry : def.componentConfigs().entrySet()) { components.put(compEntry.getKey(), compEntry.getKey().create(compEntry.getValue())); } holders.put(entry.getKey(), new ComponentHolder(components)); } COMPONENT_HOLDERS = Collections.unmodifiableMap(holders); ``` Add accessor: ```java @Nullable public static ComponentHolder getComponents(ItemStack stack) { DataDrivenItemDefinition def = get(stack); if (def == null) return null; return COMPONENT_HOLDERS.get(def.id()); } @Nullable public static ComponentHolder getComponents(ResourceLocation id) { return COMPONENT_HOLDERS.get(id); } ``` - [ ] **Step 2: Verify compilation** Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5` Expected: BUILD SUCCESSFUL - [ ] **Step 3: Commit** ```bash git add src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenItemRegistry.java git commit -m "feat(D-01): instantiate ComponentHolder per item definition on reload" ``` --- ### Task 6: Delegate DataDrivenBondageItem lifecycle to components **Files:** - Modify: `src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java` - [ ] **Step 1: Add component delegation in lifecycle hooks** Read `DataDrivenBondageItem.java`. In `onEquipped()` and `onUnequipped()`, delegate to components: ```java @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 `canUnequip` to check component blocks: ```java @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); } ``` Add a public static helper for external code to query components: ```java /** * Get a specific component from a data-driven item stack. * @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); } ``` - [ ] **Step 2: Verify compilation** Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5` Expected: BUILD SUCCESSFUL - [ ] **Step 3: Commit** ```bash git add src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java git commit -m "feat(D-01): delegate DataDrivenBondageItem lifecycle to components" ``` --- ### Task 7: Implement LockableComponent **Files:** - Modify: `src/main/java/com/tiedup/remake/v2/bondage/component/LockableComponent.java` - [ ] **Step 1: Implement full lockable logic** Replace the stub with the full implementation. Extract lock behavior from `ILockable` (which remains as a shared interface). The component reads its config from JSON and delegates to `ILockable` default methods on the item stack: ```java 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 to ILockable interface methods on the item. * * JSON config: *
{"lockable": true}
* or *
{"lockable": {"lock_resistance": 300}}
*/ public class LockableComponent implements IItemComponent { private final int lockResistance; private LockableComponent(int lockResistance) { this.lockResistance = lockResistance; } public static IItemComponent fromJson(JsonObject config) { int resistance = 250; // default from SettingsAccessor if (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 item implements ILockable, check if locked if (stack.getItem() instanceof ILockable lockable) { return lockable.isLocked(stack); } return false; } } ``` - [ ] **Step 2: Verify compilation** Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5` Expected: BUILD SUCCESSFUL - [ ] **Step 3: Commit** ```bash git add src/main/java/com/tiedup/remake/v2/bondage/component/LockableComponent.java git commit -m "feat(D-01): implement LockableComponent with configurable lock resistance" ``` --- ### Task 8: Implement ResistanceComponent **Files:** - Modify: `src/main/java/com/tiedup/remake/v2/bondage/component/ResistanceComponent.java` - [ ] **Step 1: Implement resistance logic** ```java package com.tiedup.remake.v2.bondage.component; import com.google.gson.JsonObject; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.item.ItemStack; /** * Component: struggle resistance for data-driven items. * Replaces IHasResistance for data-driven items. * * JSON config: *
{"resistance": {"base": 150}}
*/ public class ResistanceComponent implements IItemComponent { private final int baseResistance; private ResistanceComponent(int baseResistance) { this.baseResistance = baseResistance; } public static IItemComponent fromJson(JsonObject config) { int base = 100; // default if (config.has("base")) { base = config.get("base").getAsInt(); } return new ResistanceComponent(base); } /** * Get the base resistance for this item. * Used by DataDrivenBondageItem.getBaseResistance() to replace the MAX-scan workaround. */ public int getBaseResistance() { return baseResistance; } } ``` - [ ] **Step 2: Update DataDrivenBondageItem.getBaseResistance() to use ResistanceComponent** In `DataDrivenBondageItem.java`, update `getBaseResistance()`: ```java @Override public int getBaseResistance(LivingEntity entity) { // Try stack-aware component lookup first (fixes I-03: no more MAX scan) // Note: This method is called WITHOUT a stack parameter by IHasResistance. // We still need the MAX scan as fallback until IHasResistance gets a stack-aware method. if (entity != null) { IV2BondageEquipment equip = V2EquipmentHelper.getEquipment(entity); if (equip != null) { int maxDifficulty = -1; for (Map.Entry entry : equip.getAllEquipped().entrySet()) { ItemStack stack = entry.getValue(); if (stack.getItem() == this) { // Try component first ResistanceComponent comp = DataDrivenBondageItem.getComponent( stack, ComponentType.RESISTANCE, ResistanceComponent.class); if (comp != null) { maxDifficulty = Math.max(maxDifficulty, comp.getBaseResistance()); continue; } // Fallback to escape_difficulty from definition DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack); if (def != null) { maxDifficulty = Math.max(maxDifficulty, def.escapeDifficulty()); } } } if (maxDifficulty >= 0) return maxDifficulty; } } return 100; } ``` - [ ] **Step 3: Verify compilation** Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5` Expected: BUILD SUCCESSFUL - [ ] **Step 4: Commit** ```bash git add src/main/java/com/tiedup/remake/v2/bondage/component/ResistanceComponent.java git add src/main/java/com/tiedup/remake/v2/bondage/datadriven/DataDrivenBondageItem.java git commit -m "feat(D-01): implement ResistanceComponent, fixes I-03 MAX scan for stack-aware items" ``` --- ### Task 9: Implement GaggingComponent **Files:** - Modify: `src/main/java/com/tiedup/remake/v2/bondage/component/GaggingComponent.java` - [ ] **Step 1: Implement gagging logic** ```java package com.tiedup.remake.v2.bondage.component; import com.google.gson.JsonObject; /** * Component: gagging behavior for data-driven items. * Replaces IHasGaggingEffect for data-driven items. * * JSON config: *
{"gagging": {"comprehension": 0.2, "range": 10.0}}
*/ public class GaggingComponent implements IItemComponent { 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) { double comprehension = 0.2; // default: 20% understandable double range = 10.0; // default: 10 blocks 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; } } ``` - [ ] **Step 2: Verify compilation** Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5` Expected: BUILD SUCCESSFUL - [ ] **Step 3: Commit** ```bash git add src/main/java/com/tiedup/remake/v2/bondage/component/GaggingComponent.java git commit -m "feat(D-01): implement GaggingComponent with comprehension and range" ``` --- ### Task 10: Create a test item JSON using components **Files:** - Create: `src/main/resources/data/tiedup/tiedup_items/test_gag.json` - [ ] **Step 1: Create a JSON definition that uses the new component system** ```json { "type": "tiedup:bondage_item", "display_name": "Test Ball Gag", "model": "tiedup:models/gltf/v2/handcuffs/cuffs_prototype.glb", "regions": ["MOUTH"], "animation_bones": { "idle": [] }, "pose_priority": 10, "escape_difficulty": 3, "lockable": true, "components": { "lockable": { "lock_resistance": 200 }, "resistance": { "base": 80 }, "gagging": { "comprehension": 0.15, "range": 8.0 } } } ``` - [ ] **Step 2: Verify the mod loads without errors** Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make build 2>&1 | tail -5` Expected: BUILD SUCCESSFUL Check that the JSON parses by searching for component-related log output in the run logs (manual verification — start the game client with `make run`, check for errors in log). - [ ] **Step 3: Commit** ```bash git add src/main/resources/data/tiedup/tiedup_items/test_gag.json git commit -m "feat(D-01): add test_gag.json demonstrating component system" ``` --- ### Task 11: Verify and clean up - [ ] **Step 1: Full build verification** Run: `cd /home/user/Documents/Projet/Open-TiedUp! && make rebuild 2>&1 | tail -10` Expected: BUILD SUCCESSFUL with zero errors - [ ] **Step 2: Verify no regressions in existing items** Existing data-driven items (in `data/tiedup/tiedup_items/`) should continue working without the `"components"` field. The parser should handle missing components gracefully (empty map). - [ ] **Step 3: Reindex MCP** Run the MCP reindex to update the symbol table with new classes. - [ ] **Step 4: Final commit** ```bash git add -A git commit -m "feat(D-01): Phase 1 complete - data-driven item component system Adds IItemComponent interface, ComponentType enum, ComponentHolder container, and 3 core components (LockableComponent, ResistanceComponent, GaggingComponent). Components are declared in item JSON 'components' field, parsed by DataDrivenItemParser, instantiated by DataDrivenItemRegistry, and delegated by DataDrivenBondageItem. Existing items without components continue to work unchanged." ```