1. NECK region explicitly blocked in interactLivingEntity() — prevents
V2 collars equipping without ownership setup (latent, no JSONs yet)
2. Swap rollback safety: if re-equip fails after swap failure, drop the
old bind as item entity instead of losing it silently
3. GaggingComponent: cache GagMaterial at construction time — eliminates
valueOf() log spam on every chat message with misconfigured material
4. Dual-bind prevention: check both V1 isTiedUp() AND V2 region occupied
in TyingInteractionHelper and PacketSelfBondage to prevent equipping
V2 bind on top of V1 bind
PlayerEquipment.getCurrentBindResistance/setCurrentBindResistance and
getCurrentCollarResistance/setCurrentCollarResistance all checked
instanceof ItemBind/ItemCollar — V2 DataDrivenBondageItem silently
returned 0, making V2 items escapable in 1 struggle roll.
Fix: use instanceof IHasResistance which both V1 and V2 implement.
Also fix StruggleCollar.tighten() to read ResistanceComponent directly
for V2 collars instead of IHasResistance.getBaseResistance(entity)
which triggers the singleton MAX-scan across all equipped items.
Note: isItemLocked() dead code in StruggleState is a PRE-EXISTING bug
(x10 locked penalty never applied) — tracked for separate fix.
BUG-001: TyingInteractionHelper swap now checks V2EquipResult — on failure,
rolls back by re-equipping the old bind instead of leaving target untied
BUG-002: OwnershipComponent.onUnequipped no longer double-calls
unregisterWearer — onCollarRemoved already handles it. Suppressed-alert
path calls unregister directly since onCollarRemoved is skipped.
BUG-003: PacketSelfBondage handleV2SelfBind now clears completed tying
task from PlayerBindState to prevent blocking future tying interactions
RISK-003: StruggleCollar get/setResistanceState null-guard on player
QA-001: add instanceof V2TyingPlayerTask guard before cast to prevent
ClassCastException when a V1 TyingPlayerTask was still active
QA-002: remove stack.shrink(1) after tying completion — V2TyingPlayerTask
.onComplete() already consumes the held item via heldStack.shrink(1)
- onEquipped: apply RestraintEffectUtils speed reduction for non-Player
entities with ARMS region and legs bound. Full immobilization for
WRAP/LATEX_SACK pose types.
- onUnequipped: remove speed reduction for non-Player entities
- Players use MovementStyleManager (V2 tick-based), not this legacy path
- ResistanceComponent: resistanceId delegates to SettingsAccessor at runtime,
fallback to hardcoded base for backward compat
- GaggingComponent: material field delegates to GagMaterial enum from ModConfig,
explicit comprehension/range overrides take priority
- IItemComponent: add default appendTooltip() method
- ComponentHolder: iterate components for tooltip contribution
- 6 components implement appendTooltip (lockable, resistance, gagging, shock,
gps, choking)
- DataDrivenBondageItem: call holder.appendTooltip() in appendHoverText()
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.
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().
- 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.
- 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
- 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
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.
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.
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.
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.
Add componentConfigs field (Map<ComponentType, JsonObject>) 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.
- Add optional "creator" JSON field to display author name in tooltip
- Show body regions, movement style, lock status, and escape difficulty
- Show pose priority and item ID in advanced mode (F3+H)
- Update ARTIST_GUIDE.md field reference and test JSON
Strip all Phase references, TODO/FUTURE roadmap notes, and internal
planning comments from the codebase. Run Prettier for consistent
formatting across all Java files.