Broad consolidation of the V2 bondage-item, furniture-entity, and
client-side GLTF pipeline.
Parsing and rendering
- Shared GLB parsing helpers consolidated into GlbParserUtils
(accessor reads, weight normalization, joint-index clamping,
coordinate-space conversion, animation parse, primitive loop).
- Grow-on-demand Matrix4f[] scratch pool in GltfSkinningEngine and
GltfLiveBoneReader — removes per-frame joint-matrix allocation
from the render hot path.
- emitVertex helper dedups three parallel loops in GltfMeshRenderer.
- TintColorResolver.resolve has a zero-alloc path when the item
declares no tint channels.
- itemAnimCache bounded to 256 entries (access-order LRU) with
atomic get-or-compute under the map's monitor.
Animation correctness
- First-in-joint-order wins when body and torso both map to the
same PlayerAnimator slot; duplicate writes log a single WARN.
- Multi-item composites honor the FullX / FullHeadX opt-in that
the single-item path already recognized.
- Seat transforms converted to Minecraft model-def space so
asymmetric furniture renders passengers at the correct offset.
- GlbValidator: IBM count / type / presence, JOINTS_0 presence,
animation channel target validation, multi-skin support.
Furniture correctness and anti-exploit
- Seat assignment synced via SynchedEntityData (server is
authoritative; eliminates client-server divergence on multi-seat).
- Force-mount authorization requires same dimension and a free
seat; cross-dimension distance checks rejected.
- Reconnection on login checks for seat takeover before re-mount
and force-loads the target chunk for cross-dimension cases.
- tiedup_furniture_lockpick_ctx carries a session UUID nonce so
stale context can't misroute a body-item lockpick.
- tiedup_locked_furniture survives death without keepInventory
(Forge 1.20.1 does not auto-copy persistent data on respawn).
Lifecycle and memory
- EntityCleanupHandler fans EntityLeaveLevelEvent out to every
per-entity state map on the client.
- DogPoseRenderHandler re-keyed by UUID (stable across dimension
change; entity int ids are recycled).
- PetBedRenderHandler, PlayerArmHideEventHandler, and
HeldItemHideHandler use receiveCanceled + sentinel sets so
Pre-time mutations are restored even when a downstream handler
cancels the render.
Tests
- JUnit harness with 76+ tests across GlbParserUtils, GltfPoseConverter,
FurnitureSeatGeometry, and FurnitureAuthPredicate.
- 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.
- 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
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
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.