What a match emits, and where it goes #
Every file a finished match produces: who writes it, what shape it is, how big it is, where it lands today, and what it should become.
The question this exists to answer is which artifacts belong in a player's ATProto records, which belong in S3, and which are only worth reading once — extracted into a number and thrown away.
There is a deadline on that question. matches/ in the artifacts bucket
expires after 30 days (match_artifact_retention_days in infra's artifacts
module), and the module says why: S3 is a staging area, and the durable record
of a match belongs in ATProto. Nothing extracted before day 31 can be extracted
after it.
Status: description and proposal. The inventory is what the code does today. The routing in Proposed routing is a proposal to argue with, not a decision that has been taken. The epics that would act on it are match-records, after-action and appview.
The telemetry map #
Every artifact a match emits: which process writes it, where it lives in the container, and which prefix it lands in.
This draws what runs. The prefixes it replaced were result.json turns/ shots/ diagnostics/, and four artifacts drawn as collected here used to be
deleted with the container. The readers ask for the current keys only; the
fallbacks that let an older arena image's objects answer are gone, along with
the objects.
Read left to right. The sources are the four things running inside one Fargate task. The filesystem is laid out the way the container's is. The sinks are what survives it. A line from a source is "writes into this directory"; a line out of a file is "this is where it goes". A file with no line out does not leave the container, which its dashed amber border says.
A file's fill says whose format it is — slate for MegaMek, teal for Suramadu,
indigo for arena. The sources are left uncoloured on purpose, because the two do
not line up: two of these JVMs are arena's own programs with MegaMek's engine
inside them, so MatchHost is ours while game_actions_N.tsv, written inside
its JVM, is MegaMek's.
%%{init: {"flowchart": {"htmlLabels": true, "nodeSpacing": 18, "rankSpacing": 100, "curve": "linear"}, "themeVariables": {"fontSize": "18px"}}}%%
flowchart LR
classDef mmKept fill:#1b2a38,stroke:#35d08a,stroke-width:2px,color:#dbe8f2
classDef mmGone fill:#1b2a38,stroke:#ffb066,stroke-width:2px,stroke-dasharray:5 3,color:#dbe8f2
classDef surKept fill:#0b3336,stroke:#35d08a,stroke-width:2px,color:#d5eef3
classDef surGone fill:#0b3336,stroke:#ffb066,stroke-width:2px,stroke-dasharray:5 3,color:#d5eef3
classDef arKept fill:#2a1f4d,stroke:#35d08a,stroke-width:2px,color:#e2dcf7
classDef arGone fill:#2a1f4d,stroke:#ffb066,stroke-width:2px,stroke-dasharray:5 3,color:#e2dcf7
classDef src fill:#141c27,stroke:#4c8dff,stroke-width:2px,color:#dbe6f7
classDef sink fill:#1b1420,stroke:#b98ad6,color:#ecdff6
classDef absent fill:#181818,stroke:#6b6b6b,color:#a8a8a8,stroke-dasharray:4 3
classDef svc fill:#101d22,stroke:#4fc1d6,color:#d5eef3
classDef input fill:#241d10,stroke:#c9a227,color:#f2e6c7
subgraph fargate["one Fargate task — one match, one container"]
direction LR
subgraph procs["what is running"]
direction TB
subgraph suramadu["Suramadu JVM (Webswing)"]
direction TB
pSura(["<b>Suramadu JVM</b><br/>streams the client to the browser"]):::src
pClient(["<b>MegaMek client JVM</b><br/>Swing, headless, launched by Suramadu"]):::src
end
pHost(["<b>MegaMek server JVM</b><br/>arena's MatchHost embeds the engine"]):::src
pWatch(["<b>MatchWatcher JVM</b><br/>arena's observer client"]):::src
pScripts(["<b>arena's scripts</b><br/>init · collect.sh · finalize.sh"]):::src
end
subgraph fs["the container's filesystem"]
direction TB
subgraph dScenario["/run/arena/scenario/"]
direction TB
fScenario[/"<b>match.mms</b><br/>the fight, staged from the manifest<br/>text · ~2 KB"/]:::arKept
end
subgraph dSummaries["$MM_HOME/logs/gameSummaries/"]
direction TB
fBoard[/"<b>board-round_*.png</b><br/>the board, once a phase<br/>PNG 2037×1260 · 1.6–3.3 MB each × 31<br/><i>one frame kept, as the share card</i>"/]:::mmKept
fMini[/"<b>minimap-round_*.png</b><br/>PNG 394×251 · 28 KB × 46<br/><i>the GIF's own frames</i>"/]:::mmGone
fGif[/"<b>minimap-<uuid>.gif</b><br/>the whole match, animated<br/>GIF · 2.14 MB"/]:::mmKept
end
subgraph dLogs["$MM_HOME/logs/"]
direction TB
fActions[/"<b>game_actions_N.tsv</b><br/>every action and attack,<br/>attacker → target<br/>TSV, 34 cols · 15 KB–1 MB"/]:::mmKept
fGamelog[/"<b>gamelog.html</b><br/>the full round-by-round report<br/>HTML · 125 KB–1.76 MB"/]:::mmKept
fStatus[/"<b>entitystatus.txt</b><br/>record sheets at the end<br/>text · 18.6 KB"/]:::mmKept
fMul[/"<b>salvage.mul · Bot_*.mul</b><br/>whichever MULs the game wrote<br/>MUL XML · 2–13 KB each"/]:::mmKept
fMmLog[/"<b>megamek.log</b><br/>the client's own logging<br/>text · 226 KB"/]:::mmKept
fUnified[/"<b>unified_log.log</b> + .gz<br/>root logger, third parties too<br/>text · 3 KB + rollovers"/]:::mmKept
fRanker[/"<b>bot_path_ranker.log</b><br/>Princess ranking every move<br/>text · 745 KB"/]:::mmGone
end
subgraph dSaves["$MM_HOME/savegames/"]
direction TB
fSave[/"<b>Round-N-autosave.sav.gz</b><br/>the whole game, replayable<br/>gzip · ~400 KB each"/]:::mmGone
end
subgraph dSpool["/run/arena/spool/"]
direction TB
fTurns[/"<b>NNN-rNN-PHASE.txt</b><br/>board, armour, phase report<br/>text · 2–93 KB × ~11 a round<br/><i>spooled, collected by nobody</i>"/]:::arGone
end
subgraph dState["/run/arena/state/"]
direction TB
fIdent[/"<b>identity.json</b><br/>slot → DID, handle and team<br/>JSON · 276 B · <i>sent at init</i>"/]:::arKept
fResult[/"<b>result.json</b><br/>ending, timestamps, scenario,<br/>teams, per-unit state<br/>JSON · 18–70 KB · written from<br/>the first tick, not just at victory"/]:::arKept
fStats[/"<b>stats-summary.txt</b><br/>Suramadu's stats lines, reduced<br/>text · 1.1 KB"/]:::arKept
fCard[/"<b>share-card.png</b><br/>the victory board, chosen<br/>PNG · <i>the only tenant of socials/</i>"/]:::arKept
fMarkers[/"<b>host.status · game-over</b><br/>coordination between scripts<br/>markers · bytes"/]:::arGone
end
subgraph dWeb["/run/arena/web/ — Suramadu serves this"]
direction TB
fDecided[/"<b>decided.json</b><br/>the result, for the player's page<br/>JSON · result + camo paths"/]:::arGone
end
subgraph dOut["stdout — no file at all"]
direction TB
fLines[/"<b>stats, lines</b><br/>latency, bytes, heap, per tick<br/>4122 latency samples a match"/]:::surGone
end
end
end
subgraph durable["what leaves the container"]
direction TB
subgraph bucket["S3 matches/<id>/ — expires at 30 days"]
direction TB
kManifest[("manifest.json · camo-<slot>.png<br/>the inputs — headquarters writes these<br/>before the task starts, and the container<br/>fetches the manifest through a presigned URL")]:::input
subgraph artifacts["artifacts/ — what the container produced"]
direction TB
kLaunch[("launch/<br/>what it was given")]:::sink
kRaw[("raw/<br/>verbatim MegaMek")]:::sink
kDerived[("derived/<br/>ours")]:::sink
kSocials[("derived/socials/<br/>pictures we picked")]:::sink
kDiag[("diagnostics/<br/>operational")]:::sink
end
end
browser["the player's browser"]:::sink
cw[("CloudWatch")]:::sink
posthog[("PostHog")]:::sink
end
api(["headquarters-api"]):::svc
consumers["card.rs · share.rs · daily.rs"]:::svc
records[("participant PDS records<br/>games.permadeath.match")]:::absent
pScripts --> dScenario
pScripts --> dState
pScripts --> dWeb
pClient --> dSummaries
pClient --> dLogs
pHost --> dLogs
pHost --> dSaves
pHost --> dState
pWatch --> dSpool
pSura --> dOut
fScenario --> kLaunch
fIdent --> kLaunch
fIdent -->|"a salted DID hash"| posthog
fActions --> kRaw
fGamelog --> kRaw
fStatus --> kRaw
fMul --> kRaw
fBoard --> kSocials
fGif --> kRaw
fResult --> kDerived
fStats --> kDiag
fCard --> kSocials
fMmLog --> kDiag
fUnified --> kDiag
fDecided --> browser
fLines --> cw
bucket --> api --> consumers
consumers -.->|"nothing writes these yet"| records
Sizes on the boxes are one match's, and a match's size varies by an order of magnitude with its length — see what a real match actually weighs for the same layout measured in production.
Three things the layout makes visible.
launch/ is the only class that is complete before the match is played. It
does not depend on victory, which is what makes an abandoned match attributable
and what a born-live record is written from.
raw/ is evidence and derived/ is a claim. Anything under raw/ can be
re-read with MegaMek's own tools and does not depend on us being correct;
anything under derived/ is a rendering, an extraction or a selection we made.
What is still dropped is dropped on purpose. The per-phase minimap frames
are the GIF's own frames, bot_path_ranker.log is Princess narrating every move
she considered, and the autosaves are MegaMek's serialization at one version.
Captured today #
Sizes in this section and the next are from one 8-round match and a desktop MegaMek install. They are the large end; what a real match actually weighs has the same files measured in production, where a short match runs an order of magnitude smaller.
Uploaded to matches/<matchId>/ by container/lib/collect.sh in
arena, under the four upload keys the launch manifest carries.
| Artifact | S3 key | Written by | Format | Count | Size |
|---|---|---|---|---|---|
| Result document | result.json |
MatchHost at victory, identity.json merged in by collect.sh |
JSON | 1 | ~5.8 KB per unit: ~18 KB at 3 units, ~50 KB at 8, ~70 KB at 12 |
| Board renders | shots/board-round_RRR_PP_PHASE.png |
MegaMek client, GameSummaryBoardView |
PNG 2037×1260 | 31 over 8 rounds | mean 3.32 MB, total 102.8 MB |
| Minimap frames | shots/minimap-round_R_PP_PHASE.png |
MegaMek client, GameSummaryMinimap |
PNG 394×251 | 46 over 8 rounds | 28 KB each, 1.3 MB |
| Minimap animation | shots/minimap-<game-uuid>.gif |
MegaMek client, GifGameSummaryMinimap + patch 0013 |
GIF | 1 | 2.14 MB |
| Share card source | shots/share-card.png |
collect_share_card, a copy of the last board render |
PNG | 1 | 3.3 MB |
| Latency table | diagnostics/stats-summary.txt |
finalize.sh, reduced from Suramadu's stats lines |
fixed-width text, 12 metrics | 1 | 1.1 KB |
| Client log | diagnostics/megamek.log |
MegaMek log4j2 | text | 1 | 226 KB |
| Unified log | diagnostics/unified_log.log, unified_log_N.log.gz |
MegaMek log4j2, rolls at 1 MB | text, gzip | 1 + rollovers | 3 KB + 0.2–1.2 KB each |
| Profiles | diagnostics/profile-*.collapsed |
async-profiler, only when ARENA_PROFILE is set |
collapsed stacks | 0 normally | — |
A match ships about 110 MB, and 99% of it is 31 near-identical board renders. That is the single largest fact in this document.
Two notes on the share card. The comment in collect_share_card calls it "a
few hundred kilobytes"; the file is 3.3 MB. It does not reach a feed at that
size — card.rs redraws its own 1200×630 card
from the result and the board — so this is a cost paid in S3 and bandwidth for
an input, not a bad card.
Produced in the run, not captured #
All of this is written inside the container and dies with it.
| Artifact | Written by | Format | Size | Note |
|---|---|---|---|---|
logs/game_actions_N.tsv |
TWGameManager, an unconditional static GameDatasetLogger |
34-column TSV, three row types: UnitState, UnitAttack, UnitAction |
0.5–1.1 MB | Keep, and extract from. Attacker→target rows, per-phase unit state, damage, BV, positions, move paths. In one sample: 3853 state rows, 415 attacks, 287 actions |
logs/gamelog.html |
MegaMek client, keepGameLog |
HTML, the whole round-by-round report | 1.76 MB | Keep — upstream's own account of the fight |
logs/entitystatus.txt |
MegaMek client, saveEntityStatus at game end |
ASCII record sheets for every unit | 18.6 KB | Keep — cheap, and MegaMek's own wording for the same state |
logs/salvage.mul, logs/Bot_*.mul |
ClientGUI.gameEnd |
MUL XML | 13 KB / 6.4 KB | Keep — the canonical, checkable form of what was destroyed |
savegames/Round-N-autosave_*.sav.gz, autosave.sav.gz |
server AutosaveService |
gzipped Java serialization | ~400 KB each | The only fully replayable artifact, and locked to a MegaMek version |
spool/NNN-rNN-PHASE.txt |
MatchWatcher via TextRenderer |
text: ASCII board, unit table, per-location armour, last phase report | 2–93 KB × ~11 a round; 831 KB across 55 files in one 4-round match | Spooled and not collected — see below |
logs/bot_path_ranker.log (+ gz rollovers) |
Princess | text | 745 KB + ~50 KB per rollover | Excluded on purpose; large, repetitive, nothing has asked for it |
run/manifest.json |
init/10-manifest.sh |
JSON | 5.2 KB | A bearer credential. Stays in the container |
state/identity.json |
init/20-identity.sh |
JSON, slot → control/did/handle | 276 B | Already merged into the uploaded result |
web/decided.json, web/camo/* |
publish_result, publish_camo |
JSON + PNG | result + camo paths | The player's own page reads this; paths are container-local by design |
state/host.status, host.activity, game-over |
MatchHost |
markers | bytes | Coordination between the container's own scripts |
The logs/ numbers above were measured on a real MegaMek install rather than
inside a match container, and arena has never inventoried these files — no
script, patch or doc in that repo mentions game_actions, gamelog,
entitystatus or savegames. MegaMek's source says they should all appear
(game_actions unconditionally, the rest on client end-of-game paths, the
autosaves from the server), but one docker exec ls on a live match would turn
that from an inference into a fact.
Live only, never persisted #
| Stream | Producer | Shape | Measured |
|---|---|---|---|
| Pixels to the browser | Suramadu | WebSocket frames | mean 11.6 KB per message, p90 15 KB, max 2.19 MB |
| Input from the browser | Suramadu | WebSocket frames | mean 28.5 B |
| Session stats | Suramadu, log4j2-server.xml |
HH:mm:ss.SSS stats,<instance>,<metric>,<value> on stdout |
4122 latency samples in one match; reduced to stats-summary.txt |
| Client-side latency | suramadu/web/perf.js |
in-page ring buffer, 4096 records | Only with ?perf=1, read over CDP by a headless pilot. Never uploaded |
| Flares | suramadu/web/flare.js → POST /api/flare |
player-authored report | Goes to userinput.app, not to the match's artifacts |
| Container log | entrypoint, tail -F over the Suramadu log |
text | CloudWatch |
| Match counted | finalize.sh |
one PostHog event per human, against a salted DID hash | Carries no handle, slot or DID |
The layout #
Everything used to land under four prefixes — result.json, turns/,
shots/, diagnostics/ — and those names said which upload key sent it,
which is a fact about arena's scripts rather than about the data. shots/ was
the worst of them: it held MegaMek's own board renders and a picture we
composed, side by side, with nothing distinguishing the two.
matches/<id>/ has two writers, and artifacts/ is what separates them.
Above it are the match's inputs, put there by headquarters before the
container exists: the manifest, which is how the container is told what to
play and reaches it as a presigned URL, and each seat's staged camo. Inside it
is everything the container produced. So the first question the layout answers
is not what a file is but whether the match was handed it or made it, and the
prefixes below only have to sort out the second.
Within artifacts/, the boundary worth drawing is provenance: what the match was handed, what
MegaMek wrote, and what we made out of it. A verbatim MegaMek artifact is
evidence — it can be re-read with MegaMek's own tools, and its meaning does not
depend on us being correct. Ours is a claim: a rendering, an extraction, a
selection we made. And an input is neither: it is the question the match was an
answer to, and it is the only class that is complete before a single turn is
played. A reader should be able to tell the three apart without knowing which
script ran.
matches/<id>/
manifest.json what the match was told to be — written by headquarters,
fetched once by the container through a presigned URL
camo-<slot>.png each seat's colours, staged by headquarters
artifacts/ everything the container produced — arena writes only here,
and the segment is headquarters' (`matches/aws.rs`), not a
name any upload key carries
launch/ what the match was given, before it was played
scenario.mms the exact file MegaMek was handed
identity.json slot → DID, handle and team
raw/ verbatim MegaMek, byte-for-byte as it wrote it
game_actions_N.tsv every action and attack, attacker → target
gamelog.html the full round-by-round report
entitystatus.txt record sheets at the end
salvage.mul Bot_*.mul whichever MULs the game wrote
minimap.gif the match, animated
derived/ ours: extracted, rendered or selected by arena
result.json outcome, teams, per-unit state
socials/ pictures we picked, for people to look at
share-card.png the board at victory, chosen from 31 renders
diagnostics/ operational, nobody's evidence
megamek.log unified_log.log* stats-summary.txt profile-*.collapsed
Four things change from what runs today.
Capture what the match was given, at init. launch/ is written before the
first turn and is the one class that does not depend on the match finishing.
identity.json leaves the container today only by being merged into
result.json, and result.json is only written at victory — so a match that is
abandoned, crashes or is reaped uploads turn reports that no longer say who was
playing. It is 276 bytes and complete at init.
scenario.mms is the fight itself — about 2 KB, measured across the daily
challenges — and it reaches S3 in no form at all today.
Headquarters sends either scenario.name, a path into the arena image's
library, or scenario.content, the file itself — a daily challenge is compiled
into the API binary and travels that way. init/30-assets.sh resolves both to
one staged file, /run/arena/scenario/match.mms, which is what makes this
cheap: upload the staged file and a library scenario and a carried one are the
same artifact afterwards.
Storing the name instead would not be equivalent. A library path is a reference
into a particular arena image, and the image's scenario library changes between
releases, so TrainingScenarios/1-FirstRun.mms in March and the same path in
September are not promised to be the same fight. The bytes are what pins it.
Together they are what a record written at match start — the born-live half of match-records — has to be built from.
Keep the events. game_actions_N.tsv is captured rather than deleted. It is
the only artifact that records who shot whom, it costs about 1 MB, and it is the
source every kill, damage and accuracy figure would be extracted from.
Stop keeping thirty board renders. One is read — as the backdrop card.rs
draws on — and thirty-one are uploaded, at 3.3 MB each. Keeping the victory
frame as derived/socials/share-card.png saves about 99 MB a match and loses
nothing any consumer asks for. The per-phase minimap PNGs go the same way: they are the
GIF's own frames.
Keep the end-of-game record. gamelog.html, entitystatus.txt and the MULs
are ~1.8 MB together — a rounding error beside what the boards cost — and they
are MegaMek's own account of the fight, in formats MegaMek can read back. They
belong in raw/.
Give the pictures we pick their own place. derived/socials/ holds images
arena selected for a person to look at. Today there is exactly one: the board as
it stood at victory, chosen out of the 31 renders MegaMek made. The bytes are
MegaMek's and unaltered, but choosing that frame and calling it the share card
is our act, not the engine's, and what the file is for is somebody looking at
it — so it is derived, and it is social.
It sits under derived/ rather than beside it because it is not a third kind of
provenance: a picture we chose is ours, like the result and the turn reports,
and what makes it worth its own folder is only who it is for. That folder is
also where a screenshot of an interesting moment mid-match would go, if we ever
take one; today the last frame is the only one.
Headquarters draws the actual card — 1200×630, with the forces and the outcome lettered over it — on request, from this image plus the result. Caching that composed card beside its backdrop is the obvious next tenant of this folder.
The one argument for hoisting socials/ back to the top is retention: a card
someone posted to Bluesky is fetched long after 30 days, and a class that needs
its own lifetime wants its own prefix. It does not survive contact with S3.
Lifecycle rules filter on a literal prefix, and every path here is
matches/<id>/…, so the id sits above the class and no single rule can name
one class across all matches — socials/ at the top level is no more reachable
than derived/socials/ is. Giving a class its own retention means inverting the
layout to <class>/<id>/…, which is a different decision from this one and
should be taken on its own. Until then one rule covers everything under
matches/, which is what runs today.
One consequence worth naming: the prefixes are a contract. The launch manifest's
upload keys (result, turnReports, screenshots, diagnostics) are written
by headquarters and read by arena, and share.rs reads
derived/socials/share-card.png by a constant. Renaming is a coordinated change
across both repos, and an arena image running the old keys has to keep working
while it is deployed.
Routing #
Four destinations, and the test for each is different. A record is small, public, and something a stranger should be able to read out of a player's repo. A blob is bytes a player's PDS is being asked to host. S3 is a 30-day staging area. Extract means read it once, keep the number, let the file expire.
| Artifact | Proposal | Why |
|---|---|---|
Result: ending, round, startedAt/endedAt, scenario, per-force unitsStart/Remaining, bvStart/Remaining |
Record, ~1–2 KB | Everything the pilot card, a rating and a leaderboard need. This is games.permadeath.match |
Result: units[] |
Record, condensed | 5.8 KB per unit is critical slots, equipment mounts and ammo bins. Keep chassis, model, BV, status, damage level, crew and armour percentage — about 300 B per unit, so a 12-unit match is ~4 KB rather than ~70 KB |
| Result: full per-unit detail | derived/, referenced |
A record sheet is worth reading once, by one person, on a page. Not worth putting on the firehose |
game_actions_N.tsv |
raw/, and extract from it |
Kills with attribution, damage dealt and taken, accuracy. The extracted figures go in the record; the TSV is the evidence they were read from |
gamelog.html, entitystatus.txt, and whichever MULs the game wrote |
raw/ |
MegaMek's own account, in formats it can read back. Well under 2 MB together |
| Board renders | derived/socials/share-card.png, the victory frame only |
One is read, thirty-one are uploaded. Keeping the frame we chose saves ~99 MB a match |
| Minimap frames | Drop | They are the GIF's own frames |
| Minimap GIF | raw/; blob only if asked for |
2.14 MB is a lot to ask a player's PDS to hold for a replay nobody has requested yet |
| The card hq composes from it | derived/socials/, and a blob |
~270 KB once hq has drawn it, and the one image a post needs. Nothing writes it yet: it is drawn per request |
| Turn reports | Not collected | A live progress feed for an AppView that does not exist, and nothing ever read one. raw/ holds the same match as evidence at a thirtieth of the size |
| Autosaves | Drop | Replay is a real feature and this is not the mechanism for it: MegaMek's serialization, at one version |
megamek.log, unified_log*, stats-summary.txt, profiles |
diagnostics/, expire |
Operational. Never a record, and not evidence about the match either |
identity.json |
launch/, uploaded at init, and a record |
Slots, DIDs, handles and teams. The only file that says who played, and today it leaves only inside a result that an abandoned match never writes |
scenario.mms (staged) |
launch/, uploaded at init |
The fight itself, and the only thing that pins which version of a library scenario was played. Reaches S3 in no form today |
| Manifest, markers | Stay in the container | The manifest carries the upload URLs and the match token, so it is a bearer credential and cannot be an artifact. What is worth keeping out of it is already in launch/ |
The record that falls out of this #
Roughly 1–2 KB for the match, plus ~300 B per unit if the condensed units[]
lands in the same record. A 12-unit match is about 5 KB written twice per
participant (born live, dies complete — see match-records),
which is a size the firehose and a PDS can both hold without anyone thinking
about it.
What it costs #
| today | proposed | |
|---|---|---|
| Board renders | 31 × 3.3 MB = 103 MB | 1 × 3.3 MB |
| Minimap frames | 46 × 28 KB = 1.3 MB | dropped |
| Share card | 3.3 MB, a second copy of a board | ~270 KB, composed |
| MegaMek's end-of-game record | not kept | ~1.8 MB |
| The events | not kept | ~1 MB |
| What the match was given | not kept | ~2 KB |
| Per match | ~110 MB | ~9 MB |
Those are the 8-round match's figures. A shorter one is smaller throughout — the first production match came to 2.2 MB — but the ratio holds, because what the change removes is per-phase renders and what it adds is kilobytes.
What has to change first #
Done since this was written
- A result always exists.
MatchHostre-writesresult.jsononce a phase and replaces it at victory, so a match that is abandoned, times out or is killed still says what became of it.endingisvictoryorno-victory-at-exit, and an undecided document carries no winner —victoryTeam: 0,victoryPlayerId: -1, the sentinelsshare.rsalready reads as "nobody won". - Timestamps and the scenario are in it.
startedAt,endedAt, and ascenarioobject naming the fight and whether it came from the image's library or was carried by the manifest. launch/is sent at init, so identity no longer depends on an ending.
Still open
-
Kill attribution. MegaMek tracks it per unit (
Entity.getKillerId) andUnitReportdoes not emit it; it can also be derived fromraw/game_actions_N.tsv, which is now kept for every match. What is undecided is where the deriving lives — in the container at victory, on two cores, while a player waits to be handed on, or in headquarters off S3 where it can be re-run when the logic improves. arena's TODO carries the argument. Deliberately parked until matches are on-lexicon, because what the numbers are for decides where they should be computed.Worth being exact about what changed: this is unimplemented, not unrecoverable. Before the events were captured, no match that had ever been played could say who shot whom, and nothing would have recovered it. Every match from here keeps the file that answers it.
-
One consequence of a result that always exists. A daily challenge abandoned mid-play now scores on the partial fight, because
settle_dailyreads a document that is there. That is deliberate. A match abandoned before deployment still scores nothing and releases the attempt, sincebvStartis 0 andscorerefuses a zero denominator.
What a real match actually weighs #
A four-round match in production, once every part of the layout worked:
launch/identity.json · launch/scenario.mms |
JSON, text | 1.4 KiB |
raw/game_actions_0.tsv |
TSV | 27.7 KiB |
raw/entitystatus.txt · raw/Bot_@lance.blue.mul |
text, MUL | 4.5 KiB |
raw/gamelog.html |
HTML | 372.8 KiB |
raw/minimap.gif |
GIF | 813.8 KiB |
derived/result.json |
JSON | 39.7 KiB |
derived/socials/share-card.png |
PNG | 1.9 MiB |
diagnostics/ (three files) |
text | 78.9 KiB |
manifest.json, two camo-<slot>.png |
JSON, PNG | 7.8 KiB |
| total | ~3.2 MB |
Nearly two thirds of that is two pictures. The whole authoritative record of what happened — every action, every attack, attacker to target — is the 27.7 KB TSV.
The tables above this section were measured on an 8-round match captured by
hand and on a desktop MegaMek install, which is a person's own long games:
larger throughout, gamelog.html at 1.76 MB against 372 KB here. Both are
true. What scales is rounds, units and board size, so read those as the long
end and this as an ordinary one.
The turn reports, and why they are gone #
The first production listing had no derived/turns/ at all, which this
document recorded as unexplained. The cause was not the upload path.
MatchWatcher called connect() once before its wait loop, and MegaMek's
AbstractClient.connect() opens the socket once and never retries — so a
single attempt, fired while MatchHost was still loading its unit cache,
decided every match. No turn report had ever been uploaded, in any match, in
the project's history. Retrying the socket inside the deadline fixed it.
The match that then produced them produced 55 files and 831 KB, half of it in
consecutive duplicates: a dump carries phaseReport, which does not change
between phases, so FIRING_REPORT and PHYSICAL arrive as the same 93 KB
twice. And nothing read them. The AppView they were written for does not
exist, and headquarters reads the result and the board and nothing else.
So they are no longer uploaded. The observer still runs — MatchHost leaves it
unflagged so an all-human match has one not-done player holding the game shut —
and still spools, because a local run keeps its artifacts and the dumps are the
readable form when something needs debugging by eye. They die with the
container.
That decision was only answerable because raw/ exists. The same match, as
evidence, is 27.7 KB and structured; as a rendered convenience view, 831 KB and
not. Filing artifacts by what they are turns "do we still need this?" from an
argument into a comparison.
Re-measuring #
./scripts/matches/artifacts.sh <match-id> # infra
That prints every object a match left, with sizes, grouped by class — which is
where the table above came from. ./scripts/matches/list.sh gives the match
ids, and both are in the infra repo.
For anything the container did not upload, there is no substitute for looking
inside one: keep a container's state/ and MM_HOME/logs/ after a match and
measure them with du. That is how the amber rows in the inventory above were
counted, and it is the only way to count them, since by definition they never
leave.