Sync works on the deployed site; add reproducible build and deploy script

The Pages deployment is now the primary copy, so two things had to change:

- Hosted pages fall back to steam-sync.py on 127.0.0.1:8765 instead of
  refusing to sync. steam-sync.py now answers Chrome's Private Network
  Access preflight (Access-Control-Allow-Private-Network), which is what a
  public https page needs before it may call into localhost.
- The build sources lived in /tmp and would have been lost. They now live in
  build/, and build.py outputs to the repo root. deploy.sh rebuilds,
  commits and pushes in one step.

Editing index.html directly is no longer the workflow - edit build/ and run
./deploy.sh.
This commit is contained in:
Rain 2026-09-15 17:40:06 -04:00
parent 4d5545e6c5
commit a63606b5c8
17 changed files with 6120 additions and 18 deletions

252
build/ach_icons.json Normal file
View file

@ -0,0 +1,252 @@
{
"Lights Out": {
"num": 1,
"icon": "https://images.steamusercontent.com/ugc/2140958177135754992/33483A2517679F2105D029F5821418DD1D233281/",
"desc": "Defeat Olga Gurlukovich"
},
"Spaghetti Cinema": {
"num": 2,
"icon": "https://images.steamusercontent.com/ugc/2140958177135758321/BC72F568064E5D5C268D200F6382B0BD3F8DEE6B/",
"desc": "Meet Revolver"
},
"Bomb Squad": {
"num": 3,
"icon": "https://images.steamusercontent.com/ugc/2140958177135758930/E64E34B9E08B8F9C96C34C6DA45AE5CF6D0B7003/",
"desc": "Learn how to defuse C4 bombs from Peter Stillman"
},
"Lady Fortune": {
"num": 4,
"icon": "https://images.steamusercontent.com/ugc/2140958177135759201/550DD6E41F6D40451134C4E847DFFEA44601734D/",
"desc": "Survive Fortune\u2019s Attack"
},
"Party\u2019s Over": {
"num": 5,
"icon": "https://images.steamusercontent.com/ugc/2140958177135759509/74E791DB6ECD619EED7313A2F4382FEBB3AFCFBC/",
"desc": "Defeat Fatman"
},
"Thanks, Ames": {
"num": 6,
"icon": "https://images.steamusercontent.com/ugc/2140958177135760558/5E46A25E5B0BDFF0F26E93F657376EE5C6C24F3E/",
"desc": "Learn the location of the president"
},
"No-Fly Zone": {
"num": 7,
"icon": "https://images.steamusercontent.com/ugc/2140958177135760933/A22BC99C672ED7BD5A5170FC1A0E46DD41C3AB61/",
"desc": "Destroy the AV-88 Harrier II"
},
"Bohemian Candidate": {
"num": 8,
"icon": "https://images.steamusercontent.com/ugc/2140958177135776614/4D20C2252E95CFF4AE279DFB381B6498B5315F69/",
"desc": "Meet President James Johnson"
},
"Vampire Slayer": {
"num": 9,
"icon": "https://images.steamusercontent.com/ugc/2140958177135776885/0BE932AFDA4A9C857046DDBC27EE74B16A804B8F/",
"desc": "Defeat Vamp"
},
"I Think You Need a Hug, E": {
"num": 10,
"icon": "https://images.steamusercontent.com/ugc/2140958177135778904/EB4EF236A1B364E861F8CCF0588F1FACE5CD86A3/",
"desc": "Find Emma Emmerich"
},
"Sharing Is Caring": {
"num": 11,
"icon": "https://images.steamusercontent.com/ugc/2140958177135780873/BD7DCD2395C79925B68C7006CC63AE6402E75A06/",
"desc": "Befriend Olga Gurlukovich"
},
"No Ray, Jos\u00e9": {
"num": 12,
"icon": "https://images.steamusercontent.com/ugc/2140958177135781258/EFE5274E4CE35FA4FEA13CF93A2A191C2B0EDB57/",
"desc": "Defeat Metal Gear RAY"
},
"Another Snake Bites the Dust": {
"num": 13,
"icon": "https://images.steamusercontent.com/ugc/2140958177135781716/8304A788B9099C1CBE87D963BCA7EB3B15EB1EC6/",
"desc": "Defeat Solidus Snake"
},
"A Cut Above": {
"num": 14,
"icon": "https://images.steamusercontent.com/ugc/2140958177135784329/36F89E61EC5808EDCD35952C2C4C44C30CE0AD69/",
"desc": "Beat the Tanker and Plant chapters on any difficulty"
},
"Animal Control": {
"num": 15,
"icon": "https://images.steamusercontent.com/ugc/2140958177135755392/B1B11990A2D6A38E90E06B774C13E66EE2DEE53A/",
"desc": "Collect a dog tag"
},
"Weapon Completionist: Tanker": {
"num": 16,
"icon": "https://images.steamusercontent.com/ugc/2140958177135759832/1ADADF954660E5DD90C4857F582F714F2E826A55/",
"desc": "Collect every type of weapon on the Tanker"
},
"Weapon Completionist: Plant": {
"num": 17,
"icon": "https://images.steamusercontent.com/ugc/2140958177135910792/AFD91F876679EC04919CD822C55ECD25807C8799/",
"desc": "Collect every type of weapon in the Plant"
},
"Moving Day": {
"num": 18,
"icon": "https://images.steamusercontent.com/ugc/2140958177135784031/D5F815103756B9D3C747BB2B7ABB0762D94FB272/",
"desc": "Collect all boxes"
},
"Yorkie": {
"num": 19,
"icon": "https://images.steamusercontent.com/ugc/2140958177136173819/87F0A2B539658DE6AEDD4A48A62840315333C6E7/",
"desc": "Get the bandana"
},
"Poodle": {
"num": 20,
"icon": "https://images.steamusercontent.com/ugc/2140958177136232954/7E891DAD15B9202482027D5A02AF944D6D942C03/",
"desc": "Get the Tanker stealth suit"
},
"Beagle": {
"num": 21,
"icon": "https://images.steamusercontent.com/ugc/2140958177136250486/E2E2E06267E608248851BAEE451A0EFDDC0DFF87/",
"desc": "Obtain the Infinity Wig"
},
"Shiba Inu": {
"num": 22,
"icon": "https://images.steamusercontent.com/ugc/2140958177136279127/EF5FC86EA9713C79B5BBF9C4FCFC71BFEE6BA7FB/",
"desc": "Get the Plant stealth suit"
},
"Photo Finish": {
"num": 23,
"icon": "https://images.steamusercontent.com/ugc/2140958177135761686/D8EA3BF271A12B099620C980884D31DD5BCA502B/",
"desc": "Acquire the digital camera"
},
"Hurt Locker": {
"num": 24,
"icon": "https://images.steamusercontent.com/ugc/2140958177135758651/CDA5624B4684415250A23CE2B51E04538F92A0B0/",
"desc": "Put an enemy in a locker"
},
"To Catch a Predator": {
"num": 25,
"icon": "https://images.steamusercontent.com/ugc/2140958177136326152/97E4F9B9244F5F45AF5283BEEF19BA0D47ADB04D/",
"desc": "Lure a guard with a girlie magazine"
},
"Bye Bye Big Brother": {
"num": 26,
"icon": "https://images.steamusercontent.com/ugc/2140958177136369228/0DF7F365A8755CF88FF86A8276B9222FDBB82F57/",
"desc": "Destroy 5 cameras"
},
"Down in Smoke": {
"num": 27,
"icon": "https://images.steamusercontent.com/ugc/2140958177136463315/0A4BFB811C061E224D76C168346036A56B6DF9D9/",
"desc": "Disorient an enemy with a cloud of smoke from a fire extinguisher"
},
"Silence is Golden": {
"num": 28,
"icon": "https://images.steamusercontent.com/ugc/2140958177135758074/4144C41DE9B6FD8F4E20EC85A2DA7FFA654E457B/",
"desc": "Shoot an enemy radio"
},
"Caution: Hot": {
"num": 29,
"icon": "https://images.steamusercontent.com/ugc/2140958177136533725/FD82DCC80A0091EFFC0B4807550E4C572FF3BD69/",
"desc": "Down an enemy with steam from a pipe."
},
"Kissing Booth": {
"num": 30,
"icon": "https://images.steamusercontent.com/ugc/2140958177135760330/59252565727727DAB7F995D692CC70B850F1F560/",
"desc": "Kiss a poster in a locker"
},
"Snake Beater": {
"num": 31,
"icon": "https://images.steamusercontent.com/ugc/2140958177136595643/2EBC1BBBFB7363D23750458BB71B2C0E327E90F5/",
"desc": "Get caught by Otacon stimulating yourself"
},
"Who Ya Gonna Call?": {
"num": 32,
"icon": "https://images.steamusercontent.com/ugc/2140958177138681898/A576D1518491AF79CF79B72240F7647D13218F49/",
"desc": "Take a clear photograph of the ghost image in Hold No. 2"
},
"Sexting": {
"num": 33,
"icon": "https://images.steamusercontent.com/ugc/2140958177138684378/C931BF409EF81B2769A57AA30B2C480F433130E6/",
"desc": "Send Otacon a picture of the marine with no pants"
},
"My Rations\u2026": {
"num": 34,
"icon": "https://images.steamusercontent.com/ugc/2140958177138713012/FFC33FEA695E0CFC0A4E588A6DA8D2DD2738E8DF/",
"desc": "Find a sea louse in your rations"
},
"Johnny on the Spot": {
"num": 35,
"icon": "https://images.steamusercontent.com/ugc/2140958177138827859/F04B8B21FD970B720B3EFE532CCF0E3F09A8711D/",
"desc": "Hear Johnny\u2019s bowel noises in two locations"
},
"Vamp Eyer": {
"num": 36,
"icon": "https://images.steamusercontent.com/ugc/2140958177138834182/41AC554FB65132522963BBB74570611C47C434F4/",
"desc": "Catch a glimpse of Vamp standing in the streets of New York during the end cinematic"
},
"Love Hurts": {
"num": 37,
"icon": "https://images.steamusercontent.com/ugc/2140958177138847978/D047A74CFC1DC5B02053E446B2EFF5E1EC5FBEB3/",
"desc": "Watch Rose kill Raiden on a rooftop"
},
"Don\u2019t Taze Me, Bro": {
"num": 38,
"icon": "https://images.steamusercontent.com/ugc/2140958177138861862/96200A814B27AA8D4FE8666F01B16A3389055910/",
"desc": "Tranquilize 100 enemies"
},
"Nothing Personal": {
"num": 39,
"icon": "https://images.steamusercontent.com/ugc/2140958177138871847/614116AEBDFB99D916609B7AFBEA36D21DA70C92/",
"desc": "Break the neck of 30 enemies"
},
"Rent Money": {
"num": 40,
"icon": "https://images.steamusercontent.com/ugc/2140958177138871847/614116AEBDFB99D916609B7AFBEA36D21DA70C92/",
"desc": "Beat 30 enemies unconscious"
},
"Steel Grip": {
"num": 41,
"icon": "https://images.steamusercontent.com/ugc/2140958177138885772/862A012B7336607FE22DC16ED700EA766190DE85/",
"desc": "Attain grip level 3"
},
"Casting Theater": {
"num": 42,
"icon": "https://images.steamusercontent.com/ugc/2140958177138962742/92C53189E3353645067850DDF1F6F5A4E128FAAF/",
"desc": "Watch something in the Casting Theater"
},
"No Boss of Mine": {
"num": 43,
"icon": "https://images.steamusercontent.com/ugc/2140958177139102828/A866CC1EB85E9E5803E45D49B79AF7A4138BEAA6/",
"desc": "Complete Boss Survival"
},
"Tell Me a Tale": {
"num": 44,
"icon": "https://images.steamusercontent.com/ugc/2140958177139112017/DD9E4F8043B10C1EFD5B907461D3215214221EB0/",
"desc": "Complete at least one of the Snake Tales"
},
"Piece of Cake": {
"num": 45,
"icon": "https://images.steamusercontent.com/ugc/2140958177135761356/16A503FABA4B31252AA95DB1AF36B9BFE048E616/",
"desc": "Complete a VR or Alternative mission"
},
"In It to Win It": {
"num": 46,
"icon": "https://images.steamusercontent.com/ugc/2140958177139142157/9013B6B054ED165E9C8E33E7120454611D0D9AA9/",
"desc": "Place first in 50 different VR/Alternative missions"
},
"Pacifist Run": {
"num": 47,
"icon": "https://images.steamusercontent.com/ugc/2140958177139129560/253AF7994FDBE98D9BE471BF43F2382832B8F001/",
"desc": "Clear the game without killing anyone."
},
"Complete Stealth": {
"num": 49,
"icon": "https://images.steamusercontent.com/ugc/2140958177139132973/BEF78F1D1BD36AE986D90E28DE4711BA1AAC5588/",
"desc": "Clear the game without entering alert mode (not including events where alert mode is mandatory)"
},
"Cover-to-Cover Solid 2: Books of the Patriots": {
"num": 50,
"icon": "https://images.steamusercontent.com/ugc/2198379706926301353/7508D70EC3C479089F0462DCC63E51F65D830320/",
"desc": "Read all the pages in the Screenplay Book"
},
"Big Boss": {
"num": 51,
"icon": "https://images.steamusercontent.com/ugc/2140958177139160810/3D2648816435D33E890BD394EFBF190981BFAE0A/",
"desc": "Earn the title of Big Boss"
}
}

493
build/achievements_src.py Normal file
View file

@ -0,0 +1,493 @@
# -*- coding: utf-8 -*-
"""Hand-written guide content for the 51 MGS2 Master Collection achievements.
Keyed by Steam apiname so official icons/names can be merged automatically."""
ACH = {
# ---------------------------------------------------------------- STORY (14)
"ach_002_001": dict(cat="story", guide="""
<p><b>Unmissable.</b> Defeat Olga Gurlukovich on the Navigational Deck, Wing at the end of the Tanker chapter.</p>
<ul class="tight">
<li>Shoot the <b>spotlight</b> and the <b>bottom corners of the tarp</b> as soon as the fight starts.</li>
<li>She fires at your <i>last known position</i> roll to a blind side after every shot.</li>
<li>Taking damage here is fine: your health is refilled after the fight.</li>
</ul>"""),
"ach_002_002": dict(cat="story", guide="""
<p><b>Unmissable.</b> Pops during the Tanker story when Revolver Ocelot makes his entrance. Nothing to do.</p>"""),
"ach_002_003": dict(cat="story", guide="""
<p><b>Unmissable.</b> Pops when Peter Stillman teaches you how to defuse C4 in the Strut E Heliport at the start of the Plant chapter.</p>
<div class="infobox">Pay attention here the coolant/sensor mechanics you learn are the same ones you need for <b>Party's Over</b> and the whole bomb-defusal section.</div>"""),
"ach_002_004": dict(cat="story", guide="""
<p><b>Unmissable.</b> You do not have to beat Fortune you only have to <b>survive</b> her attack sequence. It ends on its own.</p>
<ul class="tight">
<li>Her rail gun tracks a fixed pattern; stay behind cover and keep moving.</li>
<li>If you are doing the Big Boss run, see the Fortune strategy in the <b>Big Boss Run</b> tab (walk left/right in a cardboard box at the bottom of the arena).</li>
</ul>"""),
"ach_002_005": dict(cat="story", guide="""
<p><b>Unmissable.</b> Defeat Fatman on the Big Shell helipad.</p>
<ul class="tight">
<li>Equip the <b>Sensor A</b> coolant and prioritise <b>freezing the bombs he plants</b> do not wait for all of them to be planted.</li>
<li>Melee him as he skates past to knock him down, then shoot him in the head with the <b>M9</b> while he is getting up.</li>
<li>From range, shoot him with the SOCOM to knock him down, then M9 to the head.</li>
</ul>"""),
"ach_002_006": dict(cat="story", guide="""
<p><b>Unmissable.</b> Pops after you interrogate Ames in the Shell 1 Core hostage room and learn where the President is.</p>"""),
"ach_002_007": dict(cat="story", guide="""
<p><b>Unmissable.</b> Destroy the AV-88 Harrier II.</p>
<ul class="tight">
<li>Pick up the <b>Stinger</b> the moment the fight starts and fire as fast as you can.</li>
<li>When it flies straight out, follow the contrails <b>wait for the flash</b>, then fire and take cover.</li>
<li>When it hovers above you, spam the Stinger at the middle of the jet; you do not need a lock-on.</li>
<li>Rockets: run to the far left up the stairs, then cartwheel right as it fires. Get behind the big grey container when it closes in.</li>
</ul>"""),
"ach_002_008": dict(cat="story", guide="""
<p><b>Unmissable.</b> Pops when you meet President James Johnson in the Shell 1 Core.</p>"""),
"ach_002_009": dict(cat="story", guide="""
<p><b>Unmissable.</b> Defeat Vamp.</p>
<ul class="tight">
<li>M9 to the head, then when he dives into the water shoot the <b>water</b> with the Stinger to drain his O₂.</li>
<li>Classic cheese: step off the platform so Raiden hangs from the edge, let Vamp walk over, then pull up as he attacks. Punch twice, back off, repeat.</li>
</ul>"""),
"ach_002_010": dict(cat="story", guide="""
<p><b>Unmissable.</b> Pops when you find Emma Emmerich in the Shell 2 Core filtration chamber.</p>"""),
"ach_002_011": dict(cat="story", guide="""
<p><b>Unmissable.</b> Pops during the Arsenal Gear sequence when Olga turns on Solidus and helps you.</p>"""),
"ach_002_012": dict(cat="story", guide="""
<p><b>Unmissable.</b> Defeat Metal Gear RAY.</p>
<ul class="tight">
<li>You fight roughly <b>20 RAYs</b>. Be quick with shots and listen for missile barrages from RAYs outside the arena.</li>
<li>Dodge machine-gun fire and single missiles by running left/right and cartwheeling as they get close.</li>
<li><b>Big Boss note:</b> each RAY destruction counts as mech damage see the Big Boss tab.</li>
</ul>"""),
"ach_002_013": dict(cat="story", guide="""
<p><b>Unmissable.</b> Defeat Solidus Snake on the rooftop. See the Big Boss tab for the block-and-punish strategy.</p>
<div class="warnbox"><b>Do NOT skip the cutscene that follows.</b> You need it for <b>Vamp Eyer</b>.</div>"""),
"ach_002_014": dict(cat="story", guide="""
<p><b>Unmissable.</b> Pops when you finish both chapters on any difficulty. See the <b>Roadmap</b> tab.</p>"""),
# ------------------------------------------------- MISC / MISSABLE (window)
"ach_002_015": dict(cat="misc", miss=True,
where="Tanker — Deck-A, crew's quarters (lockers)", window="Any time in the Tanker/Plant",
guide="""
<p>Put an enemy inside a locker. Easiest in the <b>Tanker, Deck-A Crew's Quarters</b>.</p>
<ol class="tight">
<li>Tranquillise or kill the guard patrolling the hallway.</li>
<li>Drag him (grab button) into the room with the lockers.</li>
<li>Open a locker, then drag him into it Snake automatically stuffs him in and shuts the door.</li>
</ol>
<p class="muted">Works as both Snake and Raiden, so this is one of the easiest to pick up in the first 5 minutes of the run.</p>"""),
"ach_002_016": dict(cat="misc", miss=True,
where="Plant — Strut F warehouse (Book), place in a guard's path", window="After Fatman for the safest Book route",
guide="""
<p>Lure a guard with the <b>Book</b> (the girlie magazine).</p>
<ol class="tight">
<li>Get the <b>Book</b> in <b>Strut F</b>: on the top floor, lean on the railing, flip over it, shimmy right until you are above the stack of crates, drop down and pick it up. (Also found in the Shell 1 Core 1F south lockers and in Emma's locker in Shell 2 Core B1.)</li>
<li>Equip the Book and drop it in a guard's patrol path.</li>
<li>When he stops to stare, the achievement pops.</li>
</ol>"""),
"ach_002_017": dict(cat="coll", miss=True,
where="Any chapter — hold up any guard or boss", window="First opportunist moment",
guide="""
<p>Collect a single dog tag. This is the first of the collectible chain and pops almost immediately.</p>
<p><b>How to hold up a guard:</b></p>
<ol class="tight">
<li>Approach from <b>behind</b>, equip the M9/SOCOM, hold the aim button while pressing the fire button so the gun is pressed into their back Snake shouts <i>"Freeze!"</i>.</li>
<li>Keep the gun raised and <b>run around to their front</b> while still aiming.</li>
<li>Aim at their <b>face</b>; they shake and drop the tag.</li>
<li>If they refuse, shoot their <b>hand or foot with a lethal weapon</b> to scare them, then tranquillise them.</li>
</ol>
<div class="infobox">The full 394-tag checklist with guard names, areas and conditional-spawn notes is in the <b>Dog Tags</b> tab.</div>"""),
"ach_002_018": dict(cat="misc", miss=True, cum=True,
where="Anywhere — cameras in story, Snake Tales and VR", window="Cumulative",
guide="""
<p>Destroy <b>5 wall-mounted cameras</b> (cumulative across story, Snake Tales and VR missions).</p>
<ul class="tight">
<li>You must use a <b>lethal</b> weapon tranquiliser shots do not break cameras.</li>
<li><b>Cyphers do NOT count</b> only the cameras mounted on walls.</li>
<li>The Tanker has several easy ones (Deck-C, Deck-D) and the Plant has them all over the Shell cores.</li>
</ul>
<div class="warnbox">If you are going for <b>Big Boss</b>, remember every camera you destroy counts toward your mech-damage limit. Do this achievement on a non-Big-Boss run.</div>"""),
"ach_002_019": dict(cat="misc", miss=True,
where="Tanker — Deck-D, crew's quarters (fire extinguisher)", window="Tanker, before the Holds",
guide="""
<p>Disorient a guard with a fire extinguisher cloud.</p>
<ol class="tight">
<li>Go to <b>Deck-D Crew's Quarters</b>, near the kitchen/food storeroom and the laser trap hallway.</li>
<li>Stand across from the laser trap and wait for the patrolling guard to stop next to the fire extinguisher.</li>
<li>Shoot the <b>extinguisher</b> (non-lethal works) so the cloud engulfs him.</li>
</ol>"""),
"ach_002_020": dict(cat="misc", miss=True, cum=True,
where="Anywhere — shoot the radio on a guard's back", window="Cumulative",
guide="""
<p>Shoot an enemy's radio.</p>
<ul class="tight">
<li>Stand behind a guard and aim at the <b>radio on their lower back</b>. You will see sparks when you hit it.</li>
<li>Non-lethal weapons work, so you can do this while farming dog tags.</li>
</ul>"""),
"ach_002_021": dict(cat="misc", miss=True,
where="Tanker — pipe-lined hallways", window="Tanker",
guide="""
<p>Down an enemy with steam from a burst pipe.</p>
<ol class="tight">
<li>Hold up (or drag) a guard next to a wall pipe, then shoot the pipe. Steam jets out.</li>
<li>Keep shooting the pipe until the guard dies you can tell it is working because he waves a hand in front of his face.</li>
<li>Alternatively, during the <b>Tanker ambush hallway</b> (narrow corridor lined with pipes) just shoot the pipes next to the soldiers.</li>
</ol>
<div class="warnbox"><b>Caution: Hot requires a kill.</b> If you are attempting Pacifist Run or Big Boss in the same session, do not trigger this on that file.</div>"""),
"ach_002_022": dict(cat="misc", miss=True,
where="Tanker — Deck-A, crew's quarters (poster locker)", window="Tanker",
guide="""
<p>Kiss the poster inside a locker.</p>
<ol class="tight">
<li>In <b>Deck-A Crew's Quarters</b>, open lockers until you find the one with a <b>pin-up poster</b> on the inside of the door.</li>
<li>Get inside the locker. The door closes behind you and the poster is right in your face.</li>
<li>Hold the aim/first-person button until you hear a <b>kissing noise</b>.</li>
</ol>
<p class="muted">Do this back-to-back with <b>Snake Beater</b>.</p>"""),
"ach_002_023": dict(cat="misc", miss=True,
where="Tanker — same locker as Kissing Booth", window="Tanker, right after Kissing Booth",
guide="""
<p>Get caught by Otacon while "stimulating yourself".</p>
<ol class="tight">
<li>After <b>Kissing Booth</b>, get out of the locker and press the first-person button to stare at the poster again you must be <b>right up against it</b>.</li>
<li>While still in first-person staring at the poster, open the <b>codec</b> and call <b>Otacon</b>.</li>
<li>The achievement pops on the call.</li>
</ol>"""),
"ach_002_024": dict(cat="misc", miss=True,
where="Tanker — Hold No. 2 (projector screens)", window="Tanker Holds — one-shot area",
guide="""
<p>Photograph the ghost image in Hold No. 2.</p>
<ol class="tight">
<li>Late in the Tanker you sneak through the marine holds. In <b>Hold No. 2</b> there are two projector screens the marines flip between.</li>
<li>Use the <b>camera Otacon gave you</b> (not the Digital Camera special item) and photograph the <b>right-hand screen</b>.</li>
<li>If the ghost image is not up, interact with the <b>projector</b> to switch to it.</li>
</ol>
<div class="warnbox">The Holds are a one-way trip grab this and <b>Sexting</b> before you leave.</div>"""),
"ach_002_025": dict(cat="misc", miss=True,
where="Tanker — Hold No. 3 (computer, bottom-right)", window="Tanker Holds — before photographing RAY",
guide="""
<p>Send Otacon a picture of the pantsless marine.</p>
<ol class="tight">
<li>In the holds, find the marine in the crowd with <b>no trousers</b> and photograph him with <b>Otacon's camera</b> — the Digital Camera special item does <b>not</b> work.</li>
<li>Continue to <b>Hold No. 3</b> (where RAY is) and use the <b>computer in the bottom-right corner</b> to send the photo.</li>
</ol>
<div class="warnbox">Send it <b>before</b> you take the RAY photographs, or you will have to replay the Tanker.</div>"""),
"ach_002_026": dict(cat="misc", miss=True,
where="Plant — Strut A (lie near the sea lice)", window="Start of the Plant chapter",
guide="""
<p>Get sea lice all over your rations.</p>
<ol class="tight">
<li>At the very start of the Plant, pick up a <b>ration</b>.</li>
<li>Lie down on the ground near the little bugs (Strut A area) and wait a louse crawls onto your rations in your inventory.</li>
</ol>
<p class="muted">To get rid of it afterwards, hold your inventory open and cycle the ration up and down quickly.</p>"""),
"ach_002_027": dict(cat="misc", miss=True,
where="Plant — Shell 1 Core (Ames) and the oil fence", window="Two separate story moments",
guide="""
<p>Hear Johnny's bowel noises in <b>two</b> locations, both in the Plant chapter, using the <b>Directional Mic</b>.</p>
<ol class="tight">
<li><b>Location 1 Ames' hostage room.</b> During the scene Ames tells you to mic Ocelot and Solidus. Aim the Directional Mic <b>slightly left</b> of that conversation until you pick up Johnny talking to himself in the bathroom. Keep it on him until he finishes.</li>
<li><b>Location 2 the oil fence.</b> While covering Emma, when she ducks behind the <b>second platform</b>, aim the mic at that platform. You will hear Emma bump into Johnny. Keep the mic on them until he leaves.</li>
</ol>
<div class="warnbox">Community reports say the two listens must happen <b>in the same game session</b> do not quit to the menu between them. Some players needed to load a save made just before the sniper section and re-listen.</div>"""),
"ach_002_028": dict(cat="misc", miss=True,
where="End of the game — rooftop epilogue cutscene", window="Final cutscene — DO NOT SKIP",
guide="""
<p>Catch a glimpse of Vamp standing in the streets of New York during the end cinematic.</p>
<ol class="tight">
<li><b>Do not skip the final cutscene.</b> This is the single most commonly missed achievement in the game.</li>
<li>When Snake asks Raiden <i>"By the way, what is that?"</i> and Raiden answers <i>"No, never heard the name before"</i>, immediately hold the <b>zoom button</b> (R2/L2 on PS, RT on Xbox) and look around with the stick.</li>
<li>Vamp is standing in the background. Zoom in on him and the achievement pops.</li>
</ol>"""),
"ach_002_029": dict(cat="misc",
where="Main Menu → Special → Casting Theater (after beating the game)", window="After completing the game",
guide="""
<p>Watch Rose kill Raiden on a rooftop using the Casting Theater.</p>
<ol class="tight">
<li>Beat the game once to unlock Casting Theater.</li>
<li>Go to <b>Main Menu Special Casting Theater</b>.</li>
<li>Select <b>Demo 8</b>.</li>
<li>Replace <b>Raiden with Rose</b> and <b>Solidus with Raiden</b>.</li>
<li>Watch the scene this pops <b>Love Hurts</b> and <b>Casting Theater</b> together.</li>
</ol>"""),
# --------------------------------------------------------- CUMULATIVE
"ach_002_030": dict(cat="misc", cum=True,
where="Anywhere — hold up enemies", window="Cumulative, carries across runs",
guide="""
<p>Hold up <b>30 enemies</b> (cumulative).</p>
<p>This comes for free while hunting dog tags you will hold up well over 30 guards across the Tanker and Plant tag runs.
The counter persists across playthroughs, so it is effectively impossible to miss.</p>"""),
"ach_002_031": dict(cat="misc", cum=True,
where="Anywhere — M9 / PSG1-T", window="Cumulative, carries across runs",
guide="""
<p>Tranquilise <b>100 enemies</b> (cumulative).</p>
<ul class="tight">
<li>Use the <b>M9</b> or <b>PSG1-T</b>. A tranquilised enemy shows <b>"Z"s</b> floating above them.</li>
<li>You should finish this naturally during the dog-tag runs always tranquillise after taking a tag.</li>
<li>Bodies wake up eventually; tranq the same guard again after he gets up if you need volume.</li>
</ul>"""),
"ach_002_032": dict(cat="misc", cum=True,
where="Anywhere — grab from behind, tap the grab button", window="Cumulative, carries across runs",
guide="""
<p>Break the neck of <b>30 enemies</b> (cumulative).</p>
<ol class="tight">
<li>Get behind an enemy and grab them.</li>
<li>Keep tapping the grab button until the neck snaps.</li>
</ol>
<div class="warnbox">Neck breaks are <b>kills</b> they void <b>Pacifist Run</b> and <b>Big Boss</b>. Farm this on a normal tag run.</div>"""),
"ach_002_033": dict(cat="misc", cum=True,
where="Anywhere — melee combos / groin punch / HF Blade stun side", window="Cumulative, carries across runs",
guide="""
<p>Knock <b>30 enemies unconscious</b> (cumulative). An unconscious enemy has <b>stars</b> above their head.</p>
<ul class="tight">
<li>Land repeated melee hits until they pass out.</li>
<li>Fastest method: hold a guard up, aim at his <b>groin</b> and punch a clean hit knocks him out in one blow.</li>
<li>Late in the game you get the <b>HF Blade</b>. Open the inventory on it and press the swap button to flip it to the <b>non-lethal side</b> (the icon turns <b>blue</b>) that side knocks enemies out instead of killing.</li>
</ul>"""),
"ach_002_034": dict(cat="misc", cum=True,
where="Tanker — Engine Room · Plant — Strut D or FA Connecting Bridge", window="Cumulative, carries across runs",
guide="""
<p>Attain <b>grip level 3</b> (the maximum). You need <b>two</b> grip levels, and each level is
<b>100 pull-ups</b> <i>or</i> <b>10 drop-and-catches</b> so the full job is <b>200 pull-ups</b> or <b>20 catches</b>.</p>
<h3>Where to do it</h3>
<ul class="tight">
<li><b>Tanker Engine Room</b> (the guide-recommended spot): two railings sit stacked directly one above the other.
Hang on the upper one, drop, and immediately mash the grab button to catch the lower one.</li>
<li><b>Plant Strut D, Sediment Pool:</b> hang off the railing, drop to the level below, and the stairs back up are right
beside you, so the loop is quick.</li>
<li><b>Plant FA Connecting Bridge:</b> also has a railing directly below the one you hang from.</li>
</ul>
<h3>The two methods</h3>
<ul class="tight">
<li><b>Drop and catch (fast):</b> flip over the railing, press <b>drop</b>, then mash the <b>grab/hang</b> button to catch the
railing below. One catch = <b>10 pull-ups</b>, so <b>20 catches</b> finishes it. If you miss, you fall and die.</li>
<li><b>Pull-ups (safe):</b> hang from any railing and hold <b>both shoulder buttons</b> to do a pull-up; repeat 200 times,
climbing back up when your grip meter runs low. Slower, but you cannot fall.</li>
</ul>
<div class="warnbox"><b>Progress resets if you die</b> without leaving the room, so step out and back in every few drops to bank
it. In the <b>Tanker Engine Room</b> specifically, take the two one-shot guard tags (<i>Jeff A Saylor Jr</i>,
<i>Kazuki Tokunaga</i>) <b>first</b> they despawn the moment you leave the room.</div>
<div class="okbox">Do this in the Tanker so it is finished <b>before the Holds</b> the hidden marine tags in Hold No. 1 and
No. 3 need long ledge shimmies that drain grip and health, and grip level 3 makes that far safer.</div>
<div class="warnbox"><b>Never</b> grind this on a Big Boss run: one missed catch is a death, and that voids the codename.</div>"""),
# --------------------------------------------------------- COLLECTIBLE
"ach_002_035": dict(cat="coll", miss=True,
where="Plant — Strut E conveyor (Box 5) or end of game", window="Plant chapter",
guide="""
<p>Acquire the digital camera. Three routes:</p>
<ul class="tight">
<li><b>Beat the game</b> you are given it automatically. Simplest.</li>
<li><b>Box 5 conveyor ride:</b> get Cardboard Box 5 in Strut E, equip it and stand on the conveyor belt. The belt stops, then carries you down into a room containing the camera.</li>
<li><b>After escorting Emma:</b> head for Snake, climb the ladder into Strut E, and immediately look for the <b>Level 5 door</b> at the top of the small area before the conveyor section. The camera is inside.</li>
</ul>"""),
"ach_002_036": dict(cat="coll", miss=True,
where="Tanker — 6 weapon types", window="Tanker chapter",
guide="""
<p>Collect all <b>6</b> weapon types on the Tanker:</p>
<table>
<tr><th>Weapon</th><th>How / where</th></tr>
<tr><td class="n">M9</td><td>Default starting weapon.</td></tr>
<tr><td class="n">USP</td><td>Automatic given to you after beating Olga.</td></tr>
<tr><td class="n">Chaff Grenades</td><td>Deck-C, crew's quarters — in the locker left of the stairs after the first camera.</td></tr>
<tr><td class="n">Stun Grenades</td><td>Deck-A crew's lounge — at the bottom of the stairs leading to the Engine Room, starboard. You pass them during the story.</td></tr>
<tr><td class="n">Hand Grenades</td><td>Engine Room bottom level, bottom-left corner near the exit at the foot of the stairs.</td></tr>
<tr><td class="n">Magazine</td><td>Automatic you get it the first time you reload a weapon.</td></tr>
</table>
<div class="warnbox">Must be done in a single Tanker playthrough; the game only checks the weapon types you have held at once.</div>"""),
"ach_002_037": dict(cat="coll", miss=True,
where="Plant — every weapon type (~18 items)", window="Plant chapter",
guide="""
<p>Collect every weapon <b>type</b> in the Plant. You only need one of each grenade type. Listed in the order they become available:</p>
<table>
<tr><th>Weapon</th><th>Where</th></tr>
<tr><td class="n">Chaff Grenades</td><td>Strut A Roof · FA connecting bridge · Strut F · Shell 1 Core 1F · Strut L Perimeter · KL connecting bridge</td></tr>
<tr><td class="n">M9</td><td>Strut F top floor, room on the right after entering from the FA top-floor bridge (also near the start on Very Easy).</td></tr>
<tr><td class="n">Magazine</td><td>Automatic when you reload a weapon.</td></tr>
<tr><td class="n">Coolant</td><td>Given by Stillman during the story.</td></tr>
<tr><td class="n">Book</td><td>Strut F hang over the top-floor railing and drop onto the crates. Also Shell 1 Core 1F south lockers, and Emma's locker in Shell 2 Core B1 Filtration Chamber 2.</td></tr>
<tr><td class="n">Stun Grenades</td><td>Strut F bottom floor · Strut E · DE connecting bridge · Shell 1 Core B1 lockers.</td></tr>
<tr><td class="n">Claymore Mines</td><td>EF connecting bridge (crawl across to collect) · Strut E Heliport · Strut A Roof after the Fortune fight · Shell 1 Core 1F south lockers.</td></tr>
<tr><td class="n">AKS-74u</td><td>Strut F bottom floor, after the Fatman fight.</td></tr>
<tr><td class="n">M4</td><td>Strut F top floor, middle-left room, after the Fatman fight.</td></tr>
<tr><td class="n">C-4</td><td>Strut F top floor top-right room (locker) · Shell 1 Core 1F south lockers · Emma's locker in Shell 2 Core B1.</td></tr>
<tr><td class="n">Directional Mic</td><td>Shell 1 Core B2 Computer Room story-required item.</td></tr>
<tr><td class="n">RGB6</td><td>Strut F, after meeting Ames top floor, <b>top-left</b> room.</td></tr>
<tr><td class="n">PSG-1</td><td>Strut F, after meeting Ames top floor, <b>middle-right</b> room.</td></tr>
<tr><td class="n">PSG-1T</td><td>Same room as the PSG-1 crawl into the vent.</td></tr>
<tr><td class="n">Grenades</td><td>Strut F, after meeting Ames top floor, <b>bottom-left</b> room.</td></tr>
<tr><td class="n">Stinger</td><td>Automatic during the Harrier fight.</td></tr>
<tr><td class="n">Nikita</td><td>Shell 2 Core B1 story-required; swim underwater to find it.</td></tr>
<tr><td class="n">HF Blade</td><td>Automatic given by Snake after you escape the torture room.</td></tr>
</table>
<div class="infobox">Most Strut F weapon rooms only open up <b>after the Fatman fight</b> and again <b>after meeting Ames</b> do a full sweep of Strut F at each of those points.</div>"""),
"ach_002_038": dict(cat="coll", miss=True,
where="Tanker (2 boxes) + Plant (5 boxes)", window="Both chapters",
guide="""
<p>Collect all <b>7</b> cardboard boxes.</p>
<h3>Tanker</h3>
<ul class="tight">
<li><b>Box 1 </b> Deck-D, crew's quarters, inside the food storage room (right of the stairs you come up).</li>
<li><b>Wet Box </b> after the Olga fight, on the Navigational Deck, Wing. As you turn the corner a guard comes out of a door; the box is up by the spotlight near where he goes.</li>
</ul>
<h3>Plant</h3>
<ul class="tight">
<li><b>Box 1 Strut A:</b> after getting the Lv. 1 Card, take the small stairs in the bottom-left of the pipe room, crawl under the pipes on your first right and climb over the box below. Needs Lv. 1 Card.</li>
<li><b>Box 2 Strut F:</b> bottom floor, in the bottom-left room with the Radar Node. Needs Lv. 1 Card.</li>
<li><b>Box 5 Strut E:</b> on the conveyor belt near the Radar Node.</li>
<li><b>Box 3 Strut E Rooftop Helipad:</b> bottom-left corner of the rooftop.</li>
<li><b>Box 4 Shell 1 Core B2:</b> across from the Radar Node, near the Directional Mic and the parrot.</li>
</ul>"""),
"ach_002_044": dict(cat="coll", miss=True,
where="Tanker — 50 tags", window="End of a completed chapter",
guide="""
<p>Get the <b>Bandana</b> (infinite ammo) requires <b>50 Tanker dog tags</b>.</p>
<p>All Very Easy (24) + all Easy (26) = exactly 50. Finish the chapter and save a <b>completed</b> file so the item carries over.</p>"""),
"ach_002_045": dict(cat="coll", miss=True,
where="Tanker — 76 tags", window="End of a completed chapter",
guide="""
<p>Get the <b>Tanker stealth suit</b> requires <b>76 Tanker dog tags</b>.</p>
<p>Very Easy (24) + Easy (26) + Normal (33) = 83, so 76 arrives partway through the Normal tag sweep. Finish the chapter and save a completed file.</p>"""),
"ach_002_046": dict(cat="coll", miss=True,
where="Plant — 80 tags", window="End of a completed chapter",
guide="""
<p>Get the <b>Brown Wig</b> (infinite ammo) requires <b>80 Plant dog tags</b>.</p>
<p>Very Easy (43) + Easy (44) = 87, so this lands during the Easy sweep. Finish the chapter and save a completed file.</p>"""),
"ach_002_047": dict(cat="coll", miss=True,
where="Plant — 120 tags", window="End of a completed chapter",
guide="""
<p>Get the <b>Plant stealth suit</b> requires <b>120 Plant dog tags</b>.</p>
<p>Very Easy (43) + Easy (44) + Normal (49) = 136, so 120 arrives partway through the Normal sweep. This is the item that makes Pacifist Run and Complete Stealth trivial, so do it before those runs.</p>"""),
# --------------------------------------------------------- CHALLENGE
"ach_002_048": dict(cat="chal", miss=True,
where="Any completed playthrough", window="Whole run",
guide="""
<p>Clear the game without killing anyone.</p>
<ul class="tight">
<li>Only use <b>non-lethal</b> weapons M9, PSG1-T, stun grenades, the HF Blade's <b>blue</b> side.</li>
<li>This includes bosses that have a <b>stamina bar</b>: Olga, Fatman, Vamp, Harrier, Solidus. Damage their stamina, not their health.</li>
<li>For <b>Solidus</b>, open the inventory on the HF Blade and swap it to the <b>non-lethal side</b>.</li>
</ul>
<div class="infobox"><b>Easiest route:</b> do this on <b>Very Easy with the Bandana equipped</b> (infinite tranq ammo) after you have unlocked it. Community reports also confirm you can earn it with a <b>Tanker-only</b> run if you started one.</div>
<div class="warnbox">Do not press the neck-break prompt, do not shoot explosive barrels into enemies, and do not let the steam pipe kill anyone on this file.</div>"""),
"ach_002_049": dict(cat="chal", miss=True,
where="Any completed playthrough", window="Whole run",
guide="""
<p>Clear the game without entering <b>Alert</b> mode (mandatory alert events excluded).</p>
<ul class="tight">
<li>You are fine while the HUD says <b>CAUTION</b>. You fail the moment it turns red <b>ALERT</b>.</li>
<li>Being spotted is OK <i>if</i> you stop the guard before he finishes his radio call you have a short window.</li>
<li><b>Easiest method:</b> equip the <b>Stealth Camo</b> and walk through the whole game invisible. Enemies and cameras cannot see you.</li>
</ul>
<div class="warnbox"><b>The one exception:</b> the <b>Shell 1 Core elevator</b> requires you to wear the disguise and equip the AK-47. The camera will spot you even with stealth equipped if you try it any other way do it properly and you stay safe.</div>
<p>The following forced alert sequences <b>do not</b> void the achievement:</p>
<ul class="tight">
<li>Boss fights</li>
<li>The soldiers hallway fight in the Tanker</li>
<li>After speaking with Ames in the hostage room</li>
<li>The Tengu soldier fight alongside Snake</li>
</ul>
<p class="muted">Save often and reload if the HUD goes red. Pair this with Pacifist Run on the same playthrough both are trivial with stealth + bandana on Very Easy.</p>"""),
"ach_002_050": dict(cat="chal", miss=True,
where="Extreme difficulty, Tanker → Plant", window="Whole run",
guide="""
<p>Earn the title of <b>Big Boss</b>. This is the hardest achievement in the game see the dedicated
<b>Big Boss Run</b> tab for full criteria, boss strategies, save points and time checkpoints.</p>
<p>Complete the game on <b>Extreme</b>, TankerPlant, with:</p>
<ul class="tight">
<li>Time under <b>3:00:00</b></li>
<li><b>0</b> continues, <b>0</b> rations used, <b>0</b> kills</li>
<li><b>3 or fewer</b> alerts</li>
<li><b>700 or fewer</b> shots fired</li>
<li>Mech damage under <b>60</b> (cameras, Cyphers, RAYs you must destroy ~20 RAYs, so keep everything else under ~40)</li>
<li>Damage received: <b>10 health bars or less</b></li>
<li><b>No special items</b>, <b>no radar</b></li>
</ul>"""),
# --------------------------------------------------------- MODES / EXTRA
"ach_002_039": dict(cat="mode",
where="Main Menu → Special → Casting Theater", window="After completing the game",
guide="""
<p>Watch anything in the Casting Theater. Pops automatically while earning <b>Love Hurts</b>.</p>
<p>Main Menu Special Casting Theater pick any demo and let it play.</p>"""),
"ach_002_040": dict(cat="mode",
where="Main Menu → Boss Survival", window="Unlocked after beating the game",
guide="""
<p>Complete <b>Boss Survival</b> all bosses from both chapters back to back:</p>
<p class="muted">Olga Tanker ambush soldiers Fatman Harrier Vamp Tengu soldiers Metal Gear RAYs Solidus</p>
<div class="okbox"><b>Pick Raiden on Easy.</b> Raiden has the <b>HF Blade</b> for the Solidus fight; Snake only gets a melee attack, which makes the last fight far harder. You can also change the difficulty of Boss Survival independently.</div>"""),
"ach_002_041": dict(cat="mode",
where="Main Menu → Snake Tales", window="Any time",
guide="""
<p>Complete at least <b>one</b> of the five Snake Tales: <i>A Wrongdoing</i>, <i>Big Shell Evil</i>, <i>Confidential Legacy</i>,
<i>Dead Man</i>, <i>External Gazer</i>.</p>
<p>The Master Collection only requires one (the 2011 HD release required all five). Pick whichever you like <b>A Wrongdoing</b> is the shortest.</p>"""),
"ach_002_042": dict(cat="mode",
where="Main Menu → VR Missions / Alternative Missions", window="Any time",
guide="""
<p>Complete any single VR or Alternative mission. One and done pick the shortest Sneaking mission you can find.</p>"""),
"ach_002_043": dict(cat="mode",
where="Main Menu → VR Missions / Alternative Missions", window="Any time",
guide="""
<p>Place <b>first</b> in 50 different VR/Alternative missions (beat the target time).</p>
<ul class="tight">
<li>There are 500+ missions, so this is only ~10% of them.</li>
<li>Short <b>Sneaking</b> and <b>Weapon</b> missions are the fastest first places.</li>
<li>The Master Collection does <b>not</b> require clearing every mission <i>Virtually Impossible</i> was removed.</li>
</ul>"""),
"ach_002_051": dict(cat="extra",
where="Master Collection launcher → MGS2 Screenplay Book", window="Any time",
guide="""
<p>Read all pages in the <b>Screenplay Book</b>.</p>
<ol class="tight">
<li>From the Master Collection launcher, open the MGS2 <b>Screenplay</b>.</li>
<li>Tab/page all the way to the <b>end</b> the achievement pops when you reach the last page.</li>
</ol>
<div class="okbox">Free achievement do it from the main menu before you even start playing.</div>"""),
}

