diff --git a/src/AcDream.App/Rendering/DollEntityBuilder.cs b/src/AcDream.App/Rendering/DollEntityBuilder.cs index 76d3bf2e..3dee4351 100644 --- a/src/AcDream.App/Rendering/DollEntityBuilder.cs +++ b/src/AcDream.App/Rendering/DollEntityBuilder.cs @@ -34,6 +34,17 @@ public static class DollEntityBuilder /// public const uint DollServerGuid = 0xDA11_D011u; + /// + /// Reserved render-local entity id for the doll. FIXED (the doll is a singleton) + /// and high enough never to collide with _liveEntityIdCounter ids. The + /// renderer passes this in animatedEntityIds so the dispatcher treats the + /// doll as animated — which BYPASSES the Tier-1 classification cache + /// (WbDrawDispatcher.cs:1142). That matters because a re-dress builds a NEW + /// WorldEntity with this SAME id; without the cache bypass the dispatcher would + /// serve the previous doll's cached batches and the new gear wouldn't appear. + /// + public const uint DollRenderId = 0xDA11_D012u; + // retail set_heading: 191.367905° about +Z, expressed as a Quaternion // (axis-angle, axis = (0,0,1), θ = 191.367905° in radians). private static readonly float _headingRad = 191.367905f * (MathF.PI / 180f); @@ -104,7 +115,7 @@ public static class DollEntityBuilder // replace it before registration. return new WorldEntity { - Id = 0u, + Id = DollRenderId, ServerGuid = DollServerGuid, SourceGfxObjOrSetupId = setupId, Position = Vector3.Zero, diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index a3ab1db0..860568d6 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -625,6 +625,12 @@ public sealed class GameWindow : IDisposable private AcDream.App.UI.Layout.InventoryController? _inventoryController; // Phase D.2b Sub-phase C — paperdoll equip-slot controller (wield drag handler). private AcDream.App.UI.Layout.PaperdollController? _paperdollController; + // Phase D.2b Sub-phase C Slice 2 — the 3-D doll viewport: an off-screen RTT renderer, the UiViewport + // widget that blits it, the inventory frame (for the open-gate), and a dirty flag (re-dress on 0xF625). + private AcDream.App.Rendering.PaperdollViewportRenderer? _paperdollViewportRenderer; + private AcDream.App.UI.UiViewport? _paperdollViewportWidget; + private AcDream.App.UI.UiNineSlicePanel? _inventoryFrame; + private bool _paperdollDollDirty = true; // Phase D.2b Task 9 — plugin UI registrations buffered before OnLoad; drained in OnLoad. private readonly AcDream.App.Plugins.BufferedUiRegistry? _uiRegistry; // Phase I.2: ImGui debug panel ViewModel. Lives for as long as @@ -2257,6 +2263,13 @@ public sealed class GameWindow : IDisposable // slot position is seen + usable; the live 3D character (the doll) is Slice 2. emptySlotSprite: contentsEmpty); + // Slice 2: capture the doll viewport widget (Type 0xD, element 0x100001D5) + the inventory + // frame so the per-frame pre-UI hook can render the 3-D doll into the widget's texture only + // when the window is open and in doll-view. The renderer itself is built after the + // WbDrawDispatcher exists (further down this method). + _paperdollViewportWidget = invLayout.FindElement(0x100001D5u) as AcDream.App.UI.UiViewport; + _inventoryFrame = inventoryFrame; + Console.WriteLine("[D.2b-B] retail inventory window from LayoutDesc importer (0x21000023)."); } else Console.WriteLine("[D.2b-B] inventory: LayoutDesc 0x21000023 not found."); @@ -2366,6 +2379,16 @@ public sealed class GameWindow : IDisposable // A.5 T22.5: apply A2C gate from quality preset. _wbDrawDispatcher.AlphaToCoverage = _resolvedQuality.AlphaToCoverage; + // Slice 2: now that the dispatcher exists, build the paperdoll doll RTT renderer and hand it to + // the viewport widget. Reuses the dispatcher + the scene-lighting UBO + _gl. The widget only + // blits the texture; the pre-UI hook drives the 3-D pass (see OnRender). + if (_paperdollViewportWidget is not null && _sceneLightingUbo is not null) + { + _paperdollViewportRenderer = new AcDream.App.Rendering.PaperdollViewportRenderer( + _gl, _wbDrawDispatcher, _sceneLightingUbo); + _paperdollViewportWidget.Renderer = _paperdollViewportRenderer; + } + // Phase A8: EnvCellRenderer init. Shares _meshShader (mesh_modern.{vert,frag}) // with the dispatcher — both consume the same global mesh buffer (VAO/IBO) // from ObjectMeshManager.GlobalBuffer. @@ -3751,6 +3774,52 @@ public sealed class GameWindow : IDisposable BasePaletteId = md.BasePaletteId, }; OnLiveEntitySpawned(newSpawn); + + // Slice 2: a player appearance change (equip / unequip) rebuilt _entitiesByServerGuid[player] + // above; flag the paperdoll doll to re-clone from it on the next doll pass (the C# analog of + // RedressCreature). Cheap flag — the rebuild is deferred to the pre-UI hook when visible. + if (update.Guid == _playerServerGuid) + _paperdollDollDirty = true; + } + + /// + /// Rebuilds the paperdoll doll from the live player entity (retail makeObject(player) + + /// DoObjDescChangesFromDefault). Clones the player's already-resolved Setup / MeshRefs / palette / + /// part overrides into a dedicated entity posed at the scene origin + /// facing the viewer. The MeshRefs are COPIED so the doll holds a stable pose independent of the + /// player's live in-world animation. Returns false (leaving the dirty flag set to retry) when the + /// player entity isn't available yet. + /// + private bool RefreshPaperdollDoll() + { + if (_paperdollViewportRenderer is null) return false; + if (!_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var pe) || pe.MeshRefs.Count == 0) + { + _paperdollViewportRenderer.SetDoll(null); + return false; // player not ready — retry next frame + } + + uint? basePal = null; + List<(uint, byte, byte)>? subs = null; + if (pe.PaletteOverride is { } po) + { + basePal = po.BasePaletteId; + subs = new List<(uint, byte, byte)>(); + foreach (var r in po.SubPalettes) subs.Add((r.SubPaletteId, r.Offset, r.Length)); + } + + List<(byte, uint)>? parts = null; + if (pe.PartOverrides.Count > 0) + { + parts = new List<(byte, uint)>(pe.PartOverrides.Count); + foreach (var p in pe.PartOverrides) parts.Add((p.PartIndex, p.GfxObjId)); + } + + var meshRefsCopy = new List(pe.MeshRefs); // frozen-pose snapshot + var doll = AcDream.App.Rendering.DollEntityBuilder.Build( + pe.SourceGfxObjOrSetupId, meshRefsCopy, basePal, subs, parts); + _paperdollViewportRenderer.SetDoll(doll); + return true; } /// @@ -9002,6 +9071,20 @@ public sealed class GameWindow : IDisposable SkipWorldGeometry: ; } + // Phase D.2b Sub-phase C Slice 2 — paperdoll 3-D doll: render the re-dressed player clone into the + // viewport's off-screen texture BEFORE the 2-D UI pass blits it, but only when the inventory window + // is open AND the paperdoll is in doll-view (the widget's Visible is driven by the Slots toggle). + // The 3-D pass is fully sealed in a GLStateScope so it doesn't disturb the world/UI state. + if (_options.RetailUi && _paperdollViewportRenderer is not null + && _paperdollViewportWidget is { Visible: true } dollWidget + && _inventoryFrame is { Visible: true }) + { + if (_paperdollDollDirty && RefreshPaperdollDoll()) + _paperdollDollDirty = false; + dollWidget.TextureHandle = _paperdollViewportRenderer.Render( + (int)dollWidget.Width, (int)dollWidget.Height); + } + // Phase D.2b — retail-look UI tree (render-only; input integration deferred). // Self-contained 2D pass: UiHost.Draw → TextRenderer.Flush sets its own // blend/depth state and restores. Drawn before ImGui so the devtools @@ -13081,6 +13164,7 @@ public sealed class GameWindow : IDisposable _terrain?.Dispose(); _terrainModernShader?.Dispose(); _sceneLightingUbo?.Dispose(); + _paperdollViewportRenderer?.Dispose(); // Slice 2: the doll's off-screen FBO + textures _particleRenderer?.Dispose(); _debugLines?.Dispose(); _uiHost?.Dispose(); diff --git a/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs b/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs index ba293f5a..d57a8200 100644 --- a/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs +++ b/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs @@ -49,6 +49,12 @@ public sealed unsafe class PaperdollViewportRenderer : IUiViewportRenderer, IDis // neverCullLandblockId the entity is never culled. 0 is unused by live streaming on the doll path. private const uint DollLandblockId = 0u; + // The doll's render id, passed as "animated" so the dispatcher bypasses the Tier-1 classification + // cache (WbDrawDispatcher.cs:1142) and re-classifies from the current MeshRefs every frame. This is + // what makes a re-dress (a new WorldEntity built with the same DollRenderId) actually take effect, + // AND what lets the idle animation (later slice, TickAnimations rebuilds MeshRefs) show. + private static readonly HashSet _dollAnimatedIds = new() { DollEntityBuilder.DollRenderId }; + public PaperdollViewportRenderer(GL gl, WbDrawDispatcher dispatcher, SceneLightingUboBinding lightUbo) { _gl = gl ?? throw new ArgumentNullException(nameof(gl)); @@ -105,7 +111,7 @@ public sealed unsafe class PaperdollViewportRenderer : IUiViewportRenderer, IDis frustum: null, neverCullLandblockId: DollLandblockId, visibleCellIds: null, - animatedEntityIds: null); + animatedEntityIds: _dollAnimatedIds); // doll treated as animated ⇒ cache-bypass (re-dress correctness) return _colorTex; }