From 5b4fd4b61de50969c2da08d46b8f80e787810b40 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 8 May 2026 15:10:22 +0200 Subject: [PATCH] phase(N.4) Adjustment 6: add PartOverrides + HiddenPartsMask to WorldEntity Resolves Adjustment 4 (Option A): WorldEntity now carries the server- sent AnimPartChange data as PartOverrides and a HiddenPartsMask bitmask. EntitySpawnAdapter.OnCreate populates AnimatedEntityState from these fields at spawn time. GameWindow's CreateObject handler converts the network-layer AnimPartChange records into lightweight PartOverride structs. This unblocks Task 22: the WbDrawDispatcher can now resolve per-part GfxObj overrides and hidden-part suppression from entity state. Co-Authored-By: Claude Opus 4.6 --- ...026-05-08-phase-n4-rendering-foundation.md | 25 +++++++++++++++++++ src/AcDream.App/Rendering/GameWindow.cs | 14 +++++++++++ .../Rendering/Wb/EntitySpawnAdapter.cs | 22 ++++++---------- src/AcDream.Core/World/WorldEntity.cs | 23 +++++++++++++++++ 4 files changed, 70 insertions(+), 14 deletions(-) diff --git a/docs/superpowers/plans/2026-05-08-phase-n4-rendering-foundation.md b/docs/superpowers/plans/2026-05-08-phase-n4-rendering-foundation.md index 706b73f..590118b 100644 --- a/docs/superpowers/plans/2026-05-08-phase-n4-rendering-foundation.md +++ b/docs/superpowers/plans/2026-05-08-phase-n4-rendering-foundation.md @@ -992,6 +992,31 @@ behavior (does the adapter call the cache with the right args?). The decode-byte conformance is structural: same function = same output. Mark Task 20 ✅ structurally; no separate test file. +### Adjustment 6 (2026-05-08): Resolved Adjustment 4 — Option A (fields on WorldEntity) + +**Context.** Adjustment 4 deferred the `HiddenPartsMask` + `AnimPartChanges` +plumbing decision to Task 22. Two options: +- **A**: add fields to `WorldEntity`, populate at spawn time +- **B**: thread as separate args into `EntitySpawnAdapter.OnCreate` + +**Decision: Option A.** Reasoning: +1. The data is already computed at spawn time in GameWindow's CreateObject + handler — adding two fields is a 4-line change. +2. Option B would spread network-layer types across the streaming subsystem, + violating the same separation-of-concerns principle as Adjustment 2. +3. The 0xF625 ObjDescEvent (appearance update) replays through the same + spawn path, so WorldEntity fields work automatically for hot-swap updates. + +**Implementation:** +- `WorldEntity` gains `PartOverrides: IReadOnlyList` (default + empty) and `HiddenPartsMask: ulong` (default 0). +- `PartOverride(byte PartIndex, uint GfxObjId)` is a lightweight record struct + in Core.World that decouples from the network-layer `CreateObject.AnimPartChange`. +- `EntitySpawnAdapter.OnCreate` now calls `state.HideParts(entity.HiddenPartsMask)` + and `state.SetPartOverride(...)` for each override. +- GameWindow's CreateObject handler builds the `PartOverride[]` from the + server-sent `AnimPartChanges` list. + ### Task 6 (original — kept for history) **Files:** diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 28f3ff5..04e185b 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -2407,6 +2407,19 @@ public sealed class GameWindow : IDisposable SubPalettes: ranges); } + AcDream.Core.World.PartOverride[] entityPartOverrides; + if (animPartChanges.Count == 0) + { + entityPartOverrides = Array.Empty(); + } + else + { + entityPartOverrides = new AcDream.Core.World.PartOverride[animPartChanges.Count]; + for (int i = 0; i < animPartChanges.Count; i++) + entityPartOverrides[i] = new AcDream.Core.World.PartOverride( + animPartChanges[i].PartIndex, animPartChanges[i].NewModelId); + } + var entity = new AcDream.Core.World.WorldEntity { Id = _liveEntityIdCounter++, @@ -2416,6 +2429,7 @@ public sealed class GameWindow : IDisposable Rotation = rot, MeshRefs = meshRefs, PaletteOverride = paletteOverride, + PartOverrides = entityPartOverrides, }; var snapshot = new AcDream.Plugin.Abstractions.WorldEntitySnapshot( diff --git a/src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs b/src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs index 128d5dd..0315c94 100644 --- a/src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs +++ b/src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs @@ -39,15 +39,10 @@ namespace AcDream.App.Rendering.Wb; /// /// /// -/// Adjustment 4: does not currently expose -/// HiddenPartsMask or AnimPartChanges as direct fields (those -/// live on the network-layer spawn record and are consumed upstream before -/// the is built). When those fields are promoted to -/// , should call -/// and -/// here. For now the mask -/// stays at 0 (no parts hidden) and no part overrides are set — the draw -/// dispatcher falls through to Setup defaults for every part. +/// Adjustment 6 (resolved Adjustment 4): now +/// carries and +/// . applies +/// both to the created . /// /// public sealed class EntitySpawnAdapter @@ -125,11 +120,10 @@ public sealed class EntitySpawnAdapter var sequencer = _sequencerFactory(entity); var state = new AnimatedEntityState(sequencer); - // Adjustment 4 placeholder: when WorldEntity gains HiddenPartsMask + - // AnimPartChanges fields, apply them here: - // state.HideParts(entity.HiddenPartsMask); - // foreach (var apc in entity.AnimPartChanges) - // state.SetPartOverride(apc.PartIndex, apc.NewModelId); + // Adjustment 6: WorldEntity now carries PartOverrides + HiddenPartsMask. + state.HideParts(entity.HiddenPartsMask); + foreach (var po in entity.PartOverrides) + state.SetPartOverride(po.PartIndex, po.GfxObjId); _stateByGuid[entity.ServerGuid] = state; return state; diff --git a/src/AcDream.Core/World/WorldEntity.cs b/src/AcDream.Core/World/WorldEntity.cs index 33a4b2c..d1dfed4 100644 --- a/src/AcDream.Core/World/WorldEntity.cs +++ b/src/AcDream.Core/World/WorldEntity.cs @@ -55,4 +55,27 @@ public sealed class WorldEntity /// visible trunk, producing "partial passthrough" bugs. /// public float Scale { get; init; } = 1.0f; + + /// + /// Server-sent part-swap overrides from AnimPartChange. Each entry + /// replaces a Setup part's GfxObj with an alternate model (clothing, weapons, + /// helmets). Carried on the entity so EntitySpawnAdapter can populate + /// AnimatedEntityState's override map at spawn time. Empty for atlas- + /// tier entities. + /// + public IReadOnlyList PartOverrides { get; init; } = Array.Empty(); + + /// + /// Bitmask of hidden Setup parts. Bit i set hides part i at + /// draw time. Sourced from the server's CreateObject record when + /// present. Zero (no parts hidden) is the default. + /// + public ulong HiddenPartsMask { get; init; } } + +/// +/// Lightweight value type for a server-sent AnimPartChange (part index +/// → replacement GfxObj id). Decouples WorldEntity (Core) from the +/// network-layer CreateObject.AnimPartChange type. +/// +public readonly record struct PartOverride(byte PartIndex, uint GfxObjId);