using System.IO; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Content; using AcDream.Core.Textures; using DatReaderWriter; using DatReaderWriter.Options; using Palette = DatReaderWriter.DBObjs.Palette; using RenderSurface = DatReaderWriter.DBObjs.RenderSurface; using StringTable = DatReaderWriter.DBObjs.StringTable; namespace AcDream.App.Tests.UI.Layout; /// /// Installed-retail-DAT acceptance gate for LA8. Opt in with /// ACDREAM_PROBE_LIVE_MOUNT=1; ACDREAM_DAT_DIR can override the /// ordinary Documents/Asheron's Call location. Reads the DATs read-only. /// public sealed class CharacterManagementLiveDatTests { [InstalledDatFact] public void EnumTable5_ResolvesAndImportsTheExactRetailScreenAndDialogs() { string datDirectory = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR") ?? Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Documents", "Asheron's Call"); using var dats = new DatCollection(datDirectory, DatAccessType.Read); const uint expectedLayoutDid = 0x21000004u; uint layoutDid = RetailDataIdResolver.Resolve( dats, CharacterManagementUiController.RootEnum, 5u); Assert.Equal(expectedLayoutDid, layoutDid); Console.WriteLine( "[LA8-DAT] category=5 enum=0x10000005 -> DID=0x21000004; " + "selected-root=0x1000039A"); ElementInfo rootInfo = Assert.IsType( LayoutImporter.ImportInfos( dats, layoutDid, CharacterManagementUiController.RootElementId)); Assert.Equal(800f, rootInfo.Width); Assert.Equal(600f, rootInfo.Height); Assert.Equal(8, rootInfo.Children.Count); Assert.Equal(0x06007576u, rootInfo.StateMedia[""].File); ImportedLayout screen = LayoutImporter.Build( rootInfo, _ => (0u, 0, 0), null, null, new DatStringResolver(dats).Resolve); var list = Assert.IsType(screen.FindElement( CharacterManagementUiController.ListElementId)); UiTemplateListEntry template = Assert.Single(list.Templates); Assert.Equal(expectedLayoutDid, template.TemplateLayoutId); Assert.Equal(0x100003A5u, template.TemplateElementId); AssertButton(screen, CharacterManagementUiController.CreateElementId, "Create Character"); AssertButton(screen, CharacterManagementUiController.EnterElementId, "ENTER"); AssertButton(screen, CharacterManagementUiController.DeleteElementId, "DELETE"); AssertButton(screen, CharacterManagementUiController.RestoreElementId, "RESTORE"); // Finding 1: retail's bottom-row Credits/Exit buttons (offsets 6/7 // from the listbox base in // gmCharacterManagementUI::ListenToElementMessage@0x004ed5a0). AssertButton(screen, CharacterManagementUiController.CreditsElementId, "CREDITS"); AssertButton(screen, CharacterManagementUiController.ExitElementId, "EXIT"); // Finding 3: the World box (retail element 0x1000039B, resolved via // UpdateWorldName@0x004ec120) imports as a plain UiText the // controller binds Runtime's ServerName-sourced snapshot field to. Assert.IsType(screen.FindElement( CharacterManagementUiController.WorldTextElementId)); Assert.DoesNotContain( Descendants(screen.Root), static element => element is UiViewport); ElementInfo rowInfo = Assert.IsType( LayoutImporter.ImportInfos(dats, layoutDid, template.TemplateElementId)); Assert.Equal(1u, rowInfo.Type); Assert.Equal(160f, rowInfo.Width); Assert.Equal(16f, rowInfo.Height); Assert.Equal(0x40000009u, rowInfo.FontDid); Assert.Equal( [ UiButtonStateMachine.Normal, UiButtonStateMachine.NormalRollover, UiButtonStateMachine.NormalPressed, UiButtonStateMachine.Highlight, UiButtonStateMachine.HighlightRollover, uint.MaxValue, ], rowInfo.States.Keys.Order().ToArray()); // Campaign LA gate round 2 finding 2: the row template's OWN authored // justify is Left (character names render left-aligned in retail, not // centered) — it carries three stateful Type-3 highlight-art children // (0x10000481-0x10000483, the Normal_rollover/Normal_pressed/Highlight/ // Highlight_rollover face art) and NO Type-12 caption child, so the row's // Left justify can only come from ElementInfo.HJustify directly, never a // lifted text child. DatWidgetFactory.BuildButton must honor it. Assert.Equal(HJustify.Left, rowInfo.HJustify); Assert.DoesNotContain(rowInfo.Children, static child => child.Type == 12u); ImportedLayout? builtRowLayout = LayoutImporter.Import( dats, template.TemplateLayoutId, template.TemplateElementId, _ => (0u, 0, 0), null, null); var builtRow = Assert.IsType(builtRowLayout!.Root); Assert.Equal(UiButton.LabelAlignment.Left, builtRow.LabelAlign); uint dialogDid = RetailDataIdResolver.Resolve(dats, 2u, 5u); Assert.Equal(0x2100003Cu, dialogDid); ImportedLayout message = BuildSelected(dats, dialogDid, 0x24u); Assert.IsType(message.Root); Assert.IsType(message.FindElement(0x3Eu)); Assert.IsType(message.FindElement(0x26u)); ImportedLayout delete = BuildSelected(dats, dialogDid, 0x2Cu); Assert.IsType(delete.Root); Assert.IsType(delete.FindElement(0x2Cu)); Assert.IsType(delete.FindElement(0x2Eu)); Assert.IsType(delete.FindElement(0x2Fu)); var strings = new DatStringResolver(dats); const uint table = 0x23000002u; Assert.Equal("DELETE", Resolve(strings, table, "ID_CharacterManagement_DeleteCharacterResponse")); Assert.Equal("Please Wait", Resolve(strings, table, "ID_CharacterManagement_PleaseWait")); Assert.Equal("Entering World", Resolve(strings, table, "ID_Character_EnteringWorld")); // Finding 1: MakeConfirmExitDialog@0x004ed250's text // (compute_str_hash("ID_CharacterManagement_ConfirmExit"), table // enum 0x10000002 -> 0x23000002). The raw DAT string carries a // literal two-character "\n" escape (this test's Resolve() helper // does not normalize it — RetailUiRuntime does, via // NormalizeRetailNewlines, before handing it to the controller). Assert.Equal("Are you sure you want to leave?\\n", Resolve(strings, table, "ID_CharacterManagement_ConfirmExit")); string confirmation = Assert.IsType(strings.ResolveTemplate( table, "ID_CharacterManagement_DeleteCharacterConfirmation", new Dictionary { [DatStringResolver.PlayerVariable] = "Test Character", })); Assert.Contains("Test Character", confirmation); Assert.Contains("'DELETE'", confirmation); StringTable stringTable = Assert.IsType(dats.Get(table)); var deleteEntry = stringTable.Strings[ DatStringResolver.ComputeHash( "ID_CharacterManagement_DeleteCharacterConfirmation")]; Assert.Equal([DatStringResolver.PlayerVariable], deleteEntry.Variables); } /// /// Campaign LA gate round 2 regression gate: EVERY non-zero StateMedia image id /// declared anywhere in the char-select screen's imported element tree (root + /// descendants + the listbox row template) must resolve to a RenderSurface in /// Portal/HighRes AND decode to something other than the 1x1 magenta placeholder. /// This is the class of gap that shipped LA8's full-screen background, listbox /// interior, and ENTER circular-fill magenta defect: root background 0x06007576 /// is PFID_CUSTOM_RAW_JPEG (a verbatim JFIF stream — see /// 's PFID_CUSTOM_RAW_JPEG /// branch for the retail mechanism), which the decoder previously had no case for /// and silently fell through to magenta. The listbox (0x1000039D) itself carries /// no own background media — it is a transparent container, so the fix for the /// ONE root id also cleared the listbox-interior and ENTER-circle symptoms (both /// were the broken root bleeding through transparent regions on top of it), which /// this test's per-id sweep proves by finding no OTHER magenta id. /// [InstalledDatFact] public void EveryDeclaredMediaId_ResolvesToADecodableTexture() { string datDirectory = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR") ?? Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Documents", "Asheron's Call"); using var dats = new DatCollection(datDirectory, DatAccessType.Read); uint layoutDid = RetailDataIdResolver.Resolve( dats, CharacterManagementUiController.RootEnum, 5u); ElementInfo rootInfo = Assert.IsType( LayoutImporter.ImportInfos( dats, layoutDid, CharacterManagementUiController.RootElementId)); var ids = new SortedDictionary(); CollectMediaIds(rootInfo, "root", ids); // Also walk the listbox's row template — it is imported separately by // AddItemFromTemplateList/TemplateResolver, not as a root descendant. ElementInfo? listInfo = FindById(rootInfo, CharacterManagementUiController.ListElementId); Assert.NotNull(listInfo); Assert.NotEmpty(listInfo!.TemplateList); foreach (var entry in listInfo.TemplateList) { ElementInfo? rowInfo = LayoutImporter.ImportInfos( dats, entry.TemplateLayoutId, entry.TemplateElementId); Assert.NotNull(rowInfo); CollectMediaIds(rowInfo!, "row-template", ids); } Console.WriteLine($"[LA8-DIAG] layout=0x{layoutDid:X8} distinct media ids={ids.Count}"); Assert.NotEmpty(ids); // The known root-background id must actually be present in this sweep — // otherwise the assertions below would vacuously pass without ever having // exercised the surface that caused the defect. Assert.Contains(0x06007576u, ids.Keys); var unresolved = new List(); foreach (var (id, where) in ids) { bool found = dats.Portal.TryGet(id, out RenderSurface? rs) || dats.HighRes.TryGet(id, out rs); if (!found) { Console.WriteLine($"[LA8-DIAG] 0x{id:X8} ({where}): NOT FOUND in Portal or HighRes"); unresolved.Add($"0x{id:X8} ({where}): missing RenderSurface"); continue; } Palette? palette = rs!.DefaultPaletteId != 0 ? dats.Get(rs.DefaultPaletteId) : null; DecodedTexture decoded = SurfaceDecoder.DecodeRenderSurface(rs, palette); bool magenta = decoded.Width == 1 && decoded.Height == 1 && decoded.Rgba8 is [0xFF, 0x00, 0xFF, 0xFF]; Console.WriteLine( $"[LA8-DIAG] 0x{id:X8} ({where}): format={rs.Format} " + $"{rs.Width}x{rs.Height} defaultPalette=0x{rs.DefaultPaletteId:X8} " + $"paletteLoaded={(palette is not null)} decoded={decoded.Width}x{decoded.Height} " + $"magenta={magenta}"); if (magenta) unresolved.Add( $"0x{id:X8} ({where}): format={rs.Format} defaultPalette=0x{rs.DefaultPaletteId:X8} " + $"paletteLoaded={(palette is not null)}"); } Assert.True( unresolved.Count == 0, "Media ids that resolved to the 1x1 magenta placeholder:\n" + string.Join('\n', unresolved)); } /// /// Campaign LA gate round 2 (background-tiling investigation): the raw dat /// authors NO edge anchors at all on the char-select root (0x1000039A) — every /// one of LeftEdge/TopEdge/RightEdge/BottomEdge is 0 ("no anchor" per /// UIElement::UpdateForParentSizeChange, acclient 0x00462640). Retail's /// own edge-anchor resize mechanism therefore NEVER touches this element's size; /// it stays a fixed 800x600 rect in retail's own widget tree. This is the pivot /// fact behind the completed AD-98 substitution /// (): since the dat itself /// asks for no resize, whatever makes the char-select scene fill an arbitrary /// window resolution in retail (background AND buttons AND listbox together) /// cannot be a per-element anchor/draw-mode difference — it has to be an /// out-of-band presentation-time scale of the whole fixed-size frame. acdream /// therefore keeps the mounted root at its authored 800x600 extent and /// stretches the ENTIRE canvas — widgets, glyphs, art — as one unit at the /// renderer's quad chokepoint, with inverse mouse mapping. /// [InstalledDatFact] public void RootAuthorsNoEdgeAnchors_RetailNeverResizesItSelf() { string datDirectory = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR") ?? Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Documents", "Asheron's Call"); using var dats = new DatCollection(datDirectory, DatAccessType.Read); uint layoutDid = RetailDataIdResolver.Resolve( dats, CharacterManagementUiController.RootEnum, 5u); ElementInfo rootInfo = Assert.IsType( LayoutImporter.ImportInfos( dats, layoutDid, CharacterManagementUiController.RootElementId)); Assert.Equal(0u, rootInfo.Left); Assert.Equal(0u, rootInfo.Top); Assert.Equal(0u, rootInfo.Right); Assert.Equal(0u, rootInfo.Bottom); Assert.Equal(3u, rootInfo.Type); // UIElement_Field — generic container, not a custom gm*UI class id } private static ElementInfo? FindById(ElementInfo info, uint id) { if (info.Id == id) return info; foreach (ElementInfo child in info.Children) { ElementInfo? found = FindById(child, id); if (found is not null) return found; } return null; } private static void CollectMediaIds(ElementInfo info, string where, SortedDictionary ids) { foreach (var (stateName, media) in info.StateMedia) { if (media.File == 0) continue; string label = $"{where} elem=0x{info.Id:X8} type={info.Type} state='{stateName}'"; if (!ids.ContainsKey(media.File)) ids[media.File] = label; else ids[media.File] += " | " + label; } foreach (ElementInfo child in info.Children) CollectMediaIds(child, where, ids); } private static ImportedLayout BuildSelected( IDatReaderWriter dats, uint layoutDid, uint rootId) { ElementInfo info = Assert.IsType( LayoutImporter.ImportInfos(dats, layoutDid, rootId)); return LayoutImporter.Build( info, _ => (0u, 0, 0), null, null, new DatStringResolver(dats).Resolve); } private static string Resolve( DatStringResolver strings, uint table, string key) => Assert.IsType(strings.Resolve( table, DatStringResolver.ComputeHash(key))); private static void AssertButton( ImportedLayout layout, uint elementId, string label) => Assert.Equal( label, Assert.IsType(layout.FindElement(elementId)).Label); private static IEnumerable Descendants(UiElement root) { yield return root; foreach (UiElement child in root.Children) foreach (UiElement descendant in Descendants(child)) yield return descendant; } } internal sealed class InstalledDatFactAttribute : FactAttribute { public InstalledDatFactAttribute() { if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_LIVE_MOUNT") != "1") { Skip = "Set ACDREAM_PROBE_LIVE_MOUNT=1 to run the installed-DAT LA8 gate."; return; } string datDirectory = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR") ?? Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Documents", "Asheron's Call"); if (!File.Exists(Path.Combine(datDirectory, "client_portal.dat"))) Skip = $"Installed client_portal.dat is required at '{datDirectory}'."; } }