Phase 5: tray app + profiles + runtime wiring
Wire the Core pieces into a runnable tray app with per-game profiles and the three-state serial-yield auto-switch: - Profiles/: RioProfile + AppConfig model; ConfigStore (System.Text.Json, round-tripped); RioIniImporter ports the legacy RIO.ini (button table, invert flags, plasma greeting); AutoSwitchResolver + AutoSwitchWatcher resolve the foreground executable into Yield (native game) / Activate (profile) / Idle, with native always winning and change-only notifications. IForegroundProcessProvider abstracts the OS. - RioRuntime assembles a profile's live pipeline: serial ButtonPressed/Released + KeyPressed/Released → InputRouter (via RioAddress); AnalogReply → AxisCalibrator → the six joystick axes; RIO commands → calibration resets + version/check requests + lamp re-init. SerialLampSink sends lamp feedback over the link; NullJoystickSink is a placeholder until the Phase 1 HID feeder exists. - RioJoy.Tray: NotifyIcon menu mirroring the legacy console menu (axis resets, version/status, raw-axes & poll-rate toggles, quit) + profile selection (auto vs. manual) + "start with Windows"; RioCoordinator owns the serial acquire/release tied to the watcher (native-game COM-port yield). OS adapters: ForegroundProcessProvider (Win32 foreground PID→exe) and AutoStartManager (HKCU Run key). - tests: 18 new xUnit tests (123 total) for config round-trip, ini import, the three-state resolver + watcher, and RioRuntime end-to-end over the fake transport (button→joystick, keypad-offset→keyboard, analog→six axes). The joystick output stays a no-op until the Phase 1 driver; on-cabinet verification of the acquire/release lifecycle remains. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
namespace RioJoy.Core.Profiles;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal INI reader (sections, <c>key = value</c>, <c>;</c>/<c>#</c> comments)
|
||||
/// sufficient to parse the legacy <c>RIO.ini</c>. Keys are case-insensitive.
|
||||
/// </summary>
|
||||
public sealed class IniFile
|
||||
{
|
||||
private readonly Dictionary<string, Dictionary<string, string>> _sections =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public static IniFile Parse(string text)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(text);
|
||||
var ini = new IniFile();
|
||||
var current = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
ini._sections[string.Empty] = current;
|
||||
|
||||
foreach (string rawLine in text.Split('\n'))
|
||||
{
|
||||
string line = rawLine.Trim();
|
||||
if (line.Length == 0 || line[0] is ';' or '#')
|
||||
continue;
|
||||
|
||||
if (line[0] == '[' && line[^1] == ']')
|
||||
{
|
||||
string section = line[1..^1].Trim();
|
||||
if (!ini._sections.TryGetValue(section, out current!))
|
||||
ini._sections[section] = current = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
continue;
|
||||
}
|
||||
|
||||
int eq = line.IndexOf('=');
|
||||
if (eq < 0)
|
||||
continue;
|
||||
|
||||
string key = line[..eq].Trim();
|
||||
string value = line[(eq + 1)..].Trim();
|
||||
if (key.Length > 0)
|
||||
current[key] = value;
|
||||
}
|
||||
|
||||
return ini;
|
||||
}
|
||||
|
||||
/// <summary>All key/value pairs in a section (empty if the section is absent).</summary>
|
||||
public IReadOnlyDictionary<string, string> Section(string name) =>
|
||||
_sections.TryGetValue(name, out Dictionary<string, string>? s)
|
||||
? s
|
||||
: new Dictionary<string, string>();
|
||||
|
||||
public string? GetString(string section, string key) =>
|
||||
Section(section).TryGetValue(key, out string? v) ? v : null;
|
||||
|
||||
public bool GetBool(string section, string key, bool fallback)
|
||||
{
|
||||
string? v = GetString(section, key);
|
||||
if (v is null)
|
||||
return fallback;
|
||||
return v.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"1" or "true" or "yes" or "on" => true,
|
||||
"0" or "false" or "no" or "off" => false,
|
||||
_ => fallback,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Parse an integer value that may be hex (<c>0x…</c>) or decimal.</summary>
|
||||
public static int ParseInt(string value)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
string v = value.Trim();
|
||||
bool neg = v.StartsWith('-');
|
||||
if (neg) v = v[1..];
|
||||
|
||||
int magnitude = v.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
|
||||
? Convert.ToInt32(v[2..], 16)
|
||||
: int.Parse(v);
|
||||
|
||||
return neg ? -magnitude : magnitude;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user