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.
- package/CHANGELOG.md +359 -1
- package/README.md +48 -27
- package/SECURITY.md +2 -2
- package/bin/blockyard.js +2 -1
- package/docs/API.md +17 -15
- package/docs/ARCHITECTURE.md +128 -10
- package/docs/CONFIGURATION.md +39 -30
- package/docs/DEFECTS.md +4 -1
- package/docs/GETTING-STARTED.md +18 -8
- package/docs/INSTALL.md +97 -37
- package/docs/MEASUREMENTS.md +147 -0
- package/docs/PLAN-SCORCHED-YARD.md +456 -0
- package/docs/PLAN-SKIES.md +142 -0
- package/docs/SECURITY-AUDIT-2026-09-16.md +647 -0
- package/docs/SECURITY.md +58 -22
- package/docs/TROUBLESHOOTING.md +44 -5
- package/docs/USER-GUIDE.md +505 -35
- package/package.json +4 -2
- package/public/404.html +1 -1
- package/public/css/app.css +393 -82
- package/public/donate-qr.png +0 -0
- package/public/index.html +353 -109
- package/public/js/agents.js +228 -51
- package/public/js/app.js +131 -16
- package/public/js/blockanoid.js +15 -7
- package/public/js/blockout.js +15 -7
- package/public/js/blockscene3d.js +230 -38
- package/public/js/charts.js +21 -21
- package/public/js/depthchart.js +31 -27
- package/public/js/details3d.js +1481 -73
- 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 +7 -1
- package/public/js/livingsky.js +494 -0
- package/public/js/login.js +8 -2
- package/public/js/markets.js +46 -8
- package/public/js/mining.js +314 -36
- package/public/js/panels.js +41 -28
- package/public/js/pricechart.js +14 -13
- package/public/js/quake.js +20 -0
- package/public/js/safenext.js +14 -0
- package/public/js/scorched.js +1051 -0
- package/public/js/scorchedai.js +227 -0
- package/public/js/scorchedair.js +286 -0
- package/public/js/scorchedfx.js +376 -0
- package/public/js/scorchedshop.js +105 -0
- package/public/js/scorchedwind.js +69 -0
- package/public/js/scorchedyard.js +1338 -0
- package/public/js/settings.js +368 -100
- package/public/js/soundcard.js +459 -0
- package/public/js/tetrust.js +15 -6
- package/public/js/tetsound.js +35 -5
- package/public/js/theme.js +235 -0
- package/public/js/wolf3d.js +22 -0
- package/public/js/x86.js +1978 -0
- package/scripts/check.js +46 -0
- package/scripts/donate-qr.py +12 -9
- package/scripts/dos-bench.js +56 -0
- package/scripts/index-build.js +9 -2
- package/scripts/pool-map.js +152 -36
- package/scripts/setup.js +142 -22
- package/scripts/shots.mjs +27 -0
- package/scripts/smoke.sh +7 -6
- package/scripts/tls.js +31 -0
- package/scripts/ui.js +4 -2
- package/server/auth/sessions.js +33 -13
- package/server/chain/blockfile.js +64 -5
- package/server/chain/index/build.js +451 -58
- package/server/chain/index/heights.js +29 -3
- package/server/chain/index/live.js +13 -7
- package/server/chain/index/rows.js +6 -1
- package/server/chain/index/store.js +28 -5
- package/server/chain/index/worker.js +23 -11
- package/server/collect/logparse.js +65 -18
- package/server/collect/markets.js +76 -7
- package/server/collect/mining.js +32 -0
- package/server/collect/monitor.js +54 -12
- package/server/collect/network.js +305 -0
- package/server/config.js +53 -22
- package/server/http/api.js +119 -18
- package/server/http/games.js +77 -0
- package/server/http/server.js +30 -5
- package/server/http/sse.js +53 -7
- package/server/main.js +66 -11
- package/server/rpc/allowlist.js +26 -0
- package/server/rpc/client.js +30 -2
- package/server/store/audit.js +6 -1
- package/server/store/history.js +19 -3
- package/server/store/ledger.js +15 -4
- package/server/tls/selfsigned.js +160 -0
- package/systemd/blockyard.service +41 -8
- package/docs/PRIVATE-LEADERBOARD.md +0 -230
- 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
|
+
};
|
package/public/js/explorer.js
CHANGED
|
@@ -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
|
|
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' };
|