Files
TeslaSuite/vPOD/PodArguments.cs
T
CydandClaude Opus 5 33f734da2d Console: internet-session roster from TeslaLobby; vPOD per-address bind
The eight internet pod rows are furniture the operator builds once in
Manage Site — the slot-to-IP map is frozen — but which of those slots a
human actually claimed changes every session. New SessionRoster reads the
roster TeslaLobby writes at the state=launching flip, and both game panes
grow a session strip above Mission Properties: a banner (session key, game,
slots claimed, waiting, written-at), Apply Session, and the Reset Pods that
until now existed only as a right-click on a Go button that is disabled
exactly when the reset is wanted.

Two properties shape all of it. Only claimed slots appear in the file, so
absence is the unclaimed signal and every failure path — truncated, stale,
unreadable, refused — degrades to "no roster", which is byte-for-byte
today's arcade behaviour; museums run this software and a bad JSON file
must never stop a mission that would otherwise run by hand. And enabled
implies claimed, not the reverse: the operator may always sit a pilot out,
never add one, because an enabled row nobody claimed puts a dead IP in the
egg and the pods then wait on a peer that will never boot. Roster issues
join the pane's existing issue text, so the Go button is still the gate.

The roster is one file per launch generation and deliberately not a live
view: pod peer tables are boot-static, so a player whose lobby crashed is
still in every pod's table and still playable, and live tracking would
evict that working pod mid-session. Poll runs at ~1Hz off the existing
network timer with its own deadline, and the strips are built at runtime —
InitializeComponent is decompiled 1995 designer output that the
differential tests compare literally.

Go/Load also re-checks CheckAllValues at the click instead of trusting the
last status tick: a pod that died in that gap went straight into the
mission, and the post-Load barrier in NetworkScan then waited forever for a
WaitingForLaunch that never arrives.

vPOD gains -bind <ip>. Both listeners defaulted to IPAddress.Any, so a
second vPOD on the machine lost the port and the console could only ever
see one fake pod; binding each instance to its own address runs a whole
eight-pod session side by side with no cockpits. Bind all of them or none —
Windows lets IPAddress.Any take a port that specific addresses already
hold, and the unbound instance then answers for every slot nobody claimed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 13:14:38 -05:00

142 lines
4.8 KiB
C#

using System;
using System.Net;
using Munga.Net;
namespace VPod;
/// <summary>
/// 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:
/// <c>-net &lt;port&gt;</c> Munga control port (default 1501)
/// <c>-lc</c> live-camera role (cosmetic here; state model is identical)
/// <c>-mr</c> mission-review role
/// <c>-res W H</c> resolution (ignored)
/// <c>-app rp|bt</c> which ApplicationID to report (RP by default; also
/// switchable live in the UI)
/// <c>-host &lt;id&gt;</c> the responding host id reported in state responses
///
/// vPOD-only (not a real game-client option):
/// <c>-nomanage</c> disable the virtual launcher / site-management side
/// (no provisioning beacons, no TCP 53290 listener)
/// <c>-bind &lt;ip&gt;</c> 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.
/// </summary>
internal sealed class PodArguments
{
public int Port { get; private set; } = MungaSocket.ConsolePort; // 1501
/// <summary>Address both listeners bind to; null = every interface (default).</summary>
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;
}
}
}
/// <summary>
/// Mirror of the console's HostType (GameMachine=0, MissionReview=2, Console=3).
/// Duplicated here so vPOD does not depend on the console assembly.
/// </summary>
internal enum HostType
{
GameMachineHostType = 0,
MissionReviewHostType = 2,
ConsoleHostType = 3
}