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>
This commit is contained in:
+6
-1
@@ -1,9 +1,14 @@
|
|||||||
# Secrets never enter this repo
|
# Site-operational data lives with the sites, not in the repo
|
||||||
|
# (.siteconfig files change over time; the repo carries only the tools)
|
||||||
*.siteconfig
|
*.siteconfig
|
||||||
*wg-private*
|
*wg-private*
|
||||||
*.key
|
*.key
|
||||||
*.pem
|
*.pem
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
|
||||||
# Local scratch
|
# Local scratch
|
||||||
scratch/
|
scratch/
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ live in the docs below.
|
|||||||
| File | What it is |
|
| File | What it is |
|
||||||
|------|-----------|
|
|------|-----------|
|
||||||
| [docs/PODBAY-ECOSYSTEM.md](docs/PODBAY-ECOSYSTEM.md) | Survey of the existing bay software stack — every component, every protocol/port, with source pointers into the sibling repos. The ground truth SiteLink has to work with. |
|
| [docs/PODBAY-ECOSYSTEM.md](docs/PODBAY-ECOSYSTEM.md) | Survey of the existing bay software stack — every component, every protocol/port, with source pointers into the sibling repos. The ground truth SiteLink has to work with. |
|
||||||
| [docs/BRAINSTORM.md](docs/BRAINSTORM.md) | The design record: goals, the seed proposal analyzed, addressing plan, VPN topology options (routed vs bridged), master-console vs federation, per-game linking analysis, shared services, security, open questions, and a phased roadmap. |
|
| [docs/BRAINSTORM.md](docs/BRAINSTORM.md) | The design record: goals, the seed proposal analyzed, addressing plan, VPN topology options (routed vs bridged), the event authority-handover model, per-game linking analysis, shared services, security, open questions, and a phased roadmap. |
|
||||||
|
| [tools/SiteConfigMerge](tools/SiteConfigMerge/README.md) | **Working tool**: decodes TeslaConsole `.siteconfig` files and merges `<siteName>.siteconfig` inputs into one `master.siteconfig` for the central event console, renaming squads `<siteName>-<squad>`. Verified against the real TeslaConsole 4.11.4.1 loader. |
|
||||||
|
|
||||||
## Related repositories (gitea.mysticmachines.com/VWE)
|
## Related repositories (gitea.mysticmachines.com/VWE)
|
||||||
|
|
||||||
|
|||||||
+77
-50
@@ -12,7 +12,8 @@ brainstorm builds on.
|
|||||||
gets a route to the general internet.
|
gets a route to the general internet.
|
||||||
3. **Fleet visibility/command**: an operator (eventually a master console) can see — and
|
3. **Fleet visibility/command**: an operator (eventually a master console) can see — and
|
||||||
where appropriate command — pods at every site.
|
where appropriate command — pods at every site.
|
||||||
4. **Shared front-of-house**: cross-site events share a queue, callsigns, scores, voice.
|
4. **Shared front-of-house**: cross-site events share a queue, callsigns, scores.
|
||||||
|
(Voice: backburnered — see §6.)
|
||||||
5. **Zero changes to cockpit hardware**, minimal changes to bay-local operation: a bay
|
5. **Zero changes to cockpit hardware**, minimal changes to bay-local operation: a bay
|
||||||
must keep working stand-alone when the link is down.
|
must keep working stand-alone when the link is down.
|
||||||
|
|
||||||
@@ -38,7 +39,9 @@ are about *how*, not *whether*.
|
|||||||
PQS, Mumble, NTP, a neutral dedicated game host. Nothing site-specific lives there.
|
PQS, Mumble, NTP, a neutral dedicated game host. Nothing site-specific lives there.
|
||||||
- **Site ID registry lives in this repo** (`sites/` — one small file per site: ID, name,
|
- **Site ID registry lives in this repo** (`sites/` — one small file per site: ID, name,
|
||||||
operator, subnet, contact). First allocation: `10.0.1.0/24` = Fallout Shelter Arcade;
|
operator, subnet, contact). First allocation: `10.0.1.0/24` = Fallout Shelter Arcade;
|
||||||
`10.0.2.0/24` = next site. 254 sites max — plenty.
|
`10.0.2.0/24` = next site. **Scale reality (2026): 6 active pod bays, fewer than 120
|
||||||
|
cockpits in existence** — 254 site IDs is beyond generous, and every topology choice
|
||||||
|
below can assume single-digit sites.
|
||||||
- **Netmask choice is the real design fork** (see §3): hosts configured `/24` + gateway
|
- **Netmask choice is the real design fork** (see §3): hosts configured `/24` + gateway
|
||||||
= routed model; hosts configured `/16` = requires a bridged L2 overlay.
|
= routed model; hosts configured `/16` = requires a bridged L2 overlay.
|
||||||
- Renumbering mechanics: TeslaConsole already owns pod IP config (provisioning writes
|
- Renumbering mechanics: TeslaConsole already owns pod IP config (provisioning writes
|
||||||
@@ -88,36 +91,33 @@ L3. The bay machines themselves never run VPN software; only the per-site gatewa
|
|||||||
|
|
||||||
The seed proposal: merge all `.siteconfig` files → one master console commands the fleet.
|
The seed proposal: merge all `.siteconfig` files → one master console commands the fleet.
|
||||||
|
|
||||||
**Reality check on "concatenate":** `local.siteconfig` is a BinaryFormatter object graph
|
**The merge tool exists:** [`tools/SiteConfigMerge`](../tools/SiteConfigMerge/README.md)
|
||||||
(Squad → Pods), not a text file — merging means a small tool that deserializes N
|
decodes `<siteName>.siteconfig` files and emits one `master.siteconfig`, renaming every
|
||||||
siteconfigs and emits one multi-squad config. Straightforward since TeslaConsole is
|
squad **`<siteName>-<original squad name>`** ("FSA-bay1", "Pharaoh-bay1", …) — one
|
||||||
rebuilt from source; the natural mapping is **one Squad per site** ("FSA-bay1",
|
squad group per site, which TeslaConsole's UI already organizes naturally. Pod records
|
||||||
"Pharaoh-bay1", …). TeslaConsole's UI already organizes pods by squad.
|
pass through byte-for-byte; verified against the real TeslaConsole 4.11.4.1 loader.
|
||||||
|
|
||||||
**Security consequence:** a pod's `mKey` in the siteconfig *is* command authority over
|
**The operating model (settled 2026-07-10): event-scoped authority handover.** When a
|
||||||
that pod. Shipping your siteconfig to a master console = granting fleet-wide control.
|
SiteLink event runs, **every participating site willingly hands console authority to
|
||||||
Fine between trusted operators, but do it deliberately:
|
the central console for the duration of the event** — concretely, by contributing its
|
||||||
|
`<siteName>.siteconfig` to the merged master. Outside events, each site console is
|
||||||
|
authoritative over its own bay. Notes that keep this clean:
|
||||||
|
|
||||||
- Siteconfigs travel operator-to-operator over a secure channel; **never through this
|
- The pod keys inside a siteconfig have no practical value outside the bay's
|
||||||
repo** (repo carries the merge tool + templates only).
|
air-gapped network (anyone with bay access has the file anyway) — handing the file
|
||||||
- A master console with all keys is a single high-value target — it lives on shared
|
over *is* the handover ceremony, not a security event.
|
||||||
infra (`10.0.0.0/24`), not on someone's laptop.
|
- Siteconfigs change over time, so they are exchanged fresh per event and are not
|
||||||
|
stored in this repo (tools only).
|
||||||
|
- Nothing *enforces* the handover — TeslaLauncher has no arbitration if a site console
|
||||||
|
and the central console command the same pod concurrently. Convention: site consoles
|
||||||
|
stand down for the event window. A soft "event mode" lock in TeslaConsole is a
|
||||||
|
nice-to-have if the convention ever gets violated in practice.
|
||||||
|
- Remaining technical to-do for the central console: WAN-tolerant RPC timeouts, and
|
||||||
|
it should live on shared infra (`10.0.0.0/24`) with a decent uplink.
|
||||||
|
|
||||||
**Three architectures, in order of effort:**
|
**Federation (a coordinator service so raw keys never leave a site) is off the table**
|
||||||
|
at current scale — 6 bays run by trusted operators doesn't need it. Recorded here only
|
||||||
1. **Merged-config master console (the seed).** One TeslaConsole instance with all
|
in case the fleet's trust model ever changes.
|
||||||
squads/keys. Cheapest to reach; needs the merge tool + WAN-tolerant RPC timeouts.
|
|
||||||
Risks: WAN blip mid-install, and two consoles (site + master) commanding the same pod
|
|
||||||
concurrently — the launcher has no arbitration. Convention needed: master commands
|
|
||||||
only during cross-site events, site console otherwise.
|
|
||||||
2. **Hybrid (probably the sweet spot).** Site consoles stay authoritative for
|
|
||||||
provisioning/installs; the master console gets *visibility everywhere* +
|
|
||||||
*game-orchestration rights* during events. Could be as simple as (1) plus agreed
|
|
||||||
scope, or a read-only fleet mode in TeslaConsole.
|
|
||||||
3. **Federation service.** A SiteLink coordinator each site console registers with; the
|
|
||||||
coordinator brokers cross-site game setup, no raw pod keys leave a site. Cleanest
|
|
||||||
trust story, most new code. Later phase, if the fleet grows past a handful of
|
|
||||||
trusted operators.
|
|
||||||
|
|
||||||
## 5. Linking the games themselves
|
## 5. Linking the games themselves
|
||||||
|
|
||||||
@@ -126,10 +126,26 @@ Fine between trusted operators, but do it deliberately:
|
|||||||
host placement:
|
host placement:
|
||||||
- a) Host at one site's console (simplest; other site eats the WAN RTT; O(n²) load on
|
- a) Host at one site's console (simplest; other site eats the WAN RTT; O(n²) load on
|
||||||
that console's uplink);
|
that console's uplink);
|
||||||
- b) **Neutral dedicated host** on shared infra (`mw4dedicatedui` exists!) —
|
- b) **Neutral host at the WireGuard hub (preferred direction, 2026-07-10).**
|
||||||
symmetric latency for both sites, best uplink, and the camera/review stations at
|
Symmetric latency for every site, best uplink — and it exploits an operational
|
||||||
*each* site can join as local spectators. Needs validation that the dedicated
|
fact: **the Firestorm server is almost always the Live Cam for that game.** A
|
||||||
server builds/runs (it's in the solution but unexercised in the modern work).
|
hub-hosted host running the camera-ship role turns the neutral server into the
|
||||||
|
event's broadcast point: capture its output and **stream the show to every
|
||||||
|
participating site** (Live Cam screens everywhere show the same feed), and
|
||||||
|
optionally **stream it publicly to the internet** as the fleet's shop window.
|
||||||
|
Considerations: the camera ship *renders*, so the hub box needs a real GPU
|
||||||
|
(physical box at the hub location or a GPU cloud instance — not a bare VPS);
|
||||||
|
`mw4dedicatedui` (headless) remains the fallback if hub rendering is impractical,
|
||||||
|
with a camera-ship client joining from a site instead. Streaming transport is
|
||||||
|
one-way and latency-tolerant: OBS capture → SRT/RTMP internally, Twitch/YouTube
|
||||||
|
for the public leg. Needs validation either way (dedicated UI is unexercised in
|
||||||
|
the modern tree; camera-ship-as-DirectPlay-host in the CTCL flow should be
|
||||||
|
confirmed against `ctcl-game.ini`'s `*1`/cameraship entry and the PQS
|
||||||
|
`cameraship` flag).
|
||||||
|
- **The Mission Review instance lives at the hub too, for the same reasons:** one
|
||||||
|
authoritative post-match review (`ctcl-mr` role / MSRSpectator) running next to
|
||||||
|
the host that recorded the match, its output streamed to every site's Mission
|
||||||
|
Review screen — every bay debriefs from the same show.
|
||||||
- **Player counts:** the golden first milestone is **8v8 across two sites (8 cockpits
|
- **Player counts:** the golden first milestone is **8v8 across two sites (8 cockpits
|
||||||
each) — inside the stock 16-player cap, zero engine changes.** Full 16v16 site-vs-site
|
each) — inside the stock 16-player cap, zero engine changes.** Full 16v16 site-vs-site
|
||||||
needs the already-drafted 16→32 plan in `firestorm\CLAUDE.md` (compiled defaults +
|
needs the already-drafted 16→32 plan in `firestorm\CLAUDE.md` (compiled defaults +
|
||||||
@@ -178,16 +194,19 @@ Fine between trusted operators, but do it deliberately:
|
|||||||
| Service | Notes |
|
| Service | Notes |
|
||||||
|---------|-------|
|
|---------|-------|
|
||||||
| WireGuard hub | The rendezvous point; a VPS (could sit near the existing mysticmachines.com infra) or a box at the best-connected site |
|
| WireGuard hub | The rendezvous point; a VPS (could sit near the existing mysticmachines.com infra) or a box at the best-connected site |
|
||||||
| Mumble | Cross-site voice day one — the FS507D release already integrated Mumble on the bay side. Channel per team, cross-team lobby channel |
|
| Virtual scoresheet printer | A virtual printer at the hub that "prints" event debriefings/scoresheets to **PDF** (Firestorm's print path: `printdebriefing` / `mw4print`). PDFs land in one place and are retrievable from any site on the link — a simple web share on the hub over the VPN. Sites can still print paper locally from the PDFs |
|
||||||
|
| Voice (backburnered) | The FS507D release integrated Mumble, but only one operator ever ran it. Technically easy to stand up on the shared network — revisit only if event interest warrants |
|
||||||
| Queue/roster coordination (future) | Cross-site events will want one queue/roster. **PQS** — the operator-built event traffic-flow tool — is the natural seed, but for now it stays support tooling. If promoted: (a) one central instance all consoles poll — simplest, DB schema grows a `site` column; (b) per-site instances + sync — only if WAN-down resilience of the local queue matters during events. Callsign uniqueness would become fleet-wide |
|
| Queue/roster coordination (future) | Cross-site events will want one queue/roster. **PQS** — the operator-built event traffic-flow tool — is the natural seed, but for now it stays support tooling. If promoted: (a) one central instance all consoles poll — simplest, DB schema grows a `site` column; (b) per-site instances + sync — only if WAN-down resilience of the local queue matters during events. Callsign uniqueness would become fleet-wide |
|
||||||
| Neutral game host | Firestorm dedicated server for site-vs-site matches |
|
| Neutral game host + event broadcast | Firestorm host for site-vs-site matches, co-located with the WireGuard hub. Doubles as the Live Cam (the FS server usually *is* the Live Cam): stream its output to all sites' Live Cam screens, optionally to the public internet. The **Mission Review instance runs at the hub too** — one authoritative debrief streamed to every site's MR screen. Needs GPU at the hub |
|
||||||
| NTP | One clock for scores, logs, and replay/debrief alignment |
|
| NTP | One clock for scores, logs, and replay/debrief alignment |
|
||||||
| Fleet monitoring | VncThumbnailViewer pointed across the VPN; later, master-console status board |
|
| Fleet monitoring | VncThumbnailViewer pointed across the VPN; later, master-console status board |
|
||||||
|
|
||||||
Local-only forever: printer, RIO/cockpit I/O, Mission Review & Live Cam *stations*
|
Local-only forever: printer and RIO/cockpit I/O. For cross-site events the Live Cam
|
||||||
(they join the match as spectators over the VPN, but the screens/printer serve the local
|
and Mission Review *instances* run at the hub (above); each site's LC/MR *screens*
|
||||||
audience). Cross-site match results reach both printers via PQS/debrief distribution —
|
become display endpoints for the hub streams, serving the local audience. Bay-local
|
||||||
worth a small design of its own later.
|
games keep running their own LC/MR stations exactly as today. Cross-site scoresheets
|
||||||
|
go to the hub's virtual PDF printer (table above) — retrievable from any site, with
|
||||||
|
local paper printing off the PDFs where wanted.
|
||||||
|
|
||||||
## 7. Security posture
|
## 7. Security posture
|
||||||
|
|
||||||
@@ -199,9 +218,11 @@ worth a small design of its own later.
|
|||||||
- *Game plane* (DirectPlay fixed port, Munga 1501, Mumble): site↔site and site↔shared.
|
- *Game plane* (DirectPlay fixed port, Munga 1501, Mumble): site↔site and site↔shared.
|
||||||
- *Management plane* (TCP 53290 launcher RPC, VNC 5900): only site-console↔own-pods
|
- *Management plane* (TCP 53290 launcher RPC, VNC 5900): only site-console↔own-pods
|
||||||
and master-console↔pods — other sites' consoles have no business on your 53290.
|
and master-console↔pods — other sites' consoles have no business on your 53290.
|
||||||
- **Secrets:** siteconfigs (pod keys) and WireGuard private keys never enter this repo.
|
- **Repo hygiene:** WireGuard private keys never enter this repo. Siteconfigs aren't
|
||||||
Repo holds templates, tools, and *public* site registry data (subnets, endpoints'
|
stored here either — not because the pod keys matter off-site (they don't; the bays
|
||||||
hostnames, WireGuard public keys).
|
are air-gapped and anyone with bay access has the file), but because they're living
|
||||||
|
operational data exchanged fresh per event. Repo holds tools, templates, and
|
||||||
|
*public* site registry data (subnets, endpoint hostnames, WireGuard public keys).
|
||||||
- Era-Windows reality: assume every bay box is compromised-if-reachable. The allowlist
|
- Era-Windows reality: assume every bay box is compromised-if-reachable. The allowlist
|
||||||
above is the actual security boundary; the VPN is transport, not trust.
|
above is the actual security boundary; the VPN is transport, not trust.
|
||||||
|
|
||||||
@@ -220,12 +241,15 @@ worth a small design of its own later.
|
|||||||
and can camera/MR spectators join it cleanly?
|
and can camera/MR spectators join it cleanly?
|
||||||
6. **PQS promotion** — if/when the event queue tool becomes cross-site coordination:
|
6. **PQS promotion** — if/when the event queue tool becomes cross-site coordination:
|
||||||
site column vs event database; global callsign policy.
|
site column vs event database; global callsign policy.
|
||||||
7. **Master-console arbitration** — what happens when site + master consoles command the
|
7. **Console arbitration** — *policy settled 2026-07-10: sites voluntarily hand
|
||||||
same pod; do we need a soft lock ("bay is in fleet mode")?
|
authority to the central console for the event window.* Residual: nothing enforces
|
||||||
|
it (TeslaLauncher has no arbitration); an optional soft "event mode" lock in
|
||||||
|
TeslaConsole only if the convention ever fails in practice.
|
||||||
8. **Who hosts shared infra** — VPS vs best-connected site; bandwidth math for a
|
8. **Who hosts shared infra** — VPS vs best-connected site; bandwidth math for a
|
||||||
32-player O(n²) host (~upstream estimate needed from real packet captures).
|
32-player O(n²) host (~upstream estimate needed from real packet captures).
|
||||||
9. **Site count ambitions** — 2 sites soon, how many eventually? Affects hub sizing and
|
9. ~~Site count ambitions~~ — **answered 2026-07-10: 6 active bays, fewer than 120
|
||||||
whether federation (§4.3) is ever needed.
|
cockpits in existence.** Hub-and-spoke is trivially sufficient; federation is
|
||||||
|
permanently unnecessary barring a trust-model change.
|
||||||
10. **WAN-drop behavior** — for each linked flow, what breaks when the tunnel drops
|
10. **WAN-drop behavior** — for each linked flow, what breaks when the tunnel drops
|
||||||
mid-match, and does the bay cleanly fall back to stand-alone?
|
mid-match, and does the bay cleanly fall back to stand-alone?
|
||||||
11. **NetNub over routed subnets (TeslaRel410)** — how do the original 4.10 games
|
11. **NetNub over routed subnets (TeslaRel410)** — how do the original 4.10 games
|
||||||
@@ -240,11 +264,14 @@ worth a small design of its own later.
|
|||||||
RPC (53290), a 2-client Firestorm match across subnets, latency sweep, BT411
|
RPC (53290), a 2-client Firestorm match across subnets, latency sweep, BT411
|
||||||
cross-subnet egg push. *Everything above that's marked "verify" gets verified here.*
|
cross-subnet egg push. *Everything above that's marked "verify" gets verified here.*
|
||||||
- **Phase 1 — First real link.** Two sites, gateways, tunnel, renumber to
|
- **Phase 1 — First real link.** Two sites, gateways, tunnel, renumber to
|
||||||
`10.0.1.x`/`10.0.2.x`. Mumble + fleet VNC + remote Mission-Review spectating. First
|
`10.0.1.x`/`10.0.2.x`. Fleet VNC + remote Mission-Review spectating. First
|
||||||
cross-site Firestorm match at ≤16 total players (8v8) on the stock exe.
|
cross-site Firestorm match at ≤16 total players (8v8) on the stock exe.
|
||||||
- **Phase 2 — Fleet operations.** siteconfig merge tool → master console visibility
|
- **Phase 2 — Fleet operations.** ~~siteconfig merge tool~~ ✅ built
|
||||||
(hybrid model, §4.2). PQS event mode with shared queue/callsigns. Debrief/scoresheet
|
([`tools/SiteConfigMerge`](../tools/SiteConfigMerge/README.md), verified against the
|
||||||
distribution to both printers.
|
real console loader) → run an event under the authority-handover model (§4): collect
|
||||||
|
`<siteName>.siteconfig`s, merge, central console commands the fleet. Optional PQS
|
||||||
|
event mode with shared queue/callsigns; debrief/scoresheet distribution to both
|
||||||
|
printers.
|
||||||
- **Phase 3 — Full site-vs-site.** Execute the firestorm 16→32 plan (code defaults +
|
- **Phase 3 — Full site-vs-site.** Execute the firestorm 16→32 plan (code defaults +
|
||||||
drop zones), neutral dedicated host, 16v16 events.
|
drop zones), neutral dedicated host, 16v16 events.
|
||||||
- **Phase 4 — More titles, more sites.** BT411/RP cross-site as those engines mature;
|
- **Phase 4 — More titles, more sites.** BT411/RP cross-site as those engines mature;
|
||||||
|
|||||||
+23
-10
@@ -3,9 +3,13 @@
|
|||||||
Surveyed 2026-07-10 from the sibling repos under `C:\VWE`. Every claim below has a source
|
Surveyed 2026-07-10 from the sibling repos under `C:\VWE`. Every claim below has a source
|
||||||
pointer so it can be re-verified as those repos evolve.
|
pointer so it can be re-verified as those repos evolve.
|
||||||
|
|
||||||
## 1. Physical bay composition
|
## 1. Physical bay composition and fleet scale
|
||||||
|
|
||||||
A Pod Bay is **an air-gapped network of ~20 computers**:
|
A Pod Bay is **an air-gapped network of up to ~20 computers**. Sizes vary widely:
|
||||||
|
the smallest active bay is a console + 2 cockpits; the full complement is below.
|
||||||
|
Fleet scale (2026): **fewer than 120 cockpits left in existence, 6 currently active
|
||||||
|
pod bays** — every design choice in SiteLink should assume single-digit sites and
|
||||||
|
a ~120-seat ceiling.
|
||||||
|
|
||||||
| Count | Machine | Software role |
|
| Count | Machine | Software role |
|
||||||
|------:|---------|---------------|
|
|------:|---------|---------------|
|
||||||
@@ -41,11 +45,19 @@ machines on the internet.
|
|||||||
`TeslaConsole.Squad` (mGuid, mName, mOnline) → `TeslaConsole.Pod` records:
|
`TeslaConsole.Squad` (mGuid, mName, mOnline) → `TeslaConsole.Pod` records:
|
||||||
`mId, mIPAddress, mGateway, mDns, mSubnet, mHostName, mKey, mMacAddress, mName,
|
`mId, mIPAddress, mGateway, mDns, mSubnet, mHostName, mKey, mMacAddress, mName,
|
||||||
mPodArtPath, mHostType, mOnline`. Two consequences for SiteLink:
|
mPodArtPath, mHostType, mOnline`. Two consequences for SiteLink:
|
||||||
1. "Concatenating" siteconfigs = a real deserialize/merge/reserialize tool (or native
|
1. "Concatenating" siteconfigs = a real deserialize/merge/reserialize tool — **built:
|
||||||
multi-site support in TeslaConsole — buildable, since the console is now rebuilt
|
[`tools/SiteConfigMerge`](../tools/SiteConfigMerge/README.md)** decodes
|
||||||
from source). The `Squad` concept maps naturally to "one squad per site".
|
`<siteName>.siteconfig` files and merges them into `master.siteconfig`, renaming
|
||||||
2. `mKey` is the credential that lets a console command a pod. **Siteconfig files are
|
squads `<siteName>-<original squad name>`. Verified against the real
|
||||||
secrets.** Sharing one with a master console = handing over control of your bay.
|
TeslaConsole 4.11.4.1 loader. The `Squad` concept maps naturally to
|
||||||
|
"one squad per site".
|
||||||
|
2. `mKey` is the credential that lets a console command a pod — but it has **no
|
||||||
|
practical value outside the bay**: pods live on an air-gapped network, and
|
||||||
|
anyone with physical bay access has the siteconfig anyway. Handing your
|
||||||
|
siteconfig to the central console is simply how a site joins an event (see the
|
||||||
|
brainstorm's authority-handover model). Siteconfigs are exchanged
|
||||||
|
operator-to-operator as `<siteName>.siteconfig` and are not stored in this
|
||||||
|
repo — they change over time; the repo carries only the tools.
|
||||||
- **vPOD** (`TeslaSuite\vPOD\`): impersonates both a pod's launcher (TCP 53290) and a
|
- **vPOD** (`TeslaSuite\vPOD\`): impersonates both a pod's launcher (TCP 53290) and a
|
||||||
game client (Munga TCP 1501). **This is our test double for a whole remote bay** — we
|
game client (Munga TCP 1501). **This is our test double for a whole remote bay** — we
|
||||||
can prototype every SiteLink flow without touching cockpit hardware.
|
can prototype every SiteLink flow without touching cockpit hardware.
|
||||||
@@ -90,8 +102,9 @@ machines on the internet.
|
|||||||
- **Dormant internet-era code:** MSN Zone "GUN" matchmaking (`Games_GUN.cpp`,
|
- **Dormant internet-era code:** MSN Zone "GUN" matchmaking (`Games_GUN.cpp`,
|
||||||
`GUNGameList.h`) and GameSpy advertisement (`Games_GSpy.cpp`) — dead services, live
|
`GUNGameList.h`) and GameSpy advertisement (`Games_GSpy.cpp`) — dead services, live
|
||||||
code paths; a revival hook if SiteLink ever wants a fleet-wide game browser.
|
code paths; a revival hook if SiteLink ever wants a fleet-wide game browser.
|
||||||
- **2016 release integrated Mumble** for voice (FS507D postinstall) — cross-site voice is
|
- **Voice: backburnered.** The 2016 FS507D release integrated Mumble, but only one
|
||||||
a solved problem: one Mumble server on the shared network.
|
operator ever ran it. Cross-site voice is technically easy (one server on the shared
|
||||||
|
network) — revisit only if event interest warrants it.
|
||||||
|
|
||||||
### BT411 (classic BattleTech — Tesla 4.10 reconstruction, native Win32)
|
### BT411 (classic BattleTech — Tesla 4.10 reconstruction, native Win32)
|
||||||
- **Console-push model over plain TCP:** the console connects to each pod's `-net <port>`
|
- **Console-push model over plain TCP:** the console connects to each pod's `-net <port>`
|
||||||
@@ -177,5 +190,5 @@ the current Windows 10 cockpit hardware. Full plan: `TeslaRel410\emulator\PLAN.m
|
|||||||
| 47624 + 2300–2400, or fixed `DirectPlayPort` | TCP/UDP | pod ↔ session host | Firestorm DirectPlay 4 session + game traffic |
|
| 47624 + 2300–2400, or fixed `DirectPlayPort` | TCP/UDP | pod ↔ session host | Firestorm DirectPlay 4 session + game traffic |
|
||||||
| 80 | TCP | console/game → PQS box | PQS HTTP endpoints (event tooling, when deployed) |
|
| 80 | TCP | console/game → PQS box | PQS HTTP endpoints (event tooling, when deployed) |
|
||||||
| 3306 | TCP | PQS internal | MySQL (localhost) |
|
| 3306 | TCP | PQS internal | MySQL (localhost) |
|
||||||
| 64738 | TCP/UDP | all → voice server | Mumble (2016 release convention) |
|
| 64738 | TCP/UDP | all → voice server | Mumble (2016 release convention; voice is backburnered) |
|
||||||
| 5900 | TCP | operator → pods | VNC monitoring (optional) |
|
| 5900 | TCP | operator → pods | VNC monitoring (optional) |
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace SiteConfigMerge
|
||||||
|
{
|
||||||
|
internal static class Program
|
||||||
|
{
|
||||||
|
private static int Main(string[] args)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (args.Length == 0)
|
||||||
|
return Usage();
|
||||||
|
switch (args[0].ToLowerInvariant())
|
||||||
|
{
|
||||||
|
case "dump":
|
||||||
|
return Dump(args.Skip(1).ToArray());
|
||||||
|
case "merge":
|
||||||
|
return Merge(args.Skip(1).ToArray());
|
||||||
|
default:
|
||||||
|
return Usage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("ERROR: " + ex.Message);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int Usage()
|
||||||
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
@"SiteConfigMerge — decode and merge TeslaConsole .siteconfig files (SiteLink)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
SiteConfigMerge dump <file.siteconfig> [...]
|
||||||
|
Decode and print squads/pods of each file.
|
||||||
|
|
||||||
|
SiteConfigMerge merge -o <master.siteconfig> <siteName>.siteconfig [...]
|
||||||
|
Merge sites into one config. The site name is taken from each input
|
||||||
|
file name; every squad is renamed ""<siteName>-<original squad name>"".
|
||||||
|
Pod records are copied byte-for-byte from the inputs.");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int Dump(string[] files)
|
||||||
|
{
|
||||||
|
if (files.Length == 0)
|
||||||
|
return Usage();
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
var binder = new TeslaBinder();
|
||||||
|
var squads = SiteConfigFile.Read(file, binder);
|
||||||
|
Console.WriteLine($"=== {file}");
|
||||||
|
Console.WriteLine($" serialized by: {binder.CapturedAssembly ?? "(no TeslaConsole records)"}");
|
||||||
|
Console.WriteLine($" squads: {squads.Count}");
|
||||||
|
foreach (var entry in squads)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" Squad \"{entry.Squad.Name}\" guid={entry.Squad.Guid} online={entry.Squad.Online} pods={entry.Pods.Count}");
|
||||||
|
foreach (var p in entry.Pods.Select(e => e.Pod))
|
||||||
|
{
|
||||||
|
Console.WriteLine($" Pod \"{p.Name}\" host={p.HostName} type={p.HostType}");
|
||||||
|
Console.WriteLine($" ip={p.IPAddress} subnet={p.Subnet} gw={p.Gateway} dns={p.Dns}");
|
||||||
|
Console.WriteLine($" mac={FormatMac(p.MacAddress)} key={(p.Key == null || p.Key.Length == 0 ? "none" : p.Key.Length + " bytes")} " +
|
||||||
|
$"art={p.PodArtPath ?? "(null)"} id={p.Id}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int Merge(string[] args)
|
||||||
|
{
|
||||||
|
string output = null;
|
||||||
|
var inputs = new List<string>();
|
||||||
|
for (int i = 0; i < args.Length; i++)
|
||||||
|
{
|
||||||
|
if (args[i] == "-o" || args[i] == "--output")
|
||||||
|
{
|
||||||
|
if (i + 1 >= args.Length)
|
||||||
|
throw new ArgumentException("-o requires a file name");
|
||||||
|
output = args[++i];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
inputs.Add(args[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (output == null || inputs.Count == 0)
|
||||||
|
return Usage();
|
||||||
|
|
||||||
|
var binder = new TeslaBinder(); // shared: output uses the first input's assembly identity
|
||||||
|
var merged = new List<SquadEntry>();
|
||||||
|
var squadNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var podIds = new Dictionary<Guid, string>();
|
||||||
|
var podMacs = new Dictionary<string, string>();
|
||||||
|
var podIps = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
foreach (var file in inputs)
|
||||||
|
{
|
||||||
|
string site = Path.GetFileNameWithoutExtension(file);
|
||||||
|
var squads = SiteConfigFile.Read(file, binder);
|
||||||
|
Console.WriteLine($"{file}: site \"{site}\", {squads.Count} squad(s), {squads.Sum(s => s.Pods.Count)} pod(s)");
|
||||||
|
|
||||||
|
int unnamed = 0;
|
||||||
|
foreach (var entry in squads)
|
||||||
|
{
|
||||||
|
string original = entry.Squad.Name;
|
||||||
|
entry.Squad.Name = string.IsNullOrEmpty(original)
|
||||||
|
? $"{site}-squad{++unnamed}"
|
||||||
|
: $"{site}-{original}";
|
||||||
|
if (!squadNames.Add(entry.Squad.Name))
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"duplicate squad name \"{entry.Squad.Name}\" — same site file given twice?");
|
||||||
|
|
||||||
|
foreach (var p in entry.Pods.Select(e => e.Pod))
|
||||||
|
{
|
||||||
|
string where = $"{entry.Squad.Name}/\"{p.Name}\"";
|
||||||
|
if (podIds.TryGetValue(p.Id, out var prev))
|
||||||
|
Warn($"pod GUID {p.Id} appears in both {prev} and {where} — same pod imported twice?");
|
||||||
|
else
|
||||||
|
podIds[p.Id] = where;
|
||||||
|
|
||||||
|
string mac = FormatMac(p.MacAddress);
|
||||||
|
if (mac != "none")
|
||||||
|
{
|
||||||
|
if (podMacs.TryGetValue(mac, out prev))
|
||||||
|
Warn($"MAC {mac} appears in both {prev} and {where}");
|
||||||
|
else
|
||||||
|
podMacs[mac] = where;
|
||||||
|
}
|
||||||
|
|
||||||
|
string ip = p.IPAddress?.ToString();
|
||||||
|
if (!string.IsNullOrEmpty(ip) && ip != "0.0.0.0")
|
||||||
|
{
|
||||||
|
if (podIps.TryGetValue(ip, out prev))
|
||||||
|
Warn($"IP {ip} used by both {prev} and {where} — sites need renumbering before linking");
|
||||||
|
else
|
||||||
|
podIps[ip] = where;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
merged.Add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SiteConfigFile.Write(output, merged, binder);
|
||||||
|
Console.WriteLine($"wrote {output}: {merged.Count} squad(s), {merged.Sum(s => s.Pods.Count)} pod(s) " +
|
||||||
|
$"(identity: {binder.CapturedAssembly ?? TeslaBinder.DefaultAssembly})");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Warn(string message) => Console.Error.WriteLine("WARNING: " + message);
|
||||||
|
|
||||||
|
private static string FormatMac(byte[] mac) =>
|
||||||
|
mac == null || mac.Length == 0 ? "none" : BitConverter.ToString(mac).Replace('-', ':');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# SiteConfigMerge
|
||||||
|
|
||||||
|
Decodes TeslaConsole `.siteconfig` files and merges multiple sites' configs into a
|
||||||
|
single `master.siteconfig` for the central console that commands the fleet during a
|
||||||
|
SiteLink event.
|
||||||
|
|
||||||
|
The `.siteconfig` files themselves are **not** stored in this repo — they change over
|
||||||
|
time; sites hand them over as `<siteName>.siteconfig` when an event is being set up.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
SiteConfigMerge dump <file.siteconfig> [...]
|
||||||
|
```
|
||||||
|
Decode and print each file: squads, pods, IPs, MACs, host types, key presence, and
|
||||||
|
the TeslaConsole assembly identity that serialized it.
|
||||||
|
|
||||||
|
```
|
||||||
|
SiteConfigMerge merge -o master.siteconfig FSA.siteconfig Pharaoh.siteconfig [...]
|
||||||
|
```
|
||||||
|
Merge sites into one config:
|
||||||
|
|
||||||
|
- The **site name is taken from each input's file name** (`FSA.siteconfig` → `FSA`).
|
||||||
|
- Every squad is renamed **`<siteName>-<original squad name>`** (`FSA-bay1`);
|
||||||
|
unnamed squads become `<siteName>-squadN`.
|
||||||
|
- **Pod records are copied byte-for-byte** from the inputs — GUIDs, IPs, MACs, keys,
|
||||||
|
art paths all pass through untouched. Only the squad records (which carry the
|
||||||
|
name) are re-serialized.
|
||||||
|
- The output declares the TeslaConsole assembly identity captured from the first
|
||||||
|
input, so the console accepts it as its own.
|
||||||
|
|
||||||
|
Warnings (merge proceeds; read them):
|
||||||
|
- duplicate pod GUID or MAC across inputs — same pod imported twice?
|
||||||
|
- same IP at two sites — expected until sites renumber into their `10.0.<site>.0/24`;
|
||||||
|
it must be resolved before actually linking.
|
||||||
|
|
||||||
|
Hard error: duplicate post-rename squad name (same site file given twice).
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```
|
||||||
|
dotnet build -c Release
|
||||||
|
```
|
||||||
|
Targets .NET Framework 4.8 (same toolchain as TeslaSuite); output at
|
||||||
|
`bin\Release\net48\SiteConfigMerge.exe`, runs on any Windows 10/11 box.
|
||||||
|
|
||||||
|
## File format (from `TeslaSuite\Console\TeslaConsole\Site.cs`)
|
||||||
|
|
||||||
|
```
|
||||||
|
int32 squadCount
|
||||||
|
per squad:
|
||||||
|
BinaryFormatter(TeslaConsole.Squad) mGuid, mName, mOnline
|
||||||
|
int32 podCount
|
||||||
|
podCount × BinaryFormatter(TeslaConsole.Pod)
|
||||||
|
mId, mIPAddress, mGateway, mDns, mSubnet, mHostName,
|
||||||
|
mKey, mMacAddress, mName, mPodArtPath, mHostType, mOnline
|
||||||
|
```
|
||||||
|
|
||||||
|
The tool uses stand-in types with a `SerializationBinder` mapping `TeslaConsole.*`
|
||||||
|
both directions, so it has no build dependency on the TeslaSuite repo.
|
||||||
|
|
||||||
|
## Verification status (2026-07-10)
|
||||||
|
|
||||||
|
- Decoded a real `local.siteconfig` (squad `bay1`, TeslaConsole 4.11.4.1 identity).
|
||||||
|
- Merged two simulated sites; duplicate-GUID and IP-overlap warnings fired correctly.
|
||||||
|
- **The real `TeslaConsole.exe` (4.11.4.1) loaded the merged output through its own
|
||||||
|
`Site.LoadFromFile`** — squads renamed, pods intact (reflection harness).
|
||||||
|
|
||||||
|
## Deploying to the central console
|
||||||
|
|
||||||
|
TeslaConsole loads `local.siteconfig` from its common-appdata directory at startup
|
||||||
|
(`Site.Load()`). To arm the central console for an event: back up its existing
|
||||||
|
`local.siteconfig`, drop `master.siteconfig` in its place under that name, restart
|
||||||
|
the console. Restore the backup after the event.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net48</TargetFramework>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<Nullable>disable</Nullable>
|
||||||
|
<AssemblyTitle>SiteLink siteconfig merge tool</AssemblyTitle>
|
||||||
|
<Version>0.1.0</Version>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user