Files
CydandClaude Fable 5 e0d30120e0 SiteConfigMerge tool + operating-model updates from operator input
New: tools/SiteConfigMerge (net48 console app)
- dump: decode any .siteconfig (squads, pods, IPs, MACs, host types)
- merge: combine <siteName>.siteconfig inputs into master.siteconfig,
  renaming squads "<siteName>-<original squad name>"
- Pod records pass through byte-for-byte; only squad records (the
  rename) are re-serialized, under the TeslaConsole assembly identity
  captured from the input. Stand-in types + SerializationBinder, so no
  build dependency on TeslaSuite.
- Warns on duplicate pod GUID/MAC and cross-site IP overlap.
- Verified end-to-end: the real TeslaConsole.exe 4.11.4.1 loaded the
  merged master via its own Site.LoadFromFile (reflection harness).

Doc updates from operator decisions:
- Operating model settled: sites voluntarily hand console authority to
  the central console for the duration of a SiteLink event, by
  contributing their siteconfig. Federation ruled out at current scale.
- Siteconfig "secrets" framing corrected: pod keys have no practical
  value outside the air-gapped bay; files are exchanged per event and
  never stored in this repo (tools only).
- Fleet scale recorded: 6 active pod bays, <120 cockpits in existence;
  bay sizes range console+2 cockpits up to the full 20-node complement.
  Open question 9 answered.
- Hub hosting direction: neutral Firestorm host at the WireGuard hub;
  the FS server usually IS the Live Cam, so stream its output to all
  sites and optionally to the public internet. Mission Review instance
  runs at the hub too - one authoritative debrief streamed everywhere.
- Virtual PDF scoresheet printer at the hub: event debriefings print
  centrally, retrievable from any site on the link.
- Voice (Mumble) backburnered - revisit only on event interest.
- .gitignore: build outputs; siteconfig exclusion rationale reworded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 12:36:07 -05:00

170 lines
6.3 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
// CS0649: the stand-in fields are only ever assigned by BinaryFormatter via reflection.
#pragma warning disable 0649
namespace SiteConfigMerge
{
// Stand-ins for the TeslaConsole types serialized into .siteconfig files.
// Field names/types MUST match TeslaSuite\Console\TeslaConsole\{Squad,Pod,HostType}.cs
// exactly ([NonSerialized] members omitted); BinaryFormatter matches by field name.
[Serializable]
internal class Squad
{
private Guid mGuid = Guid.NewGuid();
private string mName = "";
private bool mOnline;
public Guid Guid => mGuid;
public bool Online => mOnline;
public string Name { get => mName; set => mName = value; }
}
[Serializable]
internal class Pod
{
private Guid mId = Guid.NewGuid();
private IPAddress mIPAddress = IPAddress.Any;
private IPAddress mGateway = IPAddress.Any;
private IPAddress mDns = IPAddress.Any;
private IPAddress mSubnet = IPAddress.Any;
private string mHostName = "";
private byte[] mKey;
private byte[] mMacAddress;
private string mName;
private string mPodArtPath;
private HostType mHostType;
private bool mOnline;
public Guid Id => mId;
public IPAddress IPAddress => mIPAddress;
public IPAddress Gateway => mGateway;
public IPAddress Dns => mDns;
public IPAddress Subnet => mSubnet;
public string HostName => mHostName;
public byte[] Key => mKey;
public byte[] MacAddress => mMacAddress;
public string Name => mName;
public string PodArtPath => mPodArtPath;
public HostType HostType => mHostType;
public bool Online => mOnline;
}
internal enum HostType
{
GameMachineHostType = 0,
MissionReviewHostType = 2,
ConsoleHostType = 3,
}
// Maps "TeslaConsole.*" stream types to the stand-ins on read, and writes them
// back out under the original TeslaConsole assembly identity so the console
// accepts the result. The assembly string is captured from the first input file
// (falling back to the 4.11.4.1 identity).
internal sealed class TeslaBinder : SerializationBinder
{
public const string DefaultAssembly =
"TeslaConsole, Version=4.11.4.1, Culture=neutral, PublicKeyToken=null";
public string CapturedAssembly { get; private set; }
public override Type BindToType(string assemblyName, string typeName)
{
switch (typeName)
{
case "TeslaConsole.Squad":
CapturedAssembly = CapturedAssembly ?? assemblyName;
return typeof(Squad);
case "TeslaConsole.Pod":
CapturedAssembly = CapturedAssembly ?? assemblyName;
return typeof(Pod);
case "TeslaConsole.HostType":
return typeof(HostType);
default:
return null; // system types resolve normally
}
}
public override void BindToName(Type serializedType, out string assemblyName, out string typeName)
{
assemblyName = null;
typeName = null;
if (serializedType == typeof(Squad)) typeName = "TeslaConsole.Squad";
else if (serializedType == typeof(Pod)) typeName = "TeslaConsole.Pod";
else if (serializedType == typeof(HostType)) typeName = "TeslaConsole.HostType";
if (typeName != null)
assemblyName = CapturedAssembly ?? DefaultAssembly;
}
}
internal sealed class PodEntry
{
public Pod Pod;
public byte[] Raw; // the pod's original BinaryFormatter payload, copied verbatim on write
}
internal sealed class SquadEntry
{
public Squad Squad;
public List<PodEntry> Pods = new List<PodEntry>();
}
// .siteconfig container format, per TeslaSuite\Console\TeslaConsole\Site.cs:
// int32 squadCount
// per squad: BinaryFormatter(Squad), int32 podCount, podCount x BinaryFormatter(Pod)
internal static class SiteConfigFile
{
public static List<SquadEntry> Read(string path, TeslaBinder binder)
{
byte[] bytes = File.ReadAllBytes(path);
var stream = new MemoryStream(bytes, writable: false);
var reader = new BinaryReader(stream);
var formatter = new BinaryFormatter { Binder = binder };
var squads = new List<SquadEntry>();
int squadCount = reader.ReadInt32();
for (int i = 0; i < squadCount; i++)
{
var entry = new SquadEntry { Squad = (Squad)formatter.Deserialize(stream) };
int podCount = reader.ReadInt32();
for (int j = 0; j < podCount; j++)
{
long start = stream.Position;
var pod = (Pod)formatter.Deserialize(stream);
var raw = new byte[stream.Position - start];
Array.Copy(bytes, start, raw, 0, raw.Length);
entry.Pods.Add(new PodEntry { Pod = pod, Raw = raw });
}
squads.Add(entry);
}
if (stream.Position != stream.Length)
Console.Error.WriteLine(
$"WARNING: {path}: {stream.Length - stream.Position} trailing byte(s) after the last squad were ignored.");
return squads;
}
public static void Write(string path, List<SquadEntry> squads, TeslaBinder binder)
{
using (var stream = File.Create(path))
{
var writer = new BinaryWriter(stream);
var formatter = new BinaryFormatter { Binder = binder };
writer.Write(squads.Count);
foreach (var entry in squads)
{
formatter.Serialize(stream, entry.Squad);
writer.Write(entry.Pods.Count);
foreach (var pod in entry.Pods)
stream.Write(pod.Raw, 0, pod.Raw.Length);
}
}
}
}
}