blockyard 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +929 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +191 -4
- package/SECURITY.md +38 -0
- package/bin/blockyard.js +41 -0
- package/config/pool-map.json +2620 -0
- package/docs/API.md +1577 -0
- package/docs/ARCHITECTURE.md +1394 -0
- package/docs/AUTO-UPDATE.md +269 -0
- package/docs/CONFIGURATION.md +847 -0
- package/docs/DEFECTS.md +813 -0
- package/docs/EFFECTS-AGENTS.md +448 -0
- package/docs/GETTING-STARTED.md +205 -0
- package/docs/INSTALL.md +547 -0
- package/docs/MEASUREMENTS.md +1401 -0
- package/docs/RULES.md +681 -0
- package/docs/SECURITY-AUDIT-2026-09-14.md +177 -0
- package/docs/SECURITY-AUDIT.md +258 -0
- package/docs/SECURITY.md +212 -0
- package/docs/TROUBLESHOOTING.md +332 -0
- package/docs/USER-GUIDE.md +1262 -0
- package/package.json +53 -5
- package/public/404.html +9 -0
- package/public/css/app.css +2009 -0
- package/public/donate-qr.png +0 -0
- package/public/index.html +1085 -0
- package/public/js/about.js +112 -0
- package/public/js/agents.js +1141 -0
- package/public/js/app.js +1386 -0
- package/public/js/arkanoid.js +806 -0
- package/public/js/blockanoid.js +347 -0
- package/public/js/blockout.js +347 -0
- package/public/js/blockpack.js +428 -0
- package/public/js/blockscene3d.js +2830 -0
- package/public/js/breakout.js +224 -0
- package/public/js/charts.js +635 -0
- package/public/js/depthchart.js +315 -0
- package/public/js/details3d.js +4342 -0
- package/public/js/doom.js +31 -0
- package/public/js/dosaudio.js +48 -0
- package/public/js/dosgame.js +389 -0
- package/public/js/dosio.js +186 -0
- package/public/js/dospc.js +1353 -0
- package/public/js/dosworker.js +196 -0
- package/public/js/explorer.js +405 -0
- package/public/js/feepalette.js +149 -0
- package/public/js/fmt.js +162 -0
- package/public/js/goggles.js +886 -0
- package/public/js/kiosk.js +41 -0
- package/public/js/login.js +88 -0
- package/public/js/markets.js +395 -0
- package/public/js/mining.js +1416 -0
- package/public/js/panels.js +970 -0
- package/public/js/pricechart.js +189 -0
- package/public/js/quake.js +20 -0
- package/public/js/settings.js +1096 -0
- package/public/js/soundcard.js +459 -0
- package/public/js/tetris.js +226 -0
- package/public/js/tetrust.js +356 -0
- package/public/js/tetsound.js +175 -0
- package/public/js/theme.js +235 -0
- package/public/js/wolf3d.js +22 -0
- package/public/js/x86.js +1978 -0
- package/public/login.html +33 -0
- package/scripts/blockfile-measure.js +156 -0
- package/scripts/browser-check.mjs +286 -0
- package/scripts/check.js +173 -0
- package/scripts/decode-check.js +81 -0
- package/scripts/doc-counts.js +109 -0
- package/scripts/donate-qr.py +23 -0
- package/scripts/dos-bench.js +56 -0
- package/scripts/fake-node.js +534 -0
- package/scripts/index-bench.js +216 -0
- package/scripts/index-benchmark.js +117 -0
- package/scripts/index-build.js +40 -0
- package/scripts/live-render-check.mjs +89 -0
- package/scripts/manage-users.js +132 -0
- package/scripts/motion-check.mjs +138 -0
- package/scripts/pool-map.js +157 -0
- package/scripts/setup.js +432 -0
- package/scripts/shots.mjs +278 -0
- package/scripts/smoke.sh +327 -0
- package/scripts/tls.js +31 -0
- package/scripts/ui.js +174 -0
- package/server/auth/sessions.js +221 -0
- package/server/auth/users.js +243 -0
- package/server/chain/blockfile.js +234 -0
- package/server/chain/index/build.js +210 -0
- package/server/chain/index/heights.js +36 -0
- package/server/chain/index/live.js +276 -0
- package/server/chain/index/rows.js +145 -0
- package/server/chain/index/store.js +154 -0
- package/server/chain/index/worker.js +109 -0
- package/server/chain/tx.js +310 -0
- package/server/collect/gbt.js +229 -0
- package/server/collect/logparse.js +765 -0
- package/server/collect/logtail.js +189 -0
- package/server/collect/markets.js +333 -0
- package/server/collect/mining.js +333 -0
- package/server/collect/monitor.js +2545 -0
- package/server/collect/network.js +295 -0
- package/server/collect/nextblock.js +275 -0
- package/server/collect/sync.js +386 -0
- package/server/config.js +644 -0
- package/server/http/api.js +1319 -0
- package/server/http/explorer.js +418 -0
- package/server/http/games.js +77 -0
- package/server/http/server.js +420 -0
- package/server/http/sse.js +176 -0
- package/server/http/static.js +212 -0
- package/server/main.js +673 -0
- package/server/netinfo.js +253 -0
- package/server/rpc/allowlist.js +130 -0
- package/server/rpc/client.js +414 -0
- package/server/store/audit.js +148 -0
- package/server/store/history.js +220 -0
- package/server/store/ledger.js +290 -0
- package/server/store/ring.js +173 -0
- package/server/tls/selfsigned.js +160 -0
- package/server/util/fmt.js +29 -0
- package/systemd/blockyard.service +102 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Replay whole blocks from a live node through server/chain/tx.js and compare every field against the
|
|
3
|
+
// node's own verbose decoding. A decoder is only as good as the last time it was checked against Core.
|
|
4
|
+
//
|
|
5
|
+
// node scripts/decode-check.js [--node <id>] [--blocks N] [--from <height>] [--heights h1,h2,...]
|
|
6
|
+
//
|
|
7
|
+
// Fetches each block twice, as raw bytes (getblock <hash> 0) and as Core's JSON (getblock <hash> 2),
|
|
8
|
+
// decodes the bytes, and reports every disagreement plus the count of each script type seen. Read-only
|
|
9
|
+
// RPC, through the same client and lane the monitor uses.
|
|
10
|
+
import { loadConfig } from '../server/config.js';
|
|
11
|
+
import { RpcClient } from '../server/rpc/client.js';
|
|
12
|
+
import { decodeBlock } from '../server/chain/tx.js';
|
|
13
|
+
|
|
14
|
+
const arg = (name, def) => { const i = process.argv.indexOf(`--${name}`); return i > 0 ? process.argv[i + 1] : def; };
|
|
15
|
+
const cfg = loadConfig();
|
|
16
|
+
const node = cfg.nodes.find((n) => n.id === arg('node', null)) ?? cfg.nodes[0];
|
|
17
|
+
const quiet = { info() {}, warn() {}, error() {}, debug() {} };
|
|
18
|
+
const rpc = new RpcClient(node, { ...(cfg.rpc ?? {}), ...(node.rpc ?? {}) }, { log: quiet });
|
|
19
|
+
const call = async (method, params, timeoutMs = 120_000) => {
|
|
20
|
+
const [r] = await rpc.batch([{ method, params }], { key: `decode-check:${method}:${params[0]}`, priority: 5, maxWaitMs: 120_000, timeoutMs });
|
|
21
|
+
if (!r.ok) throw new Error(`${method}: ${r.error?.message}`);
|
|
22
|
+
return r.result;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const info = await call('getblockchaininfo', []);
|
|
26
|
+
const count = Math.max(1, Number(arg('blocks', 3)));
|
|
27
|
+
const from = Number(arg('from', info.blocks - count + 1));
|
|
28
|
+
const sat = (btc) => Math.round(btc * 1e8);
|
|
29
|
+
const mismatches = [];
|
|
30
|
+
const types = {};
|
|
31
|
+
let txs = 0, inputs = 0, outputs = 0, bytes = 0, decodeMs = 0;
|
|
32
|
+
const differ = (where, got, want) => { if (got !== want && mismatches.length < 50) mismatches.push(`${where}: decoded ${JSON.stringify(got)}, node ${JSON.stringify(want)}`); return got !== want; };
|
|
33
|
+
|
|
34
|
+
const heights = arg('heights', null) ? arg('heights').split(',').map(Number) : Array.from({ length: count }, (_, i) => from + i);
|
|
35
|
+
for (const h of heights) {
|
|
36
|
+
const hash = await call('getblockhash', [h]);
|
|
37
|
+
const raw = await call('getblock', [hash, 0]);
|
|
38
|
+
const want = await call('getblock', [hash, 2], 300_000);
|
|
39
|
+
const buf = Buffer.from(raw, 'hex');
|
|
40
|
+
const t0 = performance.now();
|
|
41
|
+
const got = decodeBlock(buf, info.chain);
|
|
42
|
+
decodeMs += performance.now() - t0;
|
|
43
|
+
bytes += buf.length;
|
|
44
|
+
for (const k of ['hash', 'version', 'previousblockhash', 'merkleroot', 'time', 'bits', 'nonce', 'size', 'nTx']) differ(`block ${h} ${k}`, got[k], want[k]);
|
|
45
|
+
want.tx.forEach((w, i) => {
|
|
46
|
+
const g = got.tx[i];
|
|
47
|
+
const at = `block ${h} tx ${i} (${w.txid.slice(0, 12)})`;
|
|
48
|
+
txs++;
|
|
49
|
+
for (const k of ['txid', 'hash', 'version', 'size', 'vsize', 'weight', 'locktime']) differ(`${at} ${k}`, g[k], w[k]);
|
|
50
|
+
differ(`${at} vin count`, g.vin.length, w.vin.length);
|
|
51
|
+
w.vin.forEach((wi, j) => {
|
|
52
|
+
const gi = g.vin[j] ?? {};
|
|
53
|
+
inputs++;
|
|
54
|
+
if (wi.coinbase != null) differ(`${at} vin ${j} coinbase`, gi.coinbase, wi.coinbase);
|
|
55
|
+
else {
|
|
56
|
+
differ(`${at} vin ${j} txid`, gi.txid, wi.txid);
|
|
57
|
+
differ(`${at} vin ${j} vout`, gi.vout, wi.vout);
|
|
58
|
+
differ(`${at} vin ${j} scriptSig`, gi.scriptSig?.hex, wi.scriptSig?.hex);
|
|
59
|
+
}
|
|
60
|
+
differ(`${at} vin ${j} sequence`, gi.sequence, wi.sequence);
|
|
61
|
+
differ(`${at} vin ${j} witness`, JSON.stringify(gi.txinwitness ?? null), JSON.stringify(wi.txinwitness ?? null));
|
|
62
|
+
});
|
|
63
|
+
differ(`${at} vout count`, g.vout.length, w.vout.length);
|
|
64
|
+
w.vout.forEach((wo, j) => {
|
|
65
|
+
const go = g.vout[j] ?? { scriptPubKey: {} };
|
|
66
|
+
outputs++;
|
|
67
|
+
types[wo.scriptPubKey.type] = (types[wo.scriptPubKey.type] ?? 0) + 1;
|
|
68
|
+
differ(`${at} vout ${j} value`, go.value_sat, sat(wo.value));
|
|
69
|
+
differ(`${at} vout ${j} n`, go.n, wo.n);
|
|
70
|
+
differ(`${at} vout ${j} hex`, go.scriptPubKey.hex, wo.scriptPubKey.hex);
|
|
71
|
+
differ(`${at} vout ${j} type`, go.scriptPubKey.type, wo.scriptPubKey.type);
|
|
72
|
+
differ(`${at} vout ${j} address`, go.scriptPubKey.address ?? null, wo.scriptPubKey.address ?? null);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(`${node.id} (${info.chain}): ${heights.length} blocks (${heights[0]}..${heights[heights.length - 1]}), ${txs} transactions, ${inputs} inputs, ${outputs} outputs, ${(bytes / 1e6).toFixed(1)} MB`);
|
|
78
|
+
console.log(`decoded in ${decodeMs.toFixed(0)} ms (${(bytes / 1e6 / (decodeMs / 1000)).toFixed(1)} MB/s, ${(txs / (decodeMs / 1000)).toFixed(0)} tx/s)`);
|
|
79
|
+
console.log('script types:', JSON.stringify(types));
|
|
80
|
+
console.log(mismatches.length ? `MISMATCHES (${mismatches.length}${mismatches.length >= 50 ? '+' : ''}):\n ${mismatches.join('\n ')}` : 'every field matches the node');
|
|
81
|
+
process.exit(mismatches.length ? 1 : 0);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// One source of truth for the test count the docs quote.
|
|
3
|
+
//
|
|
4
|
+
// Why this exists as a script rather than as a grep in a test: the documented
|
|
5
|
+
// count drifted out of sync three times on 2026-09-08, and each time the fix was
|
|
6
|
+
// a sed against a string a previous sed had already rewritten, so the replacement
|
|
7
|
+
// matched nothing and the docs went on asserting a stale number. The previous
|
|
8
|
+
// guard compared README against AGENTS.md, which only proves two documents are
|
|
9
|
+
// wrong *together* — it never looked at the suite. 183 vs 180 is exactly that
|
|
10
|
+
// failure, found by hand on 2026-09-09.
|
|
11
|
+
//
|
|
12
|
+
// So: count what the suite declares, and make the docs match it.
|
|
13
|
+
//
|
|
14
|
+
// How the count is derived, and why it is the same number `node --test` prints:
|
|
15
|
+
// every test in this repo is declared at module top level with `test('name', …)`.
|
|
16
|
+
// A nested declaration (`t.test(…)`, or an indented `test(`) would make the two
|
|
17
|
+
// numbers disagree, so the scanner reports nested declarations separately and
|
|
18
|
+
// test/doc-counts.test.js fails loudly if any appear — a scanner that silently
|
|
19
|
+
// undercounts is the same class of bug as the sed that matched nothing.
|
|
20
|
+
//
|
|
21
|
+
// node scripts/doc-counts.js # print the number
|
|
22
|
+
// node scripts/doc-counts.js --check # exit 1 if the docs disagree
|
|
23
|
+
// node scripts/doc-counts.js --fix # rewrite the docs' number
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
|
|
28
|
+
export const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
29
|
+
|
|
30
|
+
const DECL = /^(?:test|test\.skip|test\.todo|test\.only)\s*\(/;
|
|
31
|
+
|
|
32
|
+
/** Test declarations per file: top-level (counted) and nested (a scanner gap). */
|
|
33
|
+
export function scanTests({ root = ROOT, dir = 'test' } = {}) {
|
|
34
|
+
const base = path.join(root, dir);
|
|
35
|
+
const rel = (f) => path.relative(root, f).split(path.sep).join('/'); // keyed with '/', on every platform
|
|
36
|
+
const files = fs.readdirSync(base, { recursive: true, withFileTypes: true })
|
|
37
|
+
.filter((e) => e.isFile() && e.name.endsWith('.test.js'))
|
|
38
|
+
.map((e) => path.join(e.parentPath ?? e.path, e.name))
|
|
39
|
+
.sort();
|
|
40
|
+
const out = { total: 0, nested: [], perFile: {}, files: files.length };
|
|
41
|
+
for (const f of files) {
|
|
42
|
+
let top = 0;
|
|
43
|
+
for (const line of fs.readFileSync(f, 'utf8').split('\n')) {
|
|
44
|
+
const trimmed = line.trimStart();
|
|
45
|
+
if (!DECL.test(trimmed)) continue;
|
|
46
|
+
// Indented means it sits inside another test's body or a helper: a subtest.
|
|
47
|
+
if (/^\s/.test(line)) out.nested.push(`${rel(f)}: ${trimmed.slice(0, 60)}`);
|
|
48
|
+
else top += 1;
|
|
49
|
+
}
|
|
50
|
+
out.perFile[rel(f)] = top;
|
|
51
|
+
out.total += top;
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Every place the docs quote a test count. Deliberately narrow ("N tests" /
|
|
57
|
+
// "N unit tests") so a measurement like "1 test failed" in prose is not edited.
|
|
58
|
+
export const DOC_FILES = ['README.md', 'AGENTS.md'];
|
|
59
|
+
|
|
60
|
+
export function docCounts({ root = ROOT } = {}) {
|
|
61
|
+
const found = [];
|
|
62
|
+
for (const rel of DOC_FILES) {
|
|
63
|
+
const text = fs.readFileSync(path.join(root, rel), 'utf8');
|
|
64
|
+
for (const m of text.matchAll(/(\d+)\s+(unit tests|tests)\b/g)) {
|
|
65
|
+
found.push({ file: rel, count: Number(m[1]), match: m[0], index: m.index });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return found;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function rewriteDocs(count, { root = ROOT, dry = false } = {}) {
|
|
72
|
+
const changes = [];
|
|
73
|
+
for (const rel of DOC_FILES) {
|
|
74
|
+
const file = path.join(root, rel);
|
|
75
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
76
|
+
const next = text.replace(/(\d+)(\s+(?:unit tests|tests)\b)/g, `${count}$2`);
|
|
77
|
+
if (next !== text) {
|
|
78
|
+
changes.push(rel);
|
|
79
|
+
if (!dry) fs.writeFileSync(file, next);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return changes;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === fs.realpathSync(process.argv[1]);
|
|
86
|
+
if (isMain) {
|
|
87
|
+
const scan = scanTests();
|
|
88
|
+
const mode = process.argv.includes('--check') ? 'check' : process.argv.includes('--fix') ? 'fix' : 'print';
|
|
89
|
+
if (mode === 'print') {
|
|
90
|
+
process.stdout.write(`${scan.total}\n`);
|
|
91
|
+
if (scan.nested.length) process.stderr.write(`warning: ${scan.nested.length} nested test declaration(s) the count cannot see:\n ${scan.nested.join('\n ')}\n`);
|
|
92
|
+
} else {
|
|
93
|
+
const docs = docCounts();
|
|
94
|
+
const wrong = docs.filter((d) => d.count !== scan.total);
|
|
95
|
+
if (mode === 'fix') {
|
|
96
|
+
const touched = rewriteDocs(scan.total);
|
|
97
|
+
process.stdout.write(`${scan.total} declared tests; ${touched.length ? `rewrote ${touched.join(', ')}` : 'docs already correct'}\n`);
|
|
98
|
+
} else if (wrong.length) {
|
|
99
|
+
process.stderr.write(
|
|
100
|
+
`documented test count is wrong. The suite declares ${scan.total} tests; the docs say `
|
|
101
|
+
+ `${wrong.map((w) => `${w.file}: "${w.match}"`).join(', ')}.\n`
|
|
102
|
+
+ 'Fix it with: node scripts/doc-counts.js --fix\n',
|
|
103
|
+
);
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
} else {
|
|
106
|
+
process.stdout.write(`docs agree with the suite at ${scan.total}\n`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Regenerate the donation QR (public/donate-qr.png, 4 px a module with the 4-module quiet zone) from
|
|
3
|
+
# the address below, with OpenCV's encoder, and check the modules written decode back to the address.
|
|
4
|
+
# Run by hand when the address changes; the PNG is committed, so nothing is generated at run time or
|
|
5
|
+
# in the browser. It was an inline SVG until 2026-09-15, when Safari painted its ground and none of
|
|
6
|
+
# its module rects; a bitmap is painted the same everywhere.
|
|
7
|
+
import cv2, numpy as np, re, sys
|
|
8
|
+
ADDR = 'bc1q249cv27lc2q7y0x53vkczgfvvgsjzhwxwv42gc'
|
|
9
|
+
img = cv2.QRCodeEncoder.create().encode(ADDR)
|
|
10
|
+
n = img.shape[0]; size = n + 8
|
|
11
|
+
d = ''.join(f'M{x+4} {y+4}h1v1h-1z' for y in range(n) for x in range(n) if img[y, x] == 0)
|
|
12
|
+
m = np.full((size * 8, size * 8), 255, np.uint8)
|
|
13
|
+
for mm in re.finditer(r'M(\d+) (\d+)h1v1h-1z', d):
|
|
14
|
+
x, y = int(mm.group(1)), int(mm.group(2)); m[y*8:(y+1)*8, x*8:(x+1)*8] = 0
|
|
15
|
+
data, _, _ = cv2.QRCodeDetector().detectAndDecode(m)
|
|
16
|
+
if data != ADDR: sys.exit(f'the QR does not decode to the address: {data!r}')
|
|
17
|
+
png = np.full((size * 4, size * 4), 255, np.uint8)
|
|
18
|
+
for mm in re.finditer(r'M(\d+) (\d+)h1v1h-1z', d):
|
|
19
|
+
x, y = int(mm.group(1)), int(mm.group(2)); png[y*4:(y+1)*4, x*4:(x+1)*4] = 0
|
|
20
|
+
cv2.imwrite('public/donate-qr.png', png)
|
|
21
|
+
check, _, _ = cv2.QRCodeDetector().detectAndDecode(cv2.imread('public/donate-qr.png'))
|
|
22
|
+
if check != ADDR: sys.exit(f'the PNG does not decode to the address: {check!r}')
|
|
23
|
+
print(f'{n}x{n} modules, public/donate-qr.png decodes to {check}')
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// How fast the DOS Diversions' emulated PC runs on this machine, headless (docs/MEASUREMENTS.md §32).
|
|
2
|
+
//
|
|
3
|
+
// Boots a game from games/ on a clock that counts instructions, lets its title and demos run, and
|
|
4
|
+
// reports instructions a second of wall time and the screens it drew. No browser: the same x86.js
|
|
5
|
+
// and dospc.js the worker runs, on Node's V8.
|
|
6
|
+
//
|
|
7
|
+
// node scripts/dos-bench.js [doom|quake] [millions of instructions]
|
|
8
|
+
//
|
|
9
|
+
// DOOM runs at 30 million instructions to the virtual second (a fast 486); Quake at 70 million and
|
|
10
|
+
// with `+timedemo demo1`, so its screens drawn over the virtual time are the frame rate its own
|
|
11
|
+
// benchmark reports on a machine exactly as fast as this emulator.
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { createPC } from '../public/js/dospc.js';
|
|
16
|
+
import { createSoundCard } from '../public/js/soundcard.js';
|
|
17
|
+
|
|
18
|
+
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
|
+
const game = process.argv[2] === 'quake' ? 'quake' : 'doom';
|
|
20
|
+
const SPEC = {
|
|
21
|
+
doom: { dir: 'doom_dos', exe: 'DOOM.EXE', args: '', ips: 30e6, total: 400 },
|
|
22
|
+
quake: { dir: 'quake_dos', exe: 'QUAKE.EXE', args: '-nocdaudio +timedemo demo1', ips: 70e6, total: 3000 },
|
|
23
|
+
}[game];
|
|
24
|
+
const DIR = path.join(ROOT, 'games', SPEC.dir);
|
|
25
|
+
const total = Number(process.argv[3] ?? SPEC.total) * 1e6;
|
|
26
|
+
|
|
27
|
+
if (!fs.existsSync(path.join(DIR, SPEC.exe))) {
|
|
28
|
+
console.error(`no ${path.join(DIR, SPEC.exe)}: this measures the shareware ${game}, which is not here`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const files = {};
|
|
32
|
+
const walk = (dir, pre) => {
|
|
33
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
34
|
+
if (e.name.startsWith('.')) continue;
|
|
35
|
+
if (e.isDirectory()) walk(path.join(dir, e.name), `${pre}${e.name}/`);
|
|
36
|
+
else files[`${pre}${e.name}`] = new Uint8Array(fs.readFileSync(path.join(dir, e.name)));
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
walk(DIR, '');
|
|
40
|
+
|
|
41
|
+
let pc;
|
|
42
|
+
const now = () => (pc ? pc.cpu.cycles : 0) / SPEC.ips * 1000;
|
|
43
|
+
pc = createPC({ files, now, args: SPEC.args, sound: (mem) => createSoundCard({ mem, rate: 44100 }) });
|
|
44
|
+
pc.boot(files[SPEC.exe]);
|
|
45
|
+
|
|
46
|
+
const t0 = process.hrtime.bigint();
|
|
47
|
+
let done = 0, graphicsAt = null;
|
|
48
|
+
while (done < total && !pc.exited) {
|
|
49
|
+
done += pc.run(1e6);
|
|
50
|
+
if (graphicsAt === null && pc.vga.mode === 0x13) graphicsAt = done;
|
|
51
|
+
}
|
|
52
|
+
const secs = Number(process.hrtime.bigint() - t0) / 1e9;
|
|
53
|
+
console.log(`${game} on node ${process.version}`);
|
|
54
|
+
console.log(`${(done / 1e6).toFixed(0)} M instructions in ${secs.toFixed(2)} s: ${(done / secs / 1e6).toFixed(1)} M a second`);
|
|
55
|
+
console.log(`graphics mode after ${(graphicsAt / 1e6).toFixed(1)} M instructions`);
|
|
56
|
+
console.log(`${(pc.vga.frames / 2).toFixed(0)} pages flipped, ${(pc.vga.writes / 64000).toFixed(0)} screens' worth written, in ${(done / SPEC.ips).toFixed(1)} virtual seconds`);
|