Previously, any GLB with keyframes on free bones would animate them,
even for standard animations like Idle. This caused accidental bone
hijacking — e.g., handcuffs freezing the player's head because the
artist keyframed all bones in Blender.
Now the Full prefix (FullIdle, FullStruggle, FullWalk) is enforced:
only Full-prefixed animations can animate free bones. Standard
animations (Idle, Struggle, Walk) only animate owned bones.
This aligns the code with the documented convention in ARTIST_GUIDE.md.
- DataDrivenItemParser: downgrade animation_bones absence log from INFO to
DEBUG (was spamming for every item without the optional field)
- GlbValidator: read 12-byte GLB header first and reject files declaring
totalLength > 50 MB before allocating (prevents OOM on malformed GLBs)
- GlbValidator: WEIGHTS_0 check now uses the same mesh selection logic as
GlbParser (prefer "Item", fallback to last non-Player) instead of
blindly checking the first mesh
Strip pipe-delimited armature prefixes (e.g., "MyRig|body" -> "body")
from bone names in parseSeatSkeleton and remapSeatAnimations, matching
the existing stripping already present in GlbParser. Without this,
isKnownBone checks fail when Blender exports prefixed bone names.
- 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