Campaign CH round 4, user-gate items 1+2. Root cause: retail ships a second (background) glyph atlas per font, dilated 2px on every side, plus two border-pixel scalars (Font.NumHorizontalBorderPixels/ NumVerticalBorderPixels) that acdream's font reader never read — so even the pre-existing outline parameter drew almost nothing once enabled. Landed together (either half alone is a no-op or a regression): - UiDatFont carries BorderX/BorderY from the DAT font resource. - UiRenderContext.DrawStringDat inflates the background blit's source and destination rect by that margin and restructures into retail's exact two-pass whole-string outline-then-fill model (UIElement_Text::DrawSelf), plus the 8-neighbour +-1px fallback for fonts with no background atlas. Corrects the stale "property 0xd" comment to the real ids, 0x21 (Outline) / 0x22 (OutlineColor). - LayoutDesc property 0x21/0x22 import (ElementInfo.Outline/ OutlineColor, LayoutImporter.ReadState, ElementReader.Merge/ ApplyCanonicalLegacyProjection, DatWidgetFactory.BuildText) so every authored-outline element across the DAT set is correct at once. - SpewBox: RetailFontId corrected from a round-3 heuristic (0x40000025) to the actually-authored 0x40000001 (18px bold serif), Outline=true set on the controller's UiText. Fill colour stays the user-gate-round-1-pinned yellow — font atlases are alpha-only (PFID_A8), so there is no baked shading that could explain the screenshot's gold as anything other than the outline itself. - Chat transcript: default fill now seeds from its authored ARGB(255,204,204,204) instead of an unrelated color-table slot (ChatTranscriptRenderer.BuildLines takes the transcript's own DefaultColor as a parameter); the 34-entry LogTextType table is untouched, and every existing CH1 conformance test stays green unmodified. Regenerated the committed chat_2100006f.json fixture from the real installed DAT, confirming end to end (not by missing-field default) that the transcript carries no outline. Tests: font-reader border fields + inflation math pinned against the real DAT font, two-pass draw ordering/tint/inflation via a new TextRenderer.DebugSpriteSegmentVerts test seam, property 0x21/0x22 import at both the ElementReader.Merge and StateDesc-property layers, SpewBox font/outline, and the chat default-shade seed with the color table proven untouched. Full Release suite: 12,610 passed / 4 skipped / 0 failed (AcDream.slnx, complete solution). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
220 lines
7.8 KiB
C#
220 lines
7.8 KiB
C#
using System.Numerics;
|
|
using AcDream.App.UI.Layout;
|
|
using DatReaderWriter.Enums;
|
|
using DatReaderWriter.Types;
|
|
|
|
namespace AcDream.App.Tests.UI.Layout;
|
|
|
|
public sealed class UiPropertyBagTests
|
|
{
|
|
[Fact]
|
|
public void Merge_UsesKeyPresence_ForExplicitFalseAndZero()
|
|
{
|
|
var baseBag = new UiPropertyBag();
|
|
baseBag.Values[0x16u] = Bool(true);
|
|
baseBag.Values[0x14u] = Enum(3u);
|
|
|
|
var derivedBag = new UiPropertyBag();
|
|
derivedBag.Values[0x16u] = Bool(false);
|
|
derivedBag.Values[0x14u] = Enum(0u);
|
|
|
|
var merged = UiPropertyBag.Merge(baseBag, derivedBag);
|
|
|
|
Assert.False(merged.Values[0x16u].BoolValue);
|
|
Assert.Equal(0ul, merged.Values[0x14u].UnsignedValue);
|
|
}
|
|
|
|
[Fact]
|
|
public void EffectiveProperty_NamedStateOverridesDirectStateByPresence()
|
|
{
|
|
var info = new ElementInfo { DefaultStateId = 1u };
|
|
info.States[UiStateInfo.DirectStateId] = State(
|
|
UiStateInfo.DirectStateId,
|
|
"",
|
|
(0x16u, Bool(true)));
|
|
info.States[1u] = State(1u, "Normal", (0x16u, Bool(false)));
|
|
|
|
Assert.True(info.TryGetEffectiveProperty(0x16u, out var property));
|
|
Assert.False(property.BoolValue);
|
|
}
|
|
|
|
[Fact]
|
|
public void ElementMerge_ExplicitCenteredStatePropertyOverridesBaseLeft()
|
|
{
|
|
var baseInfo = new ElementInfo();
|
|
baseInfo.States[UiStateInfo.DirectStateId] = State(
|
|
UiStateInfo.DirectStateId,
|
|
"",
|
|
(0x14u, Enum(0u)));
|
|
|
|
var derivedInfo = new ElementInfo();
|
|
derivedInfo.States[UiStateInfo.DirectStateId] = State(
|
|
UiStateInfo.DirectStateId,
|
|
"",
|
|
(0x14u, Enum(1u)));
|
|
|
|
var merged = ElementReader.Merge(baseInfo, derivedInfo);
|
|
|
|
Assert.Equal(HJustify.Center, merged.HJustify);
|
|
Assert.Equal(1ul, merged.States[UiStateInfo.DirectStateId]
|
|
.Properties.Values[0x14u].UnsignedValue);
|
|
}
|
|
|
|
[Fact]
|
|
public void ElementMerge_RetailTextDefaultTwo_IsLeftJustified()
|
|
{
|
|
var info = new ElementInfo();
|
|
info.States[UiStateInfo.DirectStateId] = State(
|
|
UiStateInfo.DirectStateId,
|
|
"",
|
|
(0x14u, Enum(2u)));
|
|
|
|
var merged = ElementReader.Merge(new ElementInfo(), info);
|
|
|
|
Assert.Equal(HJustify.Left, merged.HJustify);
|
|
}
|
|
|
|
// ── Outline (0x21) / OutlineColor (0x22) — Campaign CH round 4 ─────────────
|
|
// Exercises ApplyCanonicalLegacyProjection (invoked by ElementReader.Merge)
|
|
// reading raw StateDesc-shaped properties, the same layer HJustify/FontColor
|
|
// are already pinned at above — one level below the flattened ElementInfo
|
|
// fields ElementReaderTests.cs covers.
|
|
|
|
/// <summary>
|
|
/// The SpewBox line template's own shape: state 0x10000002 authors property
|
|
/// 0x21 (Outline) = true with no 0x22 (OutlineColor) — retail leaves
|
|
/// m_curOutlineColor at its ctor black default in that case.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ElementMerge_Property0x21True_SetsOutline_WithNoAuthoredColor()
|
|
{
|
|
var info = new ElementInfo { DefaultStateId = 1u };
|
|
info.States[1u] = State(1u, "0x10000002", (0x21u, Bool(true)));
|
|
|
|
var merged = ElementReader.Merge(new ElementInfo(), info);
|
|
|
|
Assert.True(merged.Outline);
|
|
Assert.Null(merged.OutlineColor);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The chat transcript base style's own shape: DirectState authors neither
|
|
/// 0x21 nor 0x22 anywhere in its chain — Outline must stay false, matching
|
|
/// "the chat transcript is NOT outlined in retail" (research doc §2.5).
|
|
/// </summary>
|
|
[Fact]
|
|
public void ElementMerge_NoOutlineProperty_StaysFalse()
|
|
{
|
|
var info = new ElementInfo();
|
|
info.States[UiStateInfo.DirectStateId] = State(
|
|
UiStateInfo.DirectStateId,
|
|
"",
|
|
(0x1Bu, Color(204, 204, 204, 255)));
|
|
|
|
var merged = ElementReader.Merge(new ElementInfo(), info);
|
|
|
|
Assert.False(merged.Outline);
|
|
Assert.Null(merged.OutlineColor);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Property 0x22 (OutlineColor) reads a ColorBaseProperty into a normalized
|
|
/// Vector4 exactly like 0x1B (FontColor) does — one of the 9 elements in the
|
|
/// whole DAT set that authors a non-default outline colour (values observed:
|
|
/// ARGB(255,17,15,7) and ARGB(255,0,0,102)).
|
|
/// </summary>
|
|
[Fact]
|
|
public void ElementMerge_Property0x22_SetsOutlineColorFromAuthoredArgb()
|
|
{
|
|
var info = new ElementInfo();
|
|
info.States[UiStateInfo.DirectStateId] = State(
|
|
UiStateInfo.DirectStateId,
|
|
"",
|
|
(0x21u, Bool(true)),
|
|
(0x22u, Color(17, 15, 7, 255)));
|
|
|
|
var merged = ElementReader.Merge(new ElementInfo(), info);
|
|
|
|
Assert.True(merged.Outline);
|
|
Assert.Equal(new Vector4(17f / 255f, 15f / 255f, 7f / 255f, 1f), merged.OutlineColor);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertProperty_PreservesNestedRetailValues()
|
|
{
|
|
var source = new StructBaseProperty { MasterPropertyId = 0xA0u };
|
|
var array = new ArrayBaseProperty { MasterPropertyId = 0xA1u };
|
|
array.Value.Add(new IntegerBaseProperty { MasterPropertyId = 0xA2u, Value = -17 });
|
|
array.Value.Add(new Bitfield64BaseProperty
|
|
{
|
|
MasterPropertyId = 0xA3u,
|
|
Value = 0xFEDCBA9876543210ul,
|
|
});
|
|
source.Value[0x55u] = array;
|
|
source.Value[0x56u] = new VectorBaseProperty
|
|
{
|
|
MasterPropertyId = 0xA4u,
|
|
Value = new Vector3(1.25f, -2.5f, 9.75f),
|
|
};
|
|
|
|
var converted = LayoutImporter.ConvertProperty(source);
|
|
|
|
Assert.Equal(UiPropertyKind.Struct, converted.Kind);
|
|
Assert.Equal(0xA0u, converted.MasterPropertyId);
|
|
var convertedArray = converted.StructValue[0x55u];
|
|
Assert.Equal(UiPropertyKind.Array, convertedArray.Kind);
|
|
Assert.Equal(0xA1u, convertedArray.MasterPropertyId);
|
|
Assert.Equal(-17, convertedArray.ArrayValue[0].IntegerValue);
|
|
Assert.Equal(0xFEDCBA9876543210ul, convertedArray.ArrayValue[1].UnsignedValue);
|
|
Assert.Equal(new Vector3(1.25f, -2.5f, 9.75f), converted.StructValue[0x56u].VectorValue);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertProperty_PreservesStringInfoAndColorBytes()
|
|
{
|
|
var text = new StringInfoBaseProperty
|
|
{
|
|
Value = new StringInfo
|
|
{
|
|
Token = 7,
|
|
StringId = 0x12345678u,
|
|
TableId = 0x23000001u,
|
|
Override = StringInfoOverrideFlag.AutoGen,
|
|
English = 2,
|
|
Comment = 3,
|
|
},
|
|
};
|
|
var color = new ColorBaseProperty
|
|
{
|
|
Value = new ColorARGB { Blue = 1, Green = 2, Red = 3, Alpha = 4 },
|
|
};
|
|
|
|
var convertedText = LayoutImporter.ConvertProperty(text);
|
|
var convertedColor = LayoutImporter.ConvertProperty(color);
|
|
|
|
Assert.Equal(
|
|
new UiStringInfoValue(7, 0x12345678u, 0x23000001u, 2, 2, 3),
|
|
convertedText.StringInfoValue);
|
|
Assert.Equal(new UiColorValue(1, 2, 3, 4), convertedColor.ColorValue);
|
|
}
|
|
|
|
private static UiPropertyValue Bool(bool value)
|
|
=> new() { Kind = UiPropertyKind.Bool, BoolValue = value };
|
|
|
|
private static UiPropertyValue Enum(uint value)
|
|
=> new() { Kind = UiPropertyKind.Enum, UnsignedValue = value };
|
|
|
|
private static UiPropertyValue Color(byte red, byte green, byte blue, byte alpha)
|
|
=> new() { Kind = UiPropertyKind.Color, ColorValue = new UiColorValue(blue, green, red, alpha) };
|
|
|
|
private static UiStateInfo State(
|
|
uint id,
|
|
string name,
|
|
params (uint Id, UiPropertyValue Value)[] properties)
|
|
{
|
|
var state = new UiStateInfo { Id = id, Name = name };
|
|
foreach (var property in properties)
|
|
state.Properties.Values[property.Id] = property.Value;
|
|
return state;
|
|
}
|
|
}
|