# 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](#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](../plan/match-records.md), [after-action](../plan/after-action.md)
> and [appview](../plan/appview.md).
## 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.
```mermaid
%%{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(["Suramadu JVM
streams the client to the browser"]):::src
pClient(["MegaMek client JVM
Swing, headless, launched by Suramadu"]):::src
end
pHost(["MegaMek server JVM
arena's MatchHost embeds the engine"]):::src
pWatch(["MatchWatcher JVM
arena's observer client"]):::src
pScripts(["arena's scripts
init · collect.sh · finalize.sh"]):::src
end
subgraph fs["the container's filesystem"]
direction TB
subgraph dScenario["/run/arena/scenario/"]
direction TB
fScenario[/"match.mms
the fight, staged from the manifest
text · ~2 KB"/]:::arKept
end
subgraph dSummaries["$MM_HOME/logs/gameSummaries/"]
direction TB
fBoard[/"board-round_*.png
the board, once a phase
PNG 2037×1260 · 1.6–3.3 MB each × 31
one frame kept, as the share card"/]:::mmKept
fMini[/"minimap-round_*.png
PNG 394×251 · 28 KB × 46
the GIF's own frames"/]:::mmGone
fGif[/"minimap-<uuid>.gif
the whole match, animated
GIF · 2.14 MB"/]:::mmKept
end
subgraph dLogs["$MM_HOME/logs/"]
direction TB
fActions[/"game_actions_N.tsv
every action and attack,
attacker → target
TSV, 34 cols · 15 KB–1 MB"/]:::mmKept
fGamelog[/"gamelog.html
the full round-by-round report
HTML · 125 KB–1.76 MB"/]:::mmKept
fStatus[/"entitystatus.txt
record sheets at the end
text · 18.6 KB"/]:::mmKept
fMul[/"salvage.mul · Bot_*.mul
whichever MULs the game wrote
MUL XML · 2–13 KB each"/]:::mmKept
fMmLog[/"megamek.log
the client's own logging
text · 226 KB"/]:::mmKept
fUnified[/"unified_log.log + .gz
root logger, third parties too
text · 3 KB + rollovers"/]:::mmKept
fRanker[/"bot_path_ranker.log
Princess ranking every move
text · 745 KB"/]:::mmGone
end
subgraph dSaves["$MM_HOME/savegames/"]
direction TB
fSave[/"Round-N-autosave.sav.gz
the whole game, replayable
gzip · ~400 KB each"/]:::mmGone
end
subgraph dSpool["/run/arena/spool/"]
direction TB
fTurns[/"NNN-rNN-PHASE.txt
board, armour, phase report
text · 2–93 KB × ~11 a round
spooled, collected by nobody"/]:::arGone
end
subgraph dState["/run/arena/state/"]
direction TB
fIdent[/"identity.json
slot → DID, handle and team
JSON · 276 B · sent at init"/]:::arKept
fResult[/"result.json
ending, timestamps, scenario,
teams, per-unit state
JSON · 18–70 KB · written from
the first tick, not just at victory"/]:::arKept
fStats[/"stats-summary.txt
Suramadu's stats lines, reduced
text · 1.1 KB"/]:::arKept
fCard[/"share-card.png
the victory board, chosen
PNG · the only tenant of socials/"/]:::arKept
fMarkers[/"host.status · game-over
coordination between scripts
markers · bytes"/]:::arGone
end
subgraph dWeb["/run/arena/web/ — Suramadu serves this"]
direction TB
fDecided[/"decided.json
the result, for the player's page
JSON · result + camo paths"/]:::arGone
end
subgraph dOut["stdout — no file at all"]
direction TB
fLines[/"stats, lines
latency, bytes, heap, per tick
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
the inputs — headquarters writes these
before the task starts, and the container
fetches the manifest through a presigned URL")]:::input
subgraph artifacts["artifacts/ — what the container produced"]
direction TB
kLaunch[("launch/
what it was given")]:::sink
kRaw[("raw/
verbatim MegaMek")]:::sink
kDerived[("derived/
ours")]:::sink
kSocials[("derived/socials/
pictures we picked")]:::sink
kDiag[("diagnostics/
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
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](#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](#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//` by `container/lib/collect.sh` in
[arena][collect], 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-.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`](../services/api/src/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,,,` 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//` 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.
```text
matches//
manifest.json what the match was told to be — written by headquarters,
fetched once by the container through a presigned URL
camo-.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](../plan/match-records.md) — 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//…`, 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 `//…`, 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](../plan/match-records.md)),
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.** `MatchHost` re-writes `result.json` once a phase
and replaces it at victory, so a match that is abandoned, times out or is
killed still says what became of it. `ending` is `victory` or
`no-victory-at-exit`, and an undecided document carries no winner —
`victoryTeam: 0`, `victoryPlayerId: -1`, the sentinels `share.rs` already
reads as "nobody won".
- **Timestamps and the scenario are in it.** `startedAt`, `endedAt`, and a
`scenario` object 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`) and
`UnitReport` does not emit it; it can also be derived from
`raw/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_daily`
reads a document that is there. That is deliberate. A match abandoned before
deployment still scores nothing and releases the attempt, since `bvStart` is
0 and `score` refuses 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-.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 # 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.
[collect]: https://tangled.org/lance.blue/arena