@raskell/wasm-ad 0.0.0-stage → 0.2.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/bin/wasmad.mjs ADDED
@@ -0,0 +1,461 @@
1
+ #!/usr/bin/env node
2
+ // Command-line tools for WASM-AD carts, in any language. Needs Node 18+ and
3
+ // nothing else.
4
+ //
5
+ // wasmad check <cart.wasm> validate a cart against SPEC.md
6
+ // wasmad meta <cart.wasm> key=value... set the `wasm-ad` metadata section
7
+ // wasmad run <cart.wasm> [options] run frames headlessly
8
+ //
9
+ // `meta` replaces the metadata section in place (or writes `-o <file>`); `spec`
10
+ // defaults to the version this tool implements. It is for toolchains that
11
+ // cannot emit custom sections themselves (AssemblyScript, TinyGo).
12
+ //
13
+ // `run` boots the cart like a host does (SPEC.md §3) and renders and makes
14
+ // sound with the reference core, player/web/core.wasm (`make player`). Options:
15
+ // --frames N frames to run (default 1)
16
+ // --keys SCRIPT held buttons per frame, e.g. "0-29:right,30-59:down+b,60:a"
17
+ // --seed N value for SEED (default 1)
18
+ // --hash F,F,... print a hash of the frame rendered at each listed frame,
19
+ // and one of all the sound
20
+ // --ppm FILE write the last rendered frame as a PPM image
21
+ // --wav FILE write the sound of every frame as a 48 kHz stereo WAV
22
+ // --core FILE a different core.wasm
23
+
24
+ import { readFileSync, realpathSync, writeFileSync } from 'node:fs';
25
+ import { createHash } from 'node:crypto';
26
+ import { fileURLToPath } from 'node:url';
27
+
28
+ const SPEC = '0.2';
29
+ const SPEC_VERSION = 0x0002;
30
+ const W = 240, H = 160;
31
+ const A = {
32
+ IO: 0x400, PAL_END: 0xC00, OAM: 0xC00, VIDEO_END: 0x19000, SAVE: 0x19000, SAVE_SIZE: 0x8000, CART: 0x28000,
33
+ VCOUNT: 0x406, KEYS: 0x700, FRAME: 0x704, SEED: 0x708, SAVECNT: 0x70C, SAVESTAT: 0x70E, SPEC: 0x710,
34
+ BG2PA: 0x420, BG2PD: 0x426, BG3PA: 0x430, BG3PD: 0x436,
35
+ SOUND_REGS: 0x460, SOUND_REGS_END: 0x4D0, SOUND_STATE: 0x780, SOUND_STATE_END: 0x800,
36
+ };
37
+ const SOUND_SAMPLES = 800;
38
+ const KEY = { a: 1, b: 2, select: 4, start: 8, right: 16, left: 32, up: 64, down: 128, r: 256, l: 512 };
39
+ const MAX_CART = 32 * 1024 * 1024;
40
+
41
+ // ---- WebAssembly binary reading -------------------------------------------
42
+
43
+ class Reader {
44
+ constructor(bytes, pos = 0, end = bytes.length) {
45
+ Object.assign(this, { bytes, pos, end });
46
+ }
47
+ byte() {
48
+ if (this.pos >= this.end) throw new Error('unexpected end of module');
49
+ return this.bytes[this.pos++];
50
+ }
51
+ u32() {
52
+ let result = 0, shift = 0, b;
53
+ do {
54
+ b = this.byte();
55
+ result |= (b & 0x7F) << shift;
56
+ shift += 7;
57
+ } while (b & 0x80);
58
+ return result >>> 0;
59
+ }
60
+ i32() {
61
+ let result = 0, shift = 0, b;
62
+ do {
63
+ b = this.byte();
64
+ result |= (b & 0x7F) << shift;
65
+ shift += 7;
66
+ } while (b & 0x80);
67
+ return shift < 32 && b & 0x40 ? result | (-1 << shift) : result | 0;
68
+ }
69
+ skip(n) { this.pos += n; }
70
+ name() {
71
+ const n = this.u32();
72
+ const s = new TextDecoder().decode(this.bytes.subarray(this.pos, this.pos + n));
73
+ this.pos += n;
74
+ return s;
75
+ }
76
+ limits() {
77
+ const flags = this.byte();
78
+ const min = this.u32();
79
+ const max = flags & 1 ? this.u32() : null;
80
+ return { flags, min, max };
81
+ }
82
+ // A constant expression; returns its value if it is a single i32.const.
83
+ constExpr() {
84
+ let value = null, first = true;
85
+ for (;;) {
86
+ const op = this.byte();
87
+ if (op === 0x0B) return value;
88
+ if (op === 0x41) value = first ? this.i32() : (this.i32(), null);
89
+ else if (op === 0x23) { this.u32(); value = null; } // global.get
90
+ else if (op === 0x42) { while (this.byte() & 0x80); value = null; }
91
+ else if (op === 0x43) { this.skip(4); value = null; }
92
+ else if (op === 0x44) { this.skip(8); value = null; }
93
+ else value = null;
94
+ first = false;
95
+ }
96
+ }
97
+ }
98
+
99
+ const VALTYPE = { 0x7F: 'i32', 0x7E: 'i64', 0x7D: 'f32', 0x7C: 'f64', 0x7B: 'v128', 0x70: 'funcref', 0x6F: 'externref' };
100
+
101
+ function sections(bytes) {
102
+ if (bytes.length < 8 || bytes[0] !== 0 || bytes[1] !== 0x61 || bytes[2] !== 0x73 || bytes[3] !== 0x6D) {
103
+ throw new Error('not a WebAssembly module');
104
+ }
105
+ const out = [];
106
+ const r = new Reader(bytes, 8);
107
+ while (r.pos < bytes.length) {
108
+ const start = r.pos;
109
+ const id = r.byte();
110
+ const size = r.u32();
111
+ const body = r.pos;
112
+ out.push({ id, start, body, end: body + size });
113
+ r.pos = body + size;
114
+ }
115
+ return out;
116
+ }
117
+
118
+ /** Parses the parts of a module that SPEC.md constrains. */
119
+ export function inspect(bytes) {
120
+ const m = { types: [], imports: [], funcs: [], memories: [], globals: [], exports: [], data: [], custom: [], start: null };
121
+ let importedFuncs = 0, importedGlobals = 0;
122
+ for (const s of sections(bytes)) {
123
+ const r = new Reader(bytes, s.body, s.end);
124
+ switch (s.id) {
125
+ case 0: m.custom.push({ name: r.name(), data: bytes.subarray(r.pos, s.end), start: s.start, end: s.end }); break;
126
+ case 1:
127
+ for (let n = r.u32(); n--;) {
128
+ if (r.byte() !== 0x60) throw new Error('unsupported type form');
129
+ const params = Array.from({ length: r.u32() }, () => VALTYPE[r.byte()]);
130
+ const results = Array.from({ length: r.u32() }, () => VALTYPE[r.byte()]);
131
+ m.types.push({ params, results });
132
+ }
133
+ break;
134
+ case 2:
135
+ for (let n = r.u32(); n--;) {
136
+ const imp = { module: r.name(), name: r.name(), kind: r.byte() };
137
+ if (imp.kind === 0) { imp.type = r.u32(); importedFuncs++; m.funcs.push(imp.type); }
138
+ else if (imp.kind === 1) { r.byte(); r.limits(); }
139
+ else if (imp.kind === 2) { imp.limits = r.limits(); m.memories.push(imp.limits); }
140
+ else if (imp.kind === 3) { r.byte(); imp.mutable = r.byte() === 1; importedGlobals++; m.globals.push({ imported: true, mutable: imp.mutable }); }
141
+ m.imports.push(imp);
142
+ }
143
+ break;
144
+ case 3: for (let n = r.u32(); n--;) m.funcs.push(r.u32()); break;
145
+ case 5: for (let n = r.u32(); n--;) m.memories.push(r.limits()); break;
146
+ case 6:
147
+ for (let n = r.u32(); n--;) {
148
+ const type = VALTYPE[r.byte()];
149
+ const mutable = r.byte() === 1;
150
+ m.globals.push({ type, mutable, init: r.constExpr() });
151
+ }
152
+ break;
153
+ case 7:
154
+ for (let n = r.u32(); n--;) m.exports.push({ name: r.name(), kind: r.byte(), index: r.u32() });
155
+ break;
156
+ case 8: m.start = r.u32(); break;
157
+ case 11:
158
+ for (let n = r.u32(); n--;) {
159
+ const flags = r.u32();
160
+ let offset = null, active = flags !== 1;
161
+ if (flags === 2) r.u32();
162
+ if (active) offset = r.constExpr();
163
+ const size = r.u32();
164
+ r.skip(size);
165
+ m.data.push({ active, offset, size });
166
+ }
167
+ break;
168
+ }
169
+ }
170
+ m.importedFuncs = importedFuncs;
171
+ m.importedGlobals = importedGlobals;
172
+ return m;
173
+ }
174
+
175
+ /** Validates a cart against SPEC.md. Returns { errors, warnings, meta, info }. */
176
+ export function check(bytes) {
177
+ const errors = [], warnings = [], info = [];
178
+ if (bytes.length > MAX_CART) errors.push(`cart is ${bytes.length} bytes; the limit is 32 MiB (§2.4)`);
179
+ let m;
180
+ try {
181
+ m = inspect(bytes);
182
+ new WebAssembly.Module(bytes);
183
+ } catch (e) {
184
+ return { errors: [`invalid module: ${e.message}`], warnings, meta: null, info };
185
+ }
186
+
187
+ // §2.2 imports
188
+ for (const imp of m.imports) {
189
+ if (imp.module === 'env' && imp.name === 'trace' && imp.kind === 0) {
190
+ const t = m.types[imp.type];
191
+ if (t.params.join() !== 'i32,i32' || t.results.length) errors.push('env.trace must have type (i32, i32) -> () (§2.2)');
192
+ } else {
193
+ errors.push(`imports ${imp.module}.${imp.name}; a cart may only import env.trace (§2.2)`);
194
+ }
195
+ }
196
+
197
+ // §2.1 exports
198
+ const exp = Object.fromEntries(m.exports.map((e) => [e.name, e]));
199
+ const funcType = (name) => m.types[m.funcs[exp[name].index]];
200
+ const sig = (t) => `(${t.params.join(', ')}) -> (${t.results.join(', ')})`;
201
+ const want = { update: '() -> ()', start: '() -> ()', scanline: '(i32) -> ()' };
202
+ if (!exp.memory || exp.memory.kind !== 2) errors.push('does not export memory (§2.1)');
203
+ else {
204
+ const mem = m.memories[exp.memory.index];
205
+ if (mem.min < 3) errors.push(`memory starts at ${mem.min} pages; at least 3 are required (§2.1)`);
206
+ if (mem.max !== null && mem.max < 256) warnings.push(`memory is capped at ${mem.max} pages; hosts allow growth to 16 MiB (§2.4)`);
207
+ info.push(`memory: ${mem.min} pages${mem.max !== null ? `, max ${mem.max}` : ''}`);
208
+ }
209
+ for (const [name, want_] of Object.entries(want)) {
210
+ if (!exp[name]) {
211
+ if (name === 'update') errors.push('does not export update (§2.1)');
212
+ continue;
213
+ }
214
+ if (exp[name].kind !== 0) { errors.push(`export ${name} is not a function (§2.1)`); continue; }
215
+ const got = sig(funcType(name));
216
+ if (got !== want_) errors.push(`${name} has type ${got}; expected ${want_} (§2.1)`);
217
+ }
218
+ info.push(`exports: ${['update', 'start', 'scanline'].filter((n) => exp[n]).join(', ')}`);
219
+ if (m.start !== null) errors.push('uses the WebAssembly start section; export `start` instead (§2.1)');
220
+
221
+ // §4 memory layout
222
+ for (const d of m.data) {
223
+ if (!d.active) continue;
224
+ if (d.offset === null) warnings.push('a data segment has a non-constant offset; cannot check it is above 0x28000');
225
+ else if ((d.offset >>> 0) < A.CART && d.size > 0) {
226
+ errors.push(`data segment at 0x${(d.offset >>> 0).toString(16)} lies below 0x28000 (§4, §13)`);
227
+ }
228
+ }
229
+ // Only the stack pointer may be a mutable global; we can't see names in a
230
+ // stripped module, so a lone i32 starting in the cart region is taken as it.
231
+ const mutable = m.globals.filter((g) => !g.imported && g.mutable);
232
+ if (mutable.length > 1) {
233
+ errors.push(`${mutable.length} mutable globals; cart state must live in linear memory, only the stack pointer may be a global (§12)`);
234
+ } else if (mutable.length === 1) {
235
+ const [g] = mutable;
236
+ if (g.type !== 'i32') errors.push(`a mutable ${g.type} global holds cart state; it must live in linear memory (§12)`);
237
+ else if (g.init !== null && (g.init >>> 0) < A.CART) {
238
+ errors.push(`a mutable global starts at 0x${(g.init >>> 0).toString(16)}: as the stack pointer it would be inside the hardware region (§4, §13), and anything else is cart state that must live in linear memory (§12)`);
239
+ } else if (g.init !== null) info.push(`stack pointer: 0x${(g.init >>> 0).toString(16)}`);
240
+ }
241
+
242
+ // §2.3 metadata
243
+ let meta = null;
244
+ const sections_ = m.custom.filter((c) => c.name === 'wasm-ad');
245
+ if (!sections_.length) errors.push('missing the "wasm-ad" metadata section (§2.3)');
246
+ else {
247
+ if (sections_.length > 1) warnings.push('more than one "wasm-ad" section; hosts read the first');
248
+ meta = parseMeta(sections_[0].data);
249
+ if (!meta.spec) errors.push('metadata has no spec key (§2.3)');
250
+ else if (!/^\d+\.\d+$/.test(meta.spec)) errors.push(`metadata spec "${meta.spec}" is not major.minor (§2.3)`);
251
+ if (!meta.title) errors.push('metadata has no title (§2.3)');
252
+ }
253
+ return { errors, warnings, meta, info };
254
+ }
255
+
256
+ export function parseMeta(data) {
257
+ const meta = {};
258
+ for (const line of new TextDecoder().decode(data).split('\n')) {
259
+ const i = line.indexOf('=');
260
+ if (i > 0) meta[line.slice(0, i).trim().toLowerCase()] = line.slice(i + 1).trim();
261
+ }
262
+ return meta;
263
+ }
264
+
265
+ function leb(n) {
266
+ const out = [];
267
+ do {
268
+ let b = n & 0x7F;
269
+ n >>>= 7;
270
+ if (n) b |= 0x80;
271
+ out.push(b);
272
+ } while (n);
273
+ return out;
274
+ }
275
+
276
+ /** Returns `bytes` with its `wasm-ad` sections replaced by one holding `meta`. */
277
+ export function setMeta(bytes, meta) {
278
+ const text = Object.entries(meta).map(([k, v]) => `${k}=${v}\n`).join('');
279
+ const name = new TextEncoder().encode('wasm-ad');
280
+ const payload = [...leb(name.length), ...name, ...new TextEncoder().encode(text)];
281
+ const section = Uint8Array.from([0, ...leb(payload.length), ...payload]);
282
+ const parts = [bytes.subarray(0, 8)];
283
+ for (const s of sections(bytes)) {
284
+ if (s.id === 0 && new Reader(bytes, s.body, s.end).name() === 'wasm-ad') continue;
285
+ parts.push(bytes.subarray(s.start, s.end));
286
+ }
287
+ parts.push(section);
288
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
289
+ let at = 0;
290
+ for (const p of parts) { out.set(p, at); at += p.length; }
291
+ return out;
292
+ }
293
+
294
+ // ---- Headless host ----------------------------------------------------------
295
+
296
+ /** Parses "0-29:right,30-59:down+b,60:a" into a frame -> KEYS function. */
297
+ export function parseKeys(script) {
298
+ const spans = (script ?? '').split(',').filter(Boolean).map((part) => {
299
+ const [range, buttons] = part.split(':');
300
+ const [from, to = from] = range.split('-').map(Number);
301
+ let mask = 0;
302
+ for (const b of (buttons ?? '').split('+').filter(Boolean)) {
303
+ if (!(b in KEY)) throw new Error(`unknown button "${b}"; use ${Object.keys(KEY).join(', ')}`);
304
+ mask |= KEY[b];
305
+ }
306
+ return { from, to, mask };
307
+ });
308
+ return (frame) => spans.reduce((k, s) => (frame >= s.from && frame <= s.to ? k | s.mask : k), 0);
309
+ }
310
+
311
+ /** Boots a cart and returns a host that runs frames (SPEC.md §3). */
312
+ export async function boot(bytes, { core: corePath, seed = 1, onTrace = () => {} } = {}) {
313
+ const core = (await WebAssembly.instantiate(readFileSync(corePath))).instance.exports;
314
+ let instance;
315
+ const decoder = new TextDecoder();
316
+ const imports = { env: { trace: (ptr, len) => onTrace(decoder.decode(new Uint8Array(instance.exports.memory.buffer, ptr, len))) } };
317
+ instance = (await WebAssembly.instantiate(bytes, imports)).instance;
318
+ const ex = instance.exports;
319
+ const mem = () => new Uint8Array(ex.memory.buffer);
320
+ const dv = () => new DataView(ex.memory.buffer);
321
+ mem().fill(0, 0, A.CART);
322
+ for (let i = 0; i < 128; i++) dv().setUint16(A.OAM + i * 8, 0x0200, true);
323
+ for (const a of [A.BG2PA, A.BG2PD, A.BG3PA, A.BG3PD]) dv().setUint16(a, 0x100, true);
324
+ dv().setUint32(A.SEED, seed >>> 0, true);
325
+ dv().setUint16(A.SPEC, SPEC_VERSION, true);
326
+ ex.start?.();
327
+
328
+ let frame = 0, sound = new Int16Array(SOUND_SAMPLES * 2);
329
+ return {
330
+ exports: ex,
331
+ /** The last frame's sound: 800 stereo pairs of i16, left first. */
332
+ get sound() { return sound; },
333
+ /** Runs one frame with `keys` held and returns the rendered RGBA8 pixels. */
334
+ frame(keys = 0) {
335
+ dv().setUint16(A.KEYS, keys & 0x3FF, true);
336
+ dv().setUint32(A.FRAME, frame >>> 0, true);
337
+ dv().setUint16(A.VCOUNT, 160, true);
338
+ ex.update();
339
+ const coreMem = new Uint8Array(core.memory.buffer, core.ad_mem(), core.ad_mem_len());
340
+ coreMem.set(mem().subarray(0, A.CART));
341
+ core.ad_sound_frame();
342
+ for (const [from, to] of [[A.SOUND_REGS, A.SOUND_REGS_END], [A.SOUND_STATE, A.SOUND_STATE_END]]) {
343
+ mem().set(coreMem.subarray(from, to), from);
344
+ }
345
+ sound = new Int16Array(core.memory.buffer, core.ad_sound_buffer(), SOUND_SAMPLES * 2).slice();
346
+ for (let y = 0; y < H; y++) {
347
+ if (ex.scanline) {
348
+ dv().setUint16(A.VCOUNT, y, true);
349
+ ex.scanline(y);
350
+ coreMem.set(mem().subarray(A.IO, A.PAL_END), A.IO);
351
+ }
352
+ core.ad_render_line(y);
353
+ }
354
+ if (dv().getUint16(A.SAVECNT, true) & 1) dv().setUint16(A.SAVECNT, dv().getUint16(A.SAVECNT, true) & ~1, true);
355
+ frame++;
356
+ return new Uint8Array(core.memory.buffer, core.ad_framebuffer(), W * H * 4).slice();
357
+ },
358
+ };
359
+ }
360
+
361
+ export function hash(...chunks) {
362
+ const h = createHash('sha256');
363
+ for (const c of chunks) h.update(new Uint8Array(c.buffer, c.byteOffset, c.byteLength));
364
+ return h.digest('hex').slice(0, 16);
365
+ }
366
+
367
+ /** A 48 kHz 16-bit stereo WAV file of `frames` (Int16Arrays from `sound`). */
368
+ export function wav(frames) {
369
+ const bytes = frames.reduce((n, f) => n + f.byteLength, 0);
370
+ const out = new DataView(new ArrayBuffer(44 + bytes));
371
+ const tag = (at, s) => [...s].forEach((c, i) => out.setUint8(at + i, c.charCodeAt(0)));
372
+ tag(0, 'RIFF'); out.setUint32(4, 36 + bytes, true); tag(8, 'WAVEfmt ');
373
+ out.setUint32(16, 16, true); out.setUint16(20, 1, true); out.setUint16(22, 2, true);
374
+ out.setUint32(24, 48000, true); out.setUint32(28, 48000 * 4, true); out.setUint16(32, 4, true);
375
+ out.setUint16(34, 16, true); tag(36, 'data'); out.setUint32(40, bytes, true);
376
+ let at = 44;
377
+ for (const f of frames) for (const s of f) { out.setInt16(at, s, true); at += 2; }
378
+ return new Uint8Array(out.buffer);
379
+ }
380
+
381
+ function ppm(pixels) {
382
+ const head = new TextEncoder().encode(`P6 ${W} ${H} 255\n`);
383
+ const out = new Uint8Array(head.length + W * H * 3);
384
+ out.set(head);
385
+ for (let i = 0; i < W * H; i++) out.set(pixels.subarray(i * 4, i * 4 + 3), head.length + i * 3);
386
+ return out;
387
+ }
388
+
389
+ // ---- CLI ----------------------------------------------------------------------
390
+
391
+ function options(args) {
392
+ const opts = { _: [] };
393
+ for (let i = 0; i < args.length; i++) {
394
+ if (args[i] === '-o') opts.o = args[++i];
395
+ else if (args[i].startsWith('--')) opts[args[i].slice(2)] = args[++i];
396
+ else opts._.push(args[i]);
397
+ }
398
+ return opts;
399
+ }
400
+
401
+ async function main([cmd, ...args]) {
402
+ const opts = options(args);
403
+ const [file, ...rest] = opts._;
404
+ if (!cmd || !file || !['check', 'meta', 'run'].includes(cmd)) {
405
+ console.error('usage: wasmad check <cart.wasm>\n wasmad meta <cart.wasm> [-o out.wasm] key=value...\n wasmad run <cart.wasm> [--frames N] [--keys SCRIPT] [--seed N] [--hash F,F] [--ppm FILE] [--wav FILE]');
406
+ return 2;
407
+ }
408
+ const bytes = new Uint8Array(readFileSync(file));
409
+
410
+ if (cmd === 'meta') {
411
+ const meta = { spec: SPEC };
412
+ for (const kv of rest) {
413
+ const i = kv.indexOf('=');
414
+ if (i <= 0) throw new Error(`expected key=value, got "${kv}"`);
415
+ meta[kv.slice(0, i).toLowerCase()] = kv.slice(i + 1);
416
+ }
417
+ if (!meta.title) throw new Error('title=... is required');
418
+ writeFileSync(opts.o ?? file, setMeta(bytes, meta));
419
+ return 0;
420
+ }
421
+
422
+ if (cmd === 'check') {
423
+ const { errors, warnings, meta, info } = check(bytes);
424
+ if (meta) console.log(`${meta.title}${meta.author ? ` by ${meta.author}` : ''} (spec ${meta.spec}), ${bytes.length} bytes`);
425
+ for (const line of info) console.log(` ${line}`);
426
+ for (const w of warnings) console.log(`warning: ${w}`);
427
+ for (const e of errors) console.log(`error: ${e}`);
428
+ if (!errors.length) console.log('ok');
429
+ return errors.length ? 1 : 0;
430
+ }
431
+
432
+ const { errors } = check(bytes);
433
+ if (errors.length) {
434
+ for (const e of errors) console.error(`error: ${e}`);
435
+ return 1;
436
+ }
437
+ const core = opts.core ?? fileURLToPath(new URL('../player/web/core.wasm', import.meta.url));
438
+ const host = await boot(bytes, { core, seed: Number(opts.seed ?? 1), onTrace: (m) => console.log(`trace: ${m}`) });
439
+ const keys = parseKeys(opts.keys);
440
+ const frames = Number(opts.frames ?? 1);
441
+ const marks = new Set((opts.hash ?? '').split(',').filter(Boolean).map(Number));
442
+ let pixels;
443
+ const sound = [];
444
+ for (let f = 0; f < frames; f++) {
445
+ pixels = host.frame(keys(f));
446
+ sound.push(host.sound);
447
+ if (marks.has(f)) console.log(`frame ${f}: ${hash(pixels)}`);
448
+ }
449
+ if (opts.hash) console.log(`sound: ${hash(...sound)}`);
450
+ if (opts.ppm) writeFileSync(opts.ppm, ppm(pixels));
451
+ if (opts.wav) writeFileSync(opts.wav, wav(sound));
452
+ return 0;
453
+ }
454
+
455
+ // Run as a command, also through a symlink such as node_modules/.bin/wasmad.
456
+ if (process.argv[1] && realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])) {
457
+ main(process.argv.slice(2)).then((code) => process.exit(code), (e) => {
458
+ console.error(`error: ${e.message}`);
459
+ process.exit(1);
460
+ });
461
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "options": {
3
+ "runtime": "stub",
4
+ "memoryBase": 163840,
5
+ "initialMemory": 4,
6
+ "use": ["abort=~lib/@raskell/wasm-ad/assembly/index/abort"],
7
+ "optimizeLevel": 3,
8
+ "shrinkLevel": 1,
9
+ "noAssert": true,
10
+ "exportRuntime": false
11
+ }
12
+ }
package/package.json CHANGED
@@ -1,6 +1,43 @@
1
1
  {
2
2
  "name": "@raskell/wasm-ad",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "AssemblyScript SDK for writing WASM-AD carts",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/raskell-io/wasm-ad.git",
9
+ "directory": "sdk/assemblyscript"
10
+ },
11
+ "type": "module",
12
+ "main": "assembly/index.ts",
13
+ "types": "assembly/index.ts",
14
+ "files": [
15
+ "assembly",
16
+ "bin",
17
+ "asconfig.json",
18
+ "cart.asconfig.json",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "scripts": {
23
+ "build": "asc examples/starter.ts --config asconfig.json -o build/starter.wasm && node ../../tools/wasmad.mjs meta build/starter.wasm title=Starter author=raskell.io \"description=The WASM-AD starter cart, in AssemblyScript\"",
24
+ "prepack": "mkdir -p bin && cp ../../tools/wasmad.mjs bin/wasmad.mjs && cp ../../LICENSE LICENSE"
25
+ },
26
+ "devDependencies": {
27
+ "assemblyscript": "0.28.20"
28
+ },
29
+ "keywords": [
30
+ "wasm-ad",
31
+ "assemblyscript",
32
+ "webassembly",
33
+ "fantasy-console",
34
+ "gba"
35
+ ],
36
+ "homepage": "https://wasmad.raskell.io",
37
+ "peerDependencies": {
38
+ "assemblyscript": ">=0.28.0 <0.29.0"
39
+ },
40
+ "bin": {
41
+ "wasmad": "bin/wasmad.mjs"
42
+ }
43
+ }