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:
@@ -10,10 +10,12 @@ namespace System.Collections.Generic
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Polyfill for CollectionExtensions.GetValueOrDefault (netstandard2.1+),
|
/// Polyfill for CollectionExtensions.GetValueOrDefault (netstandard2.1+),
|
||||||
/// absent on net48. Returns the value for <paramref name="key"/> or
|
/// absent on net48. Returns the value for <paramref name="key"/> or
|
||||||
/// default(TValue) when the key is missing.
|
/// default(TValue) when the key is missing. Takes IDictionary rather than
|
||||||
|
/// IReadOnlyDictionary: net40 (Windows XP flavor) predates the IReadOnly*
|
||||||
|
/// interfaces, so shared code uses the classic ones throughout.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static TValue? GetValueOrDefault<TKey, TValue>(
|
public static TValue? GetValueOrDefault<TKey, TValue>(
|
||||||
this IReadOnlyDictionary<TKey, TValue> dictionary, TKey key)
|
this IDictionary<TKey, TValue> dictionary, TKey key)
|
||||||
{
|
{
|
||||||
if (dictionary is null) throw new ArgumentNullException(nameof(dictionary));
|
if (dictionary is null) throw new ArgumentNullException(nameof(dictionary));
|
||||||
return dictionary.TryGetValue(key, out var value) ? value : default;
|
return dictionary.TryGetValue(key, out var value) ? value : default;
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
namespace RioJoy.Core.Compat;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// net48/net40 bridge for the handful of Task-era statics the code uses. On
|
||||||
|
/// net40 (the Windows XP flavor) these come from Microsoft.Bcl.Async's
|
||||||
|
/// <c>TaskEx</c>, and <c>SemaphoreSlim.WaitAsync</c> doesn't exist at all —
|
||||||
|
/// there the wait blocks briefly instead, which at 9600 baud (tiny writes,
|
||||||
|
/// rare contention) costs nothing measurable.
|
||||||
|
/// </summary>
|
||||||
|
internal static class TaskCompat
|
||||||
|
{
|
||||||
|
#if NET40
|
||||||
|
public static Task Run(Action action) => TaskEx.Run(action);
|
||||||
|
|
||||||
|
public static Task Delay(TimeSpan delay, CancellationToken cancellationToken) =>
|
||||||
|
TaskEx.Delay(delay, cancellationToken);
|
||||||
|
|
||||||
|
public static Task<Task> WhenAny(IEnumerable<Task> tasks) => TaskEx.WhenAny(tasks);
|
||||||
|
|
||||||
|
public static Task WhenAll(IEnumerable<Task> tasks) => TaskEx.WhenAll(tasks);
|
||||||
|
|
||||||
|
public static Task WaitAsync(SemaphoreSlim semaphore, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
semaphore.Wait(cancellationToken); // net40: no WaitAsync; block (see class doc)
|
||||||
|
return TaskEx.FromResult(true);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
public static Task Run(Action action) => Task.Run(action);
|
||||||
|
|
||||||
|
public static Task Delay(TimeSpan delay, CancellationToken cancellationToken) =>
|
||||||
|
Task.Delay(delay, cancellationToken);
|
||||||
|
|
||||||
|
public static Task<Task> WhenAny(IEnumerable<Task> tasks) => Task.WhenAny(tasks);
|
||||||
|
|
||||||
|
public static Task WhenAll(IEnumerable<Task> tasks) => Task.WhenAll(tasks);
|
||||||
|
|
||||||
|
public static Task WaitAsync(SemaphoreSlim semaphore, CancellationToken cancellationToken) =>
|
||||||
|
semaphore.WaitAsync(cancellationToken);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ public sealed record PanelGroup(
|
|||||||
bool LampCapable,
|
bool LampCapable,
|
||||||
int OriginCol,
|
int OriginCol,
|
||||||
int OriginRow,
|
int OriginRow,
|
||||||
IReadOnlyList<int?> Addresses);
|
IList<int?> Addresses);
|
||||||
|
|
||||||
/// <summary>One address button placed on the panel (absolute cell coordinates).</summary>
|
/// <summary>One address button placed on the panel (absolute cell coordinates).</summary>
|
||||||
public sealed record PanelButton(int Address, PanelGroup Group, int Col, int Row, bool LampCapable);
|
public sealed record PanelButton(int Address, PanelGroup Group, int Col, int Row, bool LampCapable);
|
||||||
@@ -66,7 +66,7 @@ public static class CockpitPanel
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>All panel groups, positioned for rendering.</summary>
|
/// <summary>All panel groups, positioned for rendering.</summary>
|
||||||
public static IReadOnlyList<PanelGroup> Groups { get; } = new[]
|
public static IList<PanelGroup> Groups { get; } = new[]
|
||||||
{
|
{
|
||||||
// Upper MFD row.
|
// Upper MFD row.
|
||||||
new PanelGroup("Upper Left MFD", PanelGroupKind.Mfd, 4, 2, true, 0, 1, Mfd(0x2F)),
|
new PanelGroup("Upper Left MFD", PanelGroupKind.Mfd, 4, 2, true, 0, 1, Mfd(0x2F)),
|
||||||
@@ -90,7 +90,7 @@ public static class CockpitPanel
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>Flatten the groups to positioned <see cref="PanelButton"/>s (absolute cells).</summary>
|
/// <summary>Flatten the groups to positioned <see cref="PanelButton"/>s (absolute cells).</summary>
|
||||||
public static IReadOnlyList<PanelButton> Buttons()
|
public static IList<PanelButton> Buttons()
|
||||||
{
|
{
|
||||||
var buttons = new List<PanelButton>();
|
var buttons = new List<PanelButton>();
|
||||||
foreach (PanelGroup g in Groups)
|
foreach (PanelGroup g in Groups)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ public sealed record SheetCell(int Row, int Col, string Text, int? Address, Shee
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class SheetLayout
|
public static class SheetLayout
|
||||||
{
|
{
|
||||||
public static IReadOnlyList<SheetCell> Load(string path) => Parse(File.ReadAllText(path));
|
public static IList<SheetCell> Load(string path) => Parse(File.ReadAllText(path));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The RIO address a cell's text encodes (two hex digits in 0x00–0x6F), or null.
|
/// The RIO address a cell's text encodes (two hex digits in 0x00–0x6F), or null.
|
||||||
@@ -66,7 +66,7 @@ public static class SheetLayout
|
|||||||
/// longer than <paramref name="maxTextLength"/> is dropped as a merged-cell
|
/// longer than <paramref name="maxTextLength"/> is dropped as a merged-cell
|
||||||
/// artifact; the clean export has none.
|
/// artifact; the clean export has none.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static IReadOnlyList<SheetCell> Parse(string csv, int maxTextLength = 40)
|
public static IList<SheetCell> Parse(string csv, int maxTextLength = 40)
|
||||||
{
|
{
|
||||||
if (csv is null) throw new ArgumentNullException(nameof(csv));
|
if (csv is null) throw new ArgumentNullException(nameof(csv));
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ public readonly record struct KeyName(string Name, byte Value);
|
|||||||
public static class KeyCatalog
|
public static class KeyCatalog
|
||||||
{
|
{
|
||||||
/// <summary>All catalogued keys, in menu order (letters, digits, F-keys, …).</summary>
|
/// <summary>All catalogued keys, in menu order (letters, digits, F-keys, …).</summary>
|
||||||
public static IReadOnlyList<KeyName> Entries { get; } = Build();
|
public static IList<KeyName> Entries { get; } = Build();
|
||||||
|
|
||||||
private static readonly Dictionary<byte, string> ByValue =
|
private static readonly Dictionary<byte, string> ByValue =
|
||||||
Entries.GroupBy(e => e.Value).ToDictionary(g => g.Key, g => g.First().Name);
|
Entries.GroupBy(e => e.Value).ToDictionary(g => g.Key, g => g.First().Name);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public sealed class RioInputMap
|
|||||||
public RioInputMap() { }
|
public RioInputMap() { }
|
||||||
|
|
||||||
/// <summary>Create a map from raw 16-bit words indexed by address.</summary>
|
/// <summary>Create a map from raw 16-bit words indexed by address.</summary>
|
||||||
public RioInputMap(IReadOnlyDictionary<int, ushort> entries)
|
public RioInputMap(IDictionary<int, ushort> entries)
|
||||||
{
|
{
|
||||||
if (entries is null) throw new ArgumentNullException(nameof(entries));
|
if (entries is null) throw new ArgumentNullException(nameof(entries));
|
||||||
foreach ((int address, ushort raw) in entries)
|
foreach ((int address, ushort raw) in entries)
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ public sealed class HidFeederJoystickSink : IJoystickSink, IDisposable
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var ifData = new SP_DEVICE_INTERFACE_DATA();
|
var ifData = new SP_DEVICE_INTERFACE_DATA();
|
||||||
ifData.cbSize = Marshal.SizeOf<SP_DEVICE_INTERFACE_DATA>();
|
ifData.cbSize = Marshal.SizeOf(typeof(SP_DEVICE_INTERFACE_DATA));
|
||||||
|
|
||||||
if (!SetupDiEnumDeviceInterfaces(devInfo, IntPtr.Zero, ref guid, 0, ref ifData))
|
if (!SetupDiEnumDeviceInterfaces(devInfo, IntPtr.Zero, ref guid, 0, ref ifData))
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ public sealed class SendInputSink : IInputSink
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void Send(INPUT input) =>
|
private static void Send(INPUT input) =>
|
||||||
SendInput(1, new[] { input }, Marshal.SizeOf<INPUT>());
|
SendInput(1, new[] { input }, Marshal.SizeOf(typeof(INPUT)));
|
||||||
|
|
||||||
// --- Win32 interop --------------------------------------------------------
|
// --- Win32 interop --------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#if !NET40 // ViGEmBus is Win10+ only; the XP flavor uses RioGamepadXP (PLAN.md §8B)
|
||||||
using Nefarius.ViGEm.Client;
|
using Nefarius.ViGEm.Client;
|
||||||
using Nefarius.ViGEm.Client.Targets;
|
using Nefarius.ViGEm.Client.Targets;
|
||||||
using Nefarius.ViGEm.Client.Targets.Xbox360;
|
using Nefarius.ViGEm.Client.Targets.Xbox360;
|
||||||
@@ -26,7 +27,7 @@ public sealed class ViGEmJoystickSink : IJoystickSink, IDisposable
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>Display names for the mappable buttons, aligned with <see cref="ButtonMap"/>.</summary>
|
/// <summary>Display names for the mappable buttons, aligned with <see cref="ButtonMap"/>.</summary>
|
||||||
public static IReadOnlyList<string> ButtonNames { get; } = new[]
|
public static IList<string> ButtonNames { get; } = new[]
|
||||||
{
|
{
|
||||||
"A", "B", "X", "Y", "LB", "RB", "Back", "Start", "L3", "R3", "Guide",
|
"A", "B", "X", "Y", "LB", "RB", "Back", "Start", "L3", "R3", "Guide",
|
||||||
};
|
};
|
||||||
@@ -125,3 +126,4 @@ public sealed class ViGEmJoystickSink : IJoystickSink, IDisposable
|
|||||||
_client.Dispose();
|
_client.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ public static class GoobieDataImporter
|
|||||||
{
|
{
|
||||||
/// <summary>A parsed data file: the field names and one label map per row.</summary>
|
/// <summary>A parsed data file: the field names and one label map per row.</summary>
|
||||||
public sealed record Sheet(
|
public sealed record Sheet(
|
||||||
IReadOnlyList<string> Fields,
|
IList<string> Fields,
|
||||||
IReadOnlyList<IReadOnlyDictionary<string, string>> Rows);
|
IList<IDictionary<string, string>> Rows);
|
||||||
|
|
||||||
public static Sheet Load(string path) => Parse(File.ReadAllText(path));
|
public static Sheet Load(string path) => Parse(File.ReadAllText(path));
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ public static class GoobieDataImporter
|
|||||||
throw new FormatException("No lists found; expected a header list of field names.");
|
throw new FormatException("No lists found; expected a header list of field names.");
|
||||||
|
|
||||||
List<string> fields = lists[0];
|
List<string> fields = lists[0];
|
||||||
var rows = new List<IReadOnlyDictionary<string, string>>();
|
var rows = new List<IDictionary<string, string>>();
|
||||||
|
|
||||||
for (int i = 1; i < lists.Count; i++)
|
for (int i = 1; i < lists.Count; i++)
|
||||||
{
|
{
|
||||||
@@ -51,7 +51,7 @@ public static class GoobieDataImporter
|
|||||||
/// Convenience: the label map for a single row (default the first), with empty
|
/// Convenience: the label map for a single row (default the first), with empty
|
||||||
/// values dropped — ready to hand to <see cref="OverlayLayoutEngine.Layout"/>.
|
/// values dropped — ready to hand to <see cref="OverlayLayoutEngine.Layout"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static IReadOnlyDictionary<string, string> LabelsForRow(Sheet sheet, int rowIndex = 0)
|
public static IDictionary<string, string> LabelsForRow(Sheet sheet, int rowIndex = 0)
|
||||||
{
|
{
|
||||||
if (sheet is null) throw new ArgumentNullException(nameof(sheet));
|
if (sheet is null) throw new ArgumentNullException(nameof(sheet));
|
||||||
if (rowIndex < 0 || rowIndex >= sheet.Rows.Count)
|
if (rowIndex < 0 || rowIndex >= sheet.Rows.Count)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public static class OverlayHitTester
|
|||||||
/// A region's rectangle is its stored cell geometry; label rotation does not
|
/// A region's rectangle is its stored cell geometry; label rotation does not
|
||||||
/// affect hit-testing.
|
/// affect hit-testing.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static IReadOnlyList<OverlayRegion> RegionsAt(OverlayTemplate template, double x, double y)
|
public static IList<OverlayRegion> RegionsAt(OverlayTemplate template, double x, double y)
|
||||||
{
|
{
|
||||||
if (template is null) throw new ArgumentNullException(nameof(template));
|
if (template is null) throw new ArgumentNullException(nameof(template));
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ public static class OverlayHitTester
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static OverlayRegion? NextAt(OverlayTemplate template, double x, double y, string? currentName)
|
public static OverlayRegion? NextAt(OverlayTemplate template, double x, double y, string? currentName)
|
||||||
{
|
{
|
||||||
IReadOnlyList<OverlayRegion> hits = RegionsAt(template, x, y);
|
IList<OverlayRegion> hits = RegionsAt(template, x, y);
|
||||||
if (hits.Count == 0)
|
if (hits.Count == 0)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ public sealed class OverlayLayoutEngine
|
|||||||
/// in <paramref name="labels"/> (or with empty text) are skipped — matching the
|
/// in <paramref name="labels"/> (or with empty text) are skipped — matching the
|
||||||
/// legacy behavior where a blank field produced no visible text.
|
/// legacy behavior where a blank field produced no visible text.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<PlacedLabel> Layout(
|
public IList<PlacedLabel> Layout(
|
||||||
OverlayTemplate template,
|
OverlayTemplate template,
|
||||||
IReadOnlyDictionary<string, string> labels,
|
IDictionary<string, string> labels,
|
||||||
OverlayLayoutOptions? options = null)
|
OverlayLayoutOptions? options = null)
|
||||||
{
|
{
|
||||||
if (template is null) throw new ArgumentNullException(nameof(template));
|
if (template is null) throw new ArgumentNullException(nameof(template));
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ public readonly struct SwitchDecision : IEquatable<SwitchDecision>
|
|||||||
|
|
||||||
public bool Equals(SwitchDecision other) => Mode == other.Mode && ReferenceEquals(Profile, other.Profile);
|
public bool Equals(SwitchDecision other) => Mode == other.Mode && ReferenceEquals(Profile, other.Profile);
|
||||||
public override bool Equals(object? obj) => obj is SwitchDecision d && Equals(d);
|
public override bool Equals(object? obj) => obj is SwitchDecision d && Equals(d);
|
||||||
public override int GetHashCode() => HashCode.Combine(Mode, Profile);
|
// Manual combine: HashCode.Combine needs Microsoft.Bcl.HashCode, which has
|
||||||
|
// no net40 build (Windows XP flavor).
|
||||||
|
public override int GetHashCode() =>
|
||||||
|
((int)Mode * 397) ^ (Profile?.GetHashCode() ?? 0);
|
||||||
public override string ToString() => Mode == SwitchMode.Activate ? $"Activate({Profile?.Name})" : Mode.ToString();
|
public override string ToString() => Mode == SwitchMode.Activate ? $"Activate({Profile?.Name})" : Mode.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ public sealed class AutoSwitchWatcher
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(interval, cancellationToken).ConfigureAwait(false);
|
await Compat.TaskCompat.Delay(interval, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ public sealed class IniFile
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>All key/value pairs in a section (empty if the section is absent).</summary>
|
/// <summary>All key/value pairs in a section (empty if the section is absent).</summary>
|
||||||
public IReadOnlyDictionary<string, string> Section(string name) =>
|
public IDictionary<string, string> Section(string name) =>
|
||||||
_sections.TryGetValue(name, out Dictionary<string, string>? s)
|
_sections.TryGetValue(name, out Dictionary<string, string>? s)
|
||||||
? s
|
? s
|
||||||
: new Dictionary<string, string>();
|
: new Dictionary<string, string>();
|
||||||
|
|||||||
@@ -1,26 +1,47 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- net48 test branch. Uses Win32 P/Invoke (SendInput, SystemParametersInfo)
|
<!-- Two flavors (PLAN.md §Phase 8): net48 = Windows 10/11 (x64, ViGEm +
|
||||||
and System.IO.Ports. Span/records/init/Index are polyfilled (System.Memory
|
RioGamepad); net40 = Windows XP SP3 (x86 cabinets — .NET 4.0 is XP's
|
||||||
+ PolySharp); System.Text.Json comes from NuGet (not in the net48 BCL). -->
|
ceiling). Modern language features come from PolySharp on both; net40
|
||||||
<TargetFramework>net48</TargetFramework>
|
async machinery comes from Microsoft.Bcl.Async (TaskEx via
|
||||||
|
Compat/TaskCompat). Win32 P/Invoke (SendInput, DeviceIoControl) and
|
||||||
|
in-box System.IO.Ports work on both. -->
|
||||||
|
<TargetFrameworks>net48;net40</TargetFrameworks>
|
||||||
<Platforms>x64</Platforms>
|
<Platforms>x64</Platforms>
|
||||||
<PlatformTarget>x64</PlatformTarget>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!-- net48 pins x64 (matches the driver + tray exe); net40 stays AnyCPU and
|
||||||
|
the XP tray exe pins x86. -->
|
||||||
|
<PropertyGroup Condition="'$(TargetFramework)' == 'net48'">
|
||||||
|
<PlatformTarget>x64</PlatformTarget>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TargetFramework)' == 'net40'">
|
||||||
|
<!-- net40 has no System.Net.Http assembly for the implicit global using. -->
|
||||||
|
<Using Remove="System.Net.Http" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Nefarius.ViGEm.Client" Version="1.21.256" />
|
|
||||||
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
|
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="Microsoft.Bcl.HashCode" Version="1.1.1" />
|
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
|
||||||
<PackageReference Include="PolySharp" Version="1.14.1">
|
<PackageReference Include="PolySharp" Version="1.14.1">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TargetFramework)' == 'net48'">
|
||||||
|
<PackageReference Include="Nefarius.ViGEm.Client" Version="1.21.256" />
|
||||||
|
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TargetFramework)' == 'net40'">
|
||||||
|
<PackageReference Include="Microsoft.Bcl.Async" Version="1.0.168" />
|
||||||
|
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -69,19 +69,19 @@ public sealed class RioSerialLink
|
|||||||
{
|
{
|
||||||
// If any running loop ends (transport closed / error / cancellation),
|
// If any running loop ends (transport closed / error / cancellation),
|
||||||
// tear the others down too.
|
// tear the others down too.
|
||||||
await Task.WhenAny(loops).ConfigureAwait(false);
|
await Compat.TaskCompat.WhenAny(loops).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
linked.Cancel();
|
linked.Cancel();
|
||||||
await Task.WhenAll(loops.Select(Swallow)).ConfigureAwait(false);
|
await Compat.TaskCompat.WhenAll(loops.Select(Swallow)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Send a pre-built packet (see <see cref="PacketBuilder"/>) to the RIO.</summary>
|
/// <summary>Send a pre-built packet (see <see cref="PacketBuilder"/>) to the RIO.</summary>
|
||||||
public async Task SendAsync(byte[] packet, CancellationToken cancellationToken = default)
|
public async Task SendAsync(byte[] packet, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
await Compat.TaskCompat.WaitAsync(_writeLock, cancellationToken).ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _transport.WriteAsync(packet, cancellationToken).ConfigureAwait(false);
|
await _transport.WriteAsync(packet, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -191,7 +191,7 @@ public sealed class RioSerialLink
|
|||||||
{
|
{
|
||||||
while (!ct.IsCancellationRequested)
|
while (!ct.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
await Task.Delay(_options.AnalogPollInterval, ct).ConfigureAwait(false);
|
await Compat.TaskCompat.Delay(_options.AnalogPollInterval, ct).ConfigureAwait(false);
|
||||||
|
|
||||||
await RequestAnalogAsync(ct).ConfigureAwait(false);
|
await RequestAnalogAsync(ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ public sealed class SerialPortTransport : IRioTransport
|
|||||||
// still releases the handle.
|
// still releases the handle.
|
||||||
}
|
}
|
||||||
|
|
||||||
Task close = Task.Run(() =>
|
Task close = Compat.TaskCompat.Run(() =>
|
||||||
{
|
{
|
||||||
try { _port.Dispose(); }
|
try { _port.Dispose(); }
|
||||||
catch { /* best-effort release */ }
|
catch { /* best-effort release */ }
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public sealed class ProfileWallpaperGenerator
|
|||||||
public string Generate(
|
public string Generate(
|
||||||
OverlayTemplate template,
|
OverlayTemplate template,
|
||||||
string templateDirectory,
|
string templateDirectory,
|
||||||
IReadOnlyDictionary<string, string> labels,
|
IDictionary<string, string> labels,
|
||||||
string outputPath,
|
string outputPath,
|
||||||
OverlayLayoutOptions? options = null)
|
OverlayLayoutOptions? options = null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public sealed class SkiaOverlayRenderer
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public SKBitmap Render(
|
public SKBitmap Render(
|
||||||
OverlayTemplate template,
|
OverlayTemplate template,
|
||||||
IReadOnlyDictionary<string, string> labels,
|
IDictionary<string, string> labels,
|
||||||
SKBitmap baseImage,
|
SKBitmap baseImage,
|
||||||
OverlayLayoutOptions? options = null)
|
OverlayLayoutOptions? options = null)
|
||||||
{
|
{
|
||||||
@@ -95,7 +95,7 @@ public sealed class SkiaOverlayRenderer
|
|||||||
/// <summary>Render and encode to PNG bytes.</summary>
|
/// <summary>Render and encode to PNG bytes.</summary>
|
||||||
public byte[] RenderToPng(
|
public byte[] RenderToPng(
|
||||||
OverlayTemplate template,
|
OverlayTemplate template,
|
||||||
IReadOnlyDictionary<string, string> labels,
|
IDictionary<string, string> labels,
|
||||||
SKBitmap baseImage,
|
SKBitmap baseImage,
|
||||||
OverlayLayoutOptions? options = null)
|
OverlayLayoutOptions? options = null)
|
||||||
{
|
{
|
||||||
@@ -110,7 +110,7 @@ public sealed class SkiaOverlayRenderer
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void RenderToFile(
|
public void RenderToFile(
|
||||||
OverlayTemplate template,
|
OverlayTemplate template,
|
||||||
IReadOnlyDictionary<string, string> labels,
|
IDictionary<string, string> labels,
|
||||||
string outputPath,
|
string outputPath,
|
||||||
OverlayLayoutOptions? options = null)
|
OverlayLayoutOptions? options = null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ namespace RioJoy.Tray.Editor;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class PanelCanvas : Control
|
internal sealed class PanelCanvas : Control
|
||||||
{
|
{
|
||||||
private static readonly IReadOnlyList<PanelButton> AllButtons = CockpitPanel.Buttons();
|
private static readonly IList<PanelButton> AllButtons = CockpitPanel.Buttons();
|
||||||
|
|
||||||
public PanelCanvas()
|
public PanelCanvas()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public static class PanelView
|
|||||||
public const int CalOriginCol = 2;
|
public const int CalOriginCol = 2;
|
||||||
public const int CalOriginRow = 9;
|
public const int CalOriginRow = 9;
|
||||||
|
|
||||||
public static IReadOnlyList<CalibrationCell> CalibrationCells { get; } = new[]
|
public static IList<CalibrationCell> CalibrationCells { get; } = new[]
|
||||||
{
|
{
|
||||||
new CalibrationCell(JoyStickSetting.InvertX, "Inv X", 2, 10),
|
new CalibrationCell(JoyStickSetting.InvertX, "Inv X", 2, 10),
|
||||||
new CalibrationCell(JoyStickSetting.InvertY, "Inv Y", 2, 11),
|
new CalibrationCell(JoyStickSetting.InvertY, "Inv Y", 2, 11),
|
||||||
@@ -51,7 +51,7 @@ public static class PanelView
|
|||||||
new CalibrationCell(JoyStickSetting.EnableZR, "ZR mix", 2, 16),
|
new CalibrationCell(JoyStickSetting.EnableZR, "ZR mix", 2, 16),
|
||||||
};
|
};
|
||||||
|
|
||||||
public static Size GridSize(IReadOnlyList<PanelButton> buttons)
|
public static Size GridSize(IList<PanelButton> buttons)
|
||||||
{
|
{
|
||||||
int maxCol = 0, maxRow = 0;
|
int maxCol = 0, maxRow = 0;
|
||||||
foreach (PanelButton b in buttons)
|
foreach (PanelButton b in buttons)
|
||||||
@@ -69,8 +69,8 @@ public static class PanelView
|
|||||||
|
|
||||||
public static void Paint(
|
public static void Paint(
|
||||||
Graphics g,
|
Graphics g,
|
||||||
IReadOnlyList<PanelButton> buttons,
|
IList<PanelButton> buttons,
|
||||||
IReadOnlyList<PanelGroup> groups,
|
IList<PanelGroup> groups,
|
||||||
Func<int, string?> label,
|
Func<int, string?> label,
|
||||||
Func<int, string?> function,
|
Func<int, string?> function,
|
||||||
Func<int, bool> lit,
|
Func<int, bool> lit,
|
||||||
@@ -225,7 +225,7 @@ public static class PanelView
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PanelButton? HitTest(IReadOnlyList<PanelButton> buttons, Point p)
|
public static PanelButton? HitTest(IList<PanelButton> buttons, Point p)
|
||||||
{
|
{
|
||||||
foreach (PanelButton b in buttons)
|
foreach (PanelButton b in buttons)
|
||||||
if (Cell(b.Col, b.Row).Contains(p))
|
if (Cell(b.Col, b.Row).Contains(p))
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ public class CockpitPanelTests
|
|||||||
public void Keypad_PhysicalLayout_MapsDigitsToAddresses()
|
public void Keypad_PhysicalLayout_MapsDigitsToAddresses()
|
||||||
{
|
{
|
||||||
// Internal keypad: top-left "1" -> 0x51, bottom-middle "0" -> 0x50, "C" -> 0x5C.
|
// 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");
|
PanelGroup pad = CockpitPanel.Groups.Single(g => g.Title == "Internal Keypad");
|
||||||
|
|
||||||
Assert.Equal(0x51, pad.Addresses[0]); // row0 col0 = "1"
|
Assert.Equal(0x51, pad.Addresses[0]); // row0 col0 = "1"
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ public class SheetLayoutTests
|
|||||||
"\"\",\"IsLit\",\"\",\"\",\"0F\"\n" +
|
"\"\",\"IsLit\",\"\",\"\",\"0F\"\n" +
|
||||||
"\"\",\"\",\"\",\"\",\"HOME\"";
|
"\"\",\"\",\"\",\"\",\"HOME\"";
|
||||||
|
|
||||||
IReadOnlyList<SheetCell> cells = SheetLayout.Parse(csv);
|
IList<SheetCell> cells = SheetLayout.Parse(csv);
|
||||||
|
|
||||||
SheetCell addr = Assert.Single(cells, c => c.Text == "0F");
|
SheetCell addr = Assert.Single(cells, c => c.Text == "0F");
|
||||||
Assert.Equal(0x0F, addr.Address);
|
Assert.Equal(0x0F, addr.Address);
|
||||||
@@ -64,7 +64,7 @@ public class SheetLayoutTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Parse_RealSheet_HasFullAddressMap()
|
public void Parse_RealSheet_HasFullAddressMap()
|
||||||
{
|
{
|
||||||
IReadOnlyList<SheetCell> cells = SheetLayout.Load(
|
IList<SheetCell> cells = SheetLayout.Load(
|
||||||
TestRepo.CustomBackground("config-sheet.csv"));
|
TestRepo.CustomBackground("config-sheet.csv"));
|
||||||
|
|
||||||
// Every RIO address 0x00..0x6F that exists should appear exactly once as a cell.
|
// Every RIO address 0x00..0x6F that exists should appear exactly once as a cell.
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public class GoobieDataImporterTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void LabelsForRow_DropsEmptyValues()
|
public void LabelsForRow_DropsEmptyValues()
|
||||||
{
|
{
|
||||||
IReadOnlyDictionary<string, string> labels =
|
IDictionary<string, string> labels =
|
||||||
GoobieDataImporter.LabelsForRow(GoobieDataImporter.Parse(Sample));
|
GoobieDataImporter.LabelsForRow(GoobieDataImporter.Parse(Sample));
|
||||||
|
|
||||||
Assert.True(labels.ContainsKey("b-00"));
|
Assert.True(labels.ContainsKey("b-00"));
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ public class OverlayHitTesterTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void RegionsAt_ReturnsSmallestFirst()
|
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));
|
Assert.Equal(new[] { "small", "big" }, hits.Select(r => r.Name));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ public class OverlayLayoutEngineTests
|
|||||||
["b-99"] = "ORPHAN", // no such region -> ignored
|
["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(2, placed.Count);
|
||||||
Assert.Equal("b-00", placed[0].RegionName);
|
Assert.Equal("b-00", placed[0].RegionName);
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ public class OverlayRenderIntegrationTests
|
|||||||
Assert.Equal(template.Width, baseImg.Width);
|
Assert.Equal(template.Width, baseImg.Width);
|
||||||
Assert.Equal(template.Height, baseImg.Height);
|
Assert.Equal(template.Height, baseImg.Height);
|
||||||
|
|
||||||
IReadOnlyDictionary<string, string> labels =
|
IDictionary<string, string> labels =
|
||||||
GoobieDataImporter.LabelsForRow(GoobieDataImporter.Load(TestRepo.CustomBackground("TEST.data")));
|
GoobieDataImporter.LabelsForRow(GoobieDataImporter.Load(TestRepo.CustomBackground("TEST.data")));
|
||||||
|
|
||||||
using SKBitmap rendered = new SkiaOverlayRenderer().Render(template, labels, baseImg);
|
using SKBitmap rendered = new SkiaOverlayRenderer().Render(template, labels, baseImg);
|
||||||
|
|||||||
Reference in New Issue
Block a user