diff --git a/deploy/build-package.ps1 b/deploy/build-package.ps1
index a716aed..a0fb481 100644
--- a/deploy/build-package.ps1
+++ b/deploy/build-package.ps1
@@ -47,12 +47,12 @@ try {
$pkgDir = Join-Path $staging 'RIOJoy'
New-Item -ItemType Directory -Force -Path $pkgDir | Out-Null
- # 1. Publish the tray app into riojoy\app (self-contained win-x64 — no .NET on target).
+ # 1. Publish the tray app into riojoy\app (net48, framework-dependent — relies on
+ # the in-box .NET Framework 4.8 present on every Windows 10/11 machine).
$appOut = Join-Path $pkgDir 'app'
- Write-Host "Publishing RioJoy.Tray ($Configuration, self-contained win-x64)..."
+ Write-Host "Publishing RioJoy.Tray ($Configuration, net48 framework-dependent)..."
& dotnet publish (Join-Path $repo 'src\RioJoy.Tray\RioJoy.Tray.csproj') `
- -c $Configuration -r win-x64 --self-contained true `
- -p:PublishSingleFile=false -p:DebugType=none `
+ -c $Configuration -p:DebugType=none `
-o $appOut | Out-Null
if ($LASTEXITCODE -ne 0) { throw 'dotnet publish failed.' }
diff --git a/src/RioJoy.Core/Calibration/AxisCalibrator.cs b/src/RioJoy.Core/Calibration/AxisCalibrator.cs
index 14387fb..d285c9c 100644
--- a/src/RioJoy.Core/Calibration/AxisCalibrator.cs
+++ b/src/RioJoy.Core/Calibration/AxisCalibrator.cs
@@ -68,7 +68,7 @@ public sealed class AxisCalibrator
Clamp(x), Clamp(y), Clamp(z), Clamp(rx), Clamp(ry), Clamp(rz));
}
- private static int Clamp(int v) => Math.Clamp(v, 0, AxisOutputs.Max);
+ private static int Clamp(int v) => RioJoy.Core.Compat.Net48Math.Clamp(v, 0, AxisOutputs.Max);
private int Throttle(int throttle)
{
diff --git a/src/RioJoy.Core/Compat/Net48Polyfills.cs b/src/RioJoy.Core/Compat/Net48Polyfills.cs
new file mode 100644
index 0000000..1f11890
--- /dev/null
+++ b/src/RioJoy.Core/Compat/Net48Polyfills.cs
@@ -0,0 +1,52 @@
+// net48 compatibility shims. On net8 these APIs are in the BCL; net48 lacks them.
+// (Language-feature polyfills — init/records/Index/Range/required — come from the
+// PolySharp source generator; this file covers runtime helpers that PolySharp
+// cannot supply because they live on existing BCL types.)
+
+namespace System.Collections.Generic
+{
+ public static class Net48CollectionExtensions
+ {
+ ///
+ /// Polyfill for CollectionExtensions.GetValueOrDefault (netstandard2.1+),
+ /// absent on net48. Returns the value for or
+ /// default(TValue) when the key is missing.
+ ///
+ public static TValue? GetValueOrDefault(
+ this IReadOnlyDictionary dictionary, TKey key)
+ {
+ if (dictionary is null) throw new ArgumentNullException(nameof(dictionary));
+ return dictionary.TryGetValue(key, out var value) ? value : default;
+ }
+
+ ///
+ /// Polyfill for KeyValuePair<,>.Deconstruct (netstandard2.1+), absent on
+ /// net48 — enables foreach (var (k, v) in dictionary).
+ ///
+ public static void Deconstruct(
+ this KeyValuePair pair, out TKey key, out TValue value)
+ {
+ key = pair.Key;
+ value = pair.Value;
+ }
+ }
+}
+
+namespace RioJoy.Core.Compat
+{
+ internal static class Net48Math
+ {
+ ///
+ /// Polyfill for Math.Clamp (netstandard2.1+), absent on net48. Returns
+ /// bounded to [, ].
+ ///
+ public static T Clamp(T value, T min, T max) where T : IComparable
+ {
+ if (min.CompareTo(max) > 0)
+ throw new ArgumentException($"{nameof(min)} ({min}) cannot be greater than {nameof(max)} ({max}).");
+ if (value.CompareTo(min) < 0) return min;
+ if (value.CompareTo(max) > 0) return max;
+ return value;
+ }
+ }
+}
diff --git a/src/RioJoy.Core/Editing/SheetLayout.cs b/src/RioJoy.Core/Editing/SheetLayout.cs
index 1f70f16..2e77367 100644
--- a/src/RioJoy.Core/Editing/SheetLayout.cs
+++ b/src/RioJoy.Core/Editing/SheetLayout.cs
@@ -68,7 +68,7 @@ public static class SheetLayout
///
public static IReadOnlyList Parse(string csv, int maxTextLength = 40)
{
- ArgumentNullException.ThrowIfNull(csv);
+ if (csv is null) throw new ArgumentNullException(nameof(csv));
string[] lines = csv.Replace("\r\n", "\n").Replace('\r', '\n').Split('\n');
var cells = new List();
diff --git a/src/RioJoy.Core/Hid/RioHidReport.cs b/src/RioJoy.Core/Hid/RioHidReport.cs
index 4a5a376..b0df9fe 100644
--- a/src/RioJoy.Core/Hid/RioHidReport.cs
+++ b/src/RioJoy.Core/Hid/RioHidReport.cs
@@ -52,7 +52,7 @@ public sealed class RioHidReport
/// Set an axis value (clamped to 0..), little-endian.
public void SetAxis(JoyAxis axis, int value)
{
- ushort v = (ushort)Math.Clamp(value, 0, AxisMax);
+ ushort v = (ushort)RioJoy.Core.Compat.Net48Math.Clamp(value, 0, AxisMax);
int offset = (int)axis * 2;
_buffer[offset] = (byte)(v & 0xFF);
_buffer[offset + 1] = (byte)(v >> 8);
diff --git a/src/RioJoy.Core/Mapping/RioInputMap.cs b/src/RioJoy.Core/Mapping/RioInputMap.cs
index 06290e8..7a3d4cd 100644
--- a/src/RioJoy.Core/Mapping/RioInputMap.cs
+++ b/src/RioJoy.Core/Mapping/RioInputMap.cs
@@ -16,7 +16,7 @@ public sealed class RioInputMap
/// Create a map from raw 16-bit words indexed by address.
public RioInputMap(IReadOnlyDictionary entries)
{
- ArgumentNullException.ThrowIfNull(entries);
+ if (entries is null) throw new ArgumentNullException(nameof(entries));
foreach ((int address, ushort raw) in entries)
this[address] = new RioMapEntry(raw);
}
diff --git a/src/RioJoy.Core/Output/HidFeederJoystickSink.cs b/src/RioJoy.Core/Output/HidFeederJoystickSink.cs
index f515f6b..a22e91e 100644
--- a/src/RioJoy.Core/Output/HidFeederJoystickSink.cs
+++ b/src/RioJoy.Core/Output/HidFeederJoystickSink.cs
@@ -1,5 +1,4 @@
using System.Runtime.InteropServices;
-using System.Runtime.Versioning;
using Microsoft.Win32.SafeHandles;
using RioJoy.Core.Calibration;
using RioJoy.Core.Hid;
@@ -14,7 +13,6 @@ namespace RioJoy.Core.Output;
/// DeviceIoControl(IOCTL_RIO_SUBMIT_REPORT). Replaces
/// once the driver is installed (Phase 1/3b).
///
-[SupportedOSPlatform("windows")]
public sealed class HidFeederJoystickSink : IJoystickSink, IDisposable
{
// Must match driver/RioGamepad/Public.h.
diff --git a/src/RioJoy.Core/Output/SendInputSink.cs b/src/RioJoy.Core/Output/SendInputSink.cs
index 33f1b26..0b745a9 100644
--- a/src/RioJoy.Core/Output/SendInputSink.cs
+++ b/src/RioJoy.Core/Output/SendInputSink.cs
@@ -1,5 +1,4 @@
using System.Runtime.InteropServices;
-using System.Runtime.Versioning;
using RioJoy.Core.Mapping;
using MouseButtonId = RioJoy.Core.Mapping.MouseButton;
@@ -12,7 +11,6 @@ namespace RioJoy.Core.Output;
/// scancodes (most do) see the keypress. Targets the interactive session, which
/// is why RIOJoy is a tray app rather than a service (see docs/PLAN.md risks).
///
-[SupportedOSPlatform("windows")]
public sealed class SendInputSink : IInputSink
{
public void KeyDown(byte virtualKey, bool extended) => SendKey(virtualKey, extended, keyUp: false);
diff --git a/src/RioJoy.Core/Output/ViGEmJoystickSink.cs b/src/RioJoy.Core/Output/ViGEmJoystickSink.cs
index a40849d..5d64aaf 100644
--- a/src/RioJoy.Core/Output/ViGEmJoystickSink.cs
+++ b/src/RioJoy.Core/Output/ViGEmJoystickSink.cs
@@ -1,4 +1,3 @@
-using System.Runtime.Versioning;
using Nefarius.ViGEm.Client;
using Nefarius.ViGEm.Client.Targets;
using Nefarius.ViGEm.Client.Targets.Xbox360;
@@ -14,7 +13,6 @@ namespace RioJoy.Core.Output;
/// triggers, and the D-pad for the hat. RIO joystick buttons beyond
/// are dropped (map those to the keyboard).
///
-[SupportedOSPlatform("windows")]
public sealed class ViGEmJoystickSink : IJoystickSink, IDisposable
{
// RIO joystick button (1-based) -> Xbox 360 button, in a natural order.
@@ -115,11 +113,11 @@ public sealed class ViGEmJoystickSink : IJoystickSink, IDisposable
// RIO axis 0..32766 (centre 16383) -> Xbox thumb short -32768..32767.
private static short ToThumb(int value) =>
- (short)Math.Clamp((value - AxisOutputs.Center) * 2, short.MinValue, short.MaxValue);
+ (short)RioJoy.Core.Compat.Net48Math.Clamp((value - AxisOutputs.Center) * 2, short.MinValue, short.MaxValue);
// RIO axis 0..32766 -> Xbox trigger byte 0..255.
private static byte ToTrigger(int value) =>
- (byte)Math.Clamp(value * 255 / AxisOutputs.Max, 0, 255);
+ (byte)RioJoy.Core.Compat.Net48Math.Clamp(value * 255 / AxisOutputs.Max, 0, 255);
public void Dispose()
{
diff --git a/src/RioJoy.Core/Overlay/FontFitter.cs b/src/RioJoy.Core/Overlay/FontFitter.cs
index 5723ec7..22ebc96 100644
--- a/src/RioJoy.Core/Overlay/FontFitter.cs
+++ b/src/RioJoy.Core/Overlay/FontFitter.cs
@@ -27,7 +27,7 @@ public static class FontFitter
public static double BestFitSize(
string text, string fontFamily, double width, double height, ITextMeasurer measurer)
{
- ArgumentNullException.ThrowIfNull(measurer);
+ if (measurer is null) throw new ArgumentNullException(nameof(measurer));
// State mirrors the Scheme named-let: (fontsize last-extents last-fontsize adjust).
double fontsize = 6; // minimum possible fontsize (the loop's seed)
diff --git a/src/RioJoy.Core/Overlay/GoobieDataImporter.cs b/src/RioJoy.Core/Overlay/GoobieDataImporter.cs
index fbad471..342879d 100644
--- a/src/RioJoy.Core/Overlay/GoobieDataImporter.cs
+++ b/src/RioJoy.Core/Overlay/GoobieDataImporter.cs
@@ -26,7 +26,7 @@ public static class GoobieDataImporter
/// Parse the data file text into fields + rows.
public static Sheet Parse(string text)
{
- ArgumentNullException.ThrowIfNull(text);
+ if (text is null) throw new ArgumentNullException(nameof(text));
List> lists = ReadLists(text);
if (lists.Count == 0)
@@ -53,7 +53,7 @@ public static class GoobieDataImporter
///
public static IReadOnlyDictionary LabelsForRow(Sheet sheet, int rowIndex = 0)
{
- ArgumentNullException.ThrowIfNull(sheet);
+ if (sheet is null) throw new ArgumentNullException(nameof(sheet));
if (rowIndex < 0 || rowIndex >= sheet.Rows.Count)
throw new ArgumentOutOfRangeException(nameof(rowIndex));
diff --git a/src/RioJoy.Core/Overlay/OverlayLayoutEngine.cs b/src/RioJoy.Core/Overlay/OverlayLayoutEngine.cs
index b2949ae..0d774f8 100644
--- a/src/RioJoy.Core/Overlay/OverlayLayoutEngine.cs
+++ b/src/RioJoy.Core/Overlay/OverlayLayoutEngine.cs
@@ -25,8 +25,8 @@ public sealed class OverlayLayoutEngine
IReadOnlyDictionary labels,
OverlayLayoutOptions? options = null)
{
- ArgumentNullException.ThrowIfNull(template);
- ArgumentNullException.ThrowIfNull(labels);
+ if (template is null) throw new ArgumentNullException(nameof(template));
+ if (labels is null) throw new ArgumentNullException(nameof(labels));
options ??= new OverlayLayoutOptions();
var placed = new List(template.Regions.Count);
@@ -41,8 +41,8 @@ public sealed class OverlayLayoutEngine
/// Lay out a single label within a region.
public PlacedLabel Place(OverlayRegion region, string text, OverlayLayoutOptions? options = null)
{
- ArgumentNullException.ThrowIfNull(region);
- ArgumentException.ThrowIfNullOrEmpty(text);
+ if (region is null) throw new ArgumentNullException(nameof(region));
+ if (string.IsNullOrEmpty(text)) throw new ArgumentException("Value cannot be null or empty.", nameof(text));
options ??= new OverlayLayoutOptions();
double rotation = region.RotationDegrees ?? 0;
diff --git a/src/RioJoy.Core/Overlay/OverlayTemplateStore.cs b/src/RioJoy.Core/Overlay/OverlayTemplateStore.cs
index d7ed400..720e0ab 100644
--- a/src/RioJoy.Core/Overlay/OverlayTemplateStore.cs
+++ b/src/RioJoy.Core/Overlay/OverlayTemplateStore.cs
@@ -20,20 +20,20 @@ public static class OverlayTemplateStore
public static string Serialize(OverlayTemplate template)
{
- ArgumentNullException.ThrowIfNull(template);
+ if (template is null) throw new ArgumentNullException(nameof(template));
return JsonSerializer.Serialize(template, Options);
}
public static OverlayTemplate Deserialize(string json)
{
- ArgumentException.ThrowIfNullOrWhiteSpace(json);
+ if (string.IsNullOrWhiteSpace(json)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(json));
return JsonSerializer.Deserialize(json, Options)
?? throw new JsonException("Overlay template JSON deserialized to null.");
}
public static void Save(OverlayTemplate template, string path)
{
- ArgumentException.ThrowIfNullOrWhiteSpace(path);
+ if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(path));
string? dir = Path.GetDirectoryName(Path.GetFullPath(path));
if (!string.IsNullOrEmpty(dir))
Directory.CreateDirectory(dir);
@@ -42,7 +42,7 @@ public static class OverlayTemplateStore
public static OverlayTemplate Load(string path)
{
- ArgumentException.ThrowIfNullOrWhiteSpace(path);
+ if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(path));
return Deserialize(File.ReadAllText(path));
}
}
diff --git a/src/RioJoy.Core/Plasma/PlasmaCommands.cs b/src/RioJoy.Core/Plasma/PlasmaCommands.cs
index 4f3fe0c..ead3704 100644
--- a/src/RioJoy.Core/Plasma/PlasmaCommands.cs
+++ b/src/RioJoy.Core/Plasma/PlasmaCommands.cs
@@ -48,8 +48,9 @@ public static class PlasmaCommands
/// Encode display text as raw bytes (Latin-1, one byte per char).
public static byte[] Text(string text)
{
- ArgumentNullException.ThrowIfNull(text);
- return Encoding.Latin1.GetBytes(text);
+ if (text is null) throw new ArgumentNullException(nameof(text));
+ // net48 has no Encoding.Latin1; codepage 28591 is ISO-8859-1 (Latin-1).
+ return Encoding.GetEncoding(28591).GetBytes(text);
}
///
@@ -72,7 +73,7 @@ public static class PlasmaCommands
public static (byte x, byte y, byte font, int length) ResolvePosText(
string text, byte x, byte y, byte font)
{
- ArgumentNullException.ThrowIfNull(text);
+ if (text is null) throw new ArgumentNullException(nameof(text));
int len = text.Length;
if (font != 2) // not the Score font
diff --git a/src/RioJoy.Core/Profiles/AutoSwitch.cs b/src/RioJoy.Core/Profiles/AutoSwitch.cs
index e5b7da3..67bd2fd 100644
--- a/src/RioJoy.Core/Profiles/AutoSwitch.cs
+++ b/src/RioJoy.Core/Profiles/AutoSwitch.cs
@@ -48,7 +48,7 @@ public static class AutoSwitchResolver
/// Normalize an executable name for matching: basename, no ".exe", lower-case.
public static string Normalize(string executable)
{
- ArgumentNullException.ThrowIfNull(executable);
+ if (executable is null) throw new ArgumentNullException(nameof(executable));
string name = executable.Trim();
// Strip any path (handle both separators regardless of OS).
int slash = name.LastIndexOfAny(new[] { '/', '\\' });
@@ -68,7 +68,7 @@ public static class AutoSwitchResolver
///
public static SwitchDecision Resolve(AppConfig config, string? foregroundExecutable)
{
- ArgumentNullException.ThrowIfNull(config);
+ if (config is null) throw new ArgumentNullException(nameof(config));
if (!string.IsNullOrWhiteSpace(foregroundExecutable))
{
diff --git a/src/RioJoy.Core/Profiles/AutoSwitchWatcher.cs b/src/RioJoy.Core/Profiles/AutoSwitchWatcher.cs
index a37c96c..7c481f4 100644
--- a/src/RioJoy.Core/Profiles/AutoSwitchWatcher.cs
+++ b/src/RioJoy.Core/Profiles/AutoSwitchWatcher.cs
@@ -56,9 +56,19 @@ public sealed class AutoSwitchWatcher
/// Poll on until cancelled.
public async Task RunAsync(TimeSpan interval, CancellationToken cancellationToken)
{
- using var timer = new PeriodicTimer(interval);
+ // net48 has no PeriodicTimer; poll on a Task.Delay loop instead.
Poll();
- while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false))
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ try
+ {
+ await Task.Delay(interval, cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
Poll();
+ }
}
}
diff --git a/src/RioJoy.Core/Profiles/ConfigStore.cs b/src/RioJoy.Core/Profiles/ConfigStore.cs
index 99052cb..a3f903b 100644
--- a/src/RioJoy.Core/Profiles/ConfigStore.cs
+++ b/src/RioJoy.Core/Profiles/ConfigStore.cs
@@ -18,13 +18,13 @@ public static class ConfigStore
public static string Serialize(AppConfig config)
{
- ArgumentNullException.ThrowIfNull(config);
+ if (config is null) throw new ArgumentNullException(nameof(config));
return JsonSerializer.Serialize(config, Options);
}
public static AppConfig Deserialize(string json)
{
- ArgumentException.ThrowIfNullOrWhiteSpace(json);
+ if (string.IsNullOrWhiteSpace(json)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(json));
return JsonSerializer.Deserialize(json, Options)
?? throw new JsonException("Config JSON deserialized to null.");
}
@@ -32,7 +32,7 @@ public static class ConfigStore
/// Save the config to (creating directories).
public static void Save(AppConfig config, string path)
{
- ArgumentException.ThrowIfNullOrWhiteSpace(path);
+ if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(path));
string? dir = Path.GetDirectoryName(Path.GetFullPath(path));
if (!string.IsNullOrEmpty(dir))
Directory.CreateDirectory(dir);
@@ -45,7 +45,7 @@ public static class ConfigStore
///
public static AppConfig Load(string path)
{
- ArgumentException.ThrowIfNullOrWhiteSpace(path);
+ if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(path));
return File.Exists(path) ? Deserialize(File.ReadAllText(path)) : new AppConfig();
}
}
diff --git a/src/RioJoy.Core/Profiles/IniFile.cs b/src/RioJoy.Core/Profiles/IniFile.cs
index 49b38c0..2160715 100644
--- a/src/RioJoy.Core/Profiles/IniFile.cs
+++ b/src/RioJoy.Core/Profiles/IniFile.cs
@@ -11,7 +11,7 @@ public sealed class IniFile
public static IniFile Parse(string text)
{
- ArgumentNullException.ThrowIfNull(text);
+ if (text is null) throw new ArgumentNullException(nameof(text));
var ini = new IniFile();
var current = new Dictionary(StringComparer.OrdinalIgnoreCase);
ini._sections[string.Empty] = current;
@@ -68,9 +68,9 @@ public sealed class IniFile
/// Parse an integer value that may be hex (0x…) or decimal.
public static int ParseInt(string value)
{
- ArgumentNullException.ThrowIfNull(value);
+ if (value is null) throw new ArgumentNullException(nameof(value));
string v = value.Trim();
- bool neg = v.StartsWith('-');
+ bool neg = v.StartsWith("-", StringComparison.Ordinal);
if (neg) v = v[1..];
int magnitude = v.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
diff --git a/src/RioJoy.Core/Profiles/RioIniImporter.cs b/src/RioJoy.Core/Profiles/RioIniImporter.cs
index b6b88aa..6b7487b 100644
--- a/src/RioJoy.Core/Profiles/RioIniImporter.cs
+++ b/src/RioJoy.Core/Profiles/RioIniImporter.cs
@@ -15,7 +15,7 @@ public static class RioIniImporter
/// Parse into a profile named .
public static RioProfile Import(string iniText, string name = "Imported")
{
- ArgumentNullException.ThrowIfNull(iniText);
+ if (iniText is null) throw new ArgumentNullException(nameof(iniText));
IniFile ini = IniFile.Parse(iniText);
var profile = new RioProfile
diff --git a/src/RioJoy.Core/Protocol/PacketParser.cs b/src/RioJoy.Core/Protocol/PacketParser.cs
index 5faae99..9dbc7ed 100644
--- a/src/RioJoy.Core/Protocol/PacketParser.cs
+++ b/src/RioJoy.Core/Protocol/PacketParser.cs
@@ -97,7 +97,7 @@ public sealed class PacketParser
///
public void Feed(ReadOnlySpan data, Action onEvent)
{
- ArgumentNullException.ThrowIfNull(onEvent);
+ if (onEvent is null) throw new ArgumentNullException(nameof(onEvent));
foreach (byte ch in data)
{
if (Feed(ch, out RioRxEvent ev))
diff --git a/src/RioJoy.Core/Protocol/RioPacket.cs b/src/RioJoy.Core/Protocol/RioPacket.cs
index 60adc2d..8be56fd 100644
--- a/src/RioJoy.Core/Protocol/RioPacket.cs
+++ b/src/RioJoy.Core/Protocol/RioPacket.cs
@@ -30,7 +30,7 @@ public readonly struct RioPacket
public override string ToString()
{
- var hex = Convert.ToHexString(Payload.Span);
+ var hex = BitConverter.ToString(Payload.ToArray()).Replace("-", string.Empty);
return Payload.IsEmpty ? Command.ToString() : $"{Command} [{hex}]";
}
}
diff --git a/src/RioJoy.Core/RioJoy.Core.csproj b/src/RioJoy.Core/RioJoy.Core.csproj
index b2e5387..ffb09f8 100644
--- a/src/RioJoy.Core/RioJoy.Core.csproj
+++ b/src/RioJoy.Core/RioJoy.Core.csproj
@@ -1,10 +1,12 @@
-
- net8.0-windows
+
+ net48
x64
+ x64
enable
enable
latest
@@ -13,6 +15,13 @@
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/src/RioJoy.Core/RioRuntime.cs b/src/RioJoy.Core/RioRuntime.cs
index 867a3ee..fb1d42a 100644
--- a/src/RioJoy.Core/RioRuntime.cs
+++ b/src/RioJoy.Core/RioRuntime.cs
@@ -30,8 +30,8 @@ public sealed class RioRuntime : IRioCommandSink, IDisposable
AxisCalibrator? calibrator = null)
{
_link = link ?? throw new ArgumentNullException(nameof(link));
- ArgumentNullException.ThrowIfNull(map);
- ArgumentNullException.ThrowIfNull(input);
+ if (map is null) throw new ArgumentNullException(nameof(map));
+ if (input is null) throw new ArgumentNullException(nameof(input));
_joystick = joystick ?? throw new ArgumentNullException(nameof(joystick));
_calibrator = calibrator ?? new AxisCalibrator();
diff --git a/src/RioJoy.Core/Serial/SerialPortTransport.cs b/src/RioJoy.Core/Serial/SerialPortTransport.cs
index e808671..bb2107a 100644
--- a/src/RioJoy.Core/Serial/SerialPortTransport.cs
+++ b/src/RioJoy.Core/Serial/SerialPortTransport.cs
@@ -22,7 +22,7 @@ public sealed class SerialPortTransport : IRioTransport
public SerialPortTransport(string portName)
{
- ArgumentException.ThrowIfNullOrWhiteSpace(portName);
+ if (string.IsNullOrWhiteSpace(portName)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(portName));
_port = new SerialPort(portName, BaudRate, Parity.None, 8, StopBits.One)
{
@@ -44,11 +44,21 @@ public sealed class SerialPortTransport : IRioTransport
public string Description => $"{_port.PortName} @ {BaudRate} 8N1";
- public ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken) =>
- _stream.ReadAsync(buffer, cancellationToken);
+ // net48's Stream has no Memory-based ReadAsync/WriteAsync overloads, so bridge
+ // through a pooled array and copy into/out of the caller's Memory.
+ public async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken)
+ {
+ byte[] tmp = new byte[buffer.Length];
+ int read = await _stream.ReadAsync(tmp, 0, tmp.Length, cancellationToken).ConfigureAwait(false);
+ new ReadOnlySpan(tmp, 0, read).CopyTo(buffer.Span);
+ return read;
+ }
- public ValueTask WriteAsync(ReadOnlyMemory data, CancellationToken cancellationToken) =>
- _stream.WriteAsync(data, cancellationToken);
+ public async ValueTask WriteAsync(ReadOnlyMemory data, CancellationToken cancellationToken)
+ {
+ byte[] tmp = data.ToArray();
+ await _stream.WriteAsync(tmp, 0, tmp.Length, cancellationToken).ConfigureAwait(false);
+ }
public void Dispose()
{
diff --git a/src/RioJoy.Overlay/ProfileWallpaperGenerator.cs b/src/RioJoy.Overlay/ProfileWallpaperGenerator.cs
index 7491c53..9079442 100644
--- a/src/RioJoy.Overlay/ProfileWallpaperGenerator.cs
+++ b/src/RioJoy.Overlay/ProfileWallpaperGenerator.cs
@@ -32,9 +32,9 @@ public sealed class ProfileWallpaperGenerator
string outputPath,
OverlayLayoutOptions? options = null)
{
- ArgumentNullException.ThrowIfNull(template);
- ArgumentNullException.ThrowIfNull(labels);
- ArgumentException.ThrowIfNullOrWhiteSpace(outputPath);
+ if (template is null) throw new ArgumentNullException(nameof(template));
+ if (labels is null) throw new ArgumentNullException(nameof(labels));
+ if (string.IsNullOrWhiteSpace(outputPath)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(outputPath));
if (string.IsNullOrWhiteSpace(template.BaseImagePath))
throw new InvalidOperationException("OverlayTemplate.BaseImagePath is not set.");
diff --git a/src/RioJoy.Overlay/RioJoy.Overlay.csproj b/src/RioJoy.Overlay/RioJoy.Overlay.csproj
index 90aa8c6..ac3dd16 100644
--- a/src/RioJoy.Overlay/RioJoy.Overlay.csproj
+++ b/src/RioJoy.Overlay/RioJoy.Overlay.csproj
@@ -1,8 +1,9 @@
- net8.0-windows
+ net48
x64
+ x64
enable
enable
latest
@@ -12,6 +13,10 @@
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/src/RioJoy.Overlay/SkiaOverlayRenderer.cs b/src/RioJoy.Overlay/SkiaOverlayRenderer.cs
index 0cc07fc..f20be11 100644
--- a/src/RioJoy.Overlay/SkiaOverlayRenderer.cs
+++ b/src/RioJoy.Overlay/SkiaOverlayRenderer.cs
@@ -35,9 +35,9 @@ public sealed class SkiaOverlayRenderer
SKBitmap baseImage,
OverlayLayoutOptions? options = null)
{
- ArgumentNullException.ThrowIfNull(template);
- ArgumentNullException.ThrowIfNull(labels);
- ArgumentNullException.ThrowIfNull(baseImage);
+ if (template is null) throw new ArgumentNullException(nameof(template));
+ if (labels is null) throw new ArgumentNullException(nameof(labels));
+ if (baseImage is null) throw new ArgumentNullException(nameof(baseImage));
options ??= new OverlayLayoutOptions();
var output = baseImage.Copy();
@@ -114,8 +114,8 @@ public sealed class SkiaOverlayRenderer
string outputPath,
OverlayLayoutOptions? options = null)
{
- ArgumentNullException.ThrowIfNull(template);
- ArgumentException.ThrowIfNullOrWhiteSpace(outputPath);
+ if (template is null) throw new ArgumentNullException(nameof(template));
+ if (string.IsNullOrWhiteSpace(outputPath)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(outputPath));
if (string.IsNullOrWhiteSpace(template.BaseImagePath))
throw new InvalidOperationException("OverlayTemplate.BaseImagePath is not set.");
diff --git a/src/RioJoy.Tray/Editor/PanelCanvas.cs b/src/RioJoy.Tray/Editor/PanelCanvas.cs
index 65021b5..42e07ba 100644
--- a/src/RioJoy.Tray/Editor/PanelCanvas.cs
+++ b/src/RioJoy.Tray/Editor/PanelCanvas.cs
@@ -1,4 +1,3 @@
-using System.Runtime.Versioning;
using RioJoy.Core.Editing;
namespace RioJoy.Tray.Editor;
@@ -8,7 +7,6 @@ namespace RioJoy.Tray.Editor;
/// ) and raises when a button
/// is clicked. Sized to the full panel so the host scrolls it.
///
-[SupportedOSPlatform("windows")]
internal sealed class PanelCanvas : Control
{
private static readonly IReadOnlyList AllButtons = CockpitPanel.Buttons();
diff --git a/src/RioJoy.Tray/Editor/PanelView.cs b/src/RioJoy.Tray/Editor/PanelView.cs
index c90513c..edf414a 100644
--- a/src/RioJoy.Tray/Editor/PanelView.cs
+++ b/src/RioJoy.Tray/Editor/PanelView.cs
@@ -1,4 +1,3 @@
-using System.Runtime.Versioning;
using RioJoy.Core.Editing;
namespace RioJoy.Tray.Editor;
@@ -28,7 +27,6 @@ public sealed record CalibrationCell(JoyStickSetting Setting, string Label, int
/// shaded by whether the address is assigned (Off vs Dim); keypads are neutral
/// (no lamps). Shared by and offline rendering.
///
-[SupportedOSPlatform("windows")]
public static class PanelView
{
public const int CellW = 66;
diff --git a/src/RioJoy.Tray/Editor/ProfileEditorForm.cs b/src/RioJoy.Tray/Editor/ProfileEditorForm.cs
index ba45ced..d5ab933 100644
--- a/src/RioJoy.Tray/Editor/ProfileEditorForm.cs
+++ b/src/RioJoy.Tray/Editor/ProfileEditorForm.cs
@@ -1,4 +1,3 @@
-using System.Runtime.Versioning;
using System.Text;
using RioJoy.Core.Editing;
using RioJoy.Core.Output;
@@ -17,7 +16,6 @@ namespace RioJoy.Tray.Editor;
/// wallpaper region b-XX) and (the iRIO
/// word); Save raises .
///
-[SupportedOSPlatform("windows")]
public sealed class ProfileEditorForm : Form
{
private static readonly Dictionary GroupOf =
@@ -30,7 +28,7 @@ public sealed class ProfileEditorForm : Form
private readonly PanelCanvas _canvas = new();
private readonly TextBox _nameBox = new() { Location = new Point(66, 12), Width = 234 };
- private readonly TextBox _matchBox = new() { Location = new Point(66, 40), Width = 234, PlaceholderText = "game.exe, other.exe — auto-switch" };
+ private readonly TextBox _matchBox = new() { Location = new Point(66, 40), Width = 234 };
private readonly Label _info = new() { AutoSize = true, Location = new Point(12, 68), MaximumSize = new Size(310, 0) };
private readonly TextBox _labelBox = new() { Location = new Point(70, 100), Width = 230 };
private readonly ComboBox _kindBox = new() { Location = new Point(70, 134), Width = 150, DropDownStyle = ComboBoxStyle.DropDownList };
@@ -116,7 +114,7 @@ public sealed class ProfileEditorForm : Form
StartPosition = FormStartPosition.CenterScreen;
MinimumSize = new Size(900, 500);
- _kindBox.Items.AddRange(Enum.GetNames());
+ _kindBox.Items.AddRange(Enum.GetNames(typeof(RioRouteKind)));
_kindBox.SelectedIndexChanged += OnKindChanged;
_canvas.LabelProvider = a => _profile.OverlayLabels.TryGetValue(RegionFor(a), out string? t) ? t : null;
@@ -271,11 +269,11 @@ public sealed class ProfileEditorForm : Form
_valueCombo.Items.Add(new ValueItem(h.ToString(), (byte)(int)h));
break;
case RioRouteKind.Mouse:
- foreach (MouseAction m in Enum.GetValues())
+ foreach (MouseAction m in (MouseAction[])Enum.GetValues(typeof(MouseAction)))
_valueCombo.Items.Add(new ValueItem(m.ToString(), (byte)m));
break;
case RioRouteKind.RioCommand:
- foreach (RioCommandCode c in Enum.GetValues())
+ foreach (RioCommandCode c in (RioCommandCode[])Enum.GetValues(typeof(RioCommandCode)))
_valueCombo.Items.Add(new ValueItem(c.ToString(), (byte)c));
break;
}
@@ -349,7 +347,9 @@ public sealed class ProfileEditorForm : Form
// The foreground executables that auto-activate this profile (comma-separated).
_profile.MatchExecutables = _matchBox.Text
- .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
+ .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(s => s.Trim())
+ .Where(s => s.Length > 0)
.ToList();
ApplyToCell();
diff --git a/src/RioJoy.Tray/Os/AutoStartManager.cs b/src/RioJoy.Tray/Os/AutoStartManager.cs
index eb9ee65..7289698 100644
--- a/src/RioJoy.Tray/Os/AutoStartManager.cs
+++ b/src/RioJoy.Tray/Os/AutoStartManager.cs
@@ -1,4 +1,3 @@
-using System.Runtime.Versioning;
using Microsoft.Win32;
namespace RioJoy.Tray.Os;
@@ -8,7 +7,6 @@ namespace RioJoy.Tray.Os;
/// Run registry key. Per-user (HKCU) needs no elevation and matches the
/// tray app running in the interactive session.
///
-[SupportedOSPlatform("windows")]
public static class AutoStartManager
{
private const string RunKey = @"Software\Microsoft\Windows\CurrentVersion\Run";
@@ -24,7 +22,7 @@ public static class AutoStartManager
{
using RegistryKey key = Registry.CurrentUser.CreateSubKey(RunKey);
if (enabled)
- key.SetValue(ValueName, $"\"{Environment.ProcessPath}\"");
+ key.SetValue(ValueName, $"\"{Application.ExecutablePath}\"");
else
key.DeleteValue(ValueName, throwOnMissingValue: false);
}
diff --git a/src/RioJoy.Tray/Os/ForegroundProcessProvider.cs b/src/RioJoy.Tray/Os/ForegroundProcessProvider.cs
index 3f04e65..c402006 100644
--- a/src/RioJoy.Tray/Os/ForegroundProcessProvider.cs
+++ b/src/RioJoy.Tray/Os/ForegroundProcessProvider.cs
@@ -1,6 +1,5 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
-using System.Runtime.Versioning;
using RioJoy.Core.Profiles;
namespace RioJoy.Tray.Os;
@@ -10,7 +9,6 @@ namespace RioJoy.Tray.Os;
/// foreground window's owning process and returns its executable file name. Used
/// by the auto-switch watcher to detect native games vs. supported titles.
///
-[SupportedOSPlatform("windows")]
public sealed class ForegroundProcessProvider : IForegroundProcessProvider
{
public string? GetForegroundExecutable()
diff --git a/src/RioJoy.Tray/Program.cs b/src/RioJoy.Tray/Program.cs
index 9f77fc3..eb34f5e 100644
--- a/src/RioJoy.Tray/Program.cs
+++ b/src/RioJoy.Tray/Program.cs
@@ -20,7 +20,10 @@ internal static class Program
if (!createdNew)
return; // another RIOJoy is already running in this session
- ApplicationConfiguration.Initialize();
+ // net48 has no source-generated ApplicationConfiguration.Initialize();
+ // do the equivalent setup directly.
+ Application.EnableVisualStyles();
+ Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new TrayApplicationContext());
}
}
diff --git a/src/RioJoy.Tray/RioCoordinator.cs b/src/RioJoy.Tray/RioCoordinator.cs
index 4ce6da1..035a0ce 100644
--- a/src/RioJoy.Tray/RioCoordinator.cs
+++ b/src/RioJoy.Tray/RioCoordinator.cs
@@ -1,4 +1,3 @@
-using System.Runtime.Versioning;
using RioJoy.Core;
using RioJoy.Core.Calibration;
using RioJoy.Core.Mapping;
@@ -22,7 +21,6 @@ namespace RioJoy.Tray;
/// edited (). This keeps the COM port free the rest
/// of the time so the native games can open it without clashing.
///
-[SupportedOSPlatform("windows")]
public sealed class RioCoordinator : IDisposable
{
private readonly Func _config;
diff --git a/src/RioJoy.Tray/RioJoy.Tray.csproj b/src/RioJoy.Tray/RioJoy.Tray.csproj
index a5f1f5c..4bcbb12 100644
--- a/src/RioJoy.Tray/RioJoy.Tray.csproj
+++ b/src/RioJoy.Tray/RioJoy.Tray.csproj
@@ -2,8 +2,9 @@
WinExe
- net8.0-windows
+ net48
x64
+ x64
enable
true
enable
@@ -16,4 +17,11 @@
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
diff --git a/src/RioJoy.Tray/TrayApplicationContext.cs b/src/RioJoy.Tray/TrayApplicationContext.cs
index 82f66b1..ac9204b 100644
--- a/src/RioJoy.Tray/TrayApplicationContext.cs
+++ b/src/RioJoy.Tray/TrayApplicationContext.cs
@@ -1,4 +1,3 @@
-using System.Runtime.Versioning;
using RioJoy.Core;
using RioJoy.Core.Mapping;
using RioJoy.Core.Profiles;
@@ -14,7 +13,6 @@ namespace RioJoy.Tray;
/// auto-switch watcher is polled on a UI timer so menu/status updates stay on the
/// UI thread.
///
-[SupportedOSPlatform("windows")]
internal sealed class TrayApplicationContext : ApplicationContext
{
private static readonly string ConfigPath =
diff --git a/src/RioJoy.Tray/WallpaperApplier.cs b/src/RioJoy.Tray/WallpaperApplier.cs
index 024f9fb..9370f48 100644
--- a/src/RioJoy.Tray/WallpaperApplier.cs
+++ b/src/RioJoy.Tray/WallpaperApplier.cs
@@ -1,5 +1,4 @@
using System.Runtime.InteropServices;
-using System.Runtime.Versioning;
namespace RioJoy.Tray;
@@ -14,7 +13,6 @@ namespace RioJoy.Tray;
/// a user-visible system setting, so it is only invoked on an explicit profile
/// activation, never speculatively.
///
-[SupportedOSPlatform("windows")]
public static class WallpaperApplier
{
private const int SPI_SETDESKWALLPAPER = 0x0014;
@@ -27,7 +25,7 @@ public static class WallpaperApplier
///
public static bool Apply(string imagePath)
{
- ArgumentException.ThrowIfNullOrWhiteSpace(imagePath);
+ if (string.IsNullOrWhiteSpace(imagePath)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(imagePath));
string full = Path.GetFullPath(imagePath);
if (!File.Exists(full))
throw new FileNotFoundException("Wallpaper image not found.", full);
diff --git a/tests/RioJoy.Core.Tests/Overlay/SkiaOverlayRendererTests.cs b/tests/RioJoy.Core.Tests/Overlay/SkiaOverlayRendererTests.cs
index 89db36b..87dc9f5 100644
--- a/tests/RioJoy.Core.Tests/Overlay/SkiaOverlayRendererTests.cs
+++ b/tests/RioJoy.Core.Tests/Overlay/SkiaOverlayRendererTests.cs
@@ -77,6 +77,6 @@ public class SkiaOverlayRendererTests
byte[] png = new SkiaOverlayRenderer().RenderToPng(template, new Dictionary { ["x"] = "HI" }, baseImg);
Assert.True(png.Length > 8);
- Assert.Equal(new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }, png[..8]);
+ Assert.Equal(new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }, png.AsSpan(0, 8).ToArray());
}
}
diff --git a/tests/RioJoy.Core.Tests/Protocol/PacketBuilderTests.cs b/tests/RioJoy.Core.Tests/Protocol/PacketBuilderTests.cs
index 273d2f0..709a583 100644
--- a/tests/RioJoy.Core.Tests/Protocol/PacketBuilderTests.cs
+++ b/tests/RioJoy.Core.Tests/Protocol/PacketBuilderTests.cs
@@ -41,7 +41,7 @@ public class PacketBuilderTests
byte[] packet = PacketBuilder.Build(RioCommand.AnalogReply, payload);
Assert.Equal((byte)RioCommand.AnalogReply, packet[0]);
- Assert.Equal(payload, packet[1..^1]);
- Assert.Equal(RioChecksum.Compute(packet[..^1]), packet[^1]);
+ Assert.Equal(payload, packet.AsSpan(1, packet.Length - 2).ToArray());
+ Assert.Equal(RioChecksum.Compute(packet.AsSpan(0, packet.Length - 1)), packet[^1]);
}
}
diff --git a/tests/RioJoy.Core.Tests/RioJoy.Core.Tests.csproj b/tests/RioJoy.Core.Tests/RioJoy.Core.Tests.csproj
index 650566e..fdbe434 100644
--- a/tests/RioJoy.Core.Tests/RioJoy.Core.Tests.csproj
+++ b/tests/RioJoy.Core.Tests/RioJoy.Core.Tests.csproj
@@ -1,8 +1,9 @@
- net8.0-windows
+ net48
x64
+ x64
enable
enable
latest
@@ -13,6 +14,11 @@
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/tests/RioJoy.Core.Tests/Serial/FakeTransport.cs b/tests/RioJoy.Core.Tests/Serial/FakeTransport.cs
index e820fd8..c86865e 100644
--- a/tests/RioJoy.Core.Tests/Serial/FakeTransport.cs
+++ b/tests/RioJoy.Core.Tests/Serial/FakeTransport.cs
@@ -40,7 +40,7 @@ internal sealed class FakeTransport : IRioTransport
public ValueTask WriteAsync(ReadOnlyMemory data, CancellationToken cancellationToken)
{
_writes.Writer.TryWrite(data.ToArray());
- return ValueTask.CompletedTask;
+ return default; // net48 has no ValueTask.CompletedTask; default(ValueTask) is the completed task
}
/// Read the next outbound write, failing if none arrives in time.
diff --git a/tests/RioJoy.Core.Tests/TestPolyfills.cs b/tests/RioJoy.Core.Tests/TestPolyfills.cs
new file mode 100644
index 0000000..0eca058
--- /dev/null
+++ b/tests/RioJoy.Core.Tests/TestPolyfills.cs
@@ -0,0 +1,21 @@
+namespace RioJoy.Core.Tests;
+
+///
+/// net48 test-only polyfills. Visible to all tests via enclosing-namespace scope.
+///
+internal static class TaskTestExtensions
+{
+ ///
+ /// Polyfill for Task<T>.WaitAsync(TimeSpan) (net6+), absent on net48.
+ /// Throws if the task does not complete in time.
+ ///
+ public static async Task WaitAsync(this Task task, TimeSpan timeout)
+ {
+ using var cts = new CancellationTokenSource();
+ Task completed = await Task.WhenAny(task, Task.Delay(timeout, cts.Token)).ConfigureAwait(false);
+ if (completed != task)
+ throw new TimeoutException($"Task did not complete within {timeout}.");
+ cts.Cancel(); // stop the delay timer
+ return await task.ConfigureAwait(false);
+ }
+}