Merge pull request 'feature/d01-component-system' (#5) from feature/d01-component-system into develop
Reviewed-on: #5
This commit was merged in pull request #5.
This commit is contained in:
@@ -16,7 +16,7 @@
|
|||||||
7. [Animations](#animations) — item poses, fallback chain, variants, context animations
|
7. [Animations](#animations) — item poses, fallback chain, variants, context animations
|
||||||
8. [Animation Templates](#animation-templates)
|
8. [Animation Templates](#animation-templates)
|
||||||
9. [Exporting from Blender](#exporting-from-blender)
|
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)
|
11. [Packaging as a Resource Pack](#packaging-as-a-resource-pack)
|
||||||
12. [Common Mistakes](#common-mistakes)
|
12. [Common Mistakes](#common-mistakes)
|
||||||
13. [Examples](#examples)
|
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`) |
|
| `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 |
|
| `creator` | string | No | Author/creator name, shown in the item tooltip |
|
||||||
| `animation_bones` | object | Yes | Per-animation bone whitelist (see below) |
|
| `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)
|
### 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.
|
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
|
### Pose Priority
|
||||||
|
|
||||||
When multiple items affect the same bones, the highest `pose_priority` wins.
|
When multiple items affect the same bones, the highest `pose_priority` wins.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
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<ComponentType, IItemComponent> components;
|
||||||
|
|
||||||
|
public ComponentHolder(Map<ComponentType, IItemComponent> 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 extends IItemComponent> T get(
|
||||||
|
ComponentType type,
|
||||||
|
Class<T> 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 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
public enum ComponentType {
|
||||||
|
LOCKABLE("lockable", LockableComponent::fromJson),
|
||||||
|
RESISTANCE("resistance", ResistanceComponent::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<JsonObject, IItemComponent> factory;
|
||||||
|
|
||||||
|
/** Pre-built lookup map for O(1) fromKey() instead of linear scan. */
|
||||||
|
private static final Map<String, ComponentType> BY_KEY;
|
||||||
|
static {
|
||||||
|
Map<String, ComponentType> map = new HashMap<>();
|
||||||
|
for (ComponentType type : values()) {
|
||||||
|
map.put(type.jsonKey, type);
|
||||||
|
}
|
||||||
|
BY_KEY = Collections.unmodifiableMap(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
ComponentType(
|
||||||
|
String jsonKey,
|
||||||
|
Function<JsonObject, IItemComponent> 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) {
|
||||||
|
return BY_KEY.get(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
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 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;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
comprehension = Math.max(0.0, Math.min(1.0, comprehension));
|
||||||
|
range = Math.max(0.0, range);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
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 boolean blocksUnequip(ItemStack stack, LivingEntity entity) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.tiedup.remake.v2.bondage.component;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component: lockable behavior for data-driven items.
|
||||||
|
*
|
||||||
|
* <p>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.</p>
|
||||||
|
*
|
||||||
|
* <p>Consumers retrieve the per-item lock resistance via
|
||||||
|
* {@link com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem#getItemLockResistance(net.minecraft.world.item.ItemStack)}.</p>
|
||||||
|
*
|
||||||
|
* JSON config:
|
||||||
|
* {@code "lockable": {}} or {@code "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
|
||||||
|
if (config != null && config.has("lock_resistance")) {
|
||||||
|
resistance = config.get("lock_resistance").getAsInt();
|
||||||
|
}
|
||||||
|
resistance = Math.max(0, resistance);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
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 final int baseResistance;
|
||||||
|
|
||||||
|
private ResistanceComponent(int baseResistance) {
|
||||||
|
this.baseResistance = baseResistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IItemComponent fromJson(JsonObject config) {
|
||||||
|
int base = 100;
|
||||||
|
if (config != null && config.has("base")) {
|
||||||
|
base = config.get("base").getAsInt();
|
||||||
|
}
|
||||||
|
base = Math.max(0, base);
|
||||||
|
return new ResistanceComponent(base);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the base resistance for this item.
|
||||||
|
*/
|
||||||
|
public int getBaseResistance() {
|
||||||
|
return baseResistance;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,11 @@ import com.tiedup.remake.v2.BodyRegionV2;
|
|||||||
import com.tiedup.remake.v2.bondage.IV2BondageEquipment;
|
import com.tiedup.remake.v2.bondage.IV2BondageEquipment;
|
||||||
import com.tiedup.remake.v2.bondage.V2BondageItems;
|
import com.tiedup.remake.v2.bondage.V2BondageItems;
|
||||||
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
|
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 com.tiedup.remake.v2.bondage.items.AbstractV2BondageItem;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -147,6 +152,21 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem {
|
|||||||
.entrySet()) {
|
.entrySet()) {
|
||||||
ItemStack stack = entry.getValue();
|
ItemStack stack = entry.getValue();
|
||||||
if (stack.getItem() == this) {
|
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 =
|
DataDrivenItemDefinition def =
|
||||||
DataDrivenItemRegistry.get(stack);
|
DataDrivenItemRegistry.get(stack);
|
||||||
if (def != null) {
|
if (def != null) {
|
||||||
@@ -257,6 +277,77 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem {
|
|||||||
return Component.literal(def.displayName());
|
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 <T> the component type
|
||||||
|
* @return the component, or null if the item is not data-driven or lacks this component
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static <T extends IItemComponent> T getComponent(
|
||||||
|
ItemStack stack,
|
||||||
|
ComponentType type,
|
||||||
|
Class<T> clazz
|
||||||
|
) {
|
||||||
|
ComponentHolder holder = DataDrivenItemRegistry.getComponents(stack);
|
||||||
|
if (holder == null) return null;
|
||||||
|
return holder.get(type, clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get per-item lock resistance from the LockableComponent, if present.
|
||||||
|
*
|
||||||
|
* <p>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}.</p>
|
||||||
|
*
|
||||||
|
* @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 =====
|
// ===== FACTORY =====
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.tiedup.remake.v2.bondage.datadriven;
|
package com.tiedup.remake.v2.bondage.datadriven;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
import com.tiedup.remake.v2.BodyRegionV2;
|
import com.tiedup.remake.v2.BodyRegionV2;
|
||||||
|
import com.tiedup.remake.v2.bondage.component.ComponentType;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
@@ -97,5 +99,19 @@ public record DataDrivenItemDefinition(
|
|||||||
*
|
*
|
||||||
* <p>This field is required in the JSON definition. Never null, never empty.</p>
|
* <p>This field is required in the JSON definition. Never null, never empty.</p>
|
||||||
*/
|
*/
|
||||||
Map<String, Set<String>> animationBones
|
Map<String, Set<String>> animationBones,
|
||||||
) {}
|
|
||||||
|
/** Raw component configs from JSON, keyed by ComponentType. */
|
||||||
|
Map<ComponentType, JsonObject> 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.containsKey(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ import com.google.gson.JsonElement;
|
|||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import com.google.gson.JsonParser;
|
import com.google.gson.JsonParser;
|
||||||
import com.tiedup.remake.v2.BodyRegionV2;
|
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.MovementModifier;
|
||||||
import com.tiedup.remake.v2.bondage.movement.MovementStyle;
|
import com.tiedup.remake.v2.bondage.movement.MovementStyle;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.EnumMap;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
@@ -266,6 +268,39 @@ public final class DataDrivenItemParser {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optional: components (per-component JSON configs)
|
||||||
|
Map<ComponentType, JsonObject> componentConfigs =
|
||||||
|
new EnumMap<>(ComponentType.class);
|
||||||
|
if (root.has("components")) {
|
||||||
|
JsonObject componentsObj = root.getAsJsonObject("components");
|
||||||
|
for (Map.Entry<String, JsonElement> entry :
|
||||||
|
componentsObj.entrySet()) {
|
||||||
|
ComponentType compType = ComponentType.fromKey(
|
||||||
|
entry.getKey()
|
||||||
|
);
|
||||||
|
if (compType != null) {
|
||||||
|
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(
|
||||||
|
"[DataDrivenItemParser] Unknown component type '{}' in item '{}'",
|
||||||
|
entry.getKey(),
|
||||||
|
fileId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Build the item ID from the file path
|
// Build the item ID from the file path
|
||||||
// fileId is like "tiedup:tiedup_items/leather_armbinder.json"
|
// fileId is like "tiedup:tiedup_items/leather_armbinder.json"
|
||||||
// We want "tiedup:leather_armbinder"
|
// We want "tiedup:leather_armbinder"
|
||||||
@@ -302,7 +337,8 @@ public final class DataDrivenItemParser {
|
|||||||
movementStyle,
|
movementStyle,
|
||||||
movementModifier,
|
movementModifier,
|
||||||
creator,
|
creator,
|
||||||
animationBones
|
animationBones,
|
||||||
|
componentConfigs
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
package com.tiedup.remake.v2.bondage.datadriven;
|
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.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.EnumMap;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import net.minecraft.nbt.CompoundTag;
|
import net.minecraft.nbt.CompoundTag;
|
||||||
@@ -13,9 +18,9 @@ import org.jetbrains.annotations.Nullable;
|
|||||||
* Thread-safe registry for data-driven bondage item definitions.
|
* Thread-safe registry for data-driven bondage item definitions.
|
||||||
*
|
*
|
||||||
* <p>Populated by the reload listener that scans {@code tiedup_items/} JSON files.
|
* <p>Populated by the reload listener that scans {@code tiedup_items/} JSON files.
|
||||||
* Uses volatile atomic swap (same pattern as {@link
|
* Uses a single volatile snapshot reference to ensure readers always see a
|
||||||
* com.tiedup.remake.client.animation.context.ContextGlbRegistry}) to ensure
|
* consistent pair of definitions + component holders. This prevents torn reads
|
||||||
* the render thread always sees a consistent snapshot.</p>
|
* where one map is updated but the other is stale.</p>
|
||||||
*
|
*
|
||||||
* <p>Lookup methods accept either a {@link ResourceLocation} ID directly
|
* <p>Lookup methods accept either a {@link ResourceLocation} ID directly
|
||||||
* or an {@link ItemStack} (reads the {@code tiedup_item_id} NBT tag).</p>
|
* or an {@link ItemStack} (reads the {@code tiedup_item_id} NBT tag).</p>
|
||||||
@@ -26,13 +31,25 @@ public final class DataDrivenItemRegistry {
|
|||||||
public static final String NBT_ITEM_ID = "tiedup_item_id";
|
public static final String NBT_ITEM_ID = "tiedup_item_id";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Volatile reference to an unmodifiable map. Reload builds a new map
|
* Immutable snapshot combining definitions and their component holders.
|
||||||
* and swaps atomically; consumer threads always see a consistent snapshot.
|
* Swapped atomically via a single volatile write to prevent torn reads.
|
||||||
*/
|
*/
|
||||||
private static volatile Map<
|
private record RegistrySnapshot(
|
||||||
ResourceLocation,
|
Map<ResourceLocation, DataDrivenItemDefinition> definitions,
|
||||||
DataDrivenItemDefinition
|
Map<ResourceLocation, ComponentHolder> holders
|
||||||
> DEFINITIONS = Map.of();
|
) {
|
||||||
|
static final RegistrySnapshot EMPTY = new RegistrySnapshot(Map.of(), Map.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 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() {}
|
private DataDrivenItemRegistry() {}
|
||||||
|
|
||||||
@@ -45,7 +62,11 @@ public final class DataDrivenItemRegistry {
|
|||||||
public static void reload(
|
public static void reload(
|
||||||
Map<ResourceLocation, DataDrivenItemDefinition> newDefs
|
Map<ResourceLocation, DataDrivenItemDefinition> newDefs
|
||||||
) {
|
) {
|
||||||
DEFINITIONS = Collections.unmodifiableMap(new HashMap<>(newDefs));
|
synchronized (RELOAD_LOCK) {
|
||||||
|
Map<ResourceLocation, DataDrivenItemDefinition> defs =
|
||||||
|
Collections.unmodifiableMap(new HashMap<>(newDefs));
|
||||||
|
SNAPSHOT = new RegistrySnapshot(defs, buildComponentHolders(defs));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,18 +75,23 @@ public final class DataDrivenItemRegistry {
|
|||||||
* <p>On an integrated server, both the client (assets/) and server (data/) reload
|
* <p>On an integrated server, both the client (assets/) and server (data/) reload
|
||||||
* listeners populate this registry. Using {@link #reload} would cause the second
|
* listeners populate this registry. Using {@link #reload} would cause the second
|
||||||
* listener to overwrite the first's definitions. This method builds a new map
|
* listener to overwrite the first's definitions. This method builds a new map
|
||||||
* from the existing snapshot + the new entries, then swaps atomically.</p>
|
* from the existing snapshot + the new entries, then swaps atomically as a
|
||||||
|
* single snapshot to prevent torn reads.</p>
|
||||||
*
|
*
|
||||||
* @param newDefs the definitions to merge (will overwrite existing entries with same key)
|
* @param newDefs the definitions to merge (will overwrite existing entries with same key)
|
||||||
*/
|
*/
|
||||||
public static void mergeAll(
|
public static void mergeAll(
|
||||||
Map<ResourceLocation, DataDrivenItemDefinition> newDefs
|
Map<ResourceLocation, DataDrivenItemDefinition> newDefs
|
||||||
) {
|
) {
|
||||||
Map<ResourceLocation, DataDrivenItemDefinition> merged = new HashMap<>(
|
synchronized (RELOAD_LOCK) {
|
||||||
DEFINITIONS
|
Map<ResourceLocation, DataDrivenItemDefinition> merged = new HashMap<>(
|
||||||
);
|
SNAPSHOT.definitions
|
||||||
merged.putAll(newDefs);
|
);
|
||||||
DEFINITIONS = Collections.unmodifiableMap(merged);
|
merged.putAll(newDefs);
|
||||||
|
Map<ResourceLocation, DataDrivenItemDefinition> defs =
|
||||||
|
Collections.unmodifiableMap(merged);
|
||||||
|
SNAPSHOT = new RegistrySnapshot(defs, buildComponentHolders(defs));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,7 +102,8 @@ public final class DataDrivenItemRegistry {
|
|||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public static DataDrivenItemDefinition get(ResourceLocation id) {
|
public static DataDrivenItemDefinition get(ResourceLocation id) {
|
||||||
return DEFINITIONS.get(id);
|
RegistrySnapshot snap = SNAPSHOT;
|
||||||
|
return snap.definitions.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,7 +121,8 @@ public final class DataDrivenItemRegistry {
|
|||||||
tag.getString(NBT_ITEM_ID)
|
tag.getString(NBT_ITEM_ID)
|
||||||
);
|
);
|
||||||
if (id == null) return null;
|
if (id == null) return null;
|
||||||
return DEFINITIONS.get(id);
|
RegistrySnapshot snap = SNAPSHOT;
|
||||||
|
return snap.definitions.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -103,13 +131,85 @@ public final class DataDrivenItemRegistry {
|
|||||||
* @return unmodifiable collection of all definitions
|
* @return unmodifiable collection of all definitions
|
||||||
*/
|
*/
|
||||||
public static Collection<DataDrivenItemDefinition> getAll() {
|
public static Collection<DataDrivenItemDefinition> getAll() {
|
||||||
return DEFINITIONS.values();
|
RegistrySnapshot snap = SNAPSHOT;
|
||||||
|
return snap.definitions.values();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all definitions. Called on world unload or for testing.
|
* Clear all definitions. Called on world unload or for testing.
|
||||||
*/
|
*/
|
||||||
public static void clear() {
|
public static void clear() {
|
||||||
DEFINITIONS = Map.of();
|
SNAPSHOT = RegistrySnapshot.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== COMPONENT HOLDERS =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ComponentHolder for a data-driven item stack.
|
||||||
|
*
|
||||||
|
* <p>Captures the snapshot reference once to ensure consistent reads
|
||||||
|
* between the definition lookup and the holder lookup.</p>
|
||||||
|
*
|
||||||
|
* @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) {
|
||||||
|
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 snap.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) {
|
||||||
|
RegistrySnapshot snap = SNAPSHOT;
|
||||||
|
return snap.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<ResourceLocation, ComponentHolder> buildComponentHolders(
|
||||||
|
Map<ResourceLocation, DataDrivenItemDefinition> definitions
|
||||||
|
) {
|
||||||
|
Map<ResourceLocation, ComponentHolder> holders = new HashMap<>();
|
||||||
|
for (Map.Entry<ResourceLocation, DataDrivenItemDefinition> entry :
|
||||||
|
definitions.entrySet()) {
|
||||||
|
DataDrivenItemDefinition def = entry.getValue();
|
||||||
|
Map<ComponentType, IItemComponent> components =
|
||||||
|
new EnumMap<>(ComponentType.class);
|
||||||
|
for (Map.Entry<ComponentType, JsonObject> 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user