feat(D.2b): controls.ini stylesheet loader + apply title color
Adds ControlsIni — a minimal flat-INI reader for retail's controls.ini (#AARRGGBB alpha-first color tokens; case-insensitive section/key lookup; missing file returns an empty sheet with no throw). Wires the [title] color token into the vitals panel's UiLabel in GameWindow.OnLoad, with hardcoded white as the fallback. Visually a no-op (retail's [title] color is white), but proves the stylesheet plumbing end-to-end (D.2b §7). Three unit tests cover section parsing, #AARRGGBB decode, and graceful missing-file handling. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b18403da02
commit
97bd1d2f09
3 changed files with 112 additions and 1 deletions
|
|
@ -1748,12 +1748,20 @@ public sealed class GameWindow : IDisposable
|
||||||
return (t, w, h);
|
return (t, w, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase D.2b — optional retail stylesheet. controls.ini lives under
|
||||||
|
// the AC install (ACDREAM_AC_DIR); absent → source-verified fallback.
|
||||||
|
var controls = _options.AcDir is { } acDir
|
||||||
|
? AcDream.App.UI.ControlsIni.Load(System.IO.Path.Combine(acDir, "controls", "controls.ini"))
|
||||||
|
: AcDream.App.UI.ControlsIni.Parse(string.Empty);
|
||||||
|
var titleColor = controls.TryColor("title", "color", out var tc)
|
||||||
|
? tc : new System.Numerics.Vector4(1f, 1f, 1f, 1f);
|
||||||
|
|
||||||
var panel = new AcDream.App.UI.UiNineSlicePanel(ResolveChrome)
|
var panel = new AcDream.App.UI.UiNineSlicePanel(ResolveChrome)
|
||||||
{ Left = 10, Top = 30, Width = 220, Height = 96 };
|
{ Left = 10, Top = 30, Width = 220, Height = 96 };
|
||||||
panel.AddChild(new AcDream.App.UI.UiLabel
|
panel.AddChild(new AcDream.App.UI.UiLabel
|
||||||
{
|
{
|
||||||
Text = "Vitals", Left = 8, Top = 4,
|
Text = "Vitals", Left = 8, Top = 4,
|
||||||
TextColor = new System.Numerics.Vector4(1f, 1f, 1f, 1f),
|
TextColor = titleColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
var vm = _vitalsVm!;
|
var vm = _vitalsVm!;
|
||||||
|
|
|
||||||
65
src/AcDream.App/UI/ControlsIni.cs
Normal file
65
src/AcDream.App/UI/ControlsIni.cs
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace AcDream.App.UI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimal reader for retail's <c>controls.ini</c> — a flat INI with one
|
||||||
|
/// <c>[section]</c> per element type. Colors are <c>#AARRGGBB</c> (alpha
|
||||||
|
/// first). Optional: a missing file yields an empty sheet (callers fall back
|
||||||
|
/// to hardcoded defaults). See the D.2b spec §7.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ControlsIni
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, Dictionary<string, string>> _sections;
|
||||||
|
|
||||||
|
private ControlsIni(Dictionary<string, Dictionary<string, string>> s) => _sections = s;
|
||||||
|
|
||||||
|
/// <summary>Load from disk; returns an empty sheet if the file is absent.</summary>
|
||||||
|
public static ControlsIni Load(string path)
|
||||||
|
=> System.IO.File.Exists(path)
|
||||||
|
? Parse(System.IO.File.ReadAllText(path))
|
||||||
|
: new ControlsIni(new());
|
||||||
|
|
||||||
|
public static ControlsIni Parse(string text)
|
||||||
|
{
|
||||||
|
var sections = new Dictionary<string, Dictionary<string, string>>(System.StringComparer.OrdinalIgnoreCase);
|
||||||
|
Dictionary<string, string>? cur = null;
|
||||||
|
foreach (var raw in text.Split('\n'))
|
||||||
|
{
|
||||||
|
var line = raw.Trim();
|
||||||
|
if (line.Length == 0 || line[0] == ';' || line[0] == '#') continue;
|
||||||
|
if (line[0] == '[' && line[^1] == ']')
|
||||||
|
{
|
||||||
|
var name = line[1..^1].Trim();
|
||||||
|
cur = new Dictionary<string, string>(System.StringComparer.OrdinalIgnoreCase);
|
||||||
|
sections[name] = cur;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int eq = line.IndexOf('=');
|
||||||
|
if (eq <= 0 || cur is null) continue;
|
||||||
|
cur[line[..eq].Trim()] = line[(eq + 1)..].Trim();
|
||||||
|
}
|
||||||
|
return new ControlsIni(sections);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? Get(string section, string key)
|
||||||
|
=> _sections.TryGetValue(section, out var s) && s.TryGetValue(key, out var v) ? v : null;
|
||||||
|
|
||||||
|
/// <summary>Parse a <c>#AARRGGBB</c> token into an RGBA <see cref="Vector4"/>.</summary>
|
||||||
|
public bool TryColor(string section, string key, out Vector4 color)
|
||||||
|
{
|
||||||
|
color = default;
|
||||||
|
var v = Get(section, key);
|
||||||
|
if (v is null || v.Length != 9 || v[0] != '#') return false;
|
||||||
|
if (!uint.TryParse(v.AsSpan(1), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out uint argb))
|
||||||
|
return false;
|
||||||
|
float a = ((argb >> 24) & 0xFF) / 255f;
|
||||||
|
float r = ((argb >> 16) & 0xFF) / 255f;
|
||||||
|
float g = ((argb >> 8) & 0xFF) / 255f;
|
||||||
|
float b = (argb & 0xFF) / 255f;
|
||||||
|
color = new Vector4(r, g, b, a);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
38
tests/AcDream.App.Tests/UI/ControlsIniTests.cs
Normal file
38
tests/AcDream.App.Tests/UI/ControlsIniTests.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.App.UI;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.UI;
|
||||||
|
|
||||||
|
public class ControlsIniTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Parse_ReadsSectionTokens()
|
||||||
|
{
|
||||||
|
var ini = ControlsIni.Parse(
|
||||||
|
"[title]\nheight=19\ncolor=#FFFFFFFF\nfont=font://Verdana-10-bold\n" +
|
||||||
|
"[body]\nbgcolor=#00000000\ncolor_border=#FF4F657D\n");
|
||||||
|
|
||||||
|
Assert.Equal("19", ini.Get("title", "height"));
|
||||||
|
Assert.Equal("font://Verdana-10-bold", ini.Get("title", "font"));
|
||||||
|
Assert.Null(ini.Get("title", "missing"));
|
||||||
|
Assert.Null(ini.Get("nosuch", "height"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryColor_ParsesAlphaFirstHex()
|
||||||
|
{
|
||||||
|
var ini = ControlsIni.Parse("[body]\ncolor_border=#FF4F657D\n");
|
||||||
|
Assert.True(ini.TryColor("body", "color_border", out Vector4 c));
|
||||||
|
Assert.Equal(0xFF / 255f, c.W, 5); // alpha
|
||||||
|
Assert.Equal(0x4F / 255f, c.X, 5); // red
|
||||||
|
Assert.Equal(0x65 / 255f, c.Y, 5); // green
|
||||||
|
Assert.Equal(0x7D / 255f, c.Z, 5); // blue
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Load_MissingFileReturnsEmptyNotThrow()
|
||||||
|
{
|
||||||
|
var ini = ControlsIni.Load(@"Z:\does\not\exist\controls.ini");
|
||||||
|
Assert.Null(ini.Get("title", "height")); // empty, no throw
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue