blockyard 0.0.9 → 0.1.1

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.
Files changed (97) hide show
  1. package/CHANGELOG.md +359 -1
  2. package/README.md +48 -27
  3. package/SECURITY.md +2 -2
  4. package/bin/blockyard.js +2 -1
  5. package/docs/API.md +17 -15
  6. package/docs/ARCHITECTURE.md +128 -10
  7. package/docs/CONFIGURATION.md +39 -30
  8. package/docs/DEFECTS.md +4 -1
  9. package/docs/GETTING-STARTED.md +18 -8
  10. package/docs/INSTALL.md +97 -37
  11. package/docs/MEASUREMENTS.md +147 -0
  12. package/docs/PLAN-SCORCHED-YARD.md +456 -0
  13. package/docs/PLAN-SKIES.md +142 -0
  14. package/docs/SECURITY-AUDIT-2026-09-16.md +647 -0
  15. package/docs/SECURITY.md +58 -22
  16. package/docs/TROUBLESHOOTING.md +44 -5
  17. package/docs/USER-GUIDE.md +505 -35
  18. package/package.json +4 -2
  19. package/public/404.html +1 -1
  20. package/public/css/app.css +393 -82
  21. package/public/donate-qr.png +0 -0
  22. package/public/index.html +353 -109
  23. package/public/js/agents.js +228 -51
  24. package/public/js/app.js +131 -16
  25. package/public/js/blockanoid.js +15 -7
  26. package/public/js/blockout.js +15 -7
  27. package/public/js/blockscene3d.js +230 -38
  28. package/public/js/charts.js +21 -21
  29. package/public/js/depthchart.js +31 -27
  30. package/public/js/details3d.js +1481 -73
  31. package/public/js/doom.js +31 -0
  32. package/public/js/dosaudio.js +48 -0
  33. package/public/js/dosgame.js +389 -0
  34. package/public/js/dosio.js +186 -0
  35. package/public/js/dospc.js +1353 -0
  36. package/public/js/dosworker.js +196 -0
  37. package/public/js/explorer.js +7 -1
  38. package/public/js/livingsky.js +494 -0
  39. package/public/js/login.js +8 -2
  40. package/public/js/markets.js +46 -8
  41. package/public/js/mining.js +314 -36
  42. package/public/js/panels.js +41 -28
  43. package/public/js/pricechart.js +14 -13
  44. package/public/js/quake.js +20 -0
  45. package/public/js/safenext.js +14 -0
  46. package/public/js/scorched.js +1051 -0
  47. package/public/js/scorchedai.js +227 -0
  48. package/public/js/scorchedair.js +286 -0
  49. package/public/js/scorchedfx.js +376 -0
  50. package/public/js/scorchedshop.js +105 -0
  51. package/public/js/scorchedwind.js +69 -0
  52. package/public/js/scorchedyard.js +1338 -0
  53. package/public/js/settings.js +368 -100
  54. package/public/js/soundcard.js +459 -0
  55. package/public/js/tetrust.js +15 -6
  56. package/public/js/tetsound.js +35 -5
  57. package/public/js/theme.js +235 -0
  58. package/public/js/wolf3d.js +22 -0
  59. package/public/js/x86.js +1978 -0
  60. package/scripts/check.js +46 -0
  61. package/scripts/donate-qr.py +12 -9
  62. package/scripts/dos-bench.js +56 -0
  63. package/scripts/index-build.js +9 -2
  64. package/scripts/pool-map.js +152 -36
  65. package/scripts/setup.js +142 -22
  66. package/scripts/shots.mjs +27 -0
  67. package/scripts/smoke.sh +7 -6
  68. package/scripts/tls.js +31 -0
  69. package/scripts/ui.js +4 -2
  70. package/server/auth/sessions.js +33 -13
  71. package/server/chain/blockfile.js +64 -5
  72. package/server/chain/index/build.js +451 -58
  73. package/server/chain/index/heights.js +29 -3
  74. package/server/chain/index/live.js +13 -7
  75. package/server/chain/index/rows.js +6 -1
  76. package/server/chain/index/store.js +28 -5
  77. package/server/chain/index/worker.js +23 -11
  78. package/server/collect/logparse.js +65 -18
  79. package/server/collect/markets.js +76 -7
  80. package/server/collect/mining.js +32 -0
  81. package/server/collect/monitor.js +54 -12
  82. package/server/collect/network.js +305 -0
  83. package/server/config.js +53 -22
  84. package/server/http/api.js +119 -18
  85. package/server/http/games.js +77 -0
  86. package/server/http/server.js +30 -5
  87. package/server/http/sse.js +53 -7
  88. package/server/main.js +66 -11
  89. package/server/rpc/allowlist.js +26 -0
  90. package/server/rpc/client.js +30 -2
  91. package/server/store/audit.js +6 -1
  92. package/server/store/history.js +19 -3
  93. package/server/store/ledger.js +15 -4
  94. package/server/tls/selfsigned.js +160 -0
  95. package/systemd/blockyard.service +41 -8
  96. package/docs/PRIVATE-LEADERBOARD.md +0 -230
  97. package/docs/STATE-2026-09-09.md +0 -200
