pod: fully explicit activation (--profile) + RIOJoy.Tray.Ready signal
No detection anywhere in the pod chain (Cyd: foreground detection is too slow - the pad appears after the game already enumerated controllers). --profile <name> activates immediately at startup, never runs the auto-switch watcher, and on success signals the named manual-reset event RIOJoy.Tray.Ready (process-lifetime, never stale) so a launcher waits on the signal instead of counting winmm devices. Legible failures for the launcher: exit 4 unknown profile, exit 5 activation failed with the reason on stderr - both verified live against the built exe. Editor close re-activates the explicit profile. build-pod start scripts now pass --profile <Name> --exit-with <exe>; docs updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,17 @@ internal static class Program
|
||||
/// then gone away (or never appeared within the startup grace). Also makes
|
||||
/// startup wait briefly for a predecessor instance instead of exiting, so
|
||||
/// back-to-back game launches hand the cockpit over cleanly.</para>
|
||||
///
|
||||
/// <para><c>--profile <name></c> activates that profile immediately
|
||||
/// and explicitly — <b>no foreground detection at all</b>. Pod launch
|
||||
/// scripts pass it so the ViGEm pad and the ports exist <i>before</i> the
|
||||
/// game starts and enumerates controllers; the auto-switch watcher never
|
||||
/// runs. On success the named event
|
||||
/// <see cref="TrayApplicationContext.ReadyEventName"/> is signaled — a pod
|
||||
/// launcher waits on it (with a timeout) instead of counting input
|
||||
/// devices. Exit code 4 when the named profile is not in the config; 5
|
||||
/// when explicit activation fails (reason on stderr) — distinguishable
|
||||
/// from a hang, so launcher failures stay legible.</para>
|
||||
/// </summary>
|
||||
[STAThread]
|
||||
private static int Main(string[] args)
|
||||
@@ -41,37 +52,58 @@ internal static class Program
|
||||
return ImportProfile(args);
|
||||
|
||||
CompanionTarget? exitWith;
|
||||
string? profileName;
|
||||
try
|
||||
{
|
||||
exitWith = ParseExitWith(args);
|
||||
exitWith = ParseValue(args, "--exit-with", "a process name or pid") is string target
|
||||
? CompanionTarget.Parse(target)
|
||||
: null;
|
||||
profileName = ParseValue(args, "--profile", "a profile name");
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
Console.Error.WriteLine($"usage: RioJoy.Tray [--exit-with <exe|pid>] ({ex.Message})");
|
||||
Console.Error.WriteLine($"usage: RioJoy.Tray [--profile <name>] [--exit-with <exe|pid>] ({ex.Message})");
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Validate the explicit profile up front so a pod script's typo is a
|
||||
// scriptable failure, not a silently idle tray.
|
||||
if (profileName is not null &&
|
||||
ConfigStore.Load(TrayApplicationContext.ConfigPath).FindProfile(profileName) is null)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"profile '{profileName}' not found in {TrayApplicationContext.ConfigPath}");
|
||||
return 4;
|
||||
}
|
||||
|
||||
using var instance = new Mutex(initiallyOwned: true, SingleInstanceMutex, out bool createdNew);
|
||||
if (!createdNew && !WaitForPredecessor(instance, wait: exitWith is not null))
|
||||
bool podMode = exitWith is not null || profileName is not null;
|
||||
if (!createdNew && !WaitForPredecessor(instance, wait: podMode))
|
||||
return 0; // another RIOJoy is already running in this session
|
||||
|
||||
// net48 has no source-generated ApplicationConfiguration.Initialize();
|
||||
// do the equivalent setup directly.
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
Application.Run(new TrayApplicationContext(exitWith));
|
||||
var context = new TrayApplicationContext(exitWith, profileName);
|
||||
if (!context.TryActivateExplicitProfile())
|
||||
{
|
||||
context.Dispose(); // failure reason already on stderr
|
||||
return 5;
|
||||
}
|
||||
Application.Run(context);
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static CompanionTarget? ParseExitWith(string[] args)
|
||||
private static string? ParseValue(string[] args, string flag, string what)
|
||||
{
|
||||
for (int i = 0; i < args.Length; i++)
|
||||
{
|
||||
if (!string.Equals(args[i], "--exit-with", StringComparison.OrdinalIgnoreCase))
|
||||
if (!string.Equals(args[i], flag, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
if (i + 1 >= args.Length)
|
||||
throw new ArgumentException("--exit-with needs a process name or pid");
|
||||
return CompanionTarget.Parse(args[i + 1]);
|
||||
throw new ArgumentException($"{flag} needs {what}");
|
||||
return args[i + 1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ internal sealed class TrayApplicationContext : ApplicationContext
|
||||
|
||||
private readonly AppConfig _config;
|
||||
private readonly RioCoordinator _coordinator;
|
||||
private readonly AutoSwitchWatcher _watcher;
|
||||
private readonly AutoSwitchWatcher? _watcher; // null = explicit (--profile) mode
|
||||
private readonly System.Windows.Forms.Timer _pollTimer;
|
||||
private readonly NotifyIcon _trayIcon;
|
||||
private readonly ToolStripMenuItem _statusItem;
|
||||
@@ -39,17 +39,27 @@ internal sealed class TrayApplicationContext : ApplicationContext
|
||||
private readonly CompanionTarget? _exitWith;
|
||||
private readonly CompanionExit _companionExit = new();
|
||||
private readonly Stopwatch _sinceStart = Stopwatch.StartNew();
|
||||
private readonly string? _explicitProfile;
|
||||
|
||||
public TrayApplicationContext(CompanionTarget? exitWith = null)
|
||||
public TrayApplicationContext(CompanionTarget? exitWith = null, string? explicitProfile = null)
|
||||
{
|
||||
_exitWith = exitWith;
|
||||
_explicitProfile = explicitProfile;
|
||||
_config = ConfigStore.Load(ConfigPath);
|
||||
|
||||
_coordinator = new RioCoordinator(() => _config);
|
||||
_coordinator.StatusChanged += _ => RefreshOnUiThread();
|
||||
|
||||
_watcher = new AutoSwitchWatcher(new ForegroundProcessProvider(), () => _config);
|
||||
_watcher.DecisionChanged += d => _coordinator.ApplyDecision(d);
|
||||
// Explicit (--profile) mode runs NO foreground detection at all: pod
|
||||
// launch scripts activate the profile before the game starts, so the
|
||||
// ViGEm pad and the ports already exist when the game enumerates
|
||||
// controllers — a watcher activating ~1 s after the window appears is
|
||||
// too late for startup enumeration.
|
||||
if (explicitProfile is null)
|
||||
{
|
||||
_watcher = new AutoSwitchWatcher(new ForegroundProcessProvider(), () => _config);
|
||||
_watcher.DecisionChanged += d => _coordinator.ApplyDecision(d);
|
||||
}
|
||||
|
||||
_statusItem = new ToolStripMenuItem("Status: starting…") { Enabled = false };
|
||||
|
||||
@@ -65,11 +75,54 @@ internal sealed class TrayApplicationContext : ApplicationContext
|
||||
_pollTimer = new System.Windows.Forms.Timer { Interval = (int)PollInterval.TotalMilliseconds };
|
||||
_pollTimer.Tick += (_, _) =>
|
||||
{
|
||||
_watcher.Poll();
|
||||
_watcher?.Poll();
|
||||
CheckCompanion();
|
||||
};
|
||||
_pollTimer.Start();
|
||||
_watcher.Poll();
|
||||
|
||||
if (explicitProfile is null)
|
||||
_watcher!.Poll();
|
||||
// Explicit activation runs via TryActivateExplicitProfile (called by
|
||||
// Program between construction and the message loop, so a failure can
|
||||
// become an exit code the launcher chain can read).
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The named event a pod launcher waits on instead of guessing from device
|
||||
/// enumeration: signaled once the explicit profile is active (ports
|
||||
/// acquired; virtual pad present when ViGEmBus is installed). Dies with
|
||||
/// the process, so it can never go stale.
|
||||
/// </summary>
|
||||
internal const string ReadyEventName = "RIOJoy.Tray.Ready";
|
||||
|
||||
private EventWaitHandle? _readyEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Explicit (--profile) activation: activate now, and signal
|
||||
/// <see cref="ReadyEventName"/> on success so the launcher can start the
|
||||
/// game knowing the virtual pad already exists. False = activation failed
|
||||
/// (port busy, bad endpoint, …) — the caller reports and exits.
|
||||
/// </summary>
|
||||
internal bool TryActivateExplicitProfile()
|
||||
{
|
||||
if (_explicitProfile is null)
|
||||
return true; // resident mode — nothing to do
|
||||
|
||||
// Program.Main validated the name against the same store already.
|
||||
RioProfile profile = _config.FindProfile(_explicitProfile)!;
|
||||
_coordinator.SetManualProfile(profile);
|
||||
if (_coordinator.Runtime is null)
|
||||
{
|
||||
Console.Error.WriteLine($"RioJoy: explicit activation failed: {_coordinator.Status}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Either side may create the event first (launcher-waits-then-start or
|
||||
// start-then-wait); ManualReset + same name converge on one object.
|
||||
_readyEvent = new EventWaitHandle(
|
||||
initialState: false, EventResetMode.ManualReset, ReadyEventName);
|
||||
_readyEvent.Set();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Pod-bundled mode: quit (full teardown — ports released, wallpaper restored,
|
||||
@@ -338,8 +391,16 @@ internal sealed class TrayApplicationContext : ApplicationContext
|
||||
{
|
||||
unhook?.Invoke();
|
||||
_coordinator.EndEditorSession();
|
||||
_watcher.Reset(); // re-sync with the foreground app (may re-activate a game)
|
||||
_watcher.Poll();
|
||||
if (_watcher is not null)
|
||||
{
|
||||
_watcher.Reset(); // re-sync with the foreground app (may re-activate a game)
|
||||
_watcher.Poll();
|
||||
}
|
||||
else if (_explicitProfile is not null &&
|
||||
_config.FindProfile(_explicitProfile) is RioProfile explicitProfile)
|
||||
{
|
||||
_coordinator.SetManualProfile(explicitProfile); // back to the explicit pod profile
|
||||
}
|
||||
};
|
||||
editor.Show();
|
||||
}
|
||||
@@ -439,6 +500,7 @@ internal sealed class TrayApplicationContext : ApplicationContext
|
||||
_pollTimer.Dispose();
|
||||
_coordinator.Dispose();
|
||||
_trayIcon.Dispose();
|
||||
_readyEvent?.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
|
||||
Reference in New Issue
Block a user