archive/adventure-log/.Everything from here to the changelog is the record of the retired system, kept in the present tense it was written in. None of it is in the game today; the server keeps an account action log that only the GM console reads.
A permanent, chapter-bound chronicle of everything that has ever happened on the account — hatches, advancements, breedings, floor clears, boss kills, deaths, realm events, and every battle fought. The log itself never forgets; only the depth of battle detail is pruned over time.
{timestamp, event_type, param_ids[]} — e.g. {t, HATCH, DFMC-042}, {t, ADVANCE, DFMC-044 → DFMC-045}, {t, FLOOR_CLEAR, 17}.localStorage. The newest events live expanded in advChapters (newest-first, one chapter per day, advPush / advTrim); older ones are packed into gzip advArc blocks with a day index. There is no database. No server round-trips to read the log.advChapters entry; an older day is found through the archive’s day index and unpacked from its advArc block.| Event | Params | Chapter line (rendered) |
|---|---|---|
| HATCH | name, grade, sex, eggGrade, pen or floor, bred, types | "A creature hatched: Rare Fawn (F), bred in pen 2 from an Uncommon egg." |
| HATCH_BATCH | c (count), g (grade tally), s (species breakdown), m (mutants) | "Hatched 25 eggs (17 Worn, 6 Common, 1 Uncommon, 1 Rare) → 3× Flicker, 2× Scorpling, 2× Ignis, 2× Fry, 1× Fledgling, 1× Tempest, +14 more · 1 mutant." |
| BREED | a, b, ag/bg (parent grades), pen, grade, species | "Riptide [Epic] and Naiad [Rare] in pen 3 produced a Rare Fry egg." |
| ADVANCE | from, to, lv, stage | "Fawn advanced into Hart at level 18 · stage 2." |
| MUTANT_HATCH | same fields as HATCH | "A mutant variant emerged: Epic Nommo (M), bred in pen 1 from a Rare egg." |
| LEVEL_UP | name, lv, from | "Ingot reached level 33 (from 32)." |
| BATTLE | battle_id + summary | Summary row with a Battle Log button. |
| FLOOR_CLEAR | floor | "Portal opened — floor <n> cleared." |
| BOSS_DEFEATED | name, floor, rounds, survivors | "Pelagos, the Trench Warden has fallen on floor 20 in 7 rounds · 2/3 standing." |
| DEATH | name, floor, killer, rounds, pipeline | "Monkey died on floor 31 to Gorgon — the streak is lost." |
| UPGRADE | n, f (from), t (to), k (1 win / 0 fail) | "Sublime Onyx graded Rare → Epic." / "Grading Soul of the Mage Epic → Legendary failed — the bundle was spent." |
| DISMANTLE | c (count), g (grade tally), y (yield), m (mode) | "Dismantled 37 items (22 Worn, 12 Common, 3 Rare) → +148 Scrap · +12 Essence." |
| PURCHASE | w (what), q (qty), p (price), u (currency) | "Bought the Soul of the Archer for 2,500 Ichor." |
| SOCKET | n (socketable), h (host) | "Rough Garnet socketed into Arming Sword." |
| DEVOUR | n (consumer), f (fodder), k (kind: mon / pellet / mutagen / essence), x (xp), o→g (grade), m (mutant) | "Reliquary devoured Reliquary — +12,000 XP, grade Worn → Rare, became a mutant." / "Reliquary consumed a Blood Pellet — +1,000 XP." |
| LETTER | f (floor) | "A letter was waiting on floor 101. Click to read it again." The row reopens the letter. |
| EQUIP_UPGRADE | name | Reserved - nothing writes it yet. A renderer exists ("<name> was forged."); forging currently logs no event. |
| LEAGUE_EVENT | text | Reserved - nothing writes it yet. A renderer exists (the text verbatim). |
| REALM_EVENT | season, action | Never built - nothing wrote it and there was no renderer; realms and seasons left the game on 2026-08-28 (see Death & Realms). |
HATCH (or MUTANT_HATCH) with the full per-creature fields. The soul bag's HATCH ALL button does not: however many eggs it opens, it writes exactly one HATCH_BATCH.+n more beyond that. The cap is a storage rule, not a display one — the string is written at hatch time and lives in the save forever.MUTANT_HATCH line alongside the batch line, and is also counted in the batch's mutant tally. Mutants are story events and are rare enough to deserve the room.Param IDs reference codex CSVs; the log stores no names or descriptions. Ordinary per-battle XP ticks are not logged as separate events — they live inside the battle summary.
battle_id.blAppend against a word dictionary (blW words, blS labels) into pending streams keyed by battle_id — about 44 bytes a fight. See How a fight is stored, below.The player taps the Battle Log button on a battle summary row in a chapter.
The blob is fetched by battle_id and decompressed in memory (<1 ms for a 1–3 KB blob) — only that one battle, never the rest of the history.
Turn structs are deserialized and rendered into readable turn-by-turn text via templates + codex data. Compression is invisible at the UI layer.
| Battle class | Full turn detail | Summary row |
|---|---|---|
| Boss battles | Kept forever | Forever |
| PvP battles | Kept forever | Forever |
| Ordinary wild battles | Last N days / last N battles, then compacted away | Forever |
Nothing is deleted until the archive is full. Every event and every battle's numbers are kept, because they are balancing data to be pulled off real players; what changes with age is how they are stored. The archive is capped at 1,029 KiB and sheds its oldest whole blocks past that (The budget, below).
| Age | Form | Cost |
|---|---|---|
| Newest 500 events | Expanded in advChapters, rendered in the log window | ~68 bytes each |
| Older events | Packed one-per-line and gzipped into advArc blocks (base64). Falls back to the plain packed string where CompressionStream is unavailable — still complete, just larger | ~7 bytes each, a ~10× cut |
| Newest 25 battles (legacy saves only) | Full round-by-round rendered log, last 10 rounds — only fights written before the packed store; new fights are packed (How a fight is stored) | ~5 KB each |
| Battles 26–200 | Compact row: result, floor, XP, Food, participants. The prose goes, the numbers stay | ~290 bytes each |
| Older battles | Archived into advArc as BATTLEROW entries. The chapter line falls back to the BATTLE event's own stub | ~7 bytes each |
Measured on the two worst real accounts, with the full history intact in both cases: 905 KB → 373 KB (500 live events + 3,380 archived in 38 KB) and 638 KB → 317 KB. Applied on write and on load, so an oversized save shrinks the moment it is opened.
High-frequency events use one-letter keys — {c:37,g:{Worn:22},y:'+148 Scrap'} —
and a bulk action is always one event: dismantling 113 eggs costs one line with a grade
tally, never 113 lines.
The archive is DEVICE-LOCAL and does not reach the server. The Stage 4a save diet
strips advArc, advChapters, advBattles and floors from
every upload — they were 89% of a real save's bytes, and advArc riding the upload was
itself the 2026-08-01 save-ceiling incident (68,560 B of a 127,115 B save; /api/save
refuses over 131,072 B). So /api/backup/export does not carry log history, and a
new device starts with an empty log. Pulling history off a player is the on-demand advlog extraction round
(admin-backend.html §8). Each block is {n, z} (gzip, base64) or {n, t} (plain),
plus the day index below.
Unpack a block's text into events by splitting lines on \n and fields on \x01
— ts, TYPE, JSON params:
import gzip, base64, json
for b in save.get('advArc', []):
txt = gzip.decompress(base64.b64decode(b['z'])).decode() if 'z' in b else b['t']
for line in txt.split('\n'):
ts, typ, params = line.split('\x01'); ev = (ts, typ, json.loads(params))
On release since 0.64. A packed event carries only
ts = HH:MM. The DATE lived on the chapter object, and advTrim
drops a chapter the moment its last event is archived — so the archive was a single
undated stream. Nothing was ever lost; the history simply could not be cut into days. That is the whole
of “the log only shows a day”.
DAY marker. advTrim writes
{ts:'00:00', type:'DAY', params:{k:<dayKey>, t:<date>, c:<chapter title>}}
into the packed stream when the day changes and whenever a fresh block opens. Every event
after a marker belongs to that day until the next marker.d is the list of dayKeys the block
holds; dl is the last day it declared, so the next append knows whether it owes a marker.
A day grid is drawn from d across all blocks with zero decompression;
opening one day decompresses only the blocks whose d lists it (measured: 2 of 5 worst
case, 0.7 average over 30 days).d is a cache. A reader that finds them disagreeing
trusts the marker and repairs the header.advArcCompact re-declares the running day at the head of every regrouped
block. The heal splits blindly by index, so without this it destroys the index it exists to carry.BATTLEROW is a RECORD, not a log entry. It is the compacted detail its
BATTLE line points at, is looked up by id, and must never be rendered or counted as an
event — doing so double-counts every archived day.battle_id is
'B-' + Date.now().toString(36) — epoch ms, i.e. UTC — while a
chapter's dayKey is toDateString(), the device's LOCAL date.
At UTC+9 a 07:00 battle decodes to the previous day, so id-dating puts every morning on the
wrong tile. Id-dating survives only as an approximate rescue for archives written before markers
existed, where it is accurate to within one day at the boundary.advTrim runs on every event and moves exactly one — which measured 7.4%, the same
shape as the 2026-08-01 gzip-per-single-event mistake._bak copy), so the practical budget is ~2.5 MB against a ~102 KB
non-log save. At ~37 B per archived event that is roughly 57,000 events. A full store raises
QuotaExceededError; the save path catches it, sheds the oldest archive blocks and writes again (The budget, below).The dates were never gone — an earlier note on this page said they were, and that
was wrong. Every BATTLE and BATTLEROW carries
params.id / params.i = 'B-' + Date.now().toString(36), the
exact epoch millisecond at which advPush minted the dayKey, and
new Date(ms).toDateString() is that same call — so it reproduces the
original key exactly on the device that wrote it.
advArcCompact, and is a no-op once every block carries d.Only entries with no battle anywhere before or after them stay undated, and fall to the reader’s “Earlier” tile.
On release since 0.64. The log opens on a grid of tiles, one per day, newest first. Archived days are included — which is the whole point, because a day whose events had all been packed away used to disappear from the list entirely, and that is what “the log only keeps a day” actually was.
d is {dayKey: entryCount}) precisely so that a tile does
not have to open anything to know its number. A pre-count archive shows a dash rather than a wrong number.On release since 0.64. The archive is capped at 1,029 KiB and sheds its oldest whole blocks when it exceeds that.
ADV_ARC_BUDGET).On release since 0.64 (abfb7fa). A fight used to be kept as rendered English in plain JSON — about 5 KB each, and only 25 kept, which on autoplay is under an hour of history. Packed, a fight is 44 bytes, and every number stored is the number that happened.
At 44 bytes a fight, 200 fights a day costs 9 KB and a thousand costs 43 KB. A 512 KiB budget holds around 12,000 fights.
The budget bounds the whole store (lane I, 32391b5, on dev): the sealed blocks, the pending streams, the fights kept as prose because they failed the round trip (blRaw) and the word dictionaries (blW, blS) all count toward BL_BUDGET. Past it the oldest sealed blocks and raw fights are shed together, oldest first, and the newest block always stays. The dictionaries cannot lose single entries, because sealed blocks index into them, so once they pass a quarter of the budget they are rebuilt from the fights still stored.
advChapters, advBattles, advArc and the packed battle-log store (blW … blRaw) left the save, and the hourly /api/advlog/upload check-in is gone (the server half answers 410). The PvP replay never read this log and is unchanged. The floor-101 letter and the UNLOCK lines are now notices: a short list derived from the account’s tutorial flags, shown at Settings > Notices, where the letter can be read again.turnsLog, which a fight in the packed store no longer carries. Both now read one count (the stored rounds, else the unpacked block). A victory whose Ichor the server corrected showed the server’s figure on the spoils (16) and the client’s estimate on the log row (+18); the fight’s record and its BATTLE entry now take the server’s figure too. A chapter with one entry says 1 ENTRY.blRaw and the word dictionaries count toward BL_BUDGET. Two dead helpers of the packed store, BL_BLOCK (the fight-count block size the byte seal replaced) and blGunz, are removed. Back from a battle view with no day behind it opens the chapter list; it used to build a chapter-index view that no longer exists.BL_BUDGET now bounds the WHOLE battle-log store, not only the sealed blocks: fights kept as prose because they failed the pack round trip (blRaw) count toward it and are shed oldest first together with the blocks (the newest block always stays), and once the word/sentence dictionaries (blW/blS) pass a quarter of the budget they are rebuilt from the fights still stored, so words that only the shed fights used leave with them. A fight finished while a block was being sealed is no longer lost (the seal removes only what it packed), and the idle merge of small blocks no longer re-packs the whole store on every seal, drops a block sealed while it ran, or brings back a block the budget had shed. The adventure archive keeps every event moved out while it is being re-grouped (the heal commits only if nothing touched the archive meanwhile, otherwise it runs again), and the events trimmed out of the live window when a save loads land in the loaded archive instead of being lost. The multi-device read of a round's upload now shows a day two devices both played as the union of both devices' events, not the last device's. The event table now lists LETTER and marks EQUIP_UPGRADE, LEAGUE_EVENT and REALM_EVENT as not written yet. Gates tools/probe_advlogstore.py, tools/probe_nameclaim_merge.py.blW, blS, blBlocks, blPS, blPN, blIds, blRaw) lived only in memory, so every reload lost the round-by-round detail the log opens; it now rides the LOCAL save slot beside advArc (runSnapshot saves it, the load reads it), is stripped from the server copy by serverSnap and by the save door (the server save does not grow by a byte), and stays bounded by BL_BUDGET. The login splice that reunites the device half with the server save read it from the legacy key the app deletes on every boot, so from the second login on nothing was spliced back and the adopted save overwrote the device log; it now reads the slot the app writes. A full device sheds the oldest whole block of whichever log is larger (archive or battle logs), never the newest, and when the save still will not fit the player is told once that the logs are no longer kept on this device (progress still saves to the server). Gate tools/probe_blpersist.py./api/advlog/upload refuses any round but the active one (inside its window), answers a second upload from the same account and device with no write, and takes at most 3 devices per account per round — all before the shared r2_write budget is spent, so a looping client can no longer drain the budget the save mirror pauses on.WHERE ts chapter queries and turn-action structs; the log is local save JSON (advChapters plus gzip advArc blocks with a day index) and fights are packed against a word dictionary. Storage budget said nothing is ever deleted while the archive is capped and sheds oldest blocks; the Ceiling bullet said the quota error is swallowed while the save now catches it and sheds; the 25-battle row is legacy-only; the second budget figure (22,000 / seven months) now matches the first (28,000 / nine months); How a fight is stored is on release, not ON DEV. DESCRIPTIVE ONLY.toDateString() reproduces that key on the device that wrote it; everything else brackets
between two battles by elapsed distance. A one-off heal dates the whole archive and writes the markers
in permanently, strictly additively and behind a census that refuses to commit if a single line would be
lost. 100% exact over 5,556 entries and 41 days in three timezones. Gate 15/15, red-first.HH:MM and the date
lived on a chapter that is dropped as soon as it empties, so the history was one undated stream —
the reason the log appeared to keep only a day. advTrim now writes a DAY marker
on a day change and at every fresh block, and each block header carries d (its dayKeys) and
dl (its last declared day), uncompressed, so a day grid draws from headers alone and only the
opened day is decompressed. advArcCompact re-declares the running day per regrouped block.
Also corrected here: advArc has not ridden the save since the Stage 4a diet, so the
backup export does not carry log history — this page claimed it did. Three defects were
caught by tools/probe_advday.py before shipping and are written up above: per-slice markers
becoming per-event, BATTLEROW counted as an entry, and UTC id-dating against a local dayKey.
Gate 10/10, red-first 4/10. Write side only — no reader surfaces the day index yet.ts field and losing their time — 275 of 405 in a live save; the row now
carries the timestamp stamped when the battle was recorded.HATCH_BATCH added: HATCH ALL now writes one event for the whole press instead of one per hatchling, carrying the grade tally, the capped species breakdown and the mutant count. Mutants keep their own line. Documented under Event types.advArc and old battle rows are archived rather than dropped. Same size win, no data loss:
the worst account still falls from 905 KB to 373 KB with all 3,880 entries recoverable.