@@ -0,0 +1,196 @@
1
+ // THE DOS WORKER: the PC runs here, off the page's thread, for the DOOM and Quake Diversions
2
+ // (operator, 2026-09-15: "Get DOOM working as a diversion inside blockyard with zero dependancies",
3
+ // then "get Quake working as a diversion").
4
+ //
5
+ // A worker because the emulated 486 wants every millisecond it can get: on the page's own thread it
6
+ // would share a 16 ms frame with the layout, the SSE feed and every other tab of the app, and the
7
+ // game would stutter whenever the monitor redrew a chart. Here it runs in ~10 ms slices and yields
8
+ // between them so key presses arrive; the page draws what it is sent.
9
+ //
10
+ // Messages in: boot {game, rate, controls}, run {on}, key {codes}, mouse {dx, dy, buttons}, audio {port}
11
+ // Messages out: status {text}, frame {pixels, palette?}, text {cells, cursor}, stats {mips},
12
+ // saved {name}, exit {code, cells}, error {message}
13
+ // (and controls {scheme}: rebind the running game's keys)
14
+ import { createPC } from './dospc.js';
15
+ import { createSoundCard } from './soundcard.js';
16
+ import { withControls, rebindKeys, quakeAutoexec, GAMES } from './dosio.js';
17
+
18
+ const SLICE_MS = 10;
19
+ const CHUNK = 50000; // instructions between looks for a finished picture
20
+
21
+ let pc = null, card = null, audioPort = null;
22
+ let running = false, scheduled = false;
23
+ let clock = 0, lastWall = 0; // machine time: wall time while running, frozen while not
24
+ let lastFrameSeq = -1, lastPalSeq = -1, lastText = null;
25
+ let pixels = new Uint8Array(64000); // bounced back by the page after each frame, to reuse
26
+ let lastWrites = -1, seenWrites = -1;
27
+ const keyEntries = {}; // where DOOM's key settings live, once found
28
+ const yieldChannel = new MessageChannel();
29
+ yieldChannel.port1.onmessage = () => { scheduled = false; loop(); };
30
+
31
+ const post = (m, transfer) => self.postMessage(m, transfer ?? []);
32
+ const now = () => (running ? clock + (performance.now() - lastWall) : clock);
33
+
34
+ // ------------------------------------------------------------------ saved files (IndexedDB)
35
+ // Savegames and the config the game writes on quit, kept in this browser. A browser that refuses
36
+ // IndexedDB still plays; its saves last as long as the tab.
37
+ function db() {
38
+ return new Promise((resolve) => {
39
+ try {
40
+ const req = indexedDB.open(game.db, 1);
41
+ req.onupgradeneeded = () => req.result.createObjectStore('files');
42
+ req.onsuccess = () => resolve(req.result);
43
+ req.onerror = () => resolve(null);
44
+ } catch { resolve(null); }
45
+ });
46
+ }
47
+ async function loadSaved() {
48
+ const d = await db();
49
+ if (!d) return {};
50
+ return new Promise((resolve) => {
51
+ const out = {};
52
+ try {
53
+ const tx = d.transaction('files', 'readonly');
54
+ const cur = tx.objectStore('files').openCursor();
55
+ cur.onsuccess = () => {
56
+ const c = cur.result;
57
+ if (!c) { resolve(out); return; }
58
+ if (c.value instanceof Uint8Array) out[c.key] = c.value;
59
+ c.continue();
60
+ };
61
+ cur.onerror = () => resolve(out);
62
+ } catch { resolve(out); }
63
+ });
64
+ }
65
+ async function store(name, bytes) {
66
+ const d = await db();
67
+ if (!d) return;
68
+ try {
69
+ const tx = d.transaction('files', 'readwrite');
70
+ if (bytes) tx.objectStore('files').put(bytes, name); else tx.objectStore('files').delete(name);
71
+ } catch { /* quota, private mode: the save lives for this session */ }
72
+ }
73
+
74
+ // ------------------------------------------------------------------ boot
75
+ let game = null;
76
+ async function fetchFile(name, required) {
77
+ const r = await fetch(`/games/${game.key}/${name}`, { credentials: 'same-origin' });
78
+ if (!r.ok) {
79
+ if (required) throw new Error(`${name} is not installed: put the shareware files in ${game.dir}/ on the server (HTTP ${r.status})`);
80
+ return null;
81
+ }
82
+ return new Uint8Array(await r.arrayBuffer());
83
+ }
84
+
85
+ async function boot({ game: key = 'doom', rate, controls }) {
86
+ game = { key, ...GAMES[key] };
87
+ if (!GAMES[key]) throw new Error(`no game called ${key}`);
88
+ post({ type: 'status', text: `loading ${game.label}…` });
89
+ const names = [game.exe, ...game.required, ...game.optional];
90
+ const [saved, ...got] = await Promise.all([loadSaved(), ...names.map((n, i) => fetchFile(n, i <= game.required.length))]);
91
+ const fetched = Object.fromEntries(names.map((n, i) => [n, got[i]]));
92
+ const files = {};
93
+ for (const n of [...game.required, ...game.optional]) if (fetched[n]) files[n] = fetched[n];
94
+ // the game's saves and the config it wrote on its last quit, over the shipped ones
95
+ const firstRun = !saved[game.config];
96
+ for (const [name, bytes] of Object.entries(saved)) files[name] = bytes;
97
+ if (key === 'doom' && files[game.config]) files[game.config] = withControls(files[game.config], controls);
98
+ if (key === 'quake') files['ID1/AUTOEXEC.CFG'] = quakeAutoexec({ firstRun });
99
+ pc = createPC({
100
+ files,
101
+ args: game.args,
102
+ programName: game.exe,
103
+ now,
104
+ sound: rate ? (mem) => (card = createSoundCard({ mem, rate })) : null,
105
+ onWrite: (name, bytes) => { store(name, bytes); post({ type: 'saved', name, deleted: !bytes }); },
106
+ onExit: (code) => { running = false; post({ type: 'exit', code, cells: pc.mem.slice(0xb8000, 0xb8000 + 4000) }); },
107
+ log: (m) => post({ type: 'log', text: m }),
108
+ });
109
+ pc.boot(fetched[game.exe]);
110
+ post({ type: 'status', text: 'booted' });
111
+ }
112
+
113
+ // ------------------------------------------------------------------ the loop
114
+ function loop() {
115
+ if (!running || !pc || pc.exited) return;
116
+ const start = performance.now();
117
+ let n = 0;
118
+ try {
119
+ // in chunks of about half a millisecond, looking for a finished picture after each: at 50 frames
120
+ // a second two of Quake's frame copies could otherwise land in one 10 ms slice and one never be shown
121
+ do { n += pc.run(CHUNK); present(); } while (performance.now() - start < SLICE_MS && !pc.exited);
122
+ } catch (e) {
123
+ running = false;
124
+ post({ type: 'error', message: e.message });
125
+ return;
126
+ }
127
+ const spent = performance.now() - start;
128
+ mips = mips * 0.95 + (n / Math.max(0.001, spent)) * 0.05 * 1000 / 1e6;
129
+ if (card && audioPort && card.buffered > 0) {
130
+ const chunk = card.drain();
131
+ audioPort.postMessage(chunk, [chunk.buffer]);
132
+ }
133
+ if (performance.now() - lastStats > 1000) { lastStats = performance.now(); post({ type: 'stats', mips }); }
134
+ if (!scheduled) { scheduled = true; yieldChannel.port2.postMessage(0); }
135
+ }
136
+ let mips = 0, lastStats = 0, textTick = 0;
137
+
138
+ function present() {
139
+ if (pc.vga.mode === 0x13) {
140
+ lastText = null;
141
+ // a new picture: a page flipped (DOOM), the palette changed, or the linear window was written
142
+ // (Quake copies each finished frame into A0000h and never flips). A copy can straddle two chunks,
143
+ // so writes are only shown once a chunk has passed without any: never half a frame
144
+ const writing = pc.vga.writes !== seenWrites;
145
+ seenWrites = pc.vga.writes;
146
+ if (writing && pc.vga.frames === lastFrameSeq) return;
147
+ if (pc.vga.frames === lastFrameSeq && pc.vga.palSeq === lastPalSeq && pc.vga.writes === lastWrites) return;
148
+ if (!pixels) return; // the page still has the last frame
149
+ lastFrameSeq = pc.vga.frames; lastWrites = pc.vga.writes;
150
+ const m = { type: 'frame', pixels: pc.renderIndexed(pixels) };
151
+ if (pc.vga.palSeq !== lastPalSeq) { lastPalSeq = pc.vga.palSeq; m.palette = pc.vga.pal.slice(); }
152
+ const buf = pixels.buffer;
153
+ pixels = null;
154
+ post(m, [buf]);
155
+ return;
156
+ }
157
+ lastFrameSeq = -1; lastPalSeq = -1;
158
+ if (++textTick % 20 !== 0) return; // text mode changes slowly: look every ten milliseconds
159
+ const cells = pc.mem.subarray(0xb8000, 0xb8000 + 4000);
160
+ if (lastText && lastText.every((v, i) => v === cells[i])) return;
161
+ lastText = cells.slice();
162
+ post({ type: 'text', cells: lastText.slice(), cursor: [pc.text.x, pc.text.y] });
163
+ }
164
+
165
+ function setRunning(on) {
166
+ if (on === running) return;
167
+ if (on) { lastWall = performance.now(); running = true; loop(); }
168
+ else { clock = now(); running = false; }
169
+ }
170
+
171
+ self.onmessage = async (e) => {
172
+ const m = e.data;
173
+ try {
174
+ switch (m.type) {
175
+ case 'boot': await boot(m); setRunning(true); break;
176
+ case 'run': setRunning(m.on); break;
177
+ case 'key': if (pc) for (const c of m.codes) pc.key(c); break;
178
+ case 'mouse':
179
+ if (pc) { pc.mouse.dx += m.dx; pc.mouse.dy += m.dy; pc.mouse.buttons = m.buttons; }
180
+ break;
181
+ case 'controls':
182
+ if (pc) {
183
+ // the running game's keys now, and the config it would read if it started again
184
+ rebindKeys(pc.mem, m.scheme, keyEntries);
185
+ const cfg = pc.dir.get('DEFAULT.CFG');
186
+ if (cfg) pc.dir.set('DEFAULT.CFG', withControls(cfg, m.scheme));
187
+ }
188
+ break;
189
+ case 'audio': audioPort = m.port; break;
190
+ case 'pixels': pixels = new Uint8Array(m.buffer); break;
191
+ }
192
+ } catch (err) {
193
+ running = false;
194
+ post({ type: 'error', message: err.message });
195
+ }
196
+ };
@@ -19,8 +19,14 @@
19
19
 
20
20
  const X = { key: null, data: null, error: null, loading: false, shown: null, bound: false };
21
21
 
22
+ // A malformed escape in a pasted link (`#explorer/tx/%E0`) made decodeURIComponent throw inside the
23
+ // render, and the explorer stopped drawing for that tab (audit 2026-09-16, I1). Such a link is a
24
+ // route to the explorer's home, not an exception.
25
+ const decodePart = (x) => { try { return decodeURIComponent(x); } catch { return null; } };
22
26
  export function parseRoute(r) {
23
- const [kind, id, page] = String(r ?? '').split('/').filter((x) => x !== '').map((x) => decodeURIComponent(x));
27
+ const parts = String(r ?? '').split('/').filter((x) => x !== '').map(decodePart);
28
+ if (parts.includes(null)) return { kind: 'home' };
29
+ const [kind, id, page] = parts;
24
30
  if ((kind === 'block' || kind === 'address') && id) return { kind, id, page: Math.max(0, Math.floor(Number(page) || 0)) };
25
31
  if (kind === 'tx' && id) return { kind, id };
26
32
  return { kind: 'home' };