fix #203: preserve animation on appearance updates

This commit is contained in:
Erik 2026-07-11 00:25:29 +02:00
parent ff06aa3107
commit c7607f019c
9 changed files with 237 additions and 22 deletions

View file

@ -46,6 +46,28 @@ Copy this block when adding a new issue:
---
## #203 — Unequipping armor freezes character and world animations — FIXED
**Status:** DONE — 2026-07-11 (this change)
**Severity:** HIGH
**Component:** live entities / animation / appearance updates
**Description:** After armor was removed, the local character stopped animating
and animation-dependent world interactions such as doors appeared frozen.
**Root cause:** `0xF625 ObjDescEvent` was incorrectly handled as a full
despawn/respawn. That discarded the live `WorldEntity`, animation manager,
physics host, and collision registration while the player movement controller
still referenced the discarded sequencer. Named retail instead applies
`DoObjDescChangesFromDefault` to the existing object.
**Resolution:** Appearance hydration now mutates only the existing entity's
meshes, palette, part overrides, and bounds. Entity identity, animation playback,
movement, physics, collision, selection, and dead-reckoning state survive. The
classification cache is invalidated in place. A regression test pins entity and
sequencer identity across the update. User verified the exact equip/unequip flow
in the Release client.
## #191 — Tapping W (brief forward press) glides forward without playing the step animation
## #194 — WbDrawDispatcher._groups is never pruned (minor)
@ -7161,7 +7183,7 @@ threshold.
**Component:** rendering / `WbDrawDispatcher` / `EntityClassificationCache` / `LandblockLoader`
**Resolution.** New `EntityClassificationCache` keyed by `(entityId, landblockHint)` tuple in `src/AcDream.App/Rendering/Wb/EntityClassificationCache.cs`. The dispatcher routes static entities (NOT in `_animatedEntities`) through the cache — first-frame slow-path populates flat `CachedBatch[]` (one entry per (partIdx, batchIdx) with the part-relative `RestPose` and resolved `BindlessTextureHandle`); subsequent-frame cache hits skip classification entirely and append `cached.RestPose * entityWorld` to each matching group. Animated entities bypass. Invalidation fires from `RemoveLiveEntityByServerGuid` (per-entity, `0xF747`/`0xF625`) and `RemoveEntitiesFromLandblock` (per-LB, Near→Far demote + unload).
**Resolution.** New `EntityClassificationCache` keyed by `(entityId, landblockHint)` tuple in `src/AcDream.App/Rendering/Wb/EntityClassificationCache.cs`. The dispatcher routes static entities (NOT in `_animatedEntities`) through the cache — first-frame slow-path populates flat `CachedBatch[]` (one entry per (partIdx, batchIdx) with the part-relative `RestPose` and resolved `BindlessTextureHandle`); subsequent-frame cache hits skip classification entirely and append `cached.RestPose * entityWorld` to each matching group. Animated entities bypass. Invalidation fires from `RemoveLiveEntityByServerGuid` for real despawns (`0xF747`), directly from the in-place `0xF625` appearance mutation, and from `RemoveEntitiesFromLandblock` (per-LB, Near→Far demote + unload).
**Perf result.** Entity dispatcher cpu_us **median ~1200 µs, p95 ~1500 µs** at horizon-safe + High preset on AMD Radeon RX 9070 XT @ 1440p. Pre-Tier-1 baseline was ~3500m / ~4000p95. ~66% reduction in median, ~63% in p95. Well under the A.5 spec budget (median ≤ 2.0 ms, p95 ≤ 2.5 ms). No `BUDGET_OVER` flag observed.

View file

@ -292,6 +292,13 @@ Target state: every entity in the world — player, NPC, monster, lifestone,
door, chest — becomes a `GameEntity`. The renderer iterates them and draws.
The plugin API exposes them as `WorldEntitySnapshot`. GameWindow becomes thin.
Lifecycle invariant in both the current split model and the target `GameEntity`:
an `ObjDescEvent` changes appearance in place. It may replace resolved meshes,
palette ranges, part overrides, and visual bounds, but it must preserve entity
identity plus animation, motion, physics, collision, selection, and
dead-reckoning owners. Only a real delete/despawn tears those owners down. This
matches retail's `CPhysicsObj::DoObjDescChangesFromDefault` behavior.
---
## Per-Frame Update Order (current runtime)

View file

@ -3,6 +3,13 @@
**Created:** 2026-05-10, opening move of the ISSUE #53 retry session.
**Purpose:** enumerate every code path that writes to `WorldEntity.MeshRefs` (the dispatcher's load-bearing per-entity input) and every adjacent state read by `WbDrawDispatcher.ClassifyBatches` / model-matrix composition, classify each as STATIC or DYNAMIC, and design the cache invalidation surface BEFORE touching renderer code.
> **Superseded detail (2026-07-11):** this historical audit described
> `0xF625 ObjDescEvent` as despawn + respawn. That was an acdream implementation
> artifact, not retail behavior, and caused animation state to freeze after
> equip/unequip. Appearance updates now mutate the existing entity and invalidate
> its classification cache in place, matching
> `CPhysicsObj::DoObjDescChangesFromDefault`. Real despawns remain unchanged.
This audit is the load-bearing prerequisite the prior Tier 1 attempt (commit `3639a6f`, reverted at `9b49009`) skipped. Cache design follows from the audit, not the other way around.
---

View file

@ -6,6 +6,11 @@
**Originating issue:** [docs/ISSUES.md](../../ISSUES.md) §#53.
**Phase context:** Phase Post-A.5 polish, Priority 3 (only remaining priority after #52 + #54 closed).
> **Historical correction (2026-07-11):** this spec inherited the old assumption
> that `0xF625 ObjDescEvent` was a despawn/respawn. The cache behavior remains
> valid, but appearance updates now preserve entity identity and invalidate the
> entity's cache entry in place, matching retail.
---
## §1. Problem