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>
170 lines
6.3 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|