Files
riojoy/src/RioJoy.Core/Profiles/AutoSwitch.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

94 lines
3.8 KiB
C#

namespace RioJoy.Core.Profiles;
/// <summary>The three states of the serial-port yield / profile auto-switch.</summary>
public enum SwitchMode
{
/// <summary>A native game is foreground — release the COM port and go dormant.</summary>
Yield,
/// <summary>A supported non-native game is foreground — run its profile.</summary>
Activate,
/// <summary>Nothing matched — idle (port released or a neutral default).</summary>
Idle,
}
/// <summary>The decision produced by <see cref="AutoSwitchResolver"/>.</summary>
public readonly struct SwitchDecision : IEquatable<SwitchDecision>
{
public SwitchMode Mode { get; }
/// <summary>The profile to run (set when <see cref="Mode"/> is <see cref="SwitchMode.Activate"/>).</summary>
public RioProfile? Profile { get; }
private SwitchDecision(SwitchMode mode, RioProfile? profile)
{
Mode = mode;
Profile = profile;
}
public static SwitchDecision Yield() => new(SwitchMode.Yield, null);
public static SwitchDecision Idle() => new(SwitchMode.Idle, null);
public static SwitchDecision Activate(RioProfile profile) => new(SwitchMode.Activate, 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);
// 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();
}
/// <summary>
/// Resolves the foreground executable into a <see cref="SwitchDecision"/>, the
/// pure core of the auto-switch state machine (see docs/PLAN.md §Profiles).
/// Precedence: a native game always wins (yield); otherwise the first profile that
/// lists the executable activates; otherwise the neutral profile (if any) or idle.
/// </summary>
public static class AutoSwitchResolver
{
/// <summary>Normalize an executable name for matching: basename, no ".exe", lower-case.</summary>
public static string Normalize(string 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[] { '/', '\\' });
if (slash >= 0)
name = name[(slash + 1)..];
if (name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
name = name[..^4];
return name.ToLowerInvariant();
}
private static bool Matches(IEnumerable<string> patterns, string normalizedExe) =>
patterns.Any(p => Normalize(p) == normalizedExe);
/// <summary>
/// Decide what RIOJoy should do given the current foreground executable
/// (<paramref name="foregroundExecutable"/> may be null when unknown/desktop).
/// </summary>
public static SwitchDecision Resolve(AppConfig config, string? foregroundExecutable)
{
if (config is null) throw new ArgumentNullException(nameof(config));
if (!string.IsNullOrWhiteSpace(foregroundExecutable))
{
string exe = Normalize(foregroundExecutable);
if (Matches(config.NativeGameExecutables, exe))
return SwitchDecision.Yield();
foreach (RioProfile profile in config.Profiles)
{
if (Matches(profile.MatchExecutables, exe))
return SwitchDecision.Activate(profile);
}
}
RioProfile? neutral = config.FindProfile(config.NeutralProfileName);
return neutral is not null ? SwitchDecision.Activate(neutral) : SwitchDecision.Idle();
}
}