- Use substring instead of replace for path cleanup (anchored, no double-strip risk)
- Document that corner_decorations ignores x/z offsets (by design)
- Add x_offset/z_offset to PositionedBlock for multi-block furniture clusters
- Furniture items now spread across 2-3 positions (matching original Java code)
- Offsets multiplied by inward direction for correct corner orientation
- Fix has_ceiling_chain: only oubliette has ceiling chains (was true for all 6)
- Fix firstCornerSpecial: oubliette cauldron uses x/z offset +1 (inward diagonal)
- Update parser to read x_offset/z_offset (defaults to 0)
- BUG-001: CRYPT bottom_row had unreachable mossy_stone_bricks (same f variable
makes mossy_cobblestone guard trigger first). Fixed weights: 0.20/0.10/0.70
- BUG-003: ICE ceiling ice stalactites (y_offset=10) were missing from JSON
- BUG-002: ICE snow corner layers use middle value (3) as compromise since
DecorationConfig.PositionedBlock doesn't support random_property
- Extract 11 movement fields from PlayerBindState into PlayerMovement component
- Replace volatile isStruggling/struggleStartTick pair with atomic StruggleSnapshot record
- Remove 5+2 misleading synchronized keywords (different monitors, all server-thread-only)
- Update all 36 MovementStyleManager field accesses to use getMovement() getters/setters
Q1: Remove F9 debug toggle from GltfAnimationApplier + delete dead
GltfRenderLayer + remove keybind registration
Q2: Move HumanChairHelper from state/ to util/ — pure utility with
no state dependency. 7 import updates.
Q3: Wire NECK collar equip flow in DataDrivenBondageItem:
- Target must be tied up (V1 rule preserved)
- Distance + line-of-sight validation
- Owner added to NBT before equip via CollarHelper.addOwner()
- V2EquipmentHelper handles conflict resolution
- ModSounds.COLLAR_PUT played on success
- OwnershipComponent.onEquipped registers in CollarRegistry
KidnapperTheme: fix "mask_blindfold" → "blindfold_mask" across 8 themes.
The incorrect ID produced ghost items with no definition.
DispenserBehaviors: register GenericBondageDispenseBehavior.forAnyDataDriven()
for the V2 DATA_DRIVEN_ITEM singleton. Dispatches by region from the stack's
definition. V1 per-variant registrations were deleted but V2 replacement
was missing.
StruggleSessionManager: add V2EquipmentHelper.sync(player) after bind
resistance update to prevent data loss on server restart during struggle
HangingCagePiece: add fallback ResourceLocation arrays for worldgen when
DataDrivenItemRegistry is empty (race with reload listener on initial
world creation). Registry-first with hardcoded fallbacks.
- Hood: add MOUTH to blocked_regions — prevents double gag stacking
- 8 organic items (slime, vine, web, tape): set lockable:false at top
level for consistency with can_attach_padlock:false
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()