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>
This commit is contained in:
Cyd
2026-07-11 20:41:58 -05:00
co-authored by Claude Fable 5
parent 3b2af7b79a
commit 1ff0b16015
28 changed files with 121 additions and 53 deletions
@@ -50,7 +50,7 @@ public class CockpitPanelTests
public void Keypad_PhysicalLayout_MapsDigitsToAddresses()
{
// Internal keypad: top-left "1" -> 0x51, bottom-middle "0" -> 0x50, "C" -> 0x5C.
IReadOnlyList<PanelButton> all = CockpitPanel.Buttons();
IList<PanelButton> all = CockpitPanel.Buttons();
PanelGroup pad = CockpitPanel.Groups.Single(g => g.Title == "Internal Keypad");
Assert.Equal(0x51, pad.Addresses[0]); // row0 col0 = "1"
@@ -47,7 +47,7 @@ public class SheetLayoutTests
"\"\",\"IsLit\",\"\",\"\",\"0F\"\n" +
"\"\",\"\",\"\",\"\",\"HOME\"";
IReadOnlyList<SheetCell> cells = SheetLayout.Parse(csv);
IList<SheetCell> cells = SheetLayout.Parse(csv);
SheetCell addr = Assert.Single(cells, c => c.Text == "0F");
Assert.Equal(0x0F, addr.Address);
@@ -64,7 +64,7 @@ public class SheetLayoutTests
[Fact]
public void Parse_RealSheet_HasFullAddressMap()
{
IReadOnlyList<SheetCell> cells = SheetLayout.Load(
IList<SheetCell> cells = SheetLayout.Load(
TestRepo.CustomBackground("config-sheet.csv"));
// Every RIO address 0x00..0x6F that exists should appear exactly once as a cell.
@@ -26,7 +26,7 @@ public class GoobieDataImporterTests
[Fact]
public void LabelsForRow_DropsEmptyValues()
{
IReadOnlyDictionary<string, string> labels =
IDictionary<string, string> labels =
GoobieDataImporter.LabelsForRow(GoobieDataImporter.Parse(Sample));
Assert.True(labels.ContainsKey("b-00"));
@@ -21,7 +21,7 @@ public class OverlayHitTesterTests
[Fact]
public void RegionsAt_ReturnsSmallestFirst()
{
IReadOnlyList<OverlayRegion> hits = OverlayHitTester.RegionsAt(Template(), 100, 50);
IList<OverlayRegion> hits = OverlayHitTester.RegionsAt(Template(), 100, 50);
Assert.Equal(new[] { "small", "big" }, hits.Select(r => r.Name));
}
@@ -133,7 +133,7 @@ public class OverlayLayoutEngineTests
["b-99"] = "ORPHAN", // no such region -> ignored
};
IReadOnlyList<PlacedLabel> placed = Engine.Layout(template, labels);
IList<PlacedLabel> placed = Engine.Layout(template, labels);
Assert.Equal(2, placed.Count);
Assert.Equal("b-00", placed[0].RegionName);
@@ -35,7 +35,7 @@ public class OverlayRenderIntegrationTests
Assert.Equal(template.Width, baseImg.Width);
Assert.Equal(template.Height, baseImg.Height);
IReadOnlyDictionary<string, string> labels =
IDictionary<string, string> labels =
GoobieDataImporter.LabelsForRow(GoobieDataImporter.Load(TestRepo.CustomBackground("TEST.data")));
using SKBitmap rendered = new SkiaOverlayRenderer().Render(template, labels, baseImg);