acdream/src/AcDream.App/UI/ControlsIni.cs
Erik 97bd1d2f09 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>
2026-06-14 17:31:55 +02:00

65 lines
2.6 KiB
C#

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;
}
}