SOURCE / PINNED RELEASE
Made of little things.
Black Site
- Release
- 666bc7c74ebd…
- Author-recorded commit
- 05e95af79676…
- License
- LICENSE
- Author’s source reference
- nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-d2225ddc391
Archive hash verified: 1544e072e245c4a8…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
// Plays the built napplet inside a sandboxed iframe (allow-scripts, srcdoc) the way a host
// shows it, with no shell injected (so optional domains like storage are absent), and saves
// screenshots to scripts/shots/. Usage: bun scripts/smoke-frame.mjs
import { spawn } from 'node:child_process';
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = dirname(dirname(fileURLToPath(import.meta.url)));
const out = join(root, 'scripts', 'shots');
mkdirSync(out, { recursive: true });
const CHROME = process.env.CHROME || 'C:/Program Files/Google/Chrome/Application/chrome.exe';
const html = readFileSync(join(root, 'dist', 'index.html'), 'utf8');
const hostDir = mkdtempSync(join(tmpdir(), 'bsn-host-'));
const hostFile = join(hostDir, 'host.html');
const attr = html.replace(/&/g, '&').replace(/"/g, '"');
writeFileSync(hostFile, `<!doctype html><html><body style="margin:0;background:#222">
<iframe id="f" sandbox="allow-scripts" style="border:0;width:100vw;height:100vh;display:block" srcdoc="${attr}"></iframe></body></html>`);
const port = 9335;
const chrome = spawn(CHROME, ['--headless=new', `--remote-debugging-port=${port}`, `--user-data-dir=${mkdtempSync(join(tmpdir(), 'bsn-chrome-'))}`, '--autoplay-policy=no-user-gesture-required', '--disable-site-isolation-trials', '--disable-features=IsolateOrigins,site-per-process', 'about:blank'], { stdio: 'ignore' });
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
let ws, id = 0;
const pending = new Map(), errors = [], contexts = [];
const send = (method, params = {}) => new Promise((res, rej) => { const i = ++id; pending.set(i, { res, rej }); ws.send(JSON.stringify({ id: i, method, params })); });
const shot = async (name) => { const r = await send('Page.captureScreenshot', { format: 'png' }); writeFileSync(join(out, name), Buffer.from(r.data, 'base64')); };
const mouse = async (x, y) => { for (const type of ['mousePressed', 'mouseReleased']) await send('Input.dispatchMouseEvent', { type, x, y, button: 'left', clickCount: 1 }); };
// evaluate inside the napplet frame
const inFrame = async (expr) => {
const ctx = contexts.find((c) => c.origin === 'null' || c.auxData?.frameId !== topFrame);
const r = await send('Runtime.evaluate', { expression: expr, contextId: ctx.id, returnByValue: true });
if (r.exceptionDetails) throw new Error(JSON.stringify(r.exceptionDetails).slice(0, 300));
return r.result.value;
};
let topFrame = null;
try {
let targets;
for (let i = 0; i < 50; i++) { try { targets = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json(); if (targets.length) break; } catch {} await sleep(200); }
ws = new WebSocket(targets.find((t) => t.type === 'page').webSocketDebuggerUrl);
await new Promise((r) => ws.addEventListener('open', r));
ws.addEventListener('message', (ev) => {
const m = JSON.parse(ev.data);
if (m.id && pending.has(m.id)) { const p = pending.get(m.id); pending.delete(m.id); m.error ? p.rej(new Error(m.error.message)) : p.res(m.result); }
if (m.method === 'Runtime.exceptionThrown') errors.push(m.params.exceptionDetails.exception?.description || m.params.exceptionDetails.text);
if (m.method === 'Runtime.consoleAPICalled' && m.params.type === 'error') errors.push(m.params.args.map((a) => a.value || a.description).join(' '));
if (m.method === 'Runtime.executionContextCreated') contexts.push(m.params.context);
});
await send('Page.enable'); await send('Runtime.enable');
await send('Emulation.setDeviceMetricsOverride', { width: 1280, height: 800, deviceScaleFactor: 1, mobile: false });
const nav = await send('Page.navigate', { url: 'file:///' + hostFile.replace(/\\/g, '/') });
topFrame = nav.frameId;
await sleep(2500);
await shot('1-menu.png');
const menu = await inFrame(`({ sandboxed: (() => { try { return window.parent.document && false; } catch (e) { return true; } })(), napplet: typeof window.napplet, menuShown: document.getElementById('menu').classList.contains('on'), funds: document.getElementById('bank').textContent })`);
console.log('menu', JSON.stringify(menu));
const deploy = await inFrame(`(() => { const r = document.querySelector('#menu [data-go=deploy]').getBoundingClientRect(); return [r.x + r.width / 2, r.y + r.height / 2]; })()`);
await mouse(deploy[0], deploy[1]);
await sleep(2500);
await send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: 900, y: 300 });
await sleep(500);
await shot('2-playing.png');
const canvasInk = await inFrame(`(() => { const c = document.getElementById('game'); const x = c.getContext('2d'); const d = x.getImageData(0, 0, c.width, c.height).data; let lit = 0; for (let i = 0; i < d.length; i += 4 * 97) if (d[i] + d[i + 1] + d[i + 2] > 60) lit++; return lit; })()`);
const playing = await inFrame(`({ menuHidden: !document.getElementById('menu').classList.contains('on'), hudText: !!document.getElementById('game') })`);
console.log('playing', JSON.stringify({ ...playing, litSamples: canvasInk }));
// the game saves through the store adapter even with no shell storage (session only)
const saves = await inFrame(`(() => { const s = globalThis.BLACKSITE_STORE; s.set('blacksite.map', 'docks'); return s.get('blacksite.map', 'town'); })()`);
console.log('session store works', saves === 'docks');
await send('Emulation.setDeviceMetricsOverride', { width: 420, height: 300, deviceScaleFactor: 1, mobile: false });
await sleep(600);
await shot('3-too-small.png');
const notice = await inFrame(`getComputedStyle(document.getElementById('tooSmall')).display`);
console.log('too-small notice at 420x300:', notice);
await send('Emulation.setDeviceMetricsOverride', { width: 1280, height: 800, deviceScaleFactor: 1, mobile: false });
await sleep(600);
console.log('notice hidden again at 1280x800:', (await inFrame(`getComputedStyle(document.getElementById('tooSmall')).display`)) === 'none');
console.log(errors.length ? 'ERRORS:\n' + errors.join('\n') : 'no page errors');
} finally {
ws?.close();
chrome.kill();
}
process.exit(errors.length ? 1 : 0);
