Retargets RioJoy.Core/Overlay/Tray + tests from net8.0-windows to net48 so the app can be tested as a framework-dependent build (relies on the in-box .NET Framework 4.8 on Windows 10/11). Builds clean; all 241 tests pass. Polyfills (no behavior change): - PolySharp source generator for init/records/Index/Range/required members. - System.Memory, System.Text.Json, Microsoft.Bcl.HashCode, System.Threading.Channels (tests) NuGet packages. - Compat/Net48Polyfills.cs: GetValueOrDefault, KeyValuePair.Deconstruct, Math.Clamp; tests/TestPolyfills.cs: Task.WaitAsync. Source adjustments for APIs absent on net48: - ArgumentNullException/ArgumentException.ThrowIf* inlined to manual guards. - Convert.ToHexString, Encoding.Latin1, Environment.ProcessPath, ApplicationConfiguration.Initialize, Enum.GetNames<T>/GetValues<T>, string.StartsWith(char), string.Split(char, opts), TextBox.PlaceholderText, PeriodicTimer, Memory-based Stream Read/WriteAsync, array range-slicing. - Dropped [SupportedOSPlatform] hints (net48 is single-platform). deploy/build-package.ps1: publish framework-dependent (no self-contained). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
91 lines
3.7 KiB
C#
91 lines
3.7 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);
|
|
public override int GetHashCode() => HashCode.Combine(Mode, Profile);
|
|
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();
|
|
}
|
|
}
|