MGS2 Master Collection 100% companion
A single-file companion page for 100%-ing Metal Gear Solid 2: Sons of Liberty (Master Collection Version), plus an optional local helper for live Steam sync. What's in it: - Roadmap: the 5-pass plan plus a 48-step ordered walkthrough of the Tanker and Plant, with per-step sub-checklists for dog tags, achievements and items - Achievements: all 51 with official Steam icons, missable/cumulative flags, filters, search and a full how-to for each - Dog Tags: all 394 across 10 difficulty lists, with 81 conditional-spawn notes - Area Maps: 10 guard-position maps rebuilt cell-by-cell from the community tracking spreadsheet, zoomable, with a label-size control - Photos: 48 location screenshots pinned to the step where each thing is, with cross-source duplicates removed by perceptual hash - Big Boss Run, Missables, VR/Extra Modes, Sources & Notes - Steam sync: ticks what you already own from any public profile - 10 dark colour themes, contrast-checked The page is one self-contained HTML file with no build step or dependencies. Compiled from community guides (Steam Community, Video Chums, u/Spikeyroxas, Dayngl's Guides, TrueSteamAchievements) - see README for full credits.
This commit is contained in:
commit
f558eb439e
6 changed files with 2109 additions and 0 deletions
138
steam-proxy-worker.js
Normal file
138
steam-proxy-worker.js
Normal 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.
|
||||
*
|
||||
* Optional: change ALLOW_ORIGIN below from '*' to your page's origin to stop anyone else
|
||||
* pointing their own page at your Worker.
|
||||
*
|
||||
* 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 = '*'; // e.g. 'https://yourname.github.io'
|
||||
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,
|
||||
}));
|
||||
},
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue