- 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>
119 lines
4.2 KiB
C#
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;
|
|
}
|
|
}
|