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:
Erik 2026-06-26 12:33:51 +02:00
commit ca94b479bf
61 changed files with 125881 additions and 44 deletions

View file

@ -0,0 +1,41 @@
using System.Numerics;
using AcDream.App.Rendering;
using Xunit;
namespace AcDream.App.Tests.Rendering;
public class DollCameraTests
{
[Fact]
public void Eye_position_is_retail_verbatim()
{
var cam = new DollCamera { Aspect = 100f / 214f };
Assert.True(Matrix4x4.Invert(cam.View, out var inv));
var eye = inv.Translation;
// Retail UIElement_Viewport::SetCamera position (decomp 0x004a5a51-0x004a5a61).
Assert.Equal(0.12f, eye.X, 3);
Assert.Equal(-2.4f, eye.Y, 3);
Assert.Equal(0.88f, eye.Z, 3);
}
[Fact]
public void Look_axis_is_pure_plus_y_zero_yaw()
{
// retail SetCameraDirection(0,0,0) ⇒ IDENTITY view frame ⇒ camera looks straight down +Y.
// System.Numerics CreateLookAt: forward = -(M13, M23, M33). Any yaw (Target.x≠Eye.x) would put a
// non-zero X here and turn the doll's face away — the bug this guards against.
var cam = new DollCamera { Aspect = 100f / 214f };
var forward = -new Vector3(cam.View.M13, cam.View.M23, cam.View.M33);
Assert.Equal(0f, forward.X, 4);
Assert.Equal(1f, forward.Y, 4);
Assert.Equal(0f, forward.Z, 4);
}
[Fact]
public void Projection_is_finite_and_uses_aspect()
{
var cam = new DollCamera { Aspect = 1.5f };
Assert.True(float.IsFinite(cam.Projection.M11));
Assert.NotEqual(0f, cam.Projection.M34); // perspective w = -z term
}
}

View file

@ -0,0 +1,84 @@
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering;
using AcDream.Core.World;
using Xunit;
namespace AcDream.App.Tests.Rendering;
public class DollEntityBuilderTests
{
[Fact]
public void Builds_doll_entity_with_synthetic_guid_and_player_setup()
{
// SubPaletteRange uses: SubPaletteId, Offset (byte), Length (byte)
var doll = DollEntityBuilder.Build(
setupId: 0x0200_0001u,
meshRefs: new List<MeshRef>(),
basePaletteId: 0x04000ABCu,
subPalettes: new (uint SubPaletteId, byte Offset, byte Length)[] { (0x0F00_0001u, 0, 8) },
partOverrides: new (byte PartIndex, uint GfxObjId)[] { (2, 0x0100_0042u) });
Assert.Equal(0x0200_0001u, doll.SourceGfxObjOrSetupId);
Assert.Equal(DollEntityBuilder.DollServerGuid, doll.ServerGuid);
Assert.NotEqual(0u, doll.ServerGuid);
Assert.Single(doll.PartOverrides);
Assert.Equal((byte)2, doll.PartOverrides[0].PartIndex);
Assert.Equal(0x0100_0042u, doll.PartOverrides[0].GfxObjId);
Assert.NotNull(doll.PaletteOverride);
Assert.Equal(0x04000ABCu, doll.PaletteOverride!.BasePaletteId);
Assert.Single(doll.PaletteOverride.SubPalettes);
Assert.Equal(0x0F00_0001u, doll.PaletteOverride.SubPalettes[0].SubPaletteId);
}
[Fact]
public void Null_overrides_give_empty_collections_not_null()
{
var doll = DollEntityBuilder.Build(0x0200_0001u, new List<MeshRef>(), null, null, null);
// No subpalettes => no PaletteOverride (mirrors GameWindow: only built when Count > 0)
Assert.Null(doll.PaletteOverride);
Assert.Empty(doll.PartOverrides);
}
[Fact]
public void Empty_subpalettes_give_null_palette_override()
{
// Mirror GameWindow: SubPalettes with Count == 0 => no override
var doll = DollEntityBuilder.Build(
0x0200_0001u,
new List<MeshRef>(),
basePaletteId: 0x04000001u,
subPalettes: System.Array.Empty<(uint, byte, byte)>(),
partOverrides: null);
Assert.Null(doll.PaletteOverride);
}
[Fact]
public void Heading_is_normalized_quaternion_facing_viewer()
{
var doll = DollEntityBuilder.Build(0x0200_0001u, new List<MeshRef>(), null, null, null);
Assert.True(System.MathF.Abs(doll.Rotation.LengthSquared() - 1f) < 1e-3f);
}
[Fact]
public void Position_is_world_origin()
{
var doll = DollEntityBuilder.Build(0x0200_0001u, new List<MeshRef>(), null, null, null);
Assert.Equal(Vector3.Zero, doll.Position);
}
[Fact]
public void ParentCellId_is_null_for_doll_scene()
{
var doll = DollEntityBuilder.Build(0x0200_0001u, new List<MeshRef>(), null, null, null);
Assert.Null(doll.ParentCellId);
}
[Fact]
public void MeshRefs_are_passed_through()
{
var refs = new List<MeshRef> { new MeshRef(0x01000001u, Matrix4x4.Identity) };
var doll = DollEntityBuilder.Build(0x0200_0001u, refs, null, null, null);
Assert.Same(refs, doll.MeshRefs);
}
}