467
build/build.py Normal file
View file

@ -0,0 +1,467 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Assembles the single-file MGS2 companion from the collected sources."""
import json, os, sys, re, html, shutil
HERE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.dirname(HERE) # repo root, one level above build/
sys.path.insert(0, HERE)
from achievements_src import ACH
from roadmap_src import ROADMAP, BIGBOSS
official = json.load(open(os.path.join(HERE, "steam_official.json")))
vc = json.load(open(os.path.join(HERE, "dogtags_vc.json")))
notes = json.load(open(os.path.join(HERE, "dogtag_notes.json")))
maps = json.load(open(os.path.join(HERE, "maps.json")))
# ---------------------------------------------------------------- achievements
ach_list = []
missing_guide = []
for o in official:
api = o["api"]
src = ACH.get(api)
if not src:
missing_guide.append(api)
src = dict(cat="misc", guide="<p>No notes yet.</p>")
entry = {
"id": api,
"api": api,
"name": o["name"],
"desc": o["desc"],
"icon": o["icon"],
"icoLocked": o["iconLocked"],
"cat": src.get("cat", "misc"),
}
if src.get("miss"): entry["miss"] = True
if src.get("cum"): entry["cum"] = True
if src.get("where"): entry["where"] = src["where"]
if src.get("window"): entry["window"] = src["window"]
entry["guide"] = src.get("guide", "<p>—</p>")
ach_list.append(entry)
if missing_guide:
print("!! achievements without hand-written notes:", missing_guide)
# ------------------------------------------------- in-game location screenshots
# Two sources, both hot-linked (their CDNs serve any referrer):
# steam - the Steam Community achievement guide
# dayngl - Dayngl's Guides MGS2 trophy guide (descriptive filenames, close-up shots)
# Button-prompt glyphs, trophy icons, the section divider and box-art are excluded.
guide_imgs = json.load(open(os.path.join(HERE, "steam_imgs_by_ach.json")))
guide_meta = json.load(open(os.path.join(HERE, "ach_icons.json")))
dayngl_imgs = json.load(open(os.path.join(HERE, "dayngl_real.json")))
def norm(s):
return "".join(ch for ch in s.lower() if ch.isalnum())
name_to_api = {norm(o["name"]): o["api"] for o in official}
TINY = {
"03A5065DF1B5316CC3828CACFE80ED88308EE8D7","30A3C27578B65038D6A638BD24B09419F47519BF",
"3E2512E8BE55F3A14A9C2C96819B2DB46D8B3674","4E29A232BE1405A43384DCE389E660672860E038",
"539C603F19D06579B9DD671E39E313B9E8494525","675A6525F9933AEBBA3596F7288F01A1A36A46BB",
"7B48927D0A89321440B0D9B9B32E46D9D331828B","A810C37F28E4C51A9EE589511B9614070046B9FB",
"B31F84B091C9C019AA9F35EEA761092D81A4B028","BFEE9C0D8F085A3F91285784B09666747771931A",
"D3886DAA8B8B4108625D7AC48FA19663E3823408","E2CDBD01848B7ADBEA4CE42C54199F1385E13FD6",
"FC08EA78D73A06C58AA304C6DAA576D3AC56C483"}
DIVIDER = "DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4"
shots = {}
for gname, info in guide_meta.items():
api = name_to_api.get(norm(gname))
if not api:
continue
keep = []
for u in guide_imgs.get(str(info["num"]), []):
h = u.rstrip("/").split("/")[-1]
if h in TINY or h == DIVIDER or u == info["icon"]:
continue
keep.append({"u": u, "s": "steam"})
if keep:
shots.setdefault(api, []).extend(keep)
unmatched = set()
for x in dayngl_imgs:
api = name_to_api.get(norm(x["ach"]))
if not api:
unmatched.add(x["ach"]); continue
shots.setdefault(api, []).append({"u": x["url"], "s": "dayngl"})
# (per-step cap below keeps any single step from becoming a photo wall)
## --- photos that are the SAME SHOT as a Steam one, detected by perceptual hash
## (dHash on 16x16 greyscale, Hamming distance <= 14/240). Dayngl's guide reuses the
## same screenshots as the Steam guide, so these showed the same picture twice.
DUP_DAYNGL = {
"Screenshot-metal-gear-solid-2-book-2.png", "Screenshot-metal-gear-solid-2-book-3.png",
"Screenshot-metal-gear-solid-2-book.png", "Screenshot-metal-gear-solid-2-box-3-2.png",
"Screenshot-metal-gear-solid-2-box-3-3.png", "Screenshot-metal-gear-solid-2-box-3.png",
"Screenshot-metal-gear-solid-2-camera.png", "Screenshot-metal-gear-solid-2-ghost.png",
"Screenshot-metal-gear-solid-2-locker-poster.png", "Screenshot-metal-gear-solid-2-orange-box-2.png",
"Screenshot-metal-gear-solid-2-orange-box-4.png", "Screenshot-metal-gear-solid-2-snake-beater-2.png",
"Screenshot-mgs2-sea-louse-2.png", "Screenshot-mgs2-sea-louse.png",
"Snapshot_192.png", "Snapshot_194.png", "Snapshot_195.png", "Snapshot_196.png", "Snapshot_197.png",
"Snapshot_203.png", "Snapshot_204.png", "Snapshot_208.png",
}
# --- achievements whose pictures span SEVERAL locations: each photo is pinned to one step.
# Order taken from the sequence the guides present them in (text, then its screenshot).
PHOTO_STEP = {
# Moving Day - the seven boxes live all over both chapters; each shot shows one box
"5B7C3B4E8BF3379814E8C185D356BED511BFE81D": "r1-2", # Box 1, Tanker Deck-D
"orange-box.png": "r1-2",
"1A140671AAFEBEF9A706AACA000D1756E09B5225": "r1-9", # Wet Box, Nav Deck
"0155C94B62BD63D9892146E0255F7F685202E9E5": "r1-9",
"orange-box-3.png": "r1-9",
"420301D5428D7150DAD254BB90EFF9637DBDAA07": "r2-2", # Box 1, Strut A
"strut-a-box-2": "r2-2",
"7D577E665B40A901FCCA3326640653EB5C4C6E65": "r2-8", # Box 2, Strut F
"strut-a-box-3": "r2-8",
"2E0EB1DD4C769654138A04B9DE6DC58804D1B735": "r2-9", # Box 5, Strut E conveyor
"D5931E8F90958DE3CA75693F016767AAC02C9E34": "r2-9", # Box 3, Strut E rooftop
"box-4.png": "r2-9",
"ACA684EB5386F44862EF3A34DA0056C7D8ED509B": "r2-10", # Box 4, Shell 1 Core B2
"box-4-2.png": "r2-10",
# Photo Finish - the conveyor method (Strut E) vs the Level 5 door pickup
"A0B74949CFCC8D0058DD0FF5ED6E362DBC65D94B": "r2-9",
"F446514EA80167FC12D5816BA2D7854D6628E4F5": "r2-9",
"ACC84EFADCDAE266D93B6B6FD5CEF82A8B66E22E": "r2-9",
"225FFD74A14296925890B40193A5578B1299DFB6": "r2-15", # Level 5 door
# Johnny on the Spot - mic location 1 (Ames) vs location 2 (oil fence)
"F99FE3B53E49D8AD5837E7842ACE632A2F0051D0": "r2-11",
"Snapshot_205.png": "r2-11",
"06754D9C8F7CF65B9D0C74F17FBC54E6FA8071CE": "r2-15",
"Snapshot_206.png": "r2-15",
}
SPLIT_APIS = {"ach_002_027", "ach_002_035", "ach_002_038"} # Johnny / Photo Finish / Moving Day
# strip the perceptual duplicates
before = sum(len(v) for v in shots.values())
for api in shots:
shots[api] = [x for x in shots[api] if x["u"].split("/")[-1] not in DUP_DAYNGL]
after = sum(len(v) for v in shots.values())
print("dropped %d duplicate photos (%d -> %d)" % (before - after, before, after))
for e in ach_list:
if e["api"] in shots:
e["shots"] = shots[e["api"]]
n_steam = sum(1 for v in shots.values() for x in v if x["s"] == "steam")
n_dayngl = sum(1 for v in shots.values() for x in v if x["s"] == "dayngl")
print("with screenshots: %d achievements (%d images: %d steam + %d dayngl)"
% (len(shots), n_steam + n_dayngl, n_steam, n_dayngl))
if unmatched:
print("!! dayngl images with no matching achievement:", sorted(unmatched))
# ------------------------------------------------- road-map colour coding
# The walkthrough is written by hand with <b> for "the thing you interact with" and
# <i> for guard names. Here we classify each of those and attach a semantic class so
# dog tags, items, achievements (missable especially) and bosses are visually distinct.
TAGNAMES = set()
for blk in vc:
for t in blk["tags"]:
TAGNAMES.add(norm(t["name"]))
ACH_MISS = {norm(e["name"]) for e in ach_list if e.get("miss")}
ACH_ANY = {norm(e["name"]) for e in ach_list}
BOSSES = ["olga gurlukovich","olga","fortune","fatman","harrier","vamp","solidus snake",
"solidus","metal gear ray","metal gear rays","rays","tengu","tengu soldiers"]
BOSS_NORM = {norm(b) for b in BOSSES}
ITEMS = {norm(x) for x in [
"stun grenades","chaff grenades","hand grenades","grenades","claymore mines","c4","m9","socom",
"psg-1","psg1","psg1t","rgb6","aks-74u","aks74u","m4","nikita","stinger","hf blade","coolant",
"directional mic","book","magazine","digital camera","camera","cardboard box","box","box 1","box 2",
"box 3","box 4","box 5","wet box","lv. 1 card","lv. 5 card","lv. 5 door","level 5 door","ration",
"rations","bandana","stealth camo","brown wig","orange wig","blue wig","sensor a","disguise",
"ak-47","ak","body armor","thermal goggles","mine detector","scope","night vision","cigarettes",
"radar","radar node","nikita","psg-1t","coolant spray","sensor","card","tag",
]}
AREAS = {norm(x) for x in [
"aft deck","deck-a","deck-b","deck-c","deck-d","deck-e","deck 2","deck-2","hold no. 1","hold no. 2",
"hold no. 3","holds","strut a","strut b","strut c","strut d","strut e","strut f","strut l",
"shell 1 core","shell 2 core","shell 1 core 1f","shell 1 core b1","shell 1 core b2",
"shell 2 core 1f","shell 2 core b1","arsenal gear","ascending colon","engine room",
"navigational deck","oil fence","big shell","tanker","plant","heliport","sediment pool",
"pump room","transformer room","dining hall","parcel room","warehouse","computer room",
"deep sea dock","connecting bridge","sewage treatment facility","air purification room","rooftop",
]}
def core_of(txt):
t = re.sub(r"\s*\(.*?\)\s*$", "", txt) # trailing "(Moving Day)"
t = re.sub(r"#\s*\d+\s*\.?$", "", t) # trailing "#1"
t = re.sub(r"[\s:.!]+$", "", t)
return t.strip()
def _ach_class(n):
if n in ACH_MISS: return "k-miss"
if n in ACH_ANY: return "k-ach"
if len(n) >= 12: # "Cover-to-Cover Solid 2" -> full title
for full in ACH_ANY:
if full.startswith(n):
return "k-miss" if full in ACH_MISS else "k-ach"
return None
def _item_class(n):
if n in ITEMS: return "k-item"
if len(n) >= 5 and any(i.startswith(n) for i in ITEMS if len(i) >= 5):
return "k-item" # "Bandana 50", "Brown Wig 80"
return None
def colourise_b(txt):
core = core_of(txt)
# compound labels like "disguise + AK-47" or "Bandana / Brown Wig"
if re.search(r"[+/]", core):
parts = [core_of(p) for p in re.split(r"[+/]", core)]
parts = [norm(p) for p in parts if p.strip()]
if parts and all(_item_class(p) == "k-item" for p in parts):
return "k-item"
n = norm(core)
for fn in (_ach_class, _item_class):
r = fn(n)
if r: return r
if n in BOSS_NORM or any(n.startswith(b) for b in BOSS_NORM if len(b) >= 6):
return "k-boss" # "Olga's body", "Fatman's body"
if n in AREAS:
return "k-area"
return None
def colourise_i(txt):
parts = [norm(p) for p in re.split(r"[,/]", txt)]
parts = [p for p in parts if p]
if not parts:
return None
hits = sum(1 for p in parts if p in TAGNAMES)
return "k-tag" if hits >= max(1, len(parts) * 0.6) else None
def colourise(html):
def rep_b(m):
cls = colourise_b(m.group(1))
return '<b class="%s">%s</b>' % (cls, m.group(1)) if cls else m.group(0)
def rep_i(m):
cls = colourise_i(m.group(1))
return '<i class="%s">%s</i>' % (cls, m.group(1)) if cls else m.group(0)
html = re.sub(r"<b>(.*?)</b>", rep_b, html, flags=re.S)
html = re.sub(r"<i>(.*?)</i>", rep_i, html, flags=re.S)
return html
n_col = 0
for ph in ROADMAP:
for st in ph["steps"]:
before = st["body"]
st["body"] = colourise(before)
n_col += len(re.findall(r'class="k-', st["body"]))
for bx in BIGBOSS["bosses"]:
bx["body"] = colourise(bx["body"])
print("colour-coded spans in roadmap: %d" % n_col)
# ------------------------------------------------- step sub-checklists
# Which dog-tag areas belong to which walkthrough step. Every area must appear
# exactly once or the build complains, so nothing can silently go missing.
TAGMAP = {
"r1-1": ["Aft deck"],
"r1-2": ["Deck-D, crew's quarters"],
"r1-3": ["Deck-A, crew's quarters"],
"r1-4": ["Deck-A, crew's lounge"],
"r1-5": ["Deck-B, crew's quarters"],
"r1-6": ["Deck-C, crew's quarters"],
"r1-7": ["Deck-2, port"],
"r1-8": ["Engine room"],
"r1-9": ["Navigational deck, wing"],
"r1-10": ["Hold No.1", "Hold No.2", "Hold No.3"],
"r2-1": ["Strut A Deep Sea Dock"],
"r2-2": ["Strut A roof", "Strut A Pump Room"],
"r2-3": ["AB connecting bridge", "Strut B Transformer Room"],
"r2-4": ["BC connecting bridge", "Strut C Dining Hall"],
"r2-5": ["CD connecting bridge", "Strut D Sediment Pool"],
"r2-6": ["DE connecting bridge", "Strut E Parcel Room"],
"r2-7": ["Strut E heliport"],
"r2-8": ["Strut F warehouse", "FA connecting bridge"],
"r2-9": ["Shell 1 Core, 1F"],
"r2-10": ["Shell 1 Core, B2 Computer Room"],
"r2-11": ["Shell 1 Core, B1"],
"r2-12": ["Strut L Sewage Treatment Facility"],
"r2-14": ["Shell 2 Core, 1F Air Purification Room"],
"r2-15": ["KL connecting bridge"],
"r2-16": ["Arsenal Gear-Ascending Colon"],
}
# sanity: every tag area is claimed exactly once, and no step claims a bogus area
all_areas = {}
for blk in vc:
for t in blk["tags"]:
all_areas.setdefault((blk["chapter"], t["area"]), 0)
all_areas[(blk["chapter"], t["area"])] += 0
claimed = {}
for sid, areas in TAGMAP.items():
for a in areas:
claimed[a] = claimed.get(a, 0) + 1
known = {a for (_, a) in all_areas}
missing = sorted(known - set(claimed))
dupes = sorted(a for a, n in claimed.items() if n > 1)
bogus = sorted(set(claimed) - known)
assert not missing, "tag areas not mapped to any step: %s" % missing
assert not dupes, "tag areas mapped to more than one step: %s" % dupes
assert not bogus, "TAGMAP references unknown areas: %s" % bogus
print("tag areas mapped: %d (all covered, no duplicates)" % len(known))
api_by_norm = {norm(e["name"]): e["api"] for e in ach_list}
canon = {e["api"]: e["name"] for e in ach_list}
n_sub = 0
for ph in ROADMAP:
for st in ph["steps"]:
ach, items, seen = [], [], set()
for cls, txt in re.findall(r'<b class="k-(ach|miss|item)">(.*?)</b>', st["body"], re.S):
label = re.sub(r"\s+", " ", txt).strip()
if cls in ("ach", "miss"):
api = api_by_norm.get(norm(core_of(label)))
if api and api not in seen:
seen.add(api); ach.append(api)
else:
if label not in seen:
seen.add(label); items.append(label)
st["gets"] = {"ach": ach, "item": items}
if st["id"] in TAGMAP:
st["areas"] = TAGMAP[st["id"]]
n_sub += len(ach) + len(items)
print("sub-items extracted: %d achievements + %d item labels"
% (sum(len(ph2["gets"]["ach"]) for ph2 in
[s for ph in ROADMAP for s in ph["steps"]]),
n_sub - sum(len(ph2["gets"]["ach"]) for ph2 in
[s for ph in ROADMAP for s in ph["steps"]])))
needs_split = [s["id"] for ph in ROADMAP for s in ph["steps"]
if "areas" not in s and s["gets"]["ach"] == [] and s["gets"]["item"] == []]
if needs_split:
print("steps with no sub-items at all:", ", ".join(needs_split))
# every pin must match at least one real photo, and each photo may match only one pin
_all_photo_urls = [x["u"] for v in shots.values() for x in v]
for k in PHOTO_STEP:
hits = [u for u in _all_photo_urls if k in u]
assert hits, "PHOTO_STEP key matches no photo: %s" % k
def photo_step(url):
best = None
for k, sid in PHOTO_STEP.items():
if k in url and (best is None or len(k) > len(best[0])):
best = (k, sid)
return best[1] if best else None
# every photo of a split achievement must be pinned, or it would land in the wrong place
for api in SPLIT_APIS:
for x in shots.get(api, []):
assert photo_step(x["u"]), "unpinned photo for %s: %s" % (api, x["u"])
# any achievement referenced by more than one step must be split explicitly
refs = {}
for ph in ROADMAP:
for st in ph["steps"]:
for api in st["gets"]["ach"]:
refs.setdefault(api, set()).add(st["id"])
unsplit = sorted(a for a, s in refs.items() if len(s) > 1 and shots.get(a) and a not in SPLIT_APIS)
assert not unsplit, "achievement photos span several steps but are not pinned: %s" % unsplit
# attach the photos to the steps, not the achievements
placed = set()
for ph in ROADMAP:
for st in ph["steps"]:
out, seen = [], set()
# (a) everything belonging to an achievement this step talks about
for api in st["gets"]["ach"]:
if api in SPLIT_APIS:
continue # these are pinned per-photo below
for x in shots.get(api, []):
if x["u"] in seen: continue
seen.add(x["u"]); out.append({"u": x["u"], "s": x["s"], "cap": canon[api]})
# (b) photos pinned to this step by PHOTO_STEP, even if the step never names the achievement
for api in SPLIT_APIS:
for x in shots.get(api, []):
if photo_step(x["u"]) == st["id"] and x["u"] not in seen:
seen.add(x["u"]); out.append({"u": x["u"], "s": x["s"], "cap": canon[api]})
st["photos"] = out[:8]
for x in st["photos"]: placed.add(x["u"])
if len(out) > 8:
print(" note: %s had %d photos, capped to 8" % (st["id"], len(out)))
all_used = {x["u"] for v in shots.values() for x in v}
left = all_used - placed
print("photos placed in steps: %d of %d" % (len(placed), len(all_used)))
if left:
print(" !! unplaced photos:")
for u in sorted(left): print(" ", u)
# ---------------------------------------------------------------- dog tags
BLURB = {
("Tanker","Very Easy"): "Your first pass. Guards are relaxed and there are only 24 tags — a gentle introduction to hold-ups.",
("Tanker","Easy"): "Same route, two more tags. The last two Tanker lists are what push you past the 50 and 76 thresholds.",
("Tanker","Normal"): "33 tags. Reaching 76 total Tanker tags happens partway through this list — that is the Tanker Stealth unlock.",
("Tanker","Hard"): "35 tags. Not needed for any achievement — only chase these for the Orange/Blue wigs on the Plant side or for completion's sake.",
("Tanker","Extreme"): "34 tags, and the guards hit far harder. Purely optional for achievement purposes.",
("Plant","Very Easy"): "43 tags. Watch for the conditional spawns — several guards only appear after the Fatman fight or after the Emma escort.",
("Plant","Easy"): "44 tags. This list alone pushes the Plant total past 80 for the Brown Wig.",
("Plant","Normal"): "49 tags. Crossing 120 Plant tags happens partway through here — that is the Plant Stealth unlock and the end of the required tag grind.",
("Plant","Hard"): "52 tags. Optional — needed only for the 160/200 wig thresholds.",
("Plant","Extreme"): "54 tags, including Hideo Kojima himself. Optional for achievements.",
}
tags_out = []
for i, blk in enumerate(vc):
key = (blk["chapter"], blk["difficulty"])
n = notes.get("|".join(key), {})
tags = []
for t in blk["tags"]:
e = {"name": t["name"], "area": t["area"]}
if n.get(t["name"]):
e["note"] = n[t["name"]]
tags.append(e)
tags_out.append({
"id": f"{blk['chapter'][:1].lower()}{i}",
"chapter": blk["chapter"],
"difficulty": blk["difficulty"],
"blurb": BLURB.get(key, ""),
"tags": tags,
})
TOTALS = {
"tanker": sum(len(l["tags"]) for l in tags_out if l["chapter"] == "Tanker"),
"plant": sum(len(l["tags"]) for l in tags_out if l["chapter"] == "Plant"),
"grand": sum(len(l["tags"]) for l in tags_out),
}
# ---------------------------------------------------------------- inject
def js(o):
s = json.dumps(o, ensure_ascii=False, separators=(",", ":"))
return s.replace("</", "<\\/")
tpl = open(os.path.join(HERE, "template.html"), encoding="utf-8").read()
subs = {
"/*__ACHIEVEMENTS__*/[]": js(ach_list),
"/*__ROADMAP__*/[]": js(ROADMAP),
"/*__DOGTAGS__*/[]": js(tags_out),
"/*__MAPS__*/[]": js(maps),
"/*__TOTALS__*/{}": js(TOTALS),
"/*__BIGBOSS__*/{}": js(BIGBOSS),
}
for k, v in subs.items():
if k not in tpl:
raise SystemExit("placeholder missing from template: " + k)
tpl = tpl.replace(k, v)
os.makedirs(OUT, exist_ok=True)
with open(os.path.join(OUT, "index.html"), "w", encoding="utf-8") as f:
f.write(tpl)
sync_src = os.path.join(HERE, "steam-sync.py")
if os.path.exists(sync_src):
shutil.copy2(sync_src, os.path.join(OUT, "steam-sync.py"))
for extra in ("steam-proxy-worker.js",):
p = os.path.join(HERE, extra)
if os.path.exists(p):
shutil.copy2(p, os.path.join(OUT, extra))
print("achievements :", len(ach_list))
print("roadmap steps:", sum(len(p["steps"]) for p in ROADMAP), "in", len(ROADMAP), "phases")
print("dog tag lists:", len(tags_out), "tags:", TOTALS)
print("with notes :", sum(1 for l in tags_out for t in l["tags"] if t.get("note")))
print("written :", os.path.join(OUT, "index.html"),
f"{os.path.getsize(os.path.join(OUT, 'index.html'))/1024:.1f} KB")

154
build/dayngl_real.json Normal file
View file

@ -0,0 +1,154 @@
[
{
"ach": "Hurt Locker",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Snapshot_194.png"
},
{
"ach": "Hurt Locker",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Snapshot_195.png"
},
{
"ach": "To Catch a Predator",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Screenshot-metal-gear-solid-2-book-2.png"
},
{
"ach": "To Catch a Predator",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Screenshot-metal-gear-solid-2-book.png"
},
{
"ach": "To Catch a Predator",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Screenshot-metal-gear-solid-2-book-3.png"
},
{
"ach": "Bye Bye Big Brother",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Screenshot-metal-gear-solid-2-camera.png"
},
{
"ach": "Down in Smoke",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Snapshot_193.png"
},
{
"ach": "Down in Smoke",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Snapshot_192.png"
},
{
"ach": "Silence is Golden",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Snapshot_196.png"
},
{
"ach": "Caution: Hot",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Snapshot_197.png"
},
{
"ach": "Caution: Hot",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Screenshot-mgs2-steam-shootout-9.png"
},
{
"ach": "Kissing Booth",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Screenshot-metal-gear-solid-2-locker-poster.png"
},
{
"ach": "Kissing Booth",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Screenshot-metal-gear-solid-2-snake-beater-3.png"
},
{
"ach": "Kissing Booth",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Screenshot-metal-gear-solid-2-snake-beater-2.png"
},
{
"ach": "Who Ya Gonna Call?",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Snapshot_204.png"
},
{
"ach": "Who Ya Gonna Call?",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Snapshot_203.png"
},
{
"ach": "Who Ya Gonna Call?",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Snapshot_202.png"
},
{
"ach": "Sexting",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Screenshot-metal-gear-solid-2-sexting-2.png"
},
{
"ach": "Sexting",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Screenshot-metal-gear-solid-2-ghost.png"
},
{
"ach": "My Rations\u2026",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Screenshot-mgs2-sea-louse-2.png"
},
{
"ach": "My Rations\u2026",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Screenshot-mgs2-sea-louse.png"
},
{
"ach": "Johnny on the Spot",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Snapshot_205.png"
},
{
"ach": "Johnny on the Spot",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Snapshot_206.png"
},
{
"ach": "Vamp Eyer",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Snapshot_208.png"
},
{
"ach": "Steel Grip",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Snapshot_199.png"
},
{
"ach": "Steel Grip",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Snapshot_198.png"
},
{
"ach": "Steel Grip",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Snapshot_200.png"
},
{
"ach": "Photo Finish",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Screenshot-metal-gear-solid-2-box-3.png"
},
{
"ach": "Photo Finish",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Screenshot-metal-gear-solid-2-box-3-2.png"
},
{
"ach": "Photo Finish",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Screenshot-metal-gear-solid-2-box-3-3.png"
},
{
"ach": "Moving Day",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Screenshot-metal-gear-solid-2-orange-box-2.png"
},
{
"ach": "Moving Day",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Screenshot-metal-gear-solid-2-orange-box.png"
},
{
"ach": "Moving Day",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Screenshot-metal-gear-solid-2-orange-box-4.png"
},
{
"ach": "Moving Day",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Screenshot-metal-gear-solid-2-orange-box-3.png"
},
{
"ach": "Moving Day",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Screenshot-metal-gear-solid-2-strut-a-box-2.png"
},
{
"ach": "Moving Day",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Screenshot-metal-gear-solid-2-strut-a-box-3.png"
},
{
"ach": "Moving Day",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Screenshot-metal-gear-solid-2-box-4.png"
},
{
"ach": "Moving Day",
"url": "https://i0.wp.com/daynglsgameguides.com/wp-content/uploads/2023/10/Screenshot-metal-gear-solid-2-box-4-2.png"
}
]

103
build/dogtag_notes.json Normal file
View file

@ -0,0 +1,103 @@
{
"Tanker|Very Easy": {
"Takahiro Yura": "Either Takahiro or Eric spawns, never both at once. Entrance to room seem to dictate which ones appears",
"David P LaFrenier": "moves to b when entering from Engine Room locked door",
"Eric Bravo": "Either Takahiro or Eric spawns, never both at once. Entrance to room seem to dictate which ones appears",
"Hajme Tusima": "Patrols a/b depending on entrance to room",
"Miguel Luis Cunha": "moves to 10b after Olga Boss",
"Eiji Senke": "Arrives when you are in the storeroom and will try to exit area if you leave the storeroom",
"Brendan McLeod": "Appears when approaching starboard side. Will Patrol 4b if Snake climbs Ladder",
"Jeff A Saylor Jr": "Missable - will only appear on initial entry to area, and disappear if Snake leaves the Engine Room",
"Kazuki Tokunaga": "Missable - will only appear in a cutscene when approaching IR Sensors. Will Disappear if Snake leaves the Engine Room"
},
"Tanker|Easy": {
"Matthew W Hunt": "Enter from Deck A",
"Kei Kudou Mon": "Patrols a/b depending on entrance to room",
"Cary J Schwartzman": "Patrols a/b depending on entrance to room",
"Danilo NE Carbone": "appears when in Storeroom and tries to leave when exiting storeroom",
"Michael Vogt": "Appears when approaching starboard side. Will Patrol 4b if Snake climbs Ladder",
"Rock Young": "Man in the centre of the front row",
"Xesk Malone": "Underpants Guy"
},
"Tanker|Normal": {
"Jeng L Valencia": "Enter from Deck A, Crews Quarters after initial visit",
"Daniel Kucan": "Patrols a/b depending on entrance to room",
"James I Bartelle": "Patrols after Olga Fight",
"Flavio Camargos": "Patrols a/b depending on entrance to room",
"James Broome": "appears when in Storeroom and tries to leave when exiting storeroom"
},
"Tanker|Hard": {
"Sauzando Aran": "moves to b when entering from Engine Room locked door",
"Dex J Delfrate": "Patrols a/b depending on entrance to room",
"Johan Hana": "Patrols after Olga Fight",
"Hidetosi Suzuki": "Patrols a/b depending on entrance to room",
"Masahiro Takahashi": "appears when in Storeroom and tries to leave when exiting storeroom",
"Ako Kudou": "Appears when approaching starboard side. Will Patrol 4b if Snake climbs Ladder"
},
"Tanker|Extreme": {
"Bruce Murphy": "Enter from Deck A, Crews Quarters after initial visit",
"Patrick F Coley": "moves to b when entering from Engine Room locked door",
"Adam Christopher Vazquez": "Patrols after Olga Fight",
"Andre Perron": "Patrols a/b depending on entrance to room",
"Malcolm W Bullmore": "Appears when approaching starboard side. Will Patrol 4b if Snake climbs Ladder"
},
"Plant|Very Easy": {
"Brad I Lanning": "Missable",
"Daniel P Wells": "Patrols a/b depending on entry",
"Neill W Perry": "Appears during Emma Escort",
"Charles P Pinkerton": "Missable",
"Kevin M Miller": "After Fatman",
"Derek Broadbent": "moves to [b] after Fatman",
"Artthapong Siriamonthep": "Potentially missable",
"Till Schlegel": "After Fatman",
"Amadeus Knothe": "Potentially missable",
"Csaba Millei": "After Emma Escort",
"Peter Stillman": "Fatman Boss",
"Yiqiang Yao": "After Fatman"
},
"Plant|Easy": {
"Emiko Yokoshima": "Missable",
"Takayuki Ueda": "After Fatman",
"Terry Cheung": "Missable",
"Nicholas S DeGrazia": "moves to b after Fatman",
"Goushi Kawamura": "After Fortune",
"Yuuki Ito": "After Fatman",
"William R Kelly": "Potentially missable",
"Ryosuke Sugie": "Potentially missable",
"Steve Masao Terada": "After Emma Escort",
"Peter Stillman": "Fatman Boss",
"Marius Alain Nguyen": "After Fatman",
"Boudraa Reda": "Patrols a/b depending on entry"
},
"Plant|Normal": {
"Kenji Yamaguchi": "moves to b after Fatman",
"John A McCormick": "After Fatman",
"Gary L Curtis": "After Emma Escort",
"Zheng Xuan": "When approaching area with Emma",
"Johnny Chong": "After Fatman",
"Erasmo O Metos": "When approaching area with Emma",
"Florian Busch": "Enters when approaching south with Emma",
"Markus Huendgen": "a/b depends on entrance used",
"Takuji Tada": "After Fatman",
"Peter Stillman": "Fatman Boss"
},
"Plant|Hard": {
"Michael Mantilla": "moves to b after Fatman",
"Eiichiro Kimura": "After Fatman",
"Hanbum Son": "After Fatman",
"Sverrir Fridriksson": "Enters when approaching south",
"Randy Lee Way": "After Fatman",
"Peter Stillman": "Fatman Boss"
},
"Plant|Extreme": {
"Derek Faria": "After Fatman",
"Wayne A Hindman II": "moves to b after Fatman",
"Zeeshan Raza": "After Fatman",
"Casey K Chan": "After Fatman",
"Christopher R Martinez": "After Emma Escort",
"Yuta Shimizu": "a/b depends on entrance used",
"Clayton R Worrell": "Enters when approaching south",
"Matthew T Fenelon": "After Fatman",
"Peter Stillman": "Fatman Boss"
}
}

1648
build/dogtags_vc.json Normal file

File diff suppressed because it is too large Load diff

1
build/maps.json Normal file

File diff suppressed because one or more lines are too long

411
build/roadmap_src.py Normal file
View file

@ -0,0 +1,411 @@
# -*- coding: utf-8 -*-
"""Ordered story walkthrough for the 'Everything Run' plus the later passes."""
ROADMAP = [
dict(phase="Phase 0 — Before you play", intro="""
<p>Do these first: they are free, and one of them changes how you do everything else.</p>""",
steps=[
dict(id="r0-1", title="Read the Screenplay Book to the end", area="Master Collection launcher → MGS2 Screenplay",
body="""<p>Free achievement (<b>Cover-to-Cover Solid 2</b>). Open the Screenplay from the launcher and page all the way to the last page.</p>"""),
dict(id="r0-2", title="Decide your run order and never break it", area="Planning",
body="""<p>You are going to play the game at least <b>five</b> times. Keep one save slot per run, and never overwrite a
<b>completed</b> file those are what carry the special items forward.</p>
<ul class="tight">
<li><b>Run 1</b> Very Easy, TankerPlant. Every misc/missable achievement + all 67 tags.</li>
<li><b>Run 2</b> Easy. All 70 tags.</li>
<li><b>Run 3</b> Normal. All 82 tags (you only need up to 76 Tanker / 120 Plant, but take them all).</li>
<li><b>Run 4</b> Very Easy with Stealth Camo + Bandana. Pacifist Run + Complete Stealth together.</li>
<li><b>Run 5</b> Extreme, no special items, no radar. Big Boss.</li>
</ul>
<p class="muted">Runs 13 are the same route the walkthrough below covers Run 1 in full and applies to all three.</p>"""),
dict(id="r0-3", title="Turn ON 'Hide done' if you want a clean screen", area="This page's header",
body="""<p>Use the header toggles. Export a backup of your progress before each playthrough so you can roll back a bad tap.</p>"""),
]),
dict(phase="Run 1 · Tanker (Very Easy)", intro="""
<p>Ordered by area. Everything here also applies to the Easy and Normal tag runs the only difference is
<b>which guards have tags</b>. Switch the <b>Dog Tags</b> tab to the matching difficulty as you go.</p>
<div class="infobox">Set the difficulty to <b>Very Easy</b> and start a <b>Tanker-Plant</b> game (not Tanker only)
the special-item achievements expect a completed Tanker-Plant file.</div>""",
steps=[
dict(id="r1-1", title="Aft Deck — arrival", area="Tanker · Aft Deck",
shots=["Silence is Golden"],
body="""<p>Nothing missable. Three guards carry tags here (<i>Josh Spires, Tomomi Nasu, Stephen M Driver</i> on Very Easy)
start practising your hold-ups.</p>
<p><b>Hold-up method:</b> approach from behind, jam the gun into their back (<code>Freeze!</code>), keep aiming and
run round to their front, then aim at the face.</p>
<p><b>Silence is Golden</b> comes free here: while you are behind a guard, aim at the <b>radio on his lower back</b>
and shoot it (non-lethal is fine) you can do it to every guard you hold up.</p>"""),
dict(id="r1-2", title="Deck-D, crew's quarters — Box 1 + the extinguisher", area="Tanker · Deck-D",
shots=["Down in Smoke","Moving Day"],
body="""<p>Three things here:</p>
<ul class="tight">
<li><b>Box 1 (Moving Day)</b> inside the food storage room, to the right of the stairs you come up.</li>
<li><b>Down in Smoke</b> stand across from the laser trap in the hallway; the patrolling guard stops next to a
<b>fire extinguisher</b>. Shoot the extinguisher when he is beside it.</li>
<li>Tags: <i>Miguel Luis Cunha</i> (moves after the Olga fight) and <i>Eiji Senke</i>, who only turns up while you are
inside the storeroom.</li>
</ul>"""),
dict(id="r1-3", title="Deck-A, crew's quarters — the locker room hat-trick", area="Tanker · Deck-A",
shots=["Hurt Locker","Kissing Booth","Bye Bye Big Brother"],
body="""<p><b>Do all three of these in one visit</b> — this is the most commonly botched spot in the game.</p>
<ol class="tight">
<li><b>Hurt Locker</b> tranquillise the hallway guard, drag him into the locker room, open a locker and drag him in.</li>
<li><b>Kissing Booth</b> find the locker with the <b>pin-up poster</b> inside the door, get in, and hold the
first-person button until you hear a kiss.</li>
<li><b>Snake Beater</b> get out, press first-person again and press <b>right up against the poster</b>, then open the
codec and call <b>Otacon</b>.</li>
</ol>
<p class="muted">There is also a wall camera nearby leave it alone if you care about Big Boss mech damage later, otherwise
shoot it for Bye Bye Big Brother.</p>"""),
dict(id="r1-4", title="Deck-A, crew's lounge — Stun Grenades", area="Tanker · Deck-A lounge",
body="""<p><b>Stun Grenades</b> sit at the bottom of the stairs that lead down to the Engine Room, starboard side — you
walk right past them during the story. Three tags here too.</p>"""),
dict(id="r1-5", title="Deck-B, crew's quarters", area="Tanker · Deck-B",
body="""<p>Two tags (<i>Yuki Hongoh</i> and one other on Very Easy). Nothing missable.</p>"""),
dict(id="r1-6", title="Deck-C, crew's quarters — Chaff Grenades + first camera", area="Tanker · Deck-C",
body="""<p><b>Chaff Grenades</b> are in the locker <b>left of the stairs after the first camera</b>. This is also your first
easy <b>Bye Bye Big Brother</b> camera remember cameras need a <b>lethal</b> shot.</p>"""),
dict(id="r1-7", title="Deck-2, port and starboard — the ambush", area="Tanker · Deck 2",
shots=["Caution: Hot"],
body="""<p>Three tags on Very Easy. Then the story triggers the <b>soldiers hallway ambush</b>.</p>
<div class="okbox">This ambush is a <b>forced alert</b> and does <b>not</b> void Complete Stealth.</div>
<p><b>Caution: Hot</b> is easiest right here: the ambush corridor is lined with pipes shoot the pipes next to a soldier
until he goes down.</p>"""),
dict(id="r1-8", title="Engine Room — Hand Grenades, one-time guards + Steel Grip", area="Tanker · Engine Room",
shots=["Steel Grip"],
body="""<p>Grab the <b>Hand Grenades</b> in the bottom-left corner of the bottom level, near the exit at the foot of the stairs.</p>
<div class="warnbox">Two guards here are <b>one-shot spawns</b>:
<i>Jeff A Saylor Jr</i> only appears the first time you enter the Engine Room, and <i>Kazuki Tokunaga</i> only appears in a
cutscene when you approach the IR sensors. <b>Both vanish if you leave the room.</b> Get their tags before you exit.</div>
<h4 style="margin-top:14px"> Do <b>Steel Grip</b> here this is the guide-recommended spot</h4>
<p>The Engine Room is called out as the best place for <b>Steel Grip</b> because it has <b>two railings stacked one above the
other</b>, which is exactly what the fast method needs. You need <b>two grip levels</b> to reach the maximum (level 3), and
each level is <b>100 pull-ups</b> <i>or</i> <b>10 drop-and-catches</b> so take the shortcut.</p>
<ol class="tight">
<li>Flip over the railing so you are hanging, with the lower railing visible directly below you.</li>
<li>Press the <b>drop</b> button, then immediately mash the <b>grab/hang</b> button to catch the lower railing.</li>
<li>Climb up and repeat. One clean catch = <b>10 pull-ups</b>, so <b>20 catches</b> finishes it.</li>
</ol>
<div class="warnbox"><b>Do the two one-shot guard tags first.</b> The count resets if you <b>die</b> without leaving the room,
and the usual advice is to step out and back in every few drops to bank your progress which is exactly what makes those
two guards despawn. Tags first, then hang.</div>
<div class="okbox"><b>Why here and not later:</b> the Tanker <b>Holds</b> come next, and the hidden marine tags in
Hold No. 1 and No. 3 need long <b>shimmies along ledges</b> that drain your grip and health. Getting grip level 3
<i>before</i> you reach the Holds makes that tag hunt far less lethal.</div>
<p class="muted">Plain pull-ups work too (hold both triggers, 200 of them) slower, but zero risk of falling.
Never grind this on a <b>Big Boss</b> run: a single missed catch is a death, and that voids the codename.</p>"""),
dict(id="r1-9", title="Navigational Deck, Wing — Olga + the Wet Box", area="Tanker · Deck-E bridge / Nav Deck",
body="""<p>Boss: <b>Olga Gurlukovich</b> → <b>Lights Out</b>. Shoot the spotlight and the tarp corners, and roll away from her
shots.</p>
<p>Immediately after the fight:</p>
<ul class="tight">
<li>Pick up and drop <b>Olga's body</b> for her dog tag.</li>
<li>Grab the <b>Wet Box</b> (Moving Day) on the Navigational Deck, Wing it is up by the <b>spotlight</b> near where the
guard emerges.</li>
<li><i>Brendan McLeod</i> spawns when you approach the starboard side.</li>
</ul>"""),
dict(id="r1-10", title="Holds 13 — marine tags, the ghost and the photo", area="Tanker · Holds",
shots=["Who Ya Gonna Call?","Sexting"],
body="""<p>The densest collectible section in the game, and a <b>one-way trip</b>. Save before you enter Hold No. 1.</p>
<ul class="tight">
<li><b>Who Ya Gonna Call?</b> in <b>Hold No. 2</b>, photograph the <b>right-hand projector screen</b> with Otacon's camera.
If the ghost is not up, interact with the projector to switch screens.</li>
<li><b>Sexting</b> photograph the <b>marine with no pants</b>, then send it from the <b>computer in the bottom-right of
Hold No. 3</b>. Do this <b>before</b> you photograph RAY.</li>
<li><b>Hidden marine tags:</b> after going down the first ladder, <b>shimmy</b> along the ledges there are guards tucked away
in Hold No. 1 and Hold No. 3. For the boxers marine in the middle of the crowd, tranquillise and drag the marines beside him.</li>
<li>Max out rations first shimmying drains health.</li>
</ul>
<p>Then photograph RAY and escape. <b>Lady Fortune</b> pops during the Fortune sequence on the way out.</p>"""),
dict(id="r1-11", title="Finish the Tanker → make a COMPLETED save", area="Tanker · Results screen",
body="""<p>Let the credits roll and save. Check <b>Special → Dog Tag Viewer</b> to confirm you have all <b>24</b> Very Easy tags
before moving on.</p>
<div class="infobox">Yorkie (50 tags) and Poodle (76 tags) will not pop until you finish a completed Tanker-Plant file that
contains those totals keep going into the Plant.</div>"""),
]),
dict(phase="Run 1 · Plant (Very Easy)", intro="""
<p>Same structure: work through the struts in order. The Plant is longer but its tags are more straightforward the
interesting ones are the <b>conditional spawns</b> (guards that only appear after Fatman, or after the Emma escort).</p>""",
steps=[
dict(id="r2-1", title="Strut A, Deep Sea Dock — sea lice + a boss", area="Plant · Strut A",
shots=["My Rations..."],
body="""<p>Do <b>My Rations…</b> right away: grab a ration, then <b>lie down near the bugs</b> and wait for a louse to crawl onto it.</p>
<p>Two tags here (<i>Brad I Lanning</i>, <i>Charles P Pinkerton</i>), and later the <b>Fortune</b> fight happens in this same
area <b>Lady Fortune</b>.</p>"""),
dict(id="r2-2", title="Strut A Roof + Pump Room", area="Plant · Strut A",
body="""<p>Tags on the roof (<i>Jason M Gillispie</i>) and two in the Pump Room. The <b>Box 1</b> cardboard box is in the Strut A
pipe room crawl under the pipes on your first right after the small stairs in the bottom-left, once you have the Lv. 1 Card.</p>"""),
dict(id="r2-3", title="A-B bridge → Strut B, Transformer Room", area="Plant · Strut B",
body="""<p>Two tags on the bridge, two in the transformer room. Grab the <b>Lv. 1 Card</b> along the way — it gates Box 1 and Box 2.</p>"""),
dict(id="r2-4", title="B-C bridge → Strut C, Dining Hall", area="Plant · Strut C",
body="""<p>One tag on the bridge, two in the dining hall. Simple area.</p>"""),
dict(id="r2-5", title="C-D bridge → Strut D, Sediment Pool", area="Plant · Strut D",
body="""<p>Two tags on the bridge and two to three in the sediment pool depending on difficulty.</p>
<p><b>Backup Steel Grip spot:</b> if you skipped the pull-ups in the Tanker Engine Room, Strut D's railings are the other
commonly recommended place you can drop onto the level below and the stairs to climb back up are right there, so the
loop is quick.</p>
<div class="warnbox">A soldier patrols Strut D and <b>regularly radios in</b>. If he goes down, a search party comes
looking so clear him quietly and tranquillise him again if he wakes up while you are grinding.</div>"""),
dict(id="r2-6", title="D-E bridge → Strut E, Parcel Room", area="Plant · Strut E",
body="""<p>Two tags on the bridge, two in the parcel room, and a <b>wall camera</b>.</p>"""),
dict(id="r2-7", title="Strut E Heliport — Stillman, then Fatman", area="Plant · Strut E Heliport",
body="""<p><b>Bomb Squad</b> pops during Stillman's C4 tutorial.</p>
<p>Then the bomb-defusal section around the Big Shell, then boss: <b>Fatman</b> <b>Party's Over</b>.</p>
<div class="warnbox">After the Fatman fight, <b>lift and drop Fatman's body</b> for the <i>Peter Stillman</i> dog tag — this is the
<b>last tag on every Plant difficulty list</b> and it is the one people miss.</div>
<div class="okbox">Beating Fatman <b>reopens weapon rooms in Strut F</b> (AKS-74u on the bottom floor, M4 in the top-floor
middle-left room). Go back for them.</div>"""),
dict(id="r2-8", title="Strut F warehouse — the Book, Box 2 and the weapon sweep", area="Plant · Strut F",
shots=["To Catch a Predator"],
body="""<p>Strut F is the game's collectible hub. Do a full sweep <b>now</b> and again after you meet Ames.</p>
<ul class="tight">
<li><b>To Catch a Predator</b> the <b>Book</b> is on the bottom floor on top of some crates. From the top floor, flip over
the railing, shimmy right until you are above the crates, drop and pick it up. Then drop it in a guard's path.</li>
<li><b>Box 2</b> bottom floor, bottom-left room with the Radar Node (needs Lv. 1 Card).</li>
<li><b>M9</b> top floor, room on the right after entering from the FA top-floor bridge.</li>
<li>Bottom floor after Fatman: <b>AKS-74u</b>. Top-floor middle-left: <b>M4</b>. Top-floor top-right locker: <b>C-4</b>.
Bottom floor: <b>Stun Grenades</b>.</li>
</ul>"""),
dict(id="r2-9", title="Strut E Heliport roof + Shell 1 Core 1F", area="Plant · Strut E / Shell 1",
body="""<p><b>Box 3</b> is in the bottom-left corner of the Strut E rooftop helipad. <b>Box 5</b> is on the conveyor belt in Strut E itself.</p>
<p>In <b>Shell 1 Core 1F</b>: several tags, the <b>south lockers</b> (Book / C-4 / Claymores / Stun Grenades), and cameras to shoot.</p>"""),
dict(id="r2-10", title="Shell 1 Core B2 — Box 4 + the Directional Mic", area="Plant · Shell 1 B2 Computer Room",
body="""<p><b>Box 4</b> sits across from the Radar Node on B2, near the <b>Directional Mic</b> and the parrot. The Directional Mic is
story-required here you will need it twice for <b>Johnny on the Spot</b>.</p>
<p>Four tags in the computer room.</p>"""),
dict(id="r2-11", title="Ames + Johnny (mic location 1)", area="Plant · Shell 1 Core hostage room",
shots=["Johnny on the Spot"],
body="""<p><b>Johnny on the Spot #1.</b> During the Ames scene you are told to mic Ocelot and Solidus. Aim the Directional Mic
<b>slightly left</b> of that conversation until you hear <b>Johnny</b> in the bathroom, and keep it on him until he finishes.</p>
<div class="warnbox">Do <b>not</b> quit to the menu after this do the second listen in the <b>same session</b>, or the achievement
may not register.</div>
<p>After the Ames scene, alert mode is forced it will not void Complete Stealth.</p>"""),
dict(id="r2-12", title="Shell 1 Core elevator — wear the disguise, carry the AK", area="Plant · Shell 1 Core",
body="""<p>The story requires you to use the <b>disguise + AK-47</b> here. Even with Stealth Camo equipped, the camera will
spot you if you try the elevator any other way.</p>
<p class="muted">This is the single exception to "stealth makes Complete Stealth free".</p>"""),
dict(id="r2-13", title="President Johnson → Vamp", area="Plant · Shell 1 Core",
body="""<p><b>Bohemian Candidate</b> pops when you meet the President. Then boss: <b>Vamp</b> → <b>Vampire Slayer</b>.</p>
<p>Vamp strategy: M9 to the head; when he dives in the water, shoot the water with the Stinger to drain his O₂. Then hang off
the platform edge to bait his attack and punch him twice when he comes close.</p>"""),
dict(id="r2-14", title="Shell 2 Core — Emma + the Nikita", area="Plant · Shell 2 Core B1",
body="""<p><b>I Think You Need a Hug, E</b> pops when you find Emma. Swim underwater for the story-required <b>Nikita</b>.</p>
<p>Emma's locker in the B1 Filtration Chamber 2 holds a <b>Book</b> and <b>C-4</b> — both count toward the weapon/box sweeps.</p>"""),
dict(id="r2-15", title="Emma escort + oil fence — Johnny (mic location 2) + the camera", area="Plant · oil fence",
shots=["Photo Finish","Johnny on the Spot"],
body="""<p><b>Johnny on the Spot #2.</b> While covering Emma, when she ducks behind the <b>second platform</b>, aim the Directional
Mic at that platform and listen to her run into Johnny. Keep the mic on them until he leaves. The achievement pops here if you
also did location 1.</p>
<p><b>Photo Finish</b> after the escort, when you climb the ladder into Strut E, look for the <b>Level 5 door</b> at the top of the
small area (before the conveyor section) for the Digital Camera.</p>"""),
dict(id="r2-16", title="Arsenal Gear — Snake's tag, Tengu, RAY", area="Arsenal Gear",
body="""<p>In the <b>Ascending Colon</b>, knock out the man you meet, pick him up and drop him for the first tag on every Plant
difficulty list (<i>Iroquois Pliskin / Solid Snake / Liquid Snake / Hideo Kojima</i>).</p>
<p>Then the <b>Tengu</b> fight (forced alert, safe) and boss: <b>Metal Gear RAY</b> <b>No Ray, José</b>. About 20 RAYs keep moving
and cartwheel through the missile barrages.</p>
<p>Then the <b>torture</b> room. Beating it gives you the <b>HF Blade</b> from Snake.</p>"""),
dict(id="r2-17", title="Solidus — and DO NOT SKIP THE CUTSCENE", area="Plant · rooftop",
shots=["Vamp Eyer"],
body="""<p>Boss: <b>Solidus Snake</b> → <b>Another Snake Bites the Dust</b>.</p>
<div class="warnbox"><b>Vamp Eyer:</b> let the ending cinematic play. When Snake asks <i>"By the way, what is that?"</i> and Raiden
answers <i>"No, never heard the name before"</i>, hold the zoom button and look around Vamp is standing in the street. Zoom in on him.</div>"""),
dict(id="r2-18", title="Save a completed Tanker-Plant file", area="Results screen",
body="""<p>Save after the credits. This single file is what proves your Tanker and Plant tag totals.</p>
<p>Check the totals: Tanker 24, Plant 43. Then open the <b>Dog Tags</b> tab and confirm the thresholds panel
<b>Bandana 50</b> and <b>Brown Wig 80</b> are already satisfied once you add the Easy-run tags; <b>Yorkie</b> will pop after
Run 2 and <b>Beagle</b> during Run 2 as well.</p>"""),
]),
dict(phase="Runs 2 & 3 — the dog-tag sweeps", intro="""
<p>The same route as Run 1, on <b>Easy</b> then <b>Normal</b>, picking up the tags you do not have. Nothing new is missable
you are only here for numbers.</p>""",
steps=[
dict(id="r3-1", title="Run 2 (Easy): sweep Tanker 26 + Plant 44", area="Easy difficulty",
body="""<p>When you save a completed file after this run you should cross <b>50 Tanker</b> (→ <b>Yorkie</b> / Bandana) and
<b>80 Plant</b> ( <b>Beagle</b> / Brown Wig).</p>
<p class="muted">The achievements pop when the chapter is completed with the totals present, not when the last tag is collected.</p>"""),
dict(id="r3-2", title="Run 3 (Normal): sweep Tanker 33 + Plant 49", area="Normal difficulty",
body="""<p>This run crosses <b>76 Tanker</b> (→ <b>Poodle</b> / Tanker stealth) and <b>120 Plant</b> (→ <b>Shiba Inu</b> / Plant stealth).</p>
<div class="okbox">After this run you have everything you need for the two challenge runs: <b>Stealth Camo</b> and the
<b>Bandana / Brown Wig</b> (infinite ammo). Start Run 4 from a file that loads this cleared data.</div>"""),
dict(id="r3-3", title="Optional: grab the last two wigs", area="Plant, all difficulties",
body="""<p>The <b>Orange Wig</b> (160 Plant tags) and <b>Blue Wig</b> (200 Plant tags) are <b>not</b> required for any achievement.
Only chase them if you want the Perfect/100% feel Hard (52) + Extreme (54) = 106 more tags.</p>"""),
]),
dict(phase="Run 4 — Pacifist Run + Complete Stealth together", intro="""
<p>Both of these are "whole playthrough" challenges, so do them at the same time. With the items from Run 3 this is the
easiest full playthrough you will ever do.</p>""",
steps=[
dict(id="r4-1", title="Start Very Easy with Stealth Camo + Bandana loaded", area="New game from cleared data",
body="""<p>Load the cleared save so the special items are in your inventory, then equip <b>Stealth Camo</b> permanently.</p>
<p>Equip the <b>Bandana</b> (or Brown Wig) for infinite non-lethal ammo.</p>"""),
dict(id="r4-2", title="Kill nothing, anywhere", area="Whole run",
body="""<p><b>Pacifist Run</b> requires zero kills — this includes:</p>
<ul class="tight">
<li>No neck breaks, no lethal weapons, no explosive barrels killing anyone.</li>
<li>No steam-pipe kills, no falling deaths you caused.</li>
<li>Bosses with a <b>stamina bar</b> (Olga, Fatman, Harrier, Vamp, Solidus) must be drained on stamina, not health.</li>
<li>For <b>Solidus</b>, flip the HF Blade to the <b>blue non-lethal side</b>.</li>
</ul>"""),
dict(id="r4-3", title="Do the Shell 1 elevator properly", area="Shell 1 Core",
body="""<p>Switch off stealth for this one moment and use the <b>disguise + AK-47</b> as the story demands.
Trying it in stealth gets you spotted and can void <b>Complete Stealth</b>.</p>"""),
dict(id="r4-4", title="Save-and-reload discipline for alerts", area="Whole run",
body="""<p>Save at the start of every new area. If the HUD ever turns red <b>ALERT</b>, reload immediately — the game only
checks the flag on the final results screen.</p>
<p class="muted">Forced alerts that are safe: boss fights, the Tanker hallway ambush, the post-Ames alert, the Tengu fight.</p>"""),
]),
dict(phase="Run 5 — Big Boss", intro="""
<p>The final boss of the achievement list. Everything you have learned so far gets used at once.
Full criteria, strategies and time checkpoints are in the <b>Big Boss Run</b> tab.</p>""",
steps=[
dict(id="r5-1", title="Practise the boss fights before you commit", area="Any difficulty",
body="""<p>Do a throwaway Extreme run just to learn Olga, Fatman, Harrier, Vamp and Solidus. The timer is unforgiving and
you cannot afford to learn a fight mid-run.</p>"""),
dict(id="r5-2", title="Start Extreme, Tanker→Plant, NO special items, NO radar", area="New game",
body="""<p>Do <b>not</b> load the cleared save — special items and the radar are banned and will void the codename.</p>"""),
dict(id="r5-3", title="Skip every cutscene and codec, ignore optional calls", area="Whole run",
body="""<p>Cutscenes and codec conversations all count toward your 3-hour limit. Mash the skip button the instant each one starts.</p>"""),
dict(id="r5-4", title="Hold the time checkpoints in the Big Boss tab", area="Whole run",
body="""<p>Fortune 00:45 · Fatman 00:49 · Harrier 01:10 · Vamp 01:25 · oil fence 01:40 · Tengu 01:56 · RAY 02:08 · Solidus 02:22.</p>
<p>If you are more than a couple of minutes behind at any of these, restart it is cheaper than finishing at 3:05.</p>"""),
]),
dict(phase="Off-story cleanup", intro="""
<p>Everything else is on the main menu and takes under an hour total.</p>""",
steps=[
dict(id="r6-1", title="Snake Tales — complete any one", area="Main Menu",
body="""<p><b>Tell Me a Tale.</b> A Wrongdoing is the shortest of the five.</p>"""),
dict(id="r6-2", title="Boss Survival — Raiden on Easy", area="Main Menu",
body="""<p><b>No Boss of Mine.</b> Raiden gets the HF Blade for Solidus; Snake does not.</p>"""),
dict(id="r6-3", title="VR missions — one clear, then 50 first places", area="Main Menu",
body="""<p><b>Piece of Cake</b> (any single mission) then <b>In It to Win It</b> (place 1st in 50). Pick the shortest
Sneaking missions and learn the target time.</p>"""),
dict(id="r6-4", title="Casting Theater — Demo 8 body swap", area="Main Menu → Special",
body="""<p><b>Casting Theater</b> + <b>Love Hurts</b>: Demo 8, Raiden→Rose, Solidus→Raiden, watch it play out.</p>"""),
dict(id="r6-5", title="Sweep the cumulative counters on any leftover run", area="Any playthrough",
body="""<p>Hold Up-aholic (30), Don't Taze Me Bro (100), Nothing Personal (30), Rent Money (30) and
Bye Bye Big Brother (5 cameras) all carry across playthroughs check the Achievements tab to see what is still outstanding and
farm it on a spare save.</p>
<p class="muted">Steel Grip is <b>not</b> on this list do that one on-site in the <b>Tanker Engine Room</b> (or Strut D)
where the stacked railings are, as covered in the Tanker walkthrough above.</p>"""),
]),
]
BIGBOSS = dict(
intro="""Three hours, no rations, no kills, no radar, no special items, on Extreme, with a mech-damage cap —
and you must destroy about 20 Metal Gear RAYs inside that cap. This is the one achievement that decides whether you get 100%.
Learn the route first, then execute it.""",
reqs=[
"Chapter: <b>Tanker-Plant</b> (a full run, both chapters)",
"Difficulty: <b>Extreme</b>",
"Time: <b>3:00:00 or less</b>",
"Continues: <b>0</b>",
"Rations used: <b>0</b>",
"Kills: <b>0</b>",
"Alert mode: <b>3 times or fewer</b>",
"Shots fired: <b>700 or less</b>",
"Mech damage: <b>under 60</b> — cameras, Cyphers and MG RAYs all count; you must destroy ~20 RAYs, so keep everything else under ~40",
"Damage received: <b>10 health bars or less</b>",
"<b>No special items</b> (no Bandana, no Stealth, no wigs)",
"<b>No radar</b>",
],
heals=[
"After the Olga boss fight",
"After the Fortune fight",
"After the Fatman boss fight",
"After the Vamp boss fight",
"After the first torture",
"After the Metal Gear RAYs boss fight",
],
times=[
["Fortune fight", "00:45:00"],
["Fatman fight", "00:49:00"],
["Harrier fight", "01:10:00"],
["Vamp fight", "01:25:00"],
["Cover Emma on the oil fence", "01:40:00"],
["Tengu soldiers", "01:56:00"],
["Metal Gear RAY fight", "02:08:00"],
["Solidus fight", "02:22:00"],
["Finish", "under 03:00:00"],
],
saves=[
"Before the Fortune fight",
"Before the Fatman fight",
"Before destroying the Semtex on the bridge ahead of the Harrier fight",
"Before the Vamp fight",
"Before covering Emma as she crosses the oil fence",
"Before the Tengu fight",
"Before the Metal Gear RAY fight",
"Before the Solidus fight",
],
tips=[
"<b>Cutscenes count toward the timer.</b> Skip every cutscene and codec immediately, including codec conversations.",
"<b>Do not answer optional codec calls.</b>",
"Save <b>immediately after entering a new area</b> — reloading restarts you at the start of the area you saved in.",
"If an elevator does not open right away, <b>press the button a second time</b>.",
"Use <b>Snake's roll</b> and <b>Raiden's cartwheel</b> constantly to move faster.",
"Do not hunt for ammo or items you do not need. You cannot kill or use rations, so almost everything is skippable.",
"Getting caught voids the codename anyway, so you will rarely need health outside boss fights.",
],
bosses=[
dict(name="Olga", body="""
<p>Shoot the <b>spotlight</b> and the <b>bottom corners of the tarp</b> immediately. She shoots at your last known position,
so roll to a blind side of her after each shot.</p>
<p>Taking damage here is fine your health refills after the fight.</p>"""),
dict(name="Fortune", body="""
<p>Pure timing. Equip a <b>cardboard box</b> and walk back and forth along the very bottom of the arena.</p>
<p>As soon as her <b>second shot</b> resolves, turn around and go the other way. If you were heading left, go right. Repeat
until the fight ends.</p>
<p>Watch for debris and exploding barrels. Her shots come fast expect a few attempts to get the rhythm.</p>"""),
dict(name="Fatman", body="""
<p><b>Stay close to Fatman</b> so you can freeze each bomb as it is planted rather than waiting for all of them.</p>
<ul class="tight">
<li>When he is planting, <b>melee</b> him to knock him down, then freeze the bomb while he is on the floor.</li>
<li>Melee him as he skates past, then M9 to the head while he gets up.</li>
<li>From range, shoot him with the <b>SOCOM</b> to knock him down (little health damage), then <b>M9 to the head</b>.</li>
<li>If you are fast, M9 him in the head the instant he falls and again on the way down.</li>
</ul>"""),
dict(name="Harrier", body="""
<ul class="tight">
<li>Pick up the <b>Stinger</b> immediately and fire as many shots as you can at the start.</li>
<li>While it circles the perimeter you can land a shot or two.</li>
<li>When it flies straight out, follow the contrails, <b>wait for the flash</b>, fire, then take cover fast.</li>
<li>When it hovers to burn you with thrusters, spam the Stinger at the <b>middle of the jet</b> no lock-on needed.</li>
<li>Bombs from above: drop to the bottom level.</li>
<li>Rockets: go far left up the stairs, then cartwheel right as it fires.</li>
<li>Close-range gun runs: get to the bottom floor behind the big grey box and move around it as the Harrier repositions.</li>
</ul>"""),
dict(name="Vamp", body="""
<p>This one can be cheesed.</p>
<ol class="tight">
<li>Open with an <b>M9 headshot</b>. When he dives into the water, shoot the <b>water</b> with the Stinger to drain his O₂.</li>
<li>Step slowly off the platform so Raiden <b>hangs from the edge</b> do not fall in. Vamp walks over.</li>
<li>Just before he crouches to attack, <b>pull yourself up</b> so the attack whiffs as you climb.</li>
<li>Hit him with two melee punches. When he crouches, run left or right but stay close enough to punish the slash, then
close in for two more punches. Repeat.</li>
<li>If he returns to the water, Stinger the water and restart the loop.</li>
</ol>"""),
dict(name="Tengu soldiers", body="""
<p>You have the sword now, but <b>stick to non-lethal weapons</b> running around with the blade will cost you too much health.</p>
<p>Snake will take damage. You can stand in front of him and hold the <b>sword block</b> to deflect bullets, which protects him
while he kills the enemies. You must stand still while blocking.</p>"""),
dict(name="Metal Gear RAY", body="""
<p>You fight about <b>20 RAYs</b>. Be quick with your shots and listen for missile barrages from RAYs outside the arena.</p>
<p>Machine-gun fire and the single-missile attack are dodged by running left or right and <b>cartwheeling</b> as the rounds
get close.</p>
<div class="warnbox">Every RAY you destroy counts toward your <b>mech damage</b> limit. Budget for ~20 and keep everything else
(cameras, Cyphers) near zero.</div>"""),
dict(name="Solidus", body="""
<p><b>Phase 1:</b> his melee and tentacle attacks can be blocked with the sword, and missiles can be cartwheeled or cut in half.
Bait the melee, block it, land your hits, then step back and run in again.</p>
<p><b>Phase 2:</b> hit him then run away far enough that he stops sliding point-to-point and comes straight at you. Hold
<b>block</b> to eat his elbow attack, deal damage, then disengage and repeat.</p>"""),
],
)

