Files
riojoy/src/RioJoy.Core/Overlay/GoobieDataImporter.cs
T
CydandClaude Fable 5 1ff0b16015 Phase 8A (2/2): RioJoy.Core multi-targets net48 + net40 (Windows XP flavor)
- TargetFrameworks net48;net40. net48 keeps x64 + ViGEm + System.IO.Ports
  package; net40 adds Microsoft.Bcl.Async + System.ValueTuple and uses the
  in-box SerialPort.
- Compat/TaskCompat bridges Task.Run/Delay/WhenAny/WhenAll (TaskEx on
  net40) and SemaphoreSlim.WaitAsync (net40 blocks briefly - trivial at
  9600 baud).
- IReadOnlyList/IReadOnlyDictionary -> IList/IDictionary throughout
  (net40 predates the IReadOnly* interfaces and the Bcl backport cannot
  make arrays implement them).
- HashCode.Combine replaced with a manual combine (Bcl.HashCode has no
  net40 build); Marshal.SizeOf<T> -> typeof form; ViGEmJoystickSink
  gated #if !NET40. HidFeederJoystickSink stays on both flavors - it
  will drive RioGamepadXP.sys on XP via the same contract.

Both TFMs build; 275 tests green on net48.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 20:41:58 -05:00

119 lines
4.2 KiB
C#

using System.Text;
namespace RioJoy.Core.Overlay;
/// <summary>
/// Imports the legacy "Goobie" label data file — the Google-Sheet export consumed
/// by sg-goobie-MFD.scm (see docs/reference/customBackground/TEST.data). The format
/// is Lisp-ish: <c>;</c> starts a comment to end of line, a parenthesized list of
/// bare field names (matching the template layer/region names) comes first, then
/// one parenthesized list of quoted strings per output image (row). Field 0 is the
/// row's file tag (output name); the rest are the per-region label text.
///
/// <para>Each row maps directly to an <see cref="OverlayLayoutEngine"/> label set
/// (region name → text), so this is the bridge from the old sheet to a profile's
/// <see cref="Profiles.RioProfile.OverlayLabels"/>.</para>
/// </summary>
public static class GoobieDataImporter
{
/// <summary>A parsed data file: the field names and one label map per row.</summary>
public sealed record Sheet(
IList<string> Fields,
IList<IDictionary<string, string>> Rows);
public static Sheet Load(string path) => Parse(File.ReadAllText(path));
/// <summary>Parse the data file text into fields + rows.</summary>
public static Sheet Parse(string text)
{
if (text is null) throw new ArgumentNullException(nameof(text));
List<List<string>> lists = ReadLists(text);
if (lists.Count == 0)
throw new FormatException("No lists found; expected a header list of field names.");
List<string> fields = lists[0];
var rows = new List<IDictionary<string, string>>();
for (int i = 1; i < lists.Count; i++)
{
List<string> values = lists[i];
var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
for (int f = 0; f < fields.Count && f < values.Count; f++)
map[fields[f]] = values[f];
rows.Add(map);
}
return new Sheet(fields, rows);
}
/// <summary>
/// Convenience: the label map for a single row (default the first), with empty
/// values dropped — ready to hand to <see cref="OverlayLayoutEngine.Layout"/>.
/// </summary>
public static IDictionary<string, string> LabelsForRow(Sheet sheet, int rowIndex = 0)
{
if (sheet is null) throw new ArgumentNullException(nameof(sheet));
if (rowIndex < 0 || rowIndex >= sheet.Rows.Count)
throw new ArgumentOutOfRangeException(nameof(rowIndex));
return sheet.Rows[rowIndex]
.Where(kv => !string.IsNullOrEmpty(kv.Value))
.ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase);
}
// Tokenize into parenthesized lists. Barewords and "quoted strings" are both
// returned as their text (quotes stripped); ';' comments run to end of line.
private static List<List<string>> ReadLists(string s)
{
var lists = new List<List<string>>();
List<string>? current = null;
int i = 0;
while (i < s.Length)
{
char c = s[i];
if (c == ';')
{
while (i < s.Length && s[i] != '\n') i++;
}
else if (char.IsWhiteSpace(c))
{
i++;
}
else if (c == '(')
{
current = new List<string>();
i++;
}
else if (c == ')')
{
if (current is not null) { lists.Add(current); current = null; }
i++;
}
else if (c == '"')
{
i++;
var sb = new StringBuilder();
while (i < s.Length && s[i] != '"')
{
if (s[i] == '\\' && i + 1 < s.Length) i++; // unescape \" and \\
sb.Append(s[i++]);
}
i++; // closing quote
current?.Add(sb.ToString());
}
else
{
int start = i;
while (i < s.Length && !char.IsWhiteSpace(s[i]) && s[i] is not ('(' or ')' or '"' or ';'))
i++;
current?.Add(s[start..i]);
}
}
return lists;
}
}