- 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>
75 lines
2.8 KiB
C#
75 lines
2.8 KiB
C#
namespace RioJoy.Core.Profiles;
|
|
|
|
/// <summary>Supplies the current foreground application's executable name.</summary>
|
|
public interface IForegroundProcessProvider
|
|
{
|
|
/// <summary>The foreground executable name (e.g. "game.exe"), or null if unknown.</summary>
|
|
string? GetForegroundExecutable();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Polls the foreground process and resolves the three-state auto-switch decision,
|
|
/// raising <see cref="DecisionChanged"/> only when the decision actually changes.
|
|
/// The polling cadence is driven by the host; <see cref="Poll"/> performs one
|
|
/// resolution so the logic is deterministic and unit-testable.
|
|
/// </summary>
|
|
public sealed class AutoSwitchWatcher
|
|
{
|
|
private readonly IForegroundProcessProvider _provider;
|
|
private readonly Func<AppConfig> _configAccessor;
|
|
private SwitchDecision? _last;
|
|
|
|
public AutoSwitchWatcher(IForegroundProcessProvider provider, Func<AppConfig> configAccessor)
|
|
{
|
|
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
|
|
_configAccessor = configAccessor ?? throw new ArgumentNullException(nameof(configAccessor));
|
|
}
|
|
|
|
/// <summary>Raised when the resolved decision differs from the previous one.</summary>
|
|
public event Action<SwitchDecision>? DecisionChanged;
|
|
|
|
/// <summary>The most recently resolved decision (null until the first <see cref="Poll"/>).</summary>
|
|
public SwitchDecision? Current => _last;
|
|
|
|
/// <summary>
|
|
/// Forget the last decision so the next <see cref="Poll"/> re-raises
|
|
/// <see cref="DecisionChanged"/> even if the resolved decision is unchanged. Used
|
|
/// after a manual/editor override to re-sync the coordinator with the foreground app.
|
|
/// </summary>
|
|
public void Reset() => _last = null;
|
|
|
|
/// <summary>Resolve once; raise <see cref="DecisionChanged"/> if it changed. Returns the decision.</summary>
|
|
public SwitchDecision Poll()
|
|
{
|
|
string? exe = _provider.GetForegroundExecutable();
|
|
SwitchDecision decision = AutoSwitchResolver.Resolve(_configAccessor(), exe);
|
|
|
|
if (_last is null || !_last.Value.Equals(decision))
|
|
{
|
|
_last = decision;
|
|
DecisionChanged?.Invoke(decision);
|
|
}
|
|
|
|
return decision;
|
|
}
|
|
|
|
/// <summary>Poll on <paramref name="interval"/> until cancelled.</summary>
|
|
public async Task RunAsync(TimeSpan interval, CancellationToken cancellationToken)
|
|
{
|
|
// net48 has no PeriodicTimer; poll on a Task.Delay loop instead.
|
|
Poll();
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
await Compat.TaskCompat.Delay(interval, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
break;
|
|
}
|
|
Poll();
|
|
}
|
|
}
|
|
}
|