138
build/steam-proxy-worker.js Normal file
View file

@ -0,0 +1,138 @@
/**
* steam-proxy-worker.js Cloudflare Worker that gives a *hosted* copy of
* la-li-lu-le-lo (the MGS2 Master Collection 100% companion) working Steam sync.
*
* WHY THIS EXISTS
* ---------------
* The companion reads a public Steam Community page to find out which achievements an
* account already has. That page is served as XML and sends no CORS headers, so a browser
* cannot fetch it from another origin. Running steam-sync.py locally solves this on your own
* machine; this Worker solves it once the page is hosted on GitHub Pages / Netlify / Vercel /
* Cloudflare Pages.
*
* DEPLOY (free tier is plenty this is a handful of requests per page load)
* -------------------------------------------------------------------------
* 1. https://dash.cloudflare.com -> Workers & Pages -> Create -> Worker
* 2. Paste this whole file in, replacing the starter code. Deploy.
* 3. Copy the worker URL (e.g. https://mgs2-steam-proxy.YOURNAME.workers.dev)
* 4. In index.html, set: const STEAM_PROXY = 'https://mgs2-steam-proxy.YOURNAME.workers.dev';
* 5. Done the page will use it automatically when it is not running on localhost.
*
* ALLOW_ORIGIN below is already pinned to the deploying page's origin, so nobody else
* can point their own page at this Worker. Add more origins to the list if you need to.
*
* The Worker makes exactly one outbound request, to the public Steam Community page for the
* profile it is asked about. It stores nothing and logs nothing.
*/
const APPID = '2131640';
const ALLOW_ORIGIN = 'https://rain.pages.melonbread.xyz'; // only this page may call the Worker
const UA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36';
function cors(res) {
res.headers.set('Access-Control-Allow-Origin', ALLOW_ORIGIN);
res.headers.set('Access-Control-Allow-Headers', '*');
res.headers.set('Access-Control-Allow-Methods', 'GET, OPTIONS');
res.headers.set('Cache-Control', 'no-store');
return res;
}
function json(obj) {
return new Response(JSON.stringify(obj), {
status: 200,
headers: { 'Content-Type': 'application/json; charset=utf-8' },
});
}
function cdata(block, tag) {
const m = block.match(new RegExp('<' + tag + '><!\\[CDATA\\[([\\s\\S]*?)\\]\\]></' + tag + '>'));
return m ? m[1].trim() : '';
}
export default {
async fetch(request) {
const url = new URL(request.url);
if (request.method === 'OPTIONS') return cors(new Response(null, { status: 204 }));
if (url.pathname.replace(/\/$/, '') !== '/api/steam') {
return cors(new Response('Not found — this Worker only serves /api/steam', { status: 404 }));
}
const profile = (url.searchParams.get('profile') || '').trim();
if (!profile) {
return cors(json({ ok: false, error: 'No profile given.', hint: 'Enter a vanity name or SteamID64.' }));
}
const target = /^\d{17}$/.test(profile)
? 'https://steamcommunity.com/profiles/' + profile + '/stats/' + APPID + '/?tab=achievements&xml=1'
: 'https://steamcommunity.com/id/' + encodeURIComponent(profile) + '/stats/' + APPID + '/?tab=achievements&xml=1';
let xml;
try {
const r = await fetch(target, { headers: { 'User-Agent': UA, 'Accept-Language': 'en' } });
xml = await r.text();
} catch (e) {
return cors(json({ ok: false, error: 'Could not reach Steam.', hint: 'Try again in a moment.' }));
}
const err = xml.match(/<error><!\[CDATA\[([\s\S]*?)\]\]><\/error>/);
if (err) {
return cors(json({
ok: false,
error: err[1].trim(),
hint: 'Check the spelling, or paste the whole profile URL.',
}));
}
const privacy = (xml.match(/<privacyState>(.*?)<\/privacyState>/) || [, ''])[1].trim();
if (privacy && privacy !== 'public') {
return cors(json({
ok: false,
error: 'That profile is ' + privacy + '.',
hint: "Steam > Edit Profile > Privacy Settings > set 'Game details' to Public, then sync again.",
}));
}
const achievements = [];
const re = /<achievement closed="(\d)">([\s\S]*?)<\/achievement>/g;
let m;
while ((m = re.exec(xml)) !== null) {
const body = m[2];
achievements.push({
api: cdata(body, 'apiname'),
name: cdata(body, 'name'),
desc: cdata(body, 'description'),
unlocked: m[1] === '1',
icon: cdata(body, 'iconOpen'),
iconLocked: cdata(body, 'iconClosed'),
});
}
if (!achievements.length) {
return cors(json({
ok: false,
error: 'No MGS2 Master Collection stats on that account.',
hint: 'The account must own the game (appid ' + APPID + ') and have launched it at least once.',
}));
}
const id64 = (xml.match(/<steamID64>(\d+)<\/steamID64>/) || [, ''])[1];
// This XML carries no persona name, only steamID64 and customURL, so fall back sensibly.
let persona = '';
const pm = xml.match(/<steamID>(?:<!\[CDATA\[)?([\s\S]*?)(?:\]\]>)?<\/steamID>/);
if (pm && pm[1].trim()) persona = pm[1].trim();
if (!persona) {
const cm = xml.match(/<customURL><!\[CDATA\[(.*?)\]\]><\/customURL>/);
if (cm && cm[1].trim()) persona = cm[1].trim();
}
if (!persona) persona = profile;
return cors(json({
ok: true,
game: 'METAL GEAR SOLID 2: Sons of Liberty - Master Collection Version',
appid: APPID,
steamid64: id64,
persona: persona,
unlockedCount: achievements.filter((a) => a.unlocked).length,
total: achievements.length,
achievements: achievements,
}));
},
};

