using System; using System.Net; using Munga.Net; namespace VPod; /// /// The launch options a real game client understands, parsed from the command /// line the console/launcher starts it with. vPOD accepts the same shape so it /// is a drop-in stand-in: /// -net <port> Munga control port (default 1501) /// -lc live-camera role (cosmetic here; state model is identical) /// -mr mission-review role /// -res W H resolution (ignored) /// -app rp|bt which ApplicationID to report (RP by default; also /// switchable live in the UI) /// -host <id> the responding host id reported in state responses /// /// vPOD-only (not a real game-client option): /// -nomanage disable the virtual launcher / site-management side /// (no provisioning beacons, no TCP 53290 listener) /// -bind <ip> listen on ONE address instead of every interface. /// Both listeners default to IPAddress.Any, which means /// the second vPOD on a machine loses the port and the /// console can only ever see one fake pod. Binding each /// instance to its own address lets a whole roster run /// side by side -- 200.0.0.111..118 are the internet /// session's slot addresses, so an eight-pod session can /// be exercised end to end with no cockpits and nobody /// else in the room. Add the aliases first, e.g. /// netsh interface ipv4 add address "Loopback" 200.0.0.113 255.255.255.0 /// /// DO NOT leave an unbound vPOD running alongside a bound /// roster. Verified on this box 2026-07-25: Windows lets /// IPAddress.Any bind the SAME port while specific /// addresses already hold it (Linux would refuse). The /// specific listeners still win for their own addresses, /// but the unbound one silently swallows every address no /// one claimed -- so a slot you thought was absent answers /// anyway, which is precisely the stale-pod confusion the /// console's claim gate exists to catch. Bind all of them /// or none of them. /// internal sealed class PodArguments { public int Port { get; private set; } = MungaSocket.ConsolePort; // 1501 /// Address both listeners bind to; null = every interface (default). public IPAddress Bind { get; private set; } public ApplicationID Application { get; private set; } = ApplicationID.RPL4; public HostType HostType { get; private set; } = HostType.GameMachineHostType; public int HostId { get; private set; } = 1; public bool NoManage { get; private set; } public static PodArguments Parse(string[] args) { PodArguments result = new PodArguments(); for (int i = 0; i < args.Length; i++) { string token = args[i].ToLowerInvariant(); switch (token) { case "-net": if (i + 1 < args.Length && int.TryParse(args[i + 1], out int port)) { result.Port = port; i++; } break; case "-bind": // Unparseable means "every interface" rather than a hard failure: // vPOD is a test tool and a typo here should not stop the run, but // it must be visible -- Program logs the resolved bind at startup. if (i + 1 < args.Length && IPAddress.TryParse(args[i + 1], out IPAddress bind)) { result.Bind = bind; i++; } break; case "-lc": result.HostType = HostType.MissionReviewHostType; break; case "-mr": result.HostType = HostType.MissionReviewHostType; break; case "-res": i += 2; // width height, ignored break; case "-app": if (i + 1 < args.Length) { result.Application = ParseApp(args[i + 1]); i++; } break; case "-host": if (i + 1 < args.Length && int.TryParse(args[i + 1], out int host)) { result.HostId = host; i++; } break; case "-nomanage": result.NoManage = true; break; } } return result; } private static ApplicationID ParseApp(string value) { switch (value.ToLowerInvariant()) { case "bt": case "btl4": case "battletech": return ApplicationID.BTL4; case "nd": case "ndl4": return ApplicationID.NDL4; default: return ApplicationID.RPL4; } } } /// /// Mirror of the console's HostType (GameMachine=0, MissionReview=2, Console=3). /// Duplicated here so vPOD does not depend on the console assembly. /// internal enum HostType { GameMachineHostType = 0, MissionReviewHostType = 2, ConsoleHostType = 3 }