namespace RioJoy.Core.Profiles; /// The three states of the serial-port yield / profile auto-switch. public enum SwitchMode { /// A native game is foreground — release the COM port and go dormant. Yield, /// A supported non-native game is foreground — run its profile. Activate, /// Nothing matched — idle (port released or a neutral default). Idle, } /// The decision produced by . public readonly struct SwitchDecision : IEquatable { public SwitchMode Mode { get; } /// The profile to run (set when is ). 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(); } /// /// Resolves the foreground executable into a , 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. /// public static class AutoSwitchResolver { /// Normalize an executable name for matching: basename, no ".exe", lower-case. 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 patterns, string normalizedExe) => patterns.Any(p => Normalize(p) == normalizedExe); /// /// Decide what RIOJoy should do given the current foreground executable /// ( may be null when unknown/desktop). /// 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(); } }