202
build/steam-sync.py Normal file
View file

@ -0,0 +1,202 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
steam-sync.py tiny local helper for la-li-lu-le-lo, the MGS2 Master Collection
100% companion.
It does two things:
1. serves the folder it lives in over http://127.0.0.1:8765/ (so the page gets a
real origin and localStorage saves reliably)
2. exposes /api/steam?profile=<vanity|steamid64> which fetches that profile's
public MGS2 Master Collection achievements from Steam and returns them as JSON
No API key. No third-party packages. Only one outbound request, to the public
Steam Community page for the profile you ask for.
python3 steam-sync.py # then open http://127.0.0.1:8765/
python3 steam-sync.py --open # ...and open the browser for you
PORT=9000 python3 steam-sync.py # different port
"""
import http.server
import json
import os
import re
import socket
import socketserver
import sys
import urllib.error
import urllib.parse
import urllib.request
import webbrowser
from xml.etree import ElementTree as ET
APPID = "2131640"
GAME = "METAL GEAR SOLID 2: Sons of Liberty - Master Collection Version"
PORT = int(os.environ.get("PORT", "8765"))
ROOT = os.path.dirname(os.path.abspath(__file__))
UA = ("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/120.0 Safari/537.36")
def steam_url(profile):
profile = profile.strip()
if re.fullmatch(r"\d{17}", profile):
return "https://steamcommunity.com/profiles/%s/stats/%s/?tab=achievements&xml=1" % (profile, APPID)
return "https://steamcommunity.com/id/%s/stats/%s/?tab=achievements&xml=1" % (
urllib.parse.quote(profile), APPID)
def fetch_steam(profile):
url = steam_url(profile)
req = urllib.request.Request(url, headers={"User-Agent": UA, "Accept-Language": "en"})
try:
with urllib.request.urlopen(req, timeout=25) as r:
raw = r.read()
except urllib.error.HTTPError as e:
return {"ok": False, "error": "Steam returned HTTP %s." % e.code,
"hint": "Try again in a moment; Steam rate-limits rapid requests."}
except Exception as e:
return {"ok": False, "error": "Could not reach Steam: %s" % e,
"hint": "Check your internet connection."}
try:
root = ET.fromstring(raw)
except ET.ParseError:
return {"ok": False, "error": "Steam sent something unreadable.",
"hint": "The profile may be private or Steam may be rate-limiting."}
err = root.find("error")
if err is not None and (err.text or "").strip():
msg = err.text.strip()
hint = "Check the spelling, or paste the whole profile URL."
if "private" in msg.lower():
hint = "Steam > your profile > Edit Profile > Privacy Settings > set 'Game details' to Public."
return {"ok": False, "error": msg, "hint": hint}
privacy = (root.findtext("privacyState") or "").strip()
if privacy and privacy != "public":
return {"ok": False,
"error": "That profile is %s." % privacy,
"hint": "Steam > Edit Profile > Privacy Settings > set 'Game details' to Public, then sync again."}
ach_nodes = root.findall("./achievements/achievement")
if not ach_nodes:
return {"ok": False,
"error": "No MGS2 Master Collection stats on that account.",
"hint": "The account must own the game (appid %s) and have launched it at least once." % APPID}
out = []
for a in ach_nodes:
out.append({
"api": (a.findtext("apiname") or "").strip(),
"name": (a.findtext("name") or "").strip(),
"desc": (a.findtext("description") or "").strip(),
"unlocked": a.get("closed") == "1",
"icon": (a.findtext("iconOpen") or "").strip(),
"iconLocked": (a.findtext("iconClosed") or "").strip(),
})
return {
"ok": True,
"game": GAME,
"appid": APPID,
"steamid64": (root.findtext("./player/steamID64") or "").strip(),
# This XML carries no persona name, only steamID64 and customURL.
"persona": ((root.findtext("./player/steamID")
or root.findtext("./player/customURL")
or profile) or "").strip(),
"unlockedCount": sum(1 for a in out if a["unlocked"]),
"total": len(out),
"achievements": out,
}
class Handler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kw):
super().__init__(*args, directory=ROOT, **kw)
def log_message(self, fmt, *args):
# Never let a closed/dead output pipe (e.g. when run in the background)
# take the whole server down with it.
if "/api/" not in (self.path or ""):
return
try:
sys.stderr.write(" steam-sync: %s %s\n" % (self.command, self.path.split("?")[0]))
sys.stderr.flush()
except Exception:
pass
def _cors(self):
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Headers", "*")
self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS")
# Chrome asks for this when a public https page (e.g. the Pages deployment)
# calls into 127.0.0.1. Without it the request is blocked before it is sent.
self.send_header("Access-Control-Allow-Private-Network", "true")
def do_OPTIONS(self):
self.send_response(204)
self._cors()
self.end_headers()
def do_GET(self):
if self.path.split("?")[0] != "/api/steam":
return super().do_GET()
qs = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
profile = (qs.get("profile") or [""])[0].strip()
if not profile:
payload = {"ok": False, "error": "No profile given.",
"hint": "Enter a vanity name or SteamID64."}
else:
try:
payload = fetch_steam(profile)
except Exception as e: # never 500 on the user
payload = {"ok": False, "error": "Unexpected error: %s" % e, "hint": ""}
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
self._cors()
self.end_headers()
self.wfile.write(body)
class Server(socketserver.ThreadingTCPServer):
allow_reuse_address = True
daemon_threads = True
def main():
# A stalled connection to Steam can never hang a handler thread forever.
socket.setdefaulttimeout(30)
if not os.path.exists(os.path.join(ROOT, "index.html")):
print("!! index.html is not in %s — keep steam-sync.py next to it." % ROOT)
try:
httpd = Server(("127.0.0.1", PORT), Handler)
except OSError as e:
print("!! Could not bind port %d: %s" % (PORT, e))
print(" Another copy may already be running, or set PORT=<other> and retry.")
return 1
url = "http://127.0.0.1:%d/" % PORT
print("=" * 66)
print(" la-li-lu-le-lo — MGS2 100% companion (local helper)")
print("=" * 66)
print(" serving : %s" % ROOT)
print(" page : %s" % url)
print(" api : %sapi/steam?profile=<vanity or SteamID64>" % url)
print(" stop : Ctrl+C")
print("-" * 66)
if "--open" in sys.argv:
webbrowser.open(url)
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\n stopped.")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,241 @@
{
"1": [
"https://images.steamusercontent.com/ugc/2140958177135754992/33483A2517679F2105D029F5821418DD1D233281/"
],
"2": [
"https://images.steamusercontent.com/ugc/2140958177135758321/BC72F568064E5D5C268D200F6382B0BD3F8DEE6B/"
],
"3": [
"https://images.steamusercontent.com/ugc/2140958177135758930/E64E34B9E08B8F9C96C34C6DA45AE5CF6D0B7003/"
],
"4": [
"https://images.steamusercontent.com/ugc/2140958177135759201/550DD6E41F6D40451134C4E847DFFEA44601734D/"
],
"5": [
"https://images.steamusercontent.com/ugc/2140958177135759509/74E791DB6ECD619EED7313A2F4382FEBB3AFCFBC/"
],
"6": [
"https://images.steamusercontent.com/ugc/2140958177135760558/5E46A25E5B0BDFF0F26E93F657376EE5C6C24F3E/"
],
"7": [
"https://images.steamusercontent.com/ugc/2140958177135760933/A22BC99C672ED7BD5A5170FC1A0E46DD41C3AB61/"
],
"8": [
"https://images.steamusercontent.com/ugc/2140958177135776614/4D20C2252E95CFF4AE279DFB381B6498B5315F69/"
],
"9": [
"https://images.steamusercontent.com/ugc/2140958177135776885/0BE932AFDA4A9C857046DDBC27EE74B16A804B8F/"
],
"10": [
"https://images.steamusercontent.com/ugc/2140958177135778904/EB4EF236A1B364E861F8CCF0588F1FACE5CD86A3/"
],
"11": [
"https://images.steamusercontent.com/ugc/2140958177135780873/BD7DCD2395C79925B68C7006CC63AE6402E75A06/"
],
"12": [
"https://images.steamusercontent.com/ugc/2140958177135781258/EFE5274E4CE35FA4FEA13CF93A2A191C2B0EDB57/"
],
"13": [
"https://images.steamusercontent.com/ugc/2140958177135781716/8304A788B9099C1CBE87D963BCA7EB3B15EB1EC6/"
],
"14": [
"https://images.steamusercontent.com/ugc/2140958177135784329/36F89E61EC5808EDCD35952C2C4C44C30CE0AD69/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/",
"https://images.steamusercontent.com/ugc/2140958177136556309/FC08EA78D73A06C58AA304C6DAA576D3AC56C483/"
],
"15": [
"https://images.steamusercontent.com/ugc/2140958177135755392/B1B11990A2D6A38E90E06B774C13E66EE2DEE53A/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"16": [
"https://images.steamusercontent.com/ugc/2140958177135759832/1ADADF954660E5DD90C4857F582F714F2E826A55/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"17": [
"https://images.steamusercontent.com/ugc/2140958177135910792/AFD91F876679EC04919CD822C55ECD25807C8799/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"18": [
"https://images.steamusercontent.com/ugc/2140958177135784031/D5F815103756B9D3C747BB2B7ABB0762D94FB272/",
"https://images.steamusercontent.com/ugc/2140958177136046914/5B7C3B4E8BF3379814E8C185D356BED511BFE81D/",
"https://images.steamusercontent.com/ugc/2140958177136069618/1A140671AAFEBEF9A706AACA000D1756E09B5225/",
"https://images.steamusercontent.com/ugc/2140958177136070097/0155C94B62BD63D9892146E0255F7F685202E9E5/",
"https://images.steamusercontent.com/ugc/2140958177136101138/420301D5428D7150DAD254BB90EFF9637DBDAA07/",
"https://images.steamusercontent.com/ugc/2140958177136102181/7D577E665B40A901FCCA3326640653EB5C4C6E65/",
"https://images.steamusercontent.com/ugc/2140958177136111275/2E0EB1DD4C769654138A04B9DE6DC58804D1B735/",
"https://images.steamusercontent.com/ugc/2140958177136114037/D5931E8F90958DE3CA75693F016767AAC02C9E34/",
"https://images.steamusercontent.com/ugc/2140958177136115730/ACA684EB5386F44862EF3A34DA0056C7D8ED509B/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"19": [
"https://images.steamusercontent.com/ugc/2140958177136173819/87F0A2B539658DE6AEDD4A48A62840315333C6E7/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"20": [
"https://images.steamusercontent.com/ugc/2140958177136232954/7E891DAD15B9202482027D5A02AF944D6D942C03/"
],
"21": [
"https://images.steamusercontent.com/ugc/2140958177136250486/E2E2E06267E608248851BAEE451A0EFDDC0DFF87/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"22": [
"https://images.steamusercontent.com/ugc/2140958177136279127/EF5FC86EA9713C79B5BBF9C4FCFC71BFEE6BA7FB/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"23": [
"https://images.steamusercontent.com/ugc/2140958177135761686/D8EA3BF271A12B099620C980884D31DD5BCA502B/",
"https://images.steamusercontent.com/ugc/2140958177138938509/A0B74949CFCC8D0058DD0FF5ED6E362DBC65D94B/",
"https://images.steamusercontent.com/ugc/2140958177138939198/F446514EA80167FC12D5816BA2D7854D6628E4F5/",
"https://images.steamusercontent.com/ugc/2140958177138939645/ACC84EFADCDAE266D93B6B6FD5CEF82A8B66E22E/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/",
"https://images.steamusercontent.com/ugc/2140958177136556762/225FFD74A14296925890B40193A5578B1299DFB6/"
],
"24": [
"https://images.steamusercontent.com/ugc/2140958177135758651/CDA5624B4684415250A23CE2B51E04538F92A0B0/",
"https://images.steamusercontent.com/ugc/2140958177136306499/D3886DAA8B8B4108625D7AC48FA19663E3823408/",
"https://images.steamusercontent.com/ugc/2140958177136317575/731B3D9EC271731F38832D933D2C219CC0059073/",
"https://images.steamusercontent.com/ugc/2140958177136318173/884F0C35125943218F3D5BB4553360F037C65C33/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"25": [
"https://images.steamusercontent.com/ugc/2140958177136326152/97E4F9B9244F5F45AF5283BEEF19BA0D47ADB04D/",
"https://images.steamusercontent.com/ugc/2140958177136335574/4E29A232BE1405A43384DCE389E660672860E038/",
"https://images.steamusercontent.com/ugc/2140958177136335302/A810C37F28E4C51A9EE589511B9614070046B9FB/",
"https://images.steamusercontent.com/ugc/2140958177136335818/539C603F19D06579B9DD671E39E313B9E8494525/",
"https://images.steamusercontent.com/ugc/2140958177136340818/30A3C27578B65038D6A638BD24B09419F47519BF/",
"https://images.steamusercontent.com/ugc/2140958177136357008/CD39D24C0551AFE497908F3663503831165EED68/",
"https://images.steamusercontent.com/ugc/2140958177136357970/CFA864B252691461EFBE45AB970DCE13E7190AEE/",
"https://images.steamusercontent.com/ugc/2140958177136359533/70AC04F772E0D0855F9D8B040F28FE619F508967/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"26": [
"https://images.steamusercontent.com/ugc/2140958177136369228/0DF7F365A8755CF88FF86A8276B9222FDBB82F57/",
"https://images.steamusercontent.com/ugc/2140958177136376202/6D3E8B726071028CA7BE0046277E41411C57BB99/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"27": [
"https://images.steamusercontent.com/ugc/2140958177136463315/0A4BFB811C061E224D76C168346036A56B6DF9D9/",
"https://images.steamusercontent.com/ugc/2140958177136469704/2AF929A85B11F984104CDBFDF20F2337525BF2E4/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"28": [
"https://images.steamusercontent.com/ugc/2140958177135758074/4144C41DE9B6FD8F4E20EC85A2DA7FFA654E457B/",
"https://images.steamusercontent.com/ugc/2140958177136481153/1A7A6CF09BDB63377C45F49CD8B82FF16C27F78B/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"29": [
"https://images.steamusercontent.com/ugc/2140958177136533725/FD82DCC80A0091EFFC0B4807550E4C572FF3BD69/",
"https://images.steamusercontent.com/ugc/2140958177136495694/9F18A7132965C9D3D4794711C7CA1F1C67663692/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"30": [
"https://images.steamusercontent.com/ugc/2140958177135760330/59252565727727DAB7F995D692CC70B850F1F560/",
"https://images.steamusercontent.com/ugc/2140958177136517688/03B517E0FDA1DD9AEEEA02746BDEEBAE2101B0B7/",
"https://images.steamusercontent.com/ugc/2140958177136521561/BFEE9C0D8F085A3F91285784B09666747771931A/",
"https://images.steamusercontent.com/ugc/2140958177136521741/03A5065DF1B5316CC3828CACFE80ED88308EE8D7/",
"https://images.steamusercontent.com/ugc/2140958177136591521/D6C0DD6A85100F4B701CC90790A90547FFE8A5C5/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"31": [
"https://images.steamusercontent.com/ugc/2140958177136595643/2EBC1BBBFB7363D23750458BB71B2C0E327E90F5/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"32": [
"https://images.steamusercontent.com/ugc/2140958177138681898/A576D1518491AF79CF79B72240F7647D13218F49/",
"https://images.steamusercontent.com/ugc/2140958177138675405/CA3D98BBD57FEAAA9E343E3449AF8E0EAA237602/",
"https://images.steamusercontent.com/ugc/2140958177138675738/247C45C6B1D55E086BCEA51AB712A20197511926/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"33": [
"https://images.steamusercontent.com/ugc/2140958177138684378/C931BF409EF81B2769A57AA30B2C480F433130E6/",
"https://images.steamusercontent.com/ugc/2140958177138700455/5284F920FDA6CA85D8099980CFDDE2DD3B0F0A43/",
"https://images.steamusercontent.com/ugc/2140958177138706115/7EECB9B33A9782D91924A7439974031104E6D2EF/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"34": [
"https://images.steamusercontent.com/ugc/2140958177138713012/FFC33FEA695E0CFC0A4E588A6DA8D2DD2738E8DF/",
"https://images.steamusercontent.com/ugc/2140958177138720963/30C6653838DC5FBD89363D226FB6B43FCD1CD789/",
"https://images.steamusercontent.com/ugc/2140958177138721348/D981984D99AE45EA46C75361043AD3E53C7139AE/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"35": [
"https://images.steamusercontent.com/ugc/2140958177138827859/F04B8B21FD970B720B3EFE532CCF0E3F09A8711D/",
"https://images.steamusercontent.com/ugc/2140958177138823423/F99FE3B53E49D8AD5837E7842ACE632A2F0051D0/",
"https://images.steamusercontent.com/ugc/2140958177138823748/06754D9C8F7CF65B9D0C74F17FBC54E6FA8071CE/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"36": [
"https://images.steamusercontent.com/ugc/2140958177138834182/41AC554FB65132522963BBB74570611C47C434F4/",
"https://images.steamusercontent.com/ugc/2140958177136521561/BFEE9C0D8F085A3F91285784B09666747771931A/",
"https://images.steamusercontent.com/ugc/2140958177136521741/03A5065DF1B5316CC3828CACFE80ED88308EE8D7/",
"https://images.steamusercontent.com/ugc/2140958177138843407/93DB80DBDAFEF28683EC5E536DDD8DB20CBFD5AE/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"37": [
"https://images.steamusercontent.com/ugc/2140958177138847978/D047A74CFC1DC5B02053E446B2EFF5E1EC5FBEB3/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"38": [
"https://images.steamusercontent.com/ugc/2140958177138861862/96200A814B27AA8D4FE8666F01B16A3389055910/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"39": [
"https://images.steamusercontent.com/ugc/2140958177138871847/614116AEBDFB99D916609B7AFBEA36D21DA70C92/",
"https://images.steamusercontent.com/ugc/2140958177136306499/D3886DAA8B8B4108625D7AC48FA19663E3823408/",
"https://images.steamusercontent.com/ugc/2140958177136335818/539C603F19D06579B9DD671E39E313B9E8494525/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"40": [
"https://images.steamusercontent.com/ugc/2140958177138871847/614116AEBDFB99D916609B7AFBEA36D21DA70C92/",
"https://images.steamusercontent.com/ugc/2140958177136306499/D3886DAA8B8B4108625D7AC48FA19663E3823408/",
"https://images.steamusercontent.com/ugc/2140958177136335818/539C603F19D06579B9DD671E39E313B9E8494525/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"41": [
"https://images.steamusercontent.com/ugc/2140958177138885772/862A012B7336607FE22DC16ED700EA766190DE85/",
"https://images.steamusercontent.com/ugc/2140958177138891780/B31F84B091C9C019AA9F35EEA761092D81A4B028/",
"https://images.steamusercontent.com/ugc/2140958177138892370/7B48927D0A89321440B0D9B9B32E46D9D331828B/",
"https://images.steamusercontent.com/ugc/2140958177138892832/675A6525F9933AEBBA3596F7288F01A1A36A46BB/",
"https://images.steamusercontent.com/ugc/2140958177138893175/3E2512E8BE55F3A14A9C2C96819B2DB46D8B3674/",
"https://images.steamusercontent.com/ugc/2140958177136335574/4E29A232BE1405A43384DCE389E660672860E038/",
"https://images.steamusercontent.com/ugc/2140958177136335302/A810C37F28E4C51A9EE589511B9614070046B9FB/",
"https://images.steamusercontent.com/ugc/2140958177136335818/539C603F19D06579B9DD671E39E313B9E8494525/",
"https://images.steamusercontent.com/ugc/2140958177136340818/30A3C27578B65038D6A638BD24B09419F47519BF/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"42": [
"https://images.steamusercontent.com/ugc/2140958177138962742/92C53189E3353645067850DDF1F6F5A4E128FAAF/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"43": [
"https://images.steamusercontent.com/ugc/2140958177139102828/A866CC1EB85E9E5803E45D49B79AF7A4138BEAA6/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"44": [
"https://images.steamusercontent.com/ugc/2140958177139112017/DD9E4F8043B10C1EFD5B907461D3215214221EB0/"
],
"45": [
"https://images.steamusercontent.com/ugc/2140958177135761356/16A503FABA4B31252AA95DB1AF36B9BFE048E616/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"46": [
"https://images.steamusercontent.com/ugc/2140958177139142157/9013B6B054ED165E9C8E33E7120454611D0D9AA9/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"47": [
"https://images.steamusercontent.com/ugc/2140958177139129560/253AF7994FDBE98D9BE471BF43F2382832B8F001/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"49": [
"https://images.steamusercontent.com/ugc/2140958177139132973/BEF78F1D1BD36AE986D90E28DE4711BA1AAC5588/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"50": [
"https://images.steamusercontent.com/ugc/2198379706926301353/7508D70EC3C479089F0462DCC63E51F65D830320/",
"https://images.steamusercontent.com/ugc/2140958177136160240/DB3FB14642EB5F120A00CB0AF5FC3EBA45B219D4/"
],
"51": [
"https://images.steamusercontent.com/ugc/2140958177139160810/3D2648816435D33E890BD394EFBF190981BFAE0A/",
"https://images.steamusercontent.com/ugc/2140958177139206166/E2CDBD01848B7ADBEA4CE42C54199F1385E13FD6/"
]
}

359
build/steam_official.json Normal file
View file

@ -0,0 +1,359 @@
[
{
"api": "ach_002_001",
"name": "Lights Out",
"desc": "Defeat Olga Gurlukovich",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/fdb31584a0dfda90d7f2cbdd8a5a3b5d95a5c898.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/b4c69dfb9f308c80d5e142328e029f80a25e4a2d.jpg"
},
{
"api": "ach_002_002",
"name": "Spaghetti Cinema",
"desc": "Meet Revolver Ocelot",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/68871078209c6c7dfd55865cf043a0aa2e9efd62.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/e8b643bf3e0d8eb8b8ee346a4a5e378c10cb6192.jpg"
},
{
"api": "ach_002_003",
"name": "Bomb Squad",
"desc": "Learn how to defuse C4 bombs from Peter Stillman",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/a4e9e79259ba5dc1bea26dc1005c33a8d427b1bd.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/130ec44733537766bb51e9141df427a82ce981a7.jpg"
},
{
"api": "ach_002_004",
"name": "Lady Fortune",
"desc": "Survive Fortune's attack",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/3762f2a56bdf3b369932f454b8eab419efa3e2a7.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/96b1986f22e9ebe1101f0c83c66450e664214a70.jpg"
},
{
"api": "ach_002_005",
"name": "Party's Over",
"desc": "Defeat Fatman",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/347ec4ab6c5b03a8d3f91ba3bbc0c53038e2bfad.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/252e984a3d3493beef544687d31276fe612cf1b1.jpg"
},
{
"api": "ach_002_006",
"name": "Thanks, Ames",
"desc": "Learn the location of the president",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/a692b1b7ab0bff687b1e0208c86fc612e39c05da.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/23cf3525669772383cf66b1e57bea37ab12082f1.jpg"
},
{
"api": "ach_002_007",
"name": "No-Fly Zone",
"desc": "Destroy the AV-88 Harrier II",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/399da5a7a516d3c54b7421aac4bcf8f3e82d8da8.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/ce801c180cefd4c5ab609cbbc75cbdcc2b58fa08.jpg"
},
{
"api": "ach_002_008",
"name": "Bohemian Candidate",
"desc": "Meet President James Johnson",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/1183dfba0aa94cbe242a333ee42f42ebb8b5eb92.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/21464bb7546b0a59f1cccf6fcd8c8611d77f2aab.jpg"
},
{
"api": "ach_002_009",
"name": "Vampire Slayer",
"desc": "Defeat Vamp",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/4ddbe2cc6dbb8be07b577b594d4d611cf7267285.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/703a8e589aa527dd49b31eaf4a7b530cea10b0e8.jpg"
},
{
"api": "ach_002_010",
"name": "I Think You Need a Hug, E",
"desc": "Find Emma Emmerich",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/ce94d72fe726f63ab8c39b87d37079100b27ed64.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/015b984d648f63678a252a1fac8919fa5526407b.jpg"
},
{
"api": "ach_002_011",
"name": "Sharing Is Caring",
"desc": "Befriend Olga Gurlukovich",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/71bfeca4f68ab6284e06ebb89e6d924974756def.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/e0bd8c1b35b21f111bcf228ec81254a810fd89c3.jpg"
},
{
"api": "ach_002_012",
"name": "No Ray, Jos\u00e9",
"desc": "Defeat Metal Gear RAY",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/d2465273af8ccb1f9d18c81dbe3c78dc2809e0ad.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/4c7471a58d991cef47f13936bcfa18b0fb235568.jpg"
},
{
"api": "ach_002_013",
"name": "Another Snake Bites the Dust",
"desc": "Defeat Solidus Snake",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/1597d98426911867db9181733daf80dcf6aa2ef5.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/0cafb503d1113a8a25fe013f7f7ed6059e25bf81.jpg"
},
{
"api": "ach_002_014",
"name": "A Cut Above",
"desc": "Beat the Tanker and Plant chapters on any difficulty",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/d47b5e4375d622de4f1ed6a75cc25b94f250631a.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/2516839f772deb2d9f90d6252883cec316026019.jpg"
},
{
"api": "ach_002_015",
"name": "Hurt Locker",
"desc": "Put an enemy in a locker",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/5ddefa0aab0bda7e443a6c1bc71aadcacc272afc.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/632a7ae849c31088c0c94ee025faf1ebd2d64826.jpg"
},
{
"api": "ach_002_016",
"name": "To Catch a Predator",
"desc": "Lure a guard with a girlie magazine",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/65e9e9ea191e67401199a0684b1e7f5d74f56c70.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/5af6f19eaa57648f32d2a673c8648dbbbd89b3b4.jpg"
},
{
"api": "ach_002_017",
"name": "Animal Control",
"desc": "Collect a dog tag",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/a41d9c387924efbccd2e393e28e027e8173cc919.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/58f95276b2060ed23503d3029be6955d566ebdc9.jpg"
},
{
"api": "ach_002_018",
"name": "Bye Bye Big Brother",
"desc": "Destroy 5 cameras",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/478c9da91a1d51a70ef0128e063e79fa6e375fa3.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/93924a6b6973e87744d2fc513912afa4ee1e0429.jpg"
},
{
"api": "ach_002_019",
"name": "Down in Smoke",
"desc": "Disorient an enemy with a cloud of smoke from a fire extinguisher",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/809aafc45e32bcb2b0eb2920a849a48382d2c2ef.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/ce6f7ef705d1a03a7b7858bda07dca519705f280.jpg"
},
{
"api": "ach_002_020",
"name": "Silence is Golden",
"desc": "Shoot an enemy radio",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/7215add8e22790ff333261a873031392708b3c39.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/7ee1d3dd747c43a4c92bf0c5608ad439cf211509.jpg"
},
{
"api": "ach_002_021",
"name": "Caution: Hot",
"desc": "Down an enemy with steam from a pipe",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/b6a6c5205ee79aadd29635a13bb24f9f0b682572.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/496b3a748d86af160ea395147b0a1354bf71b154.jpg"
},
{
"api": "ach_002_022",
"name": "Kissing Booth",
"desc": "Kiss a poster in a locker",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/9549ec76129179205a1f04956739127732c2d1c7.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/c38059bec39ef2b44ffd4bb596dc5a623b9d461b.jpg"
},
{
"api": "ach_002_023",
"name": "Snake Beater",
"desc": "Get caught by Otacon stimulating yourself",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/9529ccf40a625cf889ed14390b8cb22d9b062f52.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/96300d1fb720d7b58a33d13c163a4c89ff2ca04a.jpg"
},
{
"api": "ach_002_024",
"name": "Who Ya Gonna Call?",
"desc": "Take a clear photograph of the ghost image in Hold No. 2",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/ce7918e70cb624ff586d6b4140f64b7699ff861b.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/6f8340f4578a853a636ecada74700344d289bf50.jpg"
},
{
"api": "ach_002_025",
"name": "Sexting",
"desc": "Send Otacon a picture of the marine with no pants",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/a5102d12a6af702c2b00e5dbf4d0f9ff6216e51d.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/c2c05a07037b8efd3314366925c478705fd595af.jpg"
},
{
"api": "ach_002_026",
"name": "My Rations...",
"desc": "Get sea lice all over your rations",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/2f89e97c000a86cbc727140c6814d4dff52222f6.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/2ce4dbf73f55e341a6d1f590d7397798d52b0563.jpg"
},
{
"api": "ach_002_027",
"name": "Johnny on the Spot",
"desc": "Hear Johnny's bowel noises in two locations",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/01613f9d4e3e383041991d6b35ccceb5e1e8329d.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/a219a7872544e9204279ac48a0a40aa742d66cfe.jpg"
},
{
"api": "ach_002_028",
"name": "Vamp Eyer",
"desc": "Catch a glimpse of Vamp standing in the streets of New York during the end cinematic",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/43f497d0a44802fb02aa8a67ac36fea673410103.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/d23f29e71307ddacbef81d7e8296c99802477931.jpg"
},
{
"api": "ach_002_029",
"name": "Love Hurts",
"desc": "Watch Rose kill Raiden on a rooftop",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/a237e2444cd8a18ff6304d900706d398abeb0975.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/f332074bcbfc00709ff95ff342610ed46486d4ec.jpg"
},
{
"api": "ach_002_030",
"name": "Hold Up-aholic",
"desc": "Hold up 30 enemies",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/01a022d0275fc32c962c181cc4e6e6c8e5d5bdd4.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/390d39ecf9b5556aed88dca4232e4c146d5f7fed.jpg"
},
{
"api": "ach_002_031",
"name": "Don't Taze Me, Bro",
"desc": "Tranquilize 100 enemies",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/dec9ece268f240d43a4b98f54f3885697cfbdb52.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/7fc5429ed921eb4230b9734562b5f11d89c91a3e.jpg"
},
{
"api": "ach_002_032",
"name": "Nothing Personal",
"desc": "Break the neck of 30 enemies",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/f80f2904fc26a0f9117acdd457612aa74d736204.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/eb13d7e61966699c9487d18e52ef35233008d7bd.jpg"
},
{
"api": "ach_002_033",
"name": "Rent Money",
"desc": "Beat 30 enemies unconscious",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/63dd05f9c0a389207b3cf6598f8b933828536b7b.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/1edaaae68cb2dbe636b6908e85110f86672ab30e.jpg"
},
{
"api": "ach_002_034",
"name": "Steel Grip",
"desc": "Attain grip level 3",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/4fc9f8e76dd89b2562a91f401e5e01d46fb94275.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/023d543945379f7c72fc43b707bc8aea6dd08592.jpg"
},
{
"api": "ach_002_035",
"name": "Photo Finish",
"desc": "Acquire the digital camera",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/c7957f06b5a10675d9dc8dd120f6ac767abd00a6.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/e56f1438a03dfc191d6d392df717cb82d0547bea.jpg"
},
{
"api": "ach_002_036",
"name": "Weapon Completionist: Tanker",
"desc": "Collect every type of weapon on the Tanker",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/0bee432cd0a2e3e155b80e4707f22fec2cd4622c.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/51e17e215e4ad597765e7bb149e05ef47b811db5.jpg"
},
{
"api": "ach_002_037",
"name": "Weapon Completionist: Plant",
"desc": "Collect every type of weapon in the Plant",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/39cd0e9c033be1c3f35bd9766db91b2212fbbbff.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/38fec5a6dfd55c407d1c47209c64241faab09d47.jpg"
},
{
"api": "ach_002_038",
"name": "Moving Day",
"desc": "Collect all boxes",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/2191f938c7f6f8f8859c5b7ef7270b160b58f721.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/507b962bfc9510a624693b1de707b7f85a15c7c0.jpg"
},
{
"api": "ach_002_039",
"name": "Casting Theater",
"desc": "Watch something in the Casting Theater",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/eee75e572b90f25084d80366a404a99e556c4ca2.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/4e5e23b8e806d28f0878bb7c9864bda910448de2.jpg"
},
{
"api": "ach_002_040",
"name": "No Boss of Mine",
"desc": "Complete Boss Survival",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/c3e5316d317ccf5034e4890154f8ca0c609c2035.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/01740255166cdc3d7c718af089673e71bfd582f5.jpg"
},
{
"api": "ach_002_041",
"name": "Tell Me a Tale",
"desc": "Complete at least one of the Snake Tales",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/d08eaf1921b4a0577a45dbbf4fb6d57558119b50.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/5cfedcb12514038d53e000ab845e82887e09232a.jpg"
},
{
"api": "ach_002_042",
"name": "Piece of Cake",
"desc": "Complete a VR or Alternative mission",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/5cde39f4f9d31b01892f97af7900b458976b9745.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/bdc4edf9b9f27e6e04552c63aac178cd42e7e66f.jpg"
},
{
"api": "ach_002_043",
"name": "In It to Win It",
"desc": "Place first in 50 different VR/Alternative missions",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/499a68ad66798e03878b97c533c2916ce1a99267.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/673969ecc766e29ead32e4cc7062c5a1119531f5.jpg"
},
{
"api": "ach_002_044",
"name": "Yorkie",
"desc": "Get the bandana",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/61a98b9e851cd7fbfa2881fa4db78142711dd645.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/5bb81962cf1854a2263fdf6d4c0b2ec39a2ada9f.jpg"
},
{
"api": "ach_002_045",
"name": "Poodle",
"desc": "Get the Tanker stealth suit",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/0f5bfb26ee2815729f3da6439617b8f5dc2d1a0a.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/6b9a433c6fb860461968a720eb5847a6db6dabab.jpg"
},
{
"api": "ach_002_046",
"name": "Beagle",
"desc": "Get the infinity wig",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/a39a765308506f9e6357494a2562eb70fa0bb1e2.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/56aad902b7864f16474a720c03dac4c511b9627d.jpg"
},
{
"api": "ach_002_047",
"name": "Shiba Inu",
"desc": "Get the Plant stealth suit",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/8828a75f84b0d205b40ea0df59f2f0a7191081fd.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/e38e65ae480313a31c3b90d209ea1616dcc6c2de.jpg"
},
{
"api": "ach_002_048",
"name": "Pacifist Run",
"desc": "Clear the game without killing anyone",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/701310b71e1cf13325280599ff63f615735649fe.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/d6706e31b8f95986c2724d628d2363da8f1f2012.jpg"
},
{
"api": "ach_002_049",
"name": "Complete Stealth",
"desc": "Clear the game without entering Alert Mode (not including events where Alert Mode is mandatory)",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/ddddc67b62ecf346e3fa6538975b84106dbdbf4a.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/0b60f6b99b2bdce2ec5532b92e93c2a69ca2c9fa.jpg"
},
{
"api": "ach_002_050",
"name": "Big Boss",
"desc": "Earn the title of Big Boss",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/77f0da41c2df32a10f211b40e924cb8d9b262b0a.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/fa74a0fa7d43970f6c01e706216f6cd775154025.jpg"
},
{
"api": "ach_002_051",
"name": "Cover-to-Cover Solid 2: Books of the Patriots",
"desc": "Read all the pages in the Screenplay Book",
"icon": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/6dfaffcb025712c398b5017e3edc6c730e8e9abf.jpg",
"iconLocked": "https://shared.akamai.steamstatic.com/community_assets/images/apps/2131640/00f81475056260510b0c9bb54b93952f289b3b70.jpg"
}
]

1582
build/template.html Normal file

File diff suppressed because it is too large Load diff