Observed the public URL serving the previous build for ~3.5 minutes after a push. Responses carry x-pages-cache: true and cache-control: max-age=600, and a cache-busting query does not bypass it. The earlier claim of ~15s was wrong. AGENTS.md and deploy.sh now say to check the repo raw file FIRST: if that matches the local build the push landed and only the cache is behind, so don't start changing things.
11 KiB
AGENTS.md
Orientation for anyone (human or agent) picking this repo up cold. Read this before changing anything.
What this is
A single-page companion for 100%-ing Metal Gear Solid 2: Sons of Liberty — Master Collection Version (Steam appid 2131640). It contains a full walkthrough, all 51 achievements, all 394 dog tags, rebuilt guard-position maps, location screenshots, and live Steam achievement sync.
- Live: https://rain.pages.melonbread.xyz/la-li-lu-le-lo/
- Repo: https://git.melonbread.xyz/rain/la-li-lu-le-lo (branch
pages, which is the default branch) - Owner:
rain/rain@melonbread.xyz
Golden rules
- Never edit
index.html. It is generated. Your change will be silently overwritten on the next build. Editbuild/and run./deploy.sh. - Run
node tools/verify.jsbefore deploying. It catches most of the ways this project breaks. - Run
python3 tools/theme-contrast.pyafter touching any colour or theme. - The build has assertions. If one fires, fix the data, do not delete the assertion.
Layout
index.html generated - do not edit
steam-sync.py generated copy of build/steam-sync.py
steam-proxy-worker.js generated copy of build/steam-proxy-worker.js
deploy.sh build + commit + push
tools/verify.js headless smoke test (stubbed DOM, no browser)
tools/theme-contrast.py WCAG contrast check across all themes
build/ ALL the sources
build.py the build script
template.html the page: CSS + JS + markup, with __PLACEHOLDER__ slots
achievements_src.py hand-written how-to text for all 51 achievements
roadmap_src.py hand-written walkthrough steps + Big Boss content
*.json extracted data (see Provenance)
build.py writes to OUT = os.path.dirname(HERE), i.e. the repo root, and copies the two helper files up from build/. So the helpers are edited in build/ and published to the root.
How the page works
One self-contained HTML file. No framework, no build step at runtime, no network needed except for hot-linked photos.
build.py injects five JSON blobs into template.html:
| Blob | What |
|---|---|
ACHIEVEMENTS |
51 entries: name, desc, official Steam icon, category, missable flag, guide HTML, photos |
ROADMAP |
7 phases / 48 steps, each with body HTML, gets, areas, photos |
DOGTAGS |
10 lists (chapter × difficulty), 394 tags with name, area, conditional note |
MAPS |
10 spreadsheet sheets: palette, RLE fill runs, text cells, merges, col widths, row heights |
BIGBOSS |
criteria, heal points, time checkpoints, boss strategies |
All interactive state lives in one localStorage key, mgs2mc-guide-v1:
S = { ach:{id:true}, tags:{'listId:index':true}, road:{stepId:true},
items:{'stepId:slug':true}, hideDone, tab, roadDiff,
steam:{profile,id64,persona,unlocked:{api:true},lastSync} }
There is a separate key mgs2-theme for the colour theme, so it is not affected by Export/Import.
Build pipeline
Order matters — later stages depend on earlier ones:
- achievements — merge
achievements_src.pywithsteam_official.json(names/descriptions/icons from Steam's own XML). - screenshots — assemble per-achievement photo lists from the two guides, drop perceptual duplicates, pin multi-location photos to steps, attach
st.photos. - colour coding — classify every
<b>/<i>in the roadmap as tag / item / achievement / missable / boss / area. - step sub-checklists — extract
getsfrom the colourised HTML, map tag areas (TAGMAP), attachst.areas. - dog tags, then inject everything into
template.html.
Assertions (do not remove)
- every dog-tag area is claimed by exactly one step (
TAGMAP) — nothing silently unmapped - every
PHOTO_STEPkey matches at least one real photo — catches typos in the 40-char hashes - every photo of a split achievement is pinned to a step
- no achievement whose photos span several steps is left unpinned
Verifying changes
node tools/verify.js loads index.html, runs its script against a stubbed DOM/canvas and checks: boot does not throw, all 9 tabs render, all 10 maps draw without non-finite geometry, no label is squashed, dog-tag sub-checkboxes cover every tag in every list, no photo appears twice, every theme has a CSS block, and no personal data shipped.
python3 tools/theme-contrast.py checks every foreground against --panel plus --on-acc against --acc, across all themes. Fails under 3.0:1.
There is no browser tooling available. browser_screenshot crashes the harness GUI process (SIGSEGV in the Electron/sharp path) — do not call it. The Node stub approach above is the substitute; it verifies logic, not layout, so layout changes cannot be verified here and should be stated as unverified.
Quick manual checks worth knowing:
grep -c 'class="lb"' index.html # photos present in the build
node -e "..." # see tools/verify.js for the stub pattern
Data provenance
All third-party content is credited in the page's Sources tab and in README.md.
| Data | Source | Notes |
|---|---|---|
| Achievement list, icons, apinames | Steam community XML | steam_official.json — 51 entries, ach_002_001…ach_002_051 |
| Achievement how-to text | Steam guide by Cole ヴ Viper + Dayngl's Guides | hand-written into achievements_src.py |
| 394 dog tags | Video Chums checklist | dogtags_vc.json (names + areas) |
| Conditional-spawn notes | u/Spikeyroxas spreadsheet | dogtag_notes.json (81 notes) |
| Area maps | same spreadsheet | maps.json — the sheets are cell-grid art, not images |
| Location photos | Steam guide + Dayngl's Guides | hot-linked, never downloaded into the repo |
| Achievement removal check | TrueSteamAchievements | confirmed 51, incl. Hold Up-aholic which the Steam guide skips |
Refreshing a dataset means re-extracting into build/*.json and re-running the build. The extraction was originally done with ad-hoc Python against the source pages; if a source changes shape, re-derive rather than patching the JSON by hand.
Traps already hit (don't repeat)
- Canvas
fillTextwithmaxWidthcondenses text, it does not truncate. Narrow clip widths turn labels into an unreadable smear.drawMapmeasures each label and steps the font down (to 7px), then lets it overflow rather than squashing.tools/verify.jsfails if any label comes out condensed. - Excel does not clip overflowing text at a coloured cell. Only a cell with actual content stops it. Clipping map labels at fill cells made them tiny; clip at text cells only.
data-submust hold the handler key, not the CSS class. It was set to the class, so missable achievements emitteddata-sub="ach miss"and their tick boxes did nothing — and missables are the whole point.- Per-achievement photo caps break per-step distribution. Capping at 4 dropped exactly the photos that needed spreading across steps. Deduplicate globally, distribute per step, cap per step.
- Normalize both sides of a name lookup. An items list written with spaces (
"stun grenades") was compared against space-stripped text, so no multi-word item ever matched — 4 spans instead of 31. pkill -f "steam-sync"kills the invoking shell, because that shell's own command line contains the pattern. Usepkill -f "[s]team-sync"in a call that does not also contain the literal string, or split kill and start into separate calls.- A bad slice edit duplicated half of
build.py. The tell was the build printing every stage twice — check build output length when editing that file. - Code that uses a computed value must run after it is computed. The photo-to-step assignment needed
gets, which is built later in the script.
Steam sync
Steam's community XML sends no CORS headers, so a browser cannot read it directly. Two ways around it:
Local helper (what is deployed uses). steam-sync.py serves the folder and proxies /api/steam?profile=…. A hosted HTTPS page calls it at http://127.0.0.1:8765, so the helper must be running on the viewer's machine:
nohup python3 ~/Desktop/la-li-lu-le-lo/steam-sync.py > /tmp/steam-sync.log 2>&1 &
It answers Access-Control-Allow-Private-Network: true on the preflight — Chrome blocks a public page from reaching localhost without it.
Cloudflare Worker. steam-proxy-worker.js does the same job with no local process. Deploy it and set STEAM_PROXY near the top of the page script. ALLOW_ORIGIN is pinned to the Pages origin.
syncEndpoint() resolves: localhost → same-origin /api/steam; STEAM_PROXY set → that; otherwise → the local helper.
Parsing notes: match achievements by apiname (ach_002_0xx), never by display name. The XML has no <steamID> persona field — fall back to customURL, then to what the user typed.
Themes
Ten dark themes, switched from the header and applied by a small script in <head> before first paint (no flash). Everything is themed through CSS custom properties on :root and html[data-theme="…"]. Any new colour must go through a variable, otherwise it will be wrong in nine themes. Tints use color-mix() so they adapt automatically.
Two deliberate choices: the area maps keep a white background (they reproduce a spreadsheet; inverting them would make the colour legend lie), and there are no light themes (removed on request).
Deployment
./deploy.sh "what changed"
Builds, commits, pushes to pages.
Do not expect the live site to update immediately. Pages sits behind a cache
(x-pages-cache: true, cache-control: public, max-age=600), so the public URL can serve the
previous build for several minutes — observed between ~15 seconds and ~3.5 minutes. A
cache-busting query string does not bypass it.
When the live copy looks stale, check the repo before debugging the deployment:
# 1. did the push actually land? (this is the source of truth)
curl -s https://git.melonbread.xyz/rain/la-li-lu-le-lo/raw/branch/pages/index.html | sha256sum
sha256sum index.html
# 2. then, once those match, wait out the cache on the public URL
curl -s -L https://rain.pages.melonbread.xyz/la-li-lu-le-lo/ | sha256sum
If (1) matches and (2) does not, it is the cache — wait, do not start changing things. If (1) does not match, the push failed and that is the real problem.
Conventions
- British-ish plain English in user-facing copy; no marketing voice.
- Comments explain why, not what, and are only added where the reasoning is non-obvious.
- Content lives in the
*_src.pyfiles as HTML strings; structural/page changes live intemplate.html. - Keep the page dependency-free. Every external thing is a hot-linked image; nothing else loads off-origin.
- Do not commit personal data.
tools/verify.jsfails ifDEFAULT_PROFILEis non-empty.