namespace TravelEar.Core;
///
/// Command line of TravelEar.Helper.exe. Parsed here so the grammar is unit-tested and
/// the mod (which launches the Helper) and the Helper agree on it.
///
/// The SinkEndpoint value: empty = system default render device.
/// Render a test tone instead of reading the pipe (spike S1 needs no game).
/// Named pipe the mod serves Sink frames on.
/// --help was given; print usage and exit.
/// Re-launch self without this flag and exit at once, so the real Helper's parent is this short-lived launcher rather than whoever started it (the game).
public sealed record HelperOptions(string EndpointSetting, bool Tone, string PipeName, bool ShowHelp, bool Detach = false,
string? CaptureDevice = null, string? CaptureFile = null)
{
/// Recording mode (T4a reference capture): record a capture device to a WAV instead of rendering the Sink.
public bool Capture => CaptureDevice is not null;
public const string DefaultPipeName = "TravelEar.Sink";
public static readonly HelperOptions Default = new("", false, DefaultPipeName, false);
/// The return pipe the Helper writes s on: the Sink pipe's name plus .Back.
public static string BackPipeName(string pipeName) => pipeName + ".Back";
/// The return pipe for this instance's .
public string BackPipe => BackPipeName(PipeName);
public const string Usage =
"TravelEar.Helper [--endpoint ] [--tone] [--pipe ] [--detach] [--help]\n" +
" --endpoint Render to the playback device whose name contains (empty = default device).\n" +
" --tone Render a 440 Hz test tone instead of reading the Sink pipe.\n" +
" --pipe Named pipe to read Sink frames from (default " + DefaultPipeName + ").\n" +
" --detach Start a second copy without this flag and exit, leaving it outside the caller's process tree.\n" +
" --capture Record the capture device whose name contains (the other machine's audio\n" +
" output on an HDMI capture card, say) to a WAV instead of rendering the Sink.\n" +
" --out Where --capture writes (default: TravelEar/calibration/peer-.wav under LOCALAPPDATA).\n" +
" --help Show this text.";
/// Builds the argument vector the mod passes when launching the Helper.
public string[] ToArgs()
{
var args = new List();
if (!string.IsNullOrWhiteSpace(EndpointSetting)) { args.Add("--endpoint"); args.Add(EndpointSetting); }
if (Tone) args.Add("--tone");
if (PipeName != DefaultPipeName) { args.Add("--pipe"); args.Add(PipeName); }
if (Detach) args.Add("--detach");
if (CaptureDevice is not null) { args.Add("--capture"); args.Add(CaptureDevice); }
if (CaptureFile is not null) { args.Add("--out"); args.Add(CaptureFile); }
return args.ToArray();
}
///
/// Parses --endpoint X / --endpoint=X, --tone, --pipe X, --help.
/// Throws on an unknown option or a missing value.
///
public static HelperOptions Parse(IReadOnlyList args)
{
var result = Default;
for (var i = 0; i < args.Count; i++)
{
var arg = args[i];
string? inlineValue = null;
var eq = arg.IndexOf('=');
if (arg.StartsWith("--", StringComparison.Ordinal) && eq > 0)
{
inlineValue = arg.Substring(eq + 1);
arg = arg.Substring(0, eq);
}
switch (arg.ToLowerInvariant())
{
case "--endpoint":
result = result with { EndpointSetting = TakeValue(args, ref i, inlineValue, arg) };
break;
case "--pipe":
var pipe = TakeValue(args, ref i, inlineValue, arg);
if (pipe.Length == 0) throw new ArgumentException("--pipe needs a non-empty name.");
result = result with { PipeName = pipe };
break;
case "--tone":
if (inlineValue is not null) throw new ArgumentException("--tone takes no value.");
result = result with { Tone = true };
break;
case "--detach":
if (inlineValue is not null) throw new ArgumentException("--detach takes no value.");
result = result with { Detach = true };
break;
case "--capture":
var device = TakeValue(args, ref i, inlineValue, arg);
if (device.Length == 0) throw new ArgumentException("--capture needs part of a capture device name.");
result = result with { CaptureDevice = device };
break;
case "--out":
var file = TakeValue(args, ref i, inlineValue, arg);
if (file.Length == 0) throw new ArgumentException("--out needs a file path.");
result = result with { CaptureFile = file };
break;
case "--help":
case "-h":
case "/?":
result = result with { ShowHelp = true };
break;
default:
throw new ArgumentException($"Unknown option '{args[i]}'.");
}
}
return result;
}
private static string TakeValue(IReadOnlyList args, ref int i, string? inlineValue, string option)
{
if (inlineValue is not null) return inlineValue;
if (i + 1 >= args.Count) throw new ArgumentException($"{option} needs a value.");
i++;
return args[i];
}
}