@raskell/wasm-ad 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +97 -2
- package/asconfig.json +12 -0
- package/assembly/index.ts +549 -0
- package/bin/wasmad.mjs +428 -0
- package/cart.asconfig.json +12 -0
- package/package.json +41 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 raskell.io
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,98 @@
|
|
|
1
|
-
#
|
|
1
|
+
# WASM-AD for AssemblyScript
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An AssemblyScript SDK for [WASM-AD](https://github.com/raskell-io/wasm-ad) carts: register
|
|
4
|
+
addresses, typed builders for backgrounds, sprites and affine matrices, input, save RAM and
|
|
5
|
+
`trace`, with no runtime allocation and no mutable globals.
|
|
6
|
+
|
|
7
|
+
## The starter cart
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install
|
|
11
|
+
npm run build # build/starter.wasm
|
|
12
|
+
node ../../tools/wasmad.mjs check build/starter.wasm
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
[`examples/starter.ts`](examples/starter.ts) is the same cart as every other SDK's starter
|
|
16
|
+
and renders bit-identical frames. Drop `build/starter.wasm` into the player.
|
|
17
|
+
|
|
18
|
+
## A new cart
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm install @raskell/wasm-ad assemblyscript@0.28
|
|
22
|
+
echo '{ "extends": "./node_modules/@raskell/wasm-ad/cart.asconfig.json" }' > asconfig.json
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { write16, reg, dispcnt, keys, key } from "@raskell/wasm-ad/assembly";
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npx asc cart.ts --config asconfig.json -o cart.wasm
|
|
31
|
+
npx wasmad meta cart.wasm title="My Cart" author=you
|
|
32
|
+
npx wasmad check cart.wasm
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`cart.asconfig.json` is the configuration below with `abort` pointing into the
|
|
36
|
+
installed package; extend it rather than copying it. `wasmad` is the cart
|
|
37
|
+
checker from `tools/wasmad.mjs`, shipped with this package.
|
|
38
|
+
|
|
39
|
+
`asconfig.json` sets what the spec requires:
|
|
40
|
+
|
|
41
|
+
| Option | Why |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `memoryBase: 163840` | Static data starts at 0x28000, above the hardware region (SPEC.md §4). |
|
|
44
|
+
| `initialMemory: 4` | At least 3 pages; 4 leaves room above the data. |
|
|
45
|
+
| `runtime: "stub"` | No garbage collector; carts do not allocate. |
|
|
46
|
+
| `use: abort=assembly/index/abort` | Traps instead of importing `env.abort`; a cart may only import `env.trace`. |
|
|
47
|
+
|
|
48
|
+
AssemblyScript cannot write custom sections, so `wasmad meta` adds the `wasm-ad` metadata
|
|
49
|
+
(`spec` is filled in). Run `wasmad check` before sharing a cart.
|
|
50
|
+
|
|
51
|
+
## Rules
|
|
52
|
+
|
|
53
|
+
A cart exports `update()`, and optionally `start()` and `scanline(y: i32)`. Do the setup in
|
|
54
|
+
`start`, not at the top level of a module, which would run before the host clears the hardware.
|
|
55
|
+
|
|
56
|
+
**Keep state in linear memory.** AssemblyScript compiles a module-level `let` or `var` to a
|
|
57
|
+
mutable WebAssembly global, which SPEC.md §12 forbids (hosts snapshot linear memory for save
|
|
58
|
+
states and netplay), and `wasmad check` rejects it. Put the cart's state in an
|
|
59
|
+
`@unmanaged` class placed in a static block of memory:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
@unmanaged
|
|
63
|
+
class State {
|
|
64
|
+
x: i32;
|
|
65
|
+
y: i32;
|
|
66
|
+
}
|
|
67
|
+
const state = changetype<State>(memory.data(offsetof<State>()));
|
|
68
|
+
|
|
69
|
+
export function start(): void {
|
|
70
|
+
state.x = 112; // memory starts zeroed; set initial values here
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`const` values are fine. Avoid `new`, string concatenation, arrays and anything else that
|
|
75
|
+
allocates; use `memory.data` for static buffers.
|
|
76
|
+
|
|
77
|
+
## API
|
|
78
|
+
|
|
79
|
+
All values are plain integers; builders take a value and return a new one.
|
|
80
|
+
|
|
81
|
+
| | |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `read16`, `write16`, `read32`, `write32` | Raw register access, e.g. `write16(reg.DISPCNT, dispcnt.MODE0 \| dispcnt.BG0)` |
|
|
84
|
+
| `reg.*`, `reg.BGCNT(i)`, `reg.BGHOFS(i)`, `reg.BGVOFS(i)` | Register addresses by their SPEC.md names |
|
|
85
|
+
| `dispcnt`, `layer`, `blend`, `key` | Bit constants; `blend.cnt(effect, first, second)`, `blend.alpha(eva, evb)` |
|
|
86
|
+
| `bgcnt.priority/charBlock/screenBlock/size/bpp8/mosaic` | `BGxCNT` builders |
|
|
87
|
+
| `rgb(r, g, b)` | 8-bit channels to RGB555 |
|
|
88
|
+
| `setBgColor(i, c)`, `setObjColor(i, c)` | Palette RAM |
|
|
89
|
+
| `bgTile(block, i)`, `objTile(i)`, `fillTile(addr, byte)` | Addresses of 32-byte 4bpp tiles |
|
|
90
|
+
| `screenBlock(b)`, `setScreenEntry(b, i, e)`, `screenEntry.make/hflip/vflip` | Background maps |
|
|
91
|
+
| `sprite.make(x, y, SpriteSize.S16x16, tile)` then `.bank/.priority/.hflip/.vflip/.semiTransparent/.window/.mosaic/.bpp8/.affine` | Sprites, packed in a `u64` |
|
|
92
|
+
| `oam.set(i, s)`, `oam.hide(i)`, `oam.hideAll()`, `oam.setAffine(slot, m)` | OAM |
|
|
93
|
+
| `affine.rotateScaleFixed(angle, sx, sy)`, `affine.IDENTITY`, `affine.make` | Affine matrices; angle in 1/256 turns, scale in 8.8 |
|
|
94
|
+
| `fixed.sin(a)`, `fixed.cos(a)` | Q14 trigonometry, identical in every SDK |
|
|
95
|
+
| `buttons.update()`, `buttons.held(k)`, `buttons.pressed(k)`, `keys()` | Input |
|
|
96
|
+
| `frame()`, `seed()` | Time and the only entropy source |
|
|
97
|
+
| `saveRam()`, `saveRestored()`, `requestSave()` | 32 KB of save RAM at 0x19000 |
|
|
98
|
+
| `trace(msg)` | UTF-8 to the host's debug log (at most 1 KB) |
|
package/asconfig.json
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
// AssemblyScript SDK for WASM-AD carts.
|
|
2
|
+
//
|
|
3
|
+
// A cart exports `update` (and optionally `start` and `scanline`) and drives
|
|
4
|
+
// the console by writing its memory-mapped hardware, which this module wraps
|
|
5
|
+
// in typed helpers. See SPEC.md for the hardware.
|
|
6
|
+
//
|
|
7
|
+
// AssemblyScript keeps module-level `let`/`var` in WebAssembly globals, but a
|
|
8
|
+
// cart's state must live in linear memory (SPEC.md §12). So this SDK has no
|
|
9
|
+
// mutable globals, its values are plain integers (no allocation), and carts
|
|
10
|
+
// keep their own state in a `@unmanaged` class placed with `memory.data`:
|
|
11
|
+
//
|
|
12
|
+
// @unmanaged class State { x: i32; y: i32 }
|
|
13
|
+
// const state = changetype<State>(memory.data(offsetof<State>()));
|
|
14
|
+
|
|
15
|
+
// ---- Memory map (SPEC.md §4) ------------------------------------------------
|
|
16
|
+
|
|
17
|
+
export const SCREEN_W: i32 = 240;
|
|
18
|
+
export const SCREEN_H: i32 = 160;
|
|
19
|
+
|
|
20
|
+
export const IO: usize = 0x00400;
|
|
21
|
+
export const PAL_BG: usize = 0x00800;
|
|
22
|
+
export const PAL_OBJ: usize = 0x00a00;
|
|
23
|
+
export const OAM: usize = 0x00c00;
|
|
24
|
+
export const VRAM: usize = 0x01000;
|
|
25
|
+
export const VRAM_SIZE: usize = 0x18000;
|
|
26
|
+
/** Sprite tile 0. */
|
|
27
|
+
export const OBJ_VRAM: usize = VRAM + 0x10000;
|
|
28
|
+
export const SAVE: usize = 0x19000;
|
|
29
|
+
export const SAVE_SIZE: usize = 0x8000;
|
|
30
|
+
/** First address the cart may use for its own data, stack and heap. */
|
|
31
|
+
export const CART: usize = 0x28000;
|
|
32
|
+
/** OAM attribute 0 that hides a sprite. */
|
|
33
|
+
export const OBJ_HIDDEN: u16 = 0x0200;
|
|
34
|
+
export const SPEC_VERSION: u16 = 0x0001;
|
|
35
|
+
|
|
36
|
+
// ---- Registers (SPEC.md §5, §6) ---------------------------------------------
|
|
37
|
+
|
|
38
|
+
/** Every register address, by its SPEC.md name. Use with `read16`/`write16`. */
|
|
39
|
+
export namespace reg {
|
|
40
|
+
export const DISPCNT: usize = 0x400;
|
|
41
|
+
export const VCOUNT: usize = 0x406;
|
|
42
|
+
export const WIN0H: usize = 0x440;
|
|
43
|
+
export const WIN1H: usize = 0x442;
|
|
44
|
+
export const WIN0V: usize = 0x444;
|
|
45
|
+
export const WIN1V: usize = 0x446;
|
|
46
|
+
export const WININ: usize = 0x448;
|
|
47
|
+
export const WINOUT: usize = 0x44a;
|
|
48
|
+
export const MOSAIC: usize = 0x44c;
|
|
49
|
+
export const BLDCNT: usize = 0x450;
|
|
50
|
+
export const BLDALPHA: usize = 0x452;
|
|
51
|
+
export const BLDY: usize = 0x454;
|
|
52
|
+
export const KEYS: usize = 0x700;
|
|
53
|
+
export const FRAME: usize = 0x704; // u32
|
|
54
|
+
export const SEED: usize = 0x708; // u32
|
|
55
|
+
export const SAVECNT: usize = 0x70c;
|
|
56
|
+
export const SAVESTAT: usize = 0x70e;
|
|
57
|
+
export const SPEC: usize = 0x710;
|
|
58
|
+
|
|
59
|
+
/** `BGxCNT` for background 0–3. */
|
|
60
|
+
// @ts-ignore: decorator
|
|
61
|
+
@inline export function BGCNT(bg: i32): usize {
|
|
62
|
+
return 0x408 + (<usize>bg << 1);
|
|
63
|
+
}
|
|
64
|
+
/** `BGxHOFS` for background 0–3. */
|
|
65
|
+
// @ts-ignore: decorator
|
|
66
|
+
@inline export function BGHOFS(bg: i32): usize {
|
|
67
|
+
return 0x410 + (<usize>bg << 2);
|
|
68
|
+
}
|
|
69
|
+
/** `BGxVOFS` for background 0–3. */
|
|
70
|
+
// @ts-ignore: decorator
|
|
71
|
+
@inline export function BGVOFS(bg: i32): usize {
|
|
72
|
+
return 0x412 + (<usize>bg << 2);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// @ts-ignore: decorator
|
|
77
|
+
@inline export function read16(addr: usize): u16 {
|
|
78
|
+
return load<u16>(addr);
|
|
79
|
+
}
|
|
80
|
+
// @ts-ignore: decorator
|
|
81
|
+
@inline export function write16(addr: usize, v: u16): void {
|
|
82
|
+
store<u16>(addr, v);
|
|
83
|
+
}
|
|
84
|
+
// @ts-ignore: decorator
|
|
85
|
+
@inline export function read32(addr: usize): u32 {
|
|
86
|
+
return load<u32>(addr);
|
|
87
|
+
}
|
|
88
|
+
// @ts-ignore: decorator
|
|
89
|
+
@inline export function write32(addr: usize, v: u32): void {
|
|
90
|
+
store<u32>(addr, v);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** `DISPCNT` bits (SPEC.md §6.1). */
|
|
94
|
+
export namespace dispcnt {
|
|
95
|
+
export const MODE0: u16 = 0;
|
|
96
|
+
export const MODE3: u16 = 3;
|
|
97
|
+
export const MODE4: u16 = 4;
|
|
98
|
+
export const MODE5: u16 = 5;
|
|
99
|
+
export const PAGE1: u16 = 1 << 4;
|
|
100
|
+
export const OBJ_1D: u16 = 1 << 6;
|
|
101
|
+
export const FORCED_BLANK: u16 = 1 << 7;
|
|
102
|
+
export const BG0: u16 = 1 << 8;
|
|
103
|
+
export const BG1: u16 = 1 << 9;
|
|
104
|
+
export const BG2: u16 = 1 << 10;
|
|
105
|
+
export const BG3: u16 = 1 << 11;
|
|
106
|
+
export const OBJ: u16 = 1 << 12;
|
|
107
|
+
export const WIN0: u16 = 1 << 13;
|
|
108
|
+
export const WIN1: u16 = 1 << 14;
|
|
109
|
+
export const OBJ_WIN: u16 = 1 << 15;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Layer bits for `BLDCNT` targets and window masks. */
|
|
113
|
+
export namespace layer {
|
|
114
|
+
export const BG0: u16 = 1 << 0;
|
|
115
|
+
export const BG1: u16 = 1 << 1;
|
|
116
|
+
export const BG2: u16 = 1 << 2;
|
|
117
|
+
export const BG3: u16 = 1 << 3;
|
|
118
|
+
export const OBJ: u16 = 1 << 4;
|
|
119
|
+
export const BACKDROP: u16 = 1 << 5;
|
|
120
|
+
/** In window masks: color effects enabled. */
|
|
121
|
+
export const EFFECTS: u16 = 1 << 5;
|
|
122
|
+
export const ALL: u16 = 0x3f;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** `BLDCNT` effects (SPEC.md §6.5). */
|
|
126
|
+
export namespace blend {
|
|
127
|
+
export const NONE: u16 = 0;
|
|
128
|
+
export const ALPHA: u16 = 1 << 6;
|
|
129
|
+
export const BRIGHTEN: u16 = 2 << 6;
|
|
130
|
+
export const DARKEN: u16 = 3 << 6;
|
|
131
|
+
|
|
132
|
+
/** `BLDCNT` value: `effect` on `first` targets, blending onto `second`. */
|
|
133
|
+
// @ts-ignore: decorator
|
|
134
|
+
@inline export function cnt(effect: u16, first: u16, second: u16): u16 {
|
|
135
|
+
return effect | first | (second << 8);
|
|
136
|
+
}
|
|
137
|
+
/** `BLDALPHA` value; coefficients 0–16. */
|
|
138
|
+
// @ts-ignore: decorator
|
|
139
|
+
@inline export function alpha(eva: u16, evb: u16): u16 {
|
|
140
|
+
return eva | (evb << 8);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** `KEYS` bits (SPEC.md §5). */
|
|
145
|
+
export namespace key {
|
|
146
|
+
export const A: u16 = 1 << 0;
|
|
147
|
+
export const B: u16 = 1 << 1;
|
|
148
|
+
export const SELECT: u16 = 1 << 2;
|
|
149
|
+
export const START: u16 = 1 << 3;
|
|
150
|
+
export const RIGHT: u16 = 1 << 4;
|
|
151
|
+
export const LEFT: u16 = 1 << 5;
|
|
152
|
+
export const UP: u16 = 1 << 6;
|
|
153
|
+
export const DOWN: u16 = 1 << 7;
|
|
154
|
+
export const R: u16 = 1 << 8;
|
|
155
|
+
export const L: u16 = 1 << 9;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---- Backgrounds (SPEC.md §6.2, §8) -----------------------------------------
|
|
159
|
+
|
|
160
|
+
/** Background map sizes, in tiles. */
|
|
161
|
+
export enum MapSize {
|
|
162
|
+
S32x32 = 0,
|
|
163
|
+
S64x32 = 1,
|
|
164
|
+
S32x64 = 2,
|
|
165
|
+
S64x64 = 3,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Builders for a `BGxCNT` value (SPEC.md §6.2), e.g. `bgcnt.priority(bgcnt.screenBlock(0, 31), 1)`. */
|
|
169
|
+
export namespace bgcnt {
|
|
170
|
+
// @ts-ignore: decorator
|
|
171
|
+
@inline export function priority(c: u16, p: u16): u16 {
|
|
172
|
+
return (c & ~3) | (p & 3);
|
|
173
|
+
}
|
|
174
|
+
// @ts-ignore: decorator
|
|
175
|
+
@inline export function charBlock(c: u16, b: u16): u16 {
|
|
176
|
+
return (c & ~0xc) | ((b & 3) << 2);
|
|
177
|
+
}
|
|
178
|
+
// @ts-ignore: decorator
|
|
179
|
+
@inline export function screenBlock(c: u16, b: u16): u16 {
|
|
180
|
+
return (c & ~0x1f00) | ((b & 31) << 8);
|
|
181
|
+
}
|
|
182
|
+
// @ts-ignore: decorator
|
|
183
|
+
@inline export function size(c: u16, s: MapSize): u16 {
|
|
184
|
+
return (c & 0x3fff) | (<u16>s << 14);
|
|
185
|
+
}
|
|
186
|
+
// @ts-ignore: decorator
|
|
187
|
+
@inline export function bpp8(c: u16): u16 {
|
|
188
|
+
return c | 0x80;
|
|
189
|
+
}
|
|
190
|
+
// @ts-ignore: decorator
|
|
191
|
+
@inline export function mosaic(c: u16): u16 {
|
|
192
|
+
return c | 0x40;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Packs 8-bit channels into RGB555. */
|
|
197
|
+
// @ts-ignore: decorator
|
|
198
|
+
@inline export function rgb(r: i32, g: i32, b: i32): u16 {
|
|
199
|
+
return <u16>(((r & 255) >> 3) | (((g & 255) >> 3) << 5) | (((b & 255) >> 3) << 10));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Sets background color `i` (0–255; 16 banks of 16 for 4bpp graphics). */
|
|
203
|
+
// @ts-ignore: decorator
|
|
204
|
+
@inline export function setBgColor(i: i32, c: u16): void {
|
|
205
|
+
store<u16>(PAL_BG + (<usize>i << 1), c);
|
|
206
|
+
}
|
|
207
|
+
/** Sets sprite color `i` (0–255). */
|
|
208
|
+
// @ts-ignore: decorator
|
|
209
|
+
@inline export function setObjColor(i: i32, c: u16): void {
|
|
210
|
+
store<u16>(PAL_OBJ + (<usize>i << 1), c);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Address of 4bpp background tile `index` counted from character block `block` (0–3). Tiles are 32 bytes. */
|
|
214
|
+
// @ts-ignore: decorator
|
|
215
|
+
@inline export function bgTile(block: i32, index: i32): usize {
|
|
216
|
+
return VRAM + <usize>block * 0x4000 + (<usize>index << 5);
|
|
217
|
+
}
|
|
218
|
+
/** Address of 4bpp sprite tile `index` (0–1023). */
|
|
219
|
+
// @ts-ignore: decorator
|
|
220
|
+
@inline export function objTile(index: i32): usize {
|
|
221
|
+
return OBJ_VRAM + (<usize>index << 5);
|
|
222
|
+
}
|
|
223
|
+
/** Fills a 32-byte 4bpp tile with one byte value, e.g. 0x11 for solid color 1. */
|
|
224
|
+
// @ts-ignore: decorator
|
|
225
|
+
@inline export function fillTile(tile: usize, byte: u8): void {
|
|
226
|
+
memory.fill(tile, byte, 32);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Address of screen block `block` (0–31): a 32×32 map of u16 entries, row-major. */
|
|
230
|
+
// @ts-ignore: decorator
|
|
231
|
+
@inline export function screenBlock(block: i32): usize {
|
|
232
|
+
return VRAM + <usize>block * 0x800;
|
|
233
|
+
}
|
|
234
|
+
/** Sets entry `i` (0–1023) of screen block `block`. */
|
|
235
|
+
// @ts-ignore: decorator
|
|
236
|
+
@inline export function setScreenEntry(block: i32, i: i32, e: u16): void {
|
|
237
|
+
store<u16>(screenBlock(block) + (<usize>i << 1), e);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Background map entries (SPEC.md §8.2). */
|
|
241
|
+
export namespace screenEntry {
|
|
242
|
+
// @ts-ignore: decorator
|
|
243
|
+
@inline export function make(tile: u16, bank: u16): u16 {
|
|
244
|
+
return (tile & 0x3ff) | ((bank & 0xf) << 12);
|
|
245
|
+
}
|
|
246
|
+
// @ts-ignore: decorator
|
|
247
|
+
@inline export function hflip(e: u16): u16 {
|
|
248
|
+
return e | (1 << 10);
|
|
249
|
+
}
|
|
250
|
+
// @ts-ignore: decorator
|
|
251
|
+
@inline export function vflip(e: u16): u16 {
|
|
252
|
+
return e | (1 << 11);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ---- Sprites (SPEC.md §9) ------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
/** Sprite sizes as shape × 4 + size (SPEC.md §9.2). */
|
|
259
|
+
export enum SpriteSize {
|
|
260
|
+
S8x8 = 0,
|
|
261
|
+
S16x16 = 1,
|
|
262
|
+
S32x32 = 2,
|
|
263
|
+
S64x64 = 3,
|
|
264
|
+
W16x8 = 4,
|
|
265
|
+
W32x8 = 5,
|
|
266
|
+
W32x16 = 6,
|
|
267
|
+
W64x32 = 7,
|
|
268
|
+
T8x16 = 8,
|
|
269
|
+
T8x32 = 9,
|
|
270
|
+
T16x32 = 10,
|
|
271
|
+
T32x64 = 11,
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Width in pixels of a sprite size. */
|
|
275
|
+
export function spriteWidth(s: SpriteSize): i32 {
|
|
276
|
+
const shape = <i32>s >> 2, size = <i32>s & 3;
|
|
277
|
+
if (shape == 0) return 8 << size;
|
|
278
|
+
if (shape == 1) return size == 0 ? 16 : size == 3 ? 64 : 32;
|
|
279
|
+
return size == 3 ? 32 : size == 2 ? 16 : 8;
|
|
280
|
+
}
|
|
281
|
+
/** Height in pixels of a sprite size. */
|
|
282
|
+
export function spriteHeight(s: SpriteSize): i32 {
|
|
283
|
+
const shape = <i32>s >> 2, size = <i32>s & 3;
|
|
284
|
+
if (shape == 0) return 8 << size;
|
|
285
|
+
if (shape == 1) return size == 3 ? 32 : size == 2 ? 16 : 8;
|
|
286
|
+
return size == 0 ? 16 : size == 3 ? 64 : 32;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** One OAM entry's three attributes, packed as attr0 | attr1 << 16 | attr2 << 32. */
|
|
290
|
+
export type Sprite = u64;
|
|
291
|
+
|
|
292
|
+
/** Builders for sprites (SPEC.md §9.1), e.g. `sprite.affine(sprite.make(x, y, SpriteSize.S16x16, 0), 0, true)`. */
|
|
293
|
+
export namespace sprite {
|
|
294
|
+
export const HIDDEN: Sprite = <u64>OBJ_HIDDEN;
|
|
295
|
+
|
|
296
|
+
// @ts-ignore: decorator
|
|
297
|
+
@inline export function attr0(s: Sprite): u16 {
|
|
298
|
+
return <u16>s;
|
|
299
|
+
}
|
|
300
|
+
// @ts-ignore: decorator
|
|
301
|
+
@inline export function attr1(s: Sprite): u16 {
|
|
302
|
+
return <u16>(s >> 16);
|
|
303
|
+
}
|
|
304
|
+
// @ts-ignore: decorator
|
|
305
|
+
@inline export function attr2(s: Sprite): u16 {
|
|
306
|
+
return <u16>(s >> 32);
|
|
307
|
+
}
|
|
308
|
+
// @ts-ignore: decorator
|
|
309
|
+
@inline export function pack(a0: u16, a1: u16, a2: u16): Sprite {
|
|
310
|
+
return <u64>a0 | (<u64>a1 << 16) | (<u64>a2 << 32);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** A sprite with its top-left at screen (x, y), using `tile` and bank 0. */
|
|
314
|
+
export function make(x: i32, y: i32, size: SpriteSize, tile: u16): Sprite {
|
|
315
|
+
const shape = <u16>(<i32>size >> 2), sz = <u16>(<i32>size & 3);
|
|
316
|
+
return pack(<u16>(y & 0xff) | (shape << 14), <u16>(x & 0x1ff) | (sz << 14), tile & 0x3ff);
|
|
317
|
+
}
|
|
318
|
+
export function bank(s: Sprite, b: u16): Sprite {
|
|
319
|
+
return pack(attr0(s), attr1(s), (attr2(s) & 0x0fff) | ((b & 0xf) << 12));
|
|
320
|
+
}
|
|
321
|
+
export function priority(s: Sprite, p: u16): Sprite {
|
|
322
|
+
return pack(attr0(s), attr1(s), (attr2(s) & ~0x0c00) | ((p & 3) << 10));
|
|
323
|
+
}
|
|
324
|
+
export function hflip(s: Sprite, on: bool): Sprite {
|
|
325
|
+
return pack(attr0(s), on ? attr1(s) | 0x1000 : attr1(s) & ~0x1000, attr2(s));
|
|
326
|
+
}
|
|
327
|
+
export function vflip(s: Sprite, on: bool): Sprite {
|
|
328
|
+
return pack(attr0(s), on ? attr1(s) | 0x2000 : attr1(s) & ~0x2000, attr2(s));
|
|
329
|
+
}
|
|
330
|
+
export function semiTransparent(s: Sprite): Sprite {
|
|
331
|
+
return pack((attr0(s) & ~0x0c00) | 0x0400, attr1(s), attr2(s));
|
|
332
|
+
}
|
|
333
|
+
export function window(s: Sprite): Sprite {
|
|
334
|
+
return pack((attr0(s) & ~0x0c00) | 0x0800, attr1(s), attr2(s));
|
|
335
|
+
}
|
|
336
|
+
export function mosaic(s: Sprite): Sprite {
|
|
337
|
+
return pack(attr0(s) | 0x1000, attr1(s), attr2(s));
|
|
338
|
+
}
|
|
339
|
+
export function bpp8(s: Sprite): Sprite {
|
|
340
|
+
return pack(attr0(s) | 0x2000, attr1(s), attr2(s));
|
|
341
|
+
}
|
|
342
|
+
/** Uses affine matrix `slot` (0–31). With `doubleSize` the bounding box
|
|
343
|
+
* doubles so rotated or enlarged pixels are not clipped. */
|
|
344
|
+
export function affine(s: Sprite, slot: u16, doubleSize: bool): Sprite {
|
|
345
|
+
const a0 = attr0(s) | 0x0100 | (doubleSize ? 0x0200 : 0);
|
|
346
|
+
const a1 = (attr1(s) & ~0x3e00) | ((slot & 31) << 9);
|
|
347
|
+
return pack(<u16>a0, a1, attr2(s));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** An affine matrix in 8.8 fixed point, mapping screen to texture space,
|
|
352
|
+
* packed as pa | pb << 16 | pc << 32 | pd << 48. */
|
|
353
|
+
export type AffineMatrix = u64;
|
|
354
|
+
|
|
355
|
+
export namespace affine {
|
|
356
|
+
export const IDENTITY: AffineMatrix = 256 | (<u64>256 << 48);
|
|
357
|
+
|
|
358
|
+
// @ts-ignore: decorator
|
|
359
|
+
@inline export function make(pa: i16, pb: i16, pc: i16, pd: i16): AffineMatrix {
|
|
360
|
+
return <u64><u16>pa | (<u64><u16>pb << 16) | (<u64><u16>pc << 32) | (<u64><u16>pd << 48);
|
|
361
|
+
}
|
|
362
|
+
// @ts-ignore: decorator
|
|
363
|
+
@inline export function pa(m: AffineMatrix): i16 {
|
|
364
|
+
return <i16>m;
|
|
365
|
+
}
|
|
366
|
+
// @ts-ignore: decorator
|
|
367
|
+
@inline export function pb(m: AffineMatrix): i16 {
|
|
368
|
+
return <i16>(m >> 16);
|
|
369
|
+
}
|
|
370
|
+
// @ts-ignore: decorator
|
|
371
|
+
@inline export function pc(m: AffineMatrix): i16 {
|
|
372
|
+
return <i16>(m >> 32);
|
|
373
|
+
}
|
|
374
|
+
// @ts-ignore: decorator
|
|
375
|
+
@inline export function pd(m: AffineMatrix): i16 {
|
|
376
|
+
return <i16>(m >> 48);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function clamp16(v: i32): i16 {
|
|
380
|
+
return <i16>(v < -32768 ? -32768 : v > 32767 ? 32767 : v);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Rotates counter-clockwise by `angle` 1/256 turns and scales by (sx, sy)
|
|
384
|
+
* in 8.8 fixed point (256 = 1×, nonzero). Integer math, identical in
|
|
385
|
+
* every WASM-AD SDK. */
|
|
386
|
+
export function rotateScaleFixed(angle: i32, sx: i32, sy: i32): AffineMatrix {
|
|
387
|
+
const s = fixed.sin(angle), c = fixed.cos(angle);
|
|
388
|
+
return make(clamp16((c * 4) / sx), clamp16((-s * 4) / sx), clamp16((s * 4) / sy), clamp16((c * 4) / sy));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** OAM: 128 sprites, plus affine matrices stored in the gaps. */
|
|
393
|
+
export namespace oam {
|
|
394
|
+
export function set(i: i32, s: Sprite): void {
|
|
395
|
+
const at = OAM + (<usize>i << 3);
|
|
396
|
+
store<u16>(at, sprite.attr0(s));
|
|
397
|
+
store<u16>(at + 2, sprite.attr1(s));
|
|
398
|
+
store<u16>(at + 4, sprite.attr2(s));
|
|
399
|
+
}
|
|
400
|
+
export function hide(i: i32): void {
|
|
401
|
+
set(i, sprite.HIDDEN);
|
|
402
|
+
}
|
|
403
|
+
export function hideAll(): void {
|
|
404
|
+
for (let i = 0; i < 128; i++) hide(i);
|
|
405
|
+
}
|
|
406
|
+
export function setAffine(slot: i32, m: AffineMatrix): void {
|
|
407
|
+
const at = OAM + <usize>slot * 32 + 6;
|
|
408
|
+
store<u16>(at, <u16>affine.pa(m));
|
|
409
|
+
store<u16>(at + 8, <u16>affine.pb(m));
|
|
410
|
+
store<u16>(at + 16, <u16>affine.pc(m));
|
|
411
|
+
store<u16>(at + 24, <u16>affine.pd(m));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ---- Fixed-point trigonometry --------------------------------------------------
|
|
416
|
+
|
|
417
|
+
/** Fixed-point trigonometry shared by all WASM-AD SDKs, so carts in any
|
|
418
|
+
* language compute bit-identical results (SPEC.md §12). */
|
|
419
|
+
export namespace fixed {
|
|
420
|
+
/** sin over a quarter turn in 1/256-turn steps, scaled by 16384 (Q14). */
|
|
421
|
+
export const SIN_Q14: usize = memory.data<i32>([
|
|
422
|
+
0, 402, 804, 1205, 1606, 2006, 2404, 2801, 3196, 3590, 3981, 4370, 4756, 5139, 5520, 5897, 6270, 6639, 7005,
|
|
423
|
+
7366, 7723, 8076, 8423, 8765, 9102, 9434, 9760, 10080, 10394, 10702, 11003, 11297, 11585, 11866, 12140, 12406,
|
|
424
|
+
12665, 12916, 13160, 13395, 13623, 13842, 14053, 14256, 14449, 14635, 14811, 14978, 15137, 15286, 15426, 15557,
|
|
425
|
+
15679, 15791, 15893, 15986, 16069, 16143, 16207, 16261, 16305, 16340, 16364, 16379, 16384,
|
|
426
|
+
]);
|
|
427
|
+
|
|
428
|
+
// @ts-ignore: decorator
|
|
429
|
+
@inline function q(i: i32): i32 {
|
|
430
|
+
return load<i32>(SIN_Q14 + (<usize>i << 2));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** sin of `angle` 1/256 turns (only the low 8 bits count), in Q14. */
|
|
434
|
+
export function sin(angle: i32): i32 {
|
|
435
|
+
const a = angle & 255;
|
|
436
|
+
if (a < 64) return q(a);
|
|
437
|
+
if (a < 128) return q(128 - a);
|
|
438
|
+
if (a < 192) return -q(a - 128);
|
|
439
|
+
return -q(256 - a);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** cos of `angle` 1/256 turns, in Q14. */
|
|
443
|
+
export function cos(angle: i32): i32 {
|
|
444
|
+
return sin(angle + 64);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ---- Input, time, save RAM -------------------------------------------------------
|
|
449
|
+
|
|
450
|
+
/** Buttons held this frame. */
|
|
451
|
+
// @ts-ignore: decorator
|
|
452
|
+
@inline export function keys(): u16 {
|
|
453
|
+
return load<u16>(reg.KEYS);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** Edge detection over `KEYS`: call `buttons.update()` at the start of every
|
|
457
|
+
* frame. Its two words of state live in linear memory. */
|
|
458
|
+
export namespace buttons {
|
|
459
|
+
const STATE: usize = memory.data(4); // held: u16, prev: u16
|
|
460
|
+
|
|
461
|
+
export function update(): void {
|
|
462
|
+
store<u16>(STATE + 2, load<u16>(STATE));
|
|
463
|
+
store<u16>(STATE, keys());
|
|
464
|
+
}
|
|
465
|
+
export function held(k: u16): bool {
|
|
466
|
+
return (load<u16>(STATE) & k) != 0;
|
|
467
|
+
}
|
|
468
|
+
/** True only on the frame the button went down. */
|
|
469
|
+
export function pressed(k: u16): bool {
|
|
470
|
+
return (load<u16>(STATE) & ~load<u16>(STATE + 2) & k) != 0;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Frames since boot. */
|
|
475
|
+
// @ts-ignore: decorator
|
|
476
|
+
@inline export function frame(): u32 {
|
|
477
|
+
return load<u32>(reg.FRAME);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** The boot-time random seed, the cart's only entropy (SPEC.md §12). */
|
|
481
|
+
// @ts-ignore: decorator
|
|
482
|
+
@inline export function seed(): u32 {
|
|
483
|
+
return load<u32>(reg.SEED);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** Address of the 32 KB battery-backed save RAM. */
|
|
487
|
+
// @ts-ignore: decorator
|
|
488
|
+
@inline export function saveRam(): usize {
|
|
489
|
+
return SAVE;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** Whether the host restored save RAM from storage at boot. */
|
|
493
|
+
// @ts-ignore: decorator
|
|
494
|
+
@inline export function saveRestored(): bool {
|
|
495
|
+
return (load<u16>(reg.SAVESTAT) & 1) != 0;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** Asks the host to persist save RAM after this frame. */
|
|
499
|
+
// @ts-ignore: decorator
|
|
500
|
+
@inline export function requestSave(): void {
|
|
501
|
+
store<u16>(reg.SAVECNT, load<u16>(reg.SAVECNT) | 1);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ---- Debug log -------------------------------------------------------------------
|
|
505
|
+
|
|
506
|
+
// @ts-ignore: decorator
|
|
507
|
+
@external("env", "trace")
|
|
508
|
+
declare function hostTrace(ptr: usize, len: i32): void;
|
|
509
|
+
|
|
510
|
+
/** Writes a message to the host's debug log, as UTF-8 (at most 1 KB). */
|
|
511
|
+
export function trace(msg: string): void {
|
|
512
|
+
const buf = memory.data(1024);
|
|
513
|
+
let n: usize = 0;
|
|
514
|
+
const len = msg.length;
|
|
515
|
+
for (let i = 0; i < len; i++) {
|
|
516
|
+
let c = <u32>msg.charCodeAt(i);
|
|
517
|
+
if (c >= 0xd800 && c < 0xdc00 && i + 1 < len) {
|
|
518
|
+
const lo = <u32>msg.charCodeAt(i + 1);
|
|
519
|
+
if (lo >= 0xdc00 && lo < 0xe000) {
|
|
520
|
+
c = 0x10000 + ((c - 0xd800) << 10) + (lo - 0xdc00);
|
|
521
|
+
i++;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
const need: usize = c < 0x80 ? 1 : c < 0x800 ? 2 : c < 0x10000 ? 3 : 4;
|
|
525
|
+
if (n + need > 1024) break;
|
|
526
|
+
if (need == 1) store<u8>(buf + n, c);
|
|
527
|
+
else if (need == 2) {
|
|
528
|
+
store<u8>(buf + n, 0xc0 | (c >> 6));
|
|
529
|
+
store<u8>(buf + n + 1, 0x80 | (c & 63));
|
|
530
|
+
} else if (need == 3) {
|
|
531
|
+
store<u8>(buf + n, 0xe0 | (c >> 12));
|
|
532
|
+
store<u8>(buf + n + 1, 0x80 | ((c >> 6) & 63));
|
|
533
|
+
store<u8>(buf + n + 2, 0x80 | (c & 63));
|
|
534
|
+
} else {
|
|
535
|
+
store<u8>(buf + n, 0xf0 | (c >> 18));
|
|
536
|
+
store<u8>(buf + n + 1, 0x80 | ((c >> 12) & 63));
|
|
537
|
+
store<u8>(buf + n + 2, 0x80 | ((c >> 6) & 63));
|
|
538
|
+
store<u8>(buf + n + 3, 0x80 | (c & 63));
|
|
539
|
+
}
|
|
540
|
+
n += need;
|
|
541
|
+
}
|
|
542
|
+
hostTrace(buf, <i32>n);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** Replaces AssemblyScript's `env.abort` import (`--use abort=...` in asconfig.json):
|
|
546
|
+
* a failed assertion or bounds check traps. */
|
|
547
|
+
export function abort(message: string | null, file: string | null, line: u32, column: u32): void {
|
|
548
|
+
unreachable();
|
|
549
|
+
}
|
package/bin/wasmad.mjs
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
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 with the
|
|
14
|
+
// reference renderer, 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
|
+
// --ppm FILE write the last rendered frame as a PPM image
|
|
20
|
+
// --core FILE a different core.wasm
|
|
21
|
+
|
|
22
|
+
import { readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
23
|
+
import { createHash } from 'node:crypto';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
|
|
26
|
+
const SPEC = '0.1';
|
|
27
|
+
const SPEC_VERSION = 0x0001;
|
|
28
|
+
const W = 240, H = 160;
|
|
29
|
+
const A = {
|
|
30
|
+
IO: 0x400, PAL_END: 0xC00, OAM: 0xC00, VIDEO_END: 0x19000, SAVE: 0x19000, SAVE_SIZE: 0x8000, CART: 0x28000,
|
|
31
|
+
VCOUNT: 0x406, KEYS: 0x700, FRAME: 0x704, SEED: 0x708, SAVECNT: 0x70C, SAVESTAT: 0x70E, SPEC: 0x710,
|
|
32
|
+
};
|
|
33
|
+
const KEY = { a: 1, b: 2, select: 4, start: 8, right: 16, left: 32, up: 64, down: 128, r: 256, l: 512 };
|
|
34
|
+
const MAX_CART = 32 * 1024 * 1024;
|
|
35
|
+
|
|
36
|
+
// ---- WebAssembly binary reading -------------------------------------------
|
|
37
|
+
|
|
38
|
+
class Reader {
|
|
39
|
+
constructor(bytes, pos = 0, end = bytes.length) {
|
|
40
|
+
Object.assign(this, { bytes, pos, end });
|
|
41
|
+
}
|
|
42
|
+
byte() {
|
|
43
|
+
if (this.pos >= this.end) throw new Error('unexpected end of module');
|
|
44
|
+
return this.bytes[this.pos++];
|
|
45
|
+
}
|
|
46
|
+
u32() {
|
|
47
|
+
let result = 0, shift = 0, b;
|
|
48
|
+
do {
|
|
49
|
+
b = this.byte();
|
|
50
|
+
result |= (b & 0x7F) << shift;
|
|
51
|
+
shift += 7;
|
|
52
|
+
} while (b & 0x80);
|
|
53
|
+
return result >>> 0;
|
|
54
|
+
}
|
|
55
|
+
i32() {
|
|
56
|
+
let result = 0, shift = 0, b;
|
|
57
|
+
do {
|
|
58
|
+
b = this.byte();
|
|
59
|
+
result |= (b & 0x7F) << shift;
|
|
60
|
+
shift += 7;
|
|
61
|
+
} while (b & 0x80);
|
|
62
|
+
return shift < 32 && b & 0x40 ? result | (-1 << shift) : result | 0;
|
|
63
|
+
}
|
|
64
|
+
skip(n) { this.pos += n; }
|
|
65
|
+
name() {
|
|
66
|
+
const n = this.u32();
|
|
67
|
+
const s = new TextDecoder().decode(this.bytes.subarray(this.pos, this.pos + n));
|
|
68
|
+
this.pos += n;
|
|
69
|
+
return s;
|
|
70
|
+
}
|
|
71
|
+
limits() {
|
|
72
|
+
const flags = this.byte();
|
|
73
|
+
const min = this.u32();
|
|
74
|
+
const max = flags & 1 ? this.u32() : null;
|
|
75
|
+
return { flags, min, max };
|
|
76
|
+
}
|
|
77
|
+
// A constant expression; returns its value if it is a single i32.const.
|
|
78
|
+
constExpr() {
|
|
79
|
+
let value = null, first = true;
|
|
80
|
+
for (;;) {
|
|
81
|
+
const op = this.byte();
|
|
82
|
+
if (op === 0x0B) return value;
|
|
83
|
+
if (op === 0x41) value = first ? this.i32() : (this.i32(), null);
|
|
84
|
+
else if (op === 0x23) { this.u32(); value = null; } // global.get
|
|
85
|
+
else if (op === 0x42) { while (this.byte() & 0x80); value = null; }
|
|
86
|
+
else if (op === 0x43) { this.skip(4); value = null; }
|
|
87
|
+
else if (op === 0x44) { this.skip(8); value = null; }
|
|
88
|
+
else value = null;
|
|
89
|
+
first = false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const VALTYPE = { 0x7F: 'i32', 0x7E: 'i64', 0x7D: 'f32', 0x7C: 'f64', 0x7B: 'v128', 0x70: 'funcref', 0x6F: 'externref' };
|
|
95
|
+
|
|
96
|
+
function sections(bytes) {
|
|
97
|
+
if (bytes.length < 8 || bytes[0] !== 0 || bytes[1] !== 0x61 || bytes[2] !== 0x73 || bytes[3] !== 0x6D) {
|
|
98
|
+
throw new Error('not a WebAssembly module');
|
|
99
|
+
}
|
|
100
|
+
const out = [];
|
|
101
|
+
const r = new Reader(bytes, 8);
|
|
102
|
+
while (r.pos < bytes.length) {
|
|
103
|
+
const start = r.pos;
|
|
104
|
+
const id = r.byte();
|
|
105
|
+
const size = r.u32();
|
|
106
|
+
const body = r.pos;
|
|
107
|
+
out.push({ id, start, body, end: body + size });
|
|
108
|
+
r.pos = body + size;
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Parses the parts of a module that SPEC.md constrains. */
|
|
114
|
+
export function inspect(bytes) {
|
|
115
|
+
const m = { types: [], imports: [], funcs: [], memories: [], globals: [], exports: [], data: [], custom: [], start: null };
|
|
116
|
+
let importedFuncs = 0, importedGlobals = 0;
|
|
117
|
+
for (const s of sections(bytes)) {
|
|
118
|
+
const r = new Reader(bytes, s.body, s.end);
|
|
119
|
+
switch (s.id) {
|
|
120
|
+
case 0: m.custom.push({ name: r.name(), data: bytes.subarray(r.pos, s.end), start: s.start, end: s.end }); break;
|
|
121
|
+
case 1:
|
|
122
|
+
for (let n = r.u32(); n--;) {
|
|
123
|
+
if (r.byte() !== 0x60) throw new Error('unsupported type form');
|
|
124
|
+
const params = Array.from({ length: r.u32() }, () => VALTYPE[r.byte()]);
|
|
125
|
+
const results = Array.from({ length: r.u32() }, () => VALTYPE[r.byte()]);
|
|
126
|
+
m.types.push({ params, results });
|
|
127
|
+
}
|
|
128
|
+
break;
|
|
129
|
+
case 2:
|
|
130
|
+
for (let n = r.u32(); n--;) {
|
|
131
|
+
const imp = { module: r.name(), name: r.name(), kind: r.byte() };
|
|
132
|
+
if (imp.kind === 0) { imp.type = r.u32(); importedFuncs++; m.funcs.push(imp.type); }
|
|
133
|
+
else if (imp.kind === 1) { r.byte(); r.limits(); }
|
|
134
|
+
else if (imp.kind === 2) { imp.limits = r.limits(); m.memories.push(imp.limits); }
|
|
135
|
+
else if (imp.kind === 3) { r.byte(); imp.mutable = r.byte() === 1; importedGlobals++; m.globals.push({ imported: true, mutable: imp.mutable }); }
|
|
136
|
+
m.imports.push(imp);
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
case 3: for (let n = r.u32(); n--;) m.funcs.push(r.u32()); break;
|
|
140
|
+
case 5: for (let n = r.u32(); n--;) m.memories.push(r.limits()); break;
|
|
141
|
+
case 6:
|
|
142
|
+
for (let n = r.u32(); n--;) {
|
|
143
|
+
const type = VALTYPE[r.byte()];
|
|
144
|
+
const mutable = r.byte() === 1;
|
|
145
|
+
m.globals.push({ type, mutable, init: r.constExpr() });
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
case 7:
|
|
149
|
+
for (let n = r.u32(); n--;) m.exports.push({ name: r.name(), kind: r.byte(), index: r.u32() });
|
|
150
|
+
break;
|
|
151
|
+
case 8: m.start = r.u32(); break;
|
|
152
|
+
case 11:
|
|
153
|
+
for (let n = r.u32(); n--;) {
|
|
154
|
+
const flags = r.u32();
|
|
155
|
+
let offset = null, active = flags !== 1;
|
|
156
|
+
if (flags === 2) r.u32();
|
|
157
|
+
if (active) offset = r.constExpr();
|
|
158
|
+
const size = r.u32();
|
|
159
|
+
r.skip(size);
|
|
160
|
+
m.data.push({ active, offset, size });
|
|
161
|
+
}
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
m.importedFuncs = importedFuncs;
|
|
166
|
+
m.importedGlobals = importedGlobals;
|
|
167
|
+
return m;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Validates a cart against SPEC.md. Returns { errors, warnings, meta, info }. */
|
|
171
|
+
export function check(bytes) {
|
|
172
|
+
const errors = [], warnings = [], info = [];
|
|
173
|
+
if (bytes.length > MAX_CART) errors.push(`cart is ${bytes.length} bytes; the limit is 32 MiB (§2.4)`);
|
|
174
|
+
let m;
|
|
175
|
+
try {
|
|
176
|
+
m = inspect(bytes);
|
|
177
|
+
new WebAssembly.Module(bytes);
|
|
178
|
+
} catch (e) {
|
|
179
|
+
return { errors: [`invalid module: ${e.message}`], warnings, meta: null, info };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// §2.2 imports
|
|
183
|
+
for (const imp of m.imports) {
|
|
184
|
+
if (imp.module === 'env' && imp.name === 'trace' && imp.kind === 0) {
|
|
185
|
+
const t = m.types[imp.type];
|
|
186
|
+
if (t.params.join() !== 'i32,i32' || t.results.length) errors.push('env.trace must have type (i32, i32) -> () (§2.2)');
|
|
187
|
+
} else {
|
|
188
|
+
errors.push(`imports ${imp.module}.${imp.name}; a cart may only import env.trace (§2.2)`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// §2.1 exports
|
|
193
|
+
const exp = Object.fromEntries(m.exports.map((e) => [e.name, e]));
|
|
194
|
+
const funcType = (name) => m.types[m.funcs[exp[name].index]];
|
|
195
|
+
const sig = (t) => `(${t.params.join(', ')}) -> (${t.results.join(', ')})`;
|
|
196
|
+
const want = { update: '() -> ()', start: '() -> ()', scanline: '(i32) -> ()' };
|
|
197
|
+
if (!exp.memory || exp.memory.kind !== 2) errors.push('does not export memory (§2.1)');
|
|
198
|
+
else {
|
|
199
|
+
const mem = m.memories[exp.memory.index];
|
|
200
|
+
if (mem.min < 3) errors.push(`memory starts at ${mem.min} pages; at least 3 are required (§2.1)`);
|
|
201
|
+
if (mem.max !== null && mem.max < 256) warnings.push(`memory is capped at ${mem.max} pages; hosts allow growth to 16 MiB (§2.4)`);
|
|
202
|
+
info.push(`memory: ${mem.min} pages${mem.max !== null ? `, max ${mem.max}` : ''}`);
|
|
203
|
+
}
|
|
204
|
+
for (const [name, want_] of Object.entries(want)) {
|
|
205
|
+
if (!exp[name]) {
|
|
206
|
+
if (name === 'update') errors.push('does not export update (§2.1)');
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (exp[name].kind !== 0) { errors.push(`export ${name} is not a function (§2.1)`); continue; }
|
|
210
|
+
const got = sig(funcType(name));
|
|
211
|
+
if (got !== want_) errors.push(`${name} has type ${got}; expected ${want_} (§2.1)`);
|
|
212
|
+
}
|
|
213
|
+
info.push(`exports: ${['update', 'start', 'scanline'].filter((n) => exp[n]).join(', ')}`);
|
|
214
|
+
if (m.start !== null) errors.push('uses the WebAssembly start section; export `start` instead (§2.1)');
|
|
215
|
+
|
|
216
|
+
// §4 memory layout
|
|
217
|
+
for (const d of m.data) {
|
|
218
|
+
if (!d.active) continue;
|
|
219
|
+
if (d.offset === null) warnings.push('a data segment has a non-constant offset; cannot check it is above 0x28000');
|
|
220
|
+
else if ((d.offset >>> 0) < A.CART && d.size > 0) {
|
|
221
|
+
errors.push(`data segment at 0x${(d.offset >>> 0).toString(16)} lies below 0x28000 (§4, §13)`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Only the stack pointer may be a mutable global; we can't see names in a
|
|
225
|
+
// stripped module, so a lone i32 starting in the cart region is taken as it.
|
|
226
|
+
const mutable = m.globals.filter((g) => !g.imported && g.mutable);
|
|
227
|
+
if (mutable.length > 1) {
|
|
228
|
+
errors.push(`${mutable.length} mutable globals; cart state must live in linear memory, only the stack pointer may be a global (§12)`);
|
|
229
|
+
} else if (mutable.length === 1) {
|
|
230
|
+
const [g] = mutable;
|
|
231
|
+
if (g.type !== 'i32') errors.push(`a mutable ${g.type} global holds cart state; it must live in linear memory (§12)`);
|
|
232
|
+
else if (g.init !== null && (g.init >>> 0) < A.CART) {
|
|
233
|
+
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)`);
|
|
234
|
+
} else if (g.init !== null) info.push(`stack pointer: 0x${(g.init >>> 0).toString(16)}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// §2.3 metadata
|
|
238
|
+
let meta = null;
|
|
239
|
+
const sections_ = m.custom.filter((c) => c.name === 'wasm-ad');
|
|
240
|
+
if (!sections_.length) errors.push('missing the "wasm-ad" metadata section (§2.3)');
|
|
241
|
+
else {
|
|
242
|
+
if (sections_.length > 1) warnings.push('more than one "wasm-ad" section; hosts read the first');
|
|
243
|
+
meta = parseMeta(sections_[0].data);
|
|
244
|
+
if (!meta.spec) errors.push('metadata has no spec key (§2.3)');
|
|
245
|
+
else if (!/^\d+\.\d+$/.test(meta.spec)) errors.push(`metadata spec "${meta.spec}" is not major.minor (§2.3)`);
|
|
246
|
+
if (!meta.title) errors.push('metadata has no title (§2.3)');
|
|
247
|
+
}
|
|
248
|
+
return { errors, warnings, meta, info };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function parseMeta(data) {
|
|
252
|
+
const meta = {};
|
|
253
|
+
for (const line of new TextDecoder().decode(data).split('\n')) {
|
|
254
|
+
const i = line.indexOf('=');
|
|
255
|
+
if (i > 0) meta[line.slice(0, i).trim().toLowerCase()] = line.slice(i + 1).trim();
|
|
256
|
+
}
|
|
257
|
+
return meta;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function leb(n) {
|
|
261
|
+
const out = [];
|
|
262
|
+
do {
|
|
263
|
+
let b = n & 0x7F;
|
|
264
|
+
n >>>= 7;
|
|
265
|
+
if (n) b |= 0x80;
|
|
266
|
+
out.push(b);
|
|
267
|
+
} while (n);
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Returns `bytes` with its `wasm-ad` sections replaced by one holding `meta`. */
|
|
272
|
+
export function setMeta(bytes, meta) {
|
|
273
|
+
const text = Object.entries(meta).map(([k, v]) => `${k}=${v}\n`).join('');
|
|
274
|
+
const name = new TextEncoder().encode('wasm-ad');
|
|
275
|
+
const payload = [...leb(name.length), ...name, ...new TextEncoder().encode(text)];
|
|
276
|
+
const section = Uint8Array.from([0, ...leb(payload.length), ...payload]);
|
|
277
|
+
const parts = [bytes.subarray(0, 8)];
|
|
278
|
+
for (const s of sections(bytes)) {
|
|
279
|
+
if (s.id === 0 && new Reader(bytes, s.body, s.end).name() === 'wasm-ad') continue;
|
|
280
|
+
parts.push(bytes.subarray(s.start, s.end));
|
|
281
|
+
}
|
|
282
|
+
parts.push(section);
|
|
283
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
284
|
+
let at = 0;
|
|
285
|
+
for (const p of parts) { out.set(p, at); at += p.length; }
|
|
286
|
+
return out;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ---- Headless host ----------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
/** Parses "0-29:right,30-59:down+b,60:a" into a frame -> KEYS function. */
|
|
292
|
+
export function parseKeys(script) {
|
|
293
|
+
const spans = (script ?? '').split(',').filter(Boolean).map((part) => {
|
|
294
|
+
const [range, buttons] = part.split(':');
|
|
295
|
+
const [from, to = from] = range.split('-').map(Number);
|
|
296
|
+
let mask = 0;
|
|
297
|
+
for (const b of (buttons ?? '').split('+').filter(Boolean)) {
|
|
298
|
+
if (!(b in KEY)) throw new Error(`unknown button "${b}"; use ${Object.keys(KEY).join(', ')}`);
|
|
299
|
+
mask |= KEY[b];
|
|
300
|
+
}
|
|
301
|
+
return { from, to, mask };
|
|
302
|
+
});
|
|
303
|
+
return (frame) => spans.reduce((k, s) => (frame >= s.from && frame <= s.to ? k | s.mask : k), 0);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Boots a cart and returns a host that runs frames (SPEC.md §3). */
|
|
307
|
+
export async function boot(bytes, { core: corePath, seed = 1, onTrace = () => {} } = {}) {
|
|
308
|
+
const core = (await WebAssembly.instantiate(readFileSync(corePath))).instance.exports;
|
|
309
|
+
let instance;
|
|
310
|
+
const decoder = new TextDecoder();
|
|
311
|
+
const imports = { env: { trace: (ptr, len) => onTrace(decoder.decode(new Uint8Array(instance.exports.memory.buffer, ptr, len))) } };
|
|
312
|
+
instance = (await WebAssembly.instantiate(bytes, imports)).instance;
|
|
313
|
+
const ex = instance.exports;
|
|
314
|
+
const mem = () => new Uint8Array(ex.memory.buffer);
|
|
315
|
+
const dv = () => new DataView(ex.memory.buffer);
|
|
316
|
+
mem().fill(0, 0, A.CART);
|
|
317
|
+
for (let i = 0; i < 128; i++) dv().setUint16(A.OAM + i * 8, 0x0200, true);
|
|
318
|
+
dv().setUint32(A.SEED, seed >>> 0, true);
|
|
319
|
+
dv().setUint16(A.SPEC, SPEC_VERSION, true);
|
|
320
|
+
ex.start?.();
|
|
321
|
+
|
|
322
|
+
let frame = 0;
|
|
323
|
+
return {
|
|
324
|
+
exports: ex,
|
|
325
|
+
/** Runs one frame with `keys` held and returns the rendered RGBA8 pixels. */
|
|
326
|
+
frame(keys = 0) {
|
|
327
|
+
dv().setUint16(A.KEYS, keys & 0x3FF, true);
|
|
328
|
+
dv().setUint32(A.FRAME, frame >>> 0, true);
|
|
329
|
+
dv().setUint16(A.VCOUNT, 160, true);
|
|
330
|
+
ex.update();
|
|
331
|
+
const coreMem = new Uint8Array(core.memory.buffer, core.ad_mem(), core.ad_mem_len());
|
|
332
|
+
coreMem.set(mem().subarray(0, A.VIDEO_END));
|
|
333
|
+
for (let y = 0; y < H; y++) {
|
|
334
|
+
if (ex.scanline) {
|
|
335
|
+
dv().setUint16(A.VCOUNT, y, true);
|
|
336
|
+
ex.scanline(y);
|
|
337
|
+
coreMem.set(mem().subarray(A.IO, A.PAL_END), A.IO);
|
|
338
|
+
}
|
|
339
|
+
core.ad_render_line(y);
|
|
340
|
+
}
|
|
341
|
+
if (dv().getUint16(A.SAVECNT, true) & 1) dv().setUint16(A.SAVECNT, dv().getUint16(A.SAVECNT, true) & ~1, true);
|
|
342
|
+
frame++;
|
|
343
|
+
return new Uint8Array(core.memory.buffer, core.ad_framebuffer(), W * H * 4).slice();
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function hash(pixels) {
|
|
349
|
+
return createHash('sha256').update(pixels).digest('hex').slice(0, 16);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function ppm(pixels) {
|
|
353
|
+
const head = new TextEncoder().encode(`P6 ${W} ${H} 255\n`);
|
|
354
|
+
const out = new Uint8Array(head.length + W * H * 3);
|
|
355
|
+
out.set(head);
|
|
356
|
+
for (let i = 0; i < W * H; i++) out.set(pixels.subarray(i * 4, i * 4 + 3), head.length + i * 3);
|
|
357
|
+
return out;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ---- CLI ----------------------------------------------------------------------
|
|
361
|
+
|
|
362
|
+
function options(args) {
|
|
363
|
+
const opts = { _: [] };
|
|
364
|
+
for (let i = 0; i < args.length; i++) {
|
|
365
|
+
if (args[i] === '-o') opts.o = args[++i];
|
|
366
|
+
else if (args[i].startsWith('--')) opts[args[i].slice(2)] = args[++i];
|
|
367
|
+
else opts._.push(args[i]);
|
|
368
|
+
}
|
|
369
|
+
return opts;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function main([cmd, ...args]) {
|
|
373
|
+
const opts = options(args);
|
|
374
|
+
const [file, ...rest] = opts._;
|
|
375
|
+
if (!cmd || !file || !['check', 'meta', 'run'].includes(cmd)) {
|
|
376
|
+
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]');
|
|
377
|
+
return 2;
|
|
378
|
+
}
|
|
379
|
+
const bytes = new Uint8Array(readFileSync(file));
|
|
380
|
+
|
|
381
|
+
if (cmd === 'meta') {
|
|
382
|
+
const meta = { spec: SPEC };
|
|
383
|
+
for (const kv of rest) {
|
|
384
|
+
const i = kv.indexOf('=');
|
|
385
|
+
if (i <= 0) throw new Error(`expected key=value, got "${kv}"`);
|
|
386
|
+
meta[kv.slice(0, i).toLowerCase()] = kv.slice(i + 1);
|
|
387
|
+
}
|
|
388
|
+
if (!meta.title) throw new Error('title=... is required');
|
|
389
|
+
writeFileSync(opts.o ?? file, setMeta(bytes, meta));
|
|
390
|
+
return 0;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (cmd === 'check') {
|
|
394
|
+
const { errors, warnings, meta, info } = check(bytes);
|
|
395
|
+
if (meta) console.log(`${meta.title}${meta.author ? ` by ${meta.author}` : ''} (spec ${meta.spec}), ${bytes.length} bytes`);
|
|
396
|
+
for (const line of info) console.log(` ${line}`);
|
|
397
|
+
for (const w of warnings) console.log(`warning: ${w}`);
|
|
398
|
+
for (const e of errors) console.log(`error: ${e}`);
|
|
399
|
+
if (!errors.length) console.log('ok');
|
|
400
|
+
return errors.length ? 1 : 0;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const { errors } = check(bytes);
|
|
404
|
+
if (errors.length) {
|
|
405
|
+
for (const e of errors) console.error(`error: ${e}`);
|
|
406
|
+
return 1;
|
|
407
|
+
}
|
|
408
|
+
const core = opts.core ?? fileURLToPath(new URL('../player/web/core.wasm', import.meta.url));
|
|
409
|
+
const host = await boot(bytes, { core, seed: Number(opts.seed ?? 1), onTrace: (m) => console.log(`trace: ${m}`) });
|
|
410
|
+
const keys = parseKeys(opts.keys);
|
|
411
|
+
const frames = Number(opts.frames ?? 1);
|
|
412
|
+
const marks = new Set((opts.hash ?? '').split(',').filter(Boolean).map(Number));
|
|
413
|
+
let pixels;
|
|
414
|
+
for (let f = 0; f < frames; f++) {
|
|
415
|
+
pixels = host.frame(keys(f));
|
|
416
|
+
if (marks.has(f)) console.log(`frame ${f}: ${hash(pixels)}`);
|
|
417
|
+
}
|
|
418
|
+
if (opts.ppm) writeFileSync(opts.ppm, ppm(pixels));
|
|
419
|
+
return 0;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Run as a command, also through a symlink such as node_modules/.bin/wasmad.
|
|
423
|
+
if (process.argv[1] && realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])) {
|
|
424
|
+
main(process.argv.slice(2)).then((code) => process.exit(code), (e) => {
|
|
425
|
+
console.error(`error: ${e.message}`);
|
|
426
|
+
process.exit(1);
|
|
427
|
+
});
|
|
428
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raskell/wasm-ad",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.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
|
+
}
|