Merge claude/hopeful-maxwell-214a12 — D.2b UI Studio + faithful importer + Character window
UI Studio (preview panels through the production renderer), importer dat-fidelity (Fix A/B/C/4/5: the importer carries dat font/justification/color; boundary look=importer / state=runtime), and the Character window Attributes tab (reads as retail). 3062 tests green. Handoff: docs/research/2026-06-26-mockup-stage-handoff.md. # Conflicts: # docs/ISSUES.md # docs/architecture/retail-divergence-register.md
This commit is contained in:
commit
ca94b479bf
61 changed files with 125881 additions and 44 deletions
|
|
@ -667,6 +667,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
|
||||
|
|
@ -2296,7 +2302,15 @@ public sealed class GameWindow : IDisposable
|
|||
sendWield: (item, mask) => _liveSession?.SendGetAndWieldItem(item, mask),
|
||||
// Empty equip slots show a visible frame (same square as the inventory grid) so every
|
||||
// slot position is seen + usable; the live 3D character (the doll) is Slice 2.
|
||||
emptySlotSprite: contentsEmpty);
|
||||
emptySlotSprite: contentsEmpty,
|
||||
datFont: vitalsDatFont); // Slice 2: caption the "Slots" toggle button
|
||||
|
||||
// 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).");
|
||||
}
|
||||
|
|
@ -2407,6 +2421,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.
|
||||
|
|
@ -3898,6 +3922,121 @@ 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="DollEntityBuilder"/> 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.
|
||||
/// </summary>
|
||||
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<AcDream.Core.World.MeshRef>(pe.MeshRefs); // dressed parts (player's live frame)
|
||||
var doll = AcDream.App.Rendering.DollEntityBuilder.Build(
|
||||
pe.SourceGfxObjOrSetupId, meshRefsCopy, basePal, subs, parts);
|
||||
ApplyPaperdollPose(doll, pe.SourceGfxObjOrSetupId); // re-pose into the paperdoll "posing" stance
|
||||
_paperdollViewportRenderer.SetDoll(doll);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the paperdoll "posing" stance DID (the model pose — left leg back) exactly as retail:
|
||||
/// <c>gmPaperDollUI</c> ctor (decomp 174243) sets <c>m_didAnimation = DBObj::GetDIDByEnum(0x10000005, 7)</c>,
|
||||
/// which is <c>DBCache::GetDIDFromEnumStatic</c> (decomp 20380) = <c>master[MasterMapId][0x10000005]</c>
|
||||
/// → submap <c>0x25000009</c> → key <c>7</c>. (Icon effects use keys 1-6 of the same submap; key 7 is the
|
||||
/// paperdoll pose.) Per-race <c>UpdateForRace</c> override deferred — the ctor default applies to all
|
||||
/// body-types for now. Returns 0 if the chain can't resolve.
|
||||
/// </summary>
|
||||
private uint ResolvePaperdollPoseDid()
|
||||
{
|
||||
var dats = _dats;
|
||||
if (dats is null) return 0u;
|
||||
uint masterDid = (uint)dats.Portal.Header.MasterMapId;
|
||||
if (masterDid == 0) return 0u;
|
||||
if (!dats.Portal.TryGet<DatReaderWriter.DBObjs.EnumIDMap>(masterDid, out var master)) return 0u;
|
||||
// DBCache::GetDIDFromEnum (decomp 20380) resolves master[arg4] → submap, then submap[arg3].
|
||||
// GetDIDFromEnumStatic(0x10000005, 7) ⇒ arg3=0x10000005, arg4=7 ⇒ master[7] → submap, submap[0x10000005].
|
||||
if (!master.ClientEnumToID.TryGetValue(7u, out var subDid)) return 0u; // master-level key = 7
|
||||
if (!dats.Portal.TryGet<DatReaderWriter.DBObjs.EnumIDMap>(subDid, out var sub)) return 0u;
|
||||
return sub.ClientEnumToID.TryGetValue(0x10000005u, out var did) ? did : 0u; // submap index = 0x10000005
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-pose the doll's already-dressed parts into the paperdoll "posing" stance — the C# analog of
|
||||
/// retail <c>set_sequence_animation(doll, m_didAnimation, …)</c>. Keeps each cloned part's GfxObjId +
|
||||
/// surface overrides (the dressed appearance), but replaces its transform with the pose animation's
|
||||
/// frame-0 per-part transform, using the same Scale·Rotate·Translate composition as the per-frame
|
||||
/// animation tick (<see cref="TickAnimations"/>, GameWindow.cs ~9999). Static (the pose is fixed). If the
|
||||
/// DID does not resolve to an <c>Animation</c>, the doll keeps its cloned pose (no regression) — the log
|
||||
/// line surfaces what resolved so the pose is verified, not guessed.
|
||||
/// </summary>
|
||||
private void ApplyPaperdollPose(AcDream.Core.World.WorldEntity doll, uint setupId)
|
||||
{
|
||||
var dats = _dats;
|
||||
if (dats is null) return;
|
||||
// Retail's paperdoll doll is STATIC — it holds the pose animation's frame, it does NOT loop.
|
||||
_animatedEntities.Remove(doll.Id);
|
||||
// poseDID is cdb-CONFIRMED = 0x030003C0 (== retail gmPaperDollUI m_didAnimation for Horan; UpdateForRace
|
||||
// did not override). Crash guard: only load a 0x03xxxxxx (Animation) DID (a non-animation id parses a
|
||||
// garbage frame count → OOM).
|
||||
uint poseDid = ResolvePaperdollPoseDid();
|
||||
if ((poseDid >> 24) != 0x03u) return;
|
||||
var anim = dats.Get<DatReaderWriter.DBObjs.Animation>(poseDid);
|
||||
var setup = dats.Get<DatReaderWriter.DBObjs.Setup>(setupId);
|
||||
if (anim is null || setup is null || anim.PartFrames.Count == 0) return;
|
||||
|
||||
// Retail plays the pose animation once and HOLDS the last frame (set_sequence_animation framerate=0,
|
||||
// RedressCreature decomp 0x004a3c22). For 0x030003C0 the animation has only two distinct keyframes —
|
||||
// frame 0 (a transitional, raised/bent arm) and frames 1..N (all byte-identical: the settled stance,
|
||||
// arms down + one leg back) — verified by dumping the dat. The held pose is therefore the last frame.
|
||||
int frameIdx = anim.PartFrames.Count - 1;
|
||||
var frame = anim.PartFrames[frameIdx];
|
||||
var src = doll.MeshRefs;
|
||||
var reposed = new List<AcDream.Core.World.MeshRef>(src.Count);
|
||||
for (int i = 0; i < src.Count; i++)
|
||||
{
|
||||
var scale = i < setup.DefaultScale.Count ? setup.DefaultScale[i] : System.Numerics.Vector3.One;
|
||||
System.Numerics.Vector3 origin = System.Numerics.Vector3.Zero;
|
||||
System.Numerics.Quaternion orient = System.Numerics.Quaternion.Identity;
|
||||
if (i < frame.Frames.Count) { origin = frame.Frames[i].Origin; orient = frame.Frames[i].Orientation; }
|
||||
var transform = System.Numerics.Matrix4x4.CreateScale(scale)
|
||||
* System.Numerics.Matrix4x4.CreateFromQuaternion(orient)
|
||||
* System.Numerics.Matrix4x4.CreateTranslation(origin);
|
||||
reposed.Add(new AcDream.Core.World.MeshRef(src[i].GfxObjId, transform) { SurfaceOverrides = src[i].SurfaceOverrides });
|
||||
}
|
||||
doll.MeshRefs = reposed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -9329,6 +9468,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
|
||||
|
|
@ -13469,6 +13622,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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue