using System.Diagnostics; using TravelEar.Core; namespace TravelEar; /// /// Starts the Helper once per game launch (REQ-SINK-LIFECYCLE). The policy (spawn once, /// never respawn) lives in ; this class only observes (config, file, /// running processes) and acts. The Helper is launched with --detach, so the process that /// stays up is a child of a launcher that exits at once, not of the game: OBS's process-tree /// matching then cannot fold the Helper's audio into the game's capture. A missing or failing /// Helper only produces a log line (docs/KNOWN-HAZARDS.md 2.1). /// internal static class HelperLauncher { public const string ProcessName = "TravelEar.Helper"; private static readonly HelperLifecycle Lifecycle = new(); /// /// BepInEx\TravelEar.Helper\TravelEar.Helper.exe: beside, not inside, the plugins folder, /// because BepInEx examines every DLL under plugins as a plugin candidate. /// public static string DefaultPath(string bepInExRoot) => Path.Combine(bepInExRoot, "TravelEar.Helper", ProcessName + ".exe"); // [impl->REQ-SINK-LIFECYCLE] public static void TryLaunch(PluginConfig settings, string bepInExRoot) { var path = string.IsNullOrWhiteSpace(settings.HelperPath.Value) ? DefaultPath(bepInExRoot) : settings.HelperPath.Value; bool alreadyRunning; try { alreadyRunning = Process.GetProcessesByName(ProcessName).Length > 0; } catch (Exception e) { Plugin.Logger.LogWarning($"Sink: could not list processes ({e.Message}); assuming no Helper is running."); alreadyRunning = false; } var decision = Lifecycle.DecideSpawn(settings.SpawnHelper.Value, File.Exists(path), alreadyRunning); switch (decision) { case SpawnDecision.SkipDisabled: Plugin.Logger.LogInfo("Sink: SpawnHelper is off; start the Helper by hand."); return; case SpawnDecision.SkipMissing: Plugin.Logger.LogWarning($"Sink: Helper not found at '{path}'. Start it by hand or set Sink.HelperPath."); return; case SpawnDecision.SkipAlreadyRunning: Plugin.Logger.LogInfo("Sink: Helper is already running."); return; case SpawnDecision.SkipAlreadySpawned: Plugin.Logger.LogInfo("Sink: Helper was already spawned this session; not respawning."); return; } try { var options = HelperOptions.Default with { EndpointSetting = settings.SinkEndpoint.Value ?? "", Detach = true }; var start = new ProcessStartInfo(path) { UseShellExecute = false, WorkingDirectory = Path.GetDirectoryName(path) ?? bepInExRoot, }; foreach (var arg in options.ToArgs()) start.ArgumentList.Add(arg); using var launcher = Process.Start(start); Plugin.Logger.LogInfo($"Sink: Helper launcher started (pid {launcher?.Id}) from '{path}' with args [{string.Join(" ", options.ToArgs())}]; the Helper detaches from it."); } catch (Exception e) { // [impl->REQ-HAZARD-NO-GAMEPLAY-IMPACT] Lifecycle.RecordSpawnFailure(); Plugin.Logger.LogWarning($"Sink: could not start the Helper: {e.Message}. Start it by hand; the mod will not retry."); } } /// /// T4a reference capture: starts a second Helper in --capture mode recording /// to . Attached (no detach): OBS is not /// involved and the window closes with the game. Failure is logged and ignored. /// public static void TryLaunchCapture(PluginConfig settings, string bepInExRoot, string device, string file) { var path = string.IsNullOrWhiteSpace(settings.HelperPath.Value) ? DefaultPath(bepInExRoot) : settings.HelperPath.Value; if (!File.Exists(path)) { Plugin.Logger.LogWarning($"Calibration capture: Helper not found at '{path}'; run it by hand: TravelEar.Helper.exe --capture \"{device}\" --out \"{file}\""); return; } try { var options = HelperOptions.Default with { CaptureDevice = device, CaptureFile = file }; var start = new ProcessStartInfo(path) { UseShellExecute = false, WorkingDirectory = Path.GetDirectoryName(path) ?? bepInExRoot, }; foreach (var arg in options.ToArgs()) start.ArgumentList.Add(arg); using var process = Process.Start(start); Plugin.Logger.LogInfo($"Calibration capture: Helper recording '{device}' to {file} (pid {process?.Id})."); } catch (Exception e) { Plugin.Logger.LogWarning($"Calibration capture: could not start the recording Helper: {e.Message}."); } } }