@wasm-gaming/engine-specs 0.2.2 → 0.2.3

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/demo/README.md CHANGED
@@ -55,6 +55,10 @@ To apply your custom branding, override any of the `:root` variables in `demo.cs
55
55
  --demo-picker-bg: rgba(255, 255, 255, 0.1);
56
56
  --demo-picker-hover-bg: rgba(255, 255, 255, 0.2);
57
57
  --demo-picker-text: #ffffff;
58
+
59
+ /* Launch button */
60
+ --demo-launch-bg: var(--demo-accent);
61
+ --demo-launch-text: #ffffff;
58
62
  }
59
63
  ```
60
64
 
@@ -80,6 +84,8 @@ To apply your custom branding, override any of the `:root` variables in `demo.cs
80
84
  | `--demo-picker-bg` | Pick File button background | `rgba(255, 255, 255, 0.5)` |
81
85
  | `--demo-pill-bg` | Metadata badge background | `rgba(var(--demo-accent-rgb), 0.1)` |
82
86
  | `--demo-file-info-bg` | Canvas overlay panel background | `rgba(255, 255, 255, 0.7)` |
87
+ | `--demo-launch-bg` | Launch button background color | `var(--demo-accent)` |
88
+ | `--demo-launch-text` | Launch button text color | `#ffffff` |
83
89
  | `--demo-font-sans` | Primary sans-serif font family | `"Outfit", system-ui, sans-serif` |
84
90
  | `--demo-font-mono` | Monospace code font family | `"Space Mono", monospace` |
85
91
 
@@ -87,8 +93,10 @@ To apply your custom branding, override any of the `:root` variables in `demo.cs
87
93
 
88
94
  ## 🛠 Features
89
95
 
96
+ - **OPFS Asset Persistence**: Automatically stores selected ROM and BIOS files in the Origin Private File System (`navigator.storage.getDirectory()`), restoring them automatically across page reloads.
97
+ - **Split Asset Drop Zones**: Independent drop/picker areas for optional **BIOS** firmware and game **ROM** assets.
98
+ - **Dedicated Launch Trigger**: Prominent launch button that activates once required game assets are selected.
90
99
  - **Automated Manifest Integration**: Page title, SDK info, asset key mapping, and file type (`accept`) filters are automatically loaded from `sdk.manifest`.
91
- - **ROM Drag & Drop**: Interactive drop zone with file picker fallback.
92
100
  - **Checksum Calculation**: Computes MD5, SHA-1, and SHA-256 hashes of loaded files asynchronously in the background.
93
101
  - **Shadow DOM Isolation**: UI components are isolated using Shadow DOM to prevent style leakage.
94
102
  - **Subpath Hosting Ready**: Uses `import.meta.url` for component templates to work seamlessly on subpaths (e.g. GitHub Pages).
@@ -0,0 +1,124 @@
1
+ <section class="drop" :class.active="dragging" :class.selected="selectedName"
2
+ @dragover.prevent="dragging = true"
3
+ @dragleave="dragging = false"
4
+ @drop.prevent="onDrop">
5
+ <strong>{{ prompt }}</strong>
6
+ <p :class.file-selected="selectedName">{{ selectedName ? 'Selected: ' + selectedName : assetHint }}</p>
7
+ <label class="picker">
8
+ {{ selectedName ? 'Change File' : buttonText }}
9
+ <input type="file" :accept="accept" @change="onPick" />
10
+ </label>
11
+ </section>
12
+
13
+ <script :setup="{
14
+ prompt,
15
+ assetHint = 'or choose one from your computer',
16
+ accept,
17
+ buttonText = 'Pick file',
18
+ fileName = '',
19
+ }">
20
+ let dragging = false;
21
+ let selectedName = fileName || "";
22
+
23
+ const extractFile = (arg) => {
24
+ if (!arg) return null;
25
+ if (arg instanceof File) return arg;
26
+ if (arg.detail instanceof File) return arg.detail;
27
+ if (arg.target?.files?.item(0)) return arg.target.files.item(0);
28
+ if (arg.dataTransfer?.files?.item(0)) return arg.dataTransfer.files.item(0);
29
+ return null;
30
+ };
31
+
32
+ const emitFile = (file) => {
33
+ if (!file) return;
34
+ selectedName = file.name;
35
+ $emit("select", file);
36
+ };
37
+
38
+ const onDrop = (event) => {
39
+ dragging = false;
40
+ const file = extractFile(event);
41
+ if (file) emitFile(file);
42
+ };
43
+
44
+ const onPick = (event) => {
45
+ const file = extractFile(event);
46
+ if (file) emitFile(file);
47
+ };
48
+ </script>
49
+
50
+ <style>
51
+ .drop {
52
+ border: 2px dashed var(--demo-drop-border, rgba(255, 255, 255, 0.6));
53
+ border-radius: var(--demo-subpanel-radius, 8px);
54
+ padding: 2rem 1.25rem;
55
+ text-align: center;
56
+ transition: all 250ms ease;
57
+ background: var(--demo-drop-bg, rgba(255, 255, 255, 0.15));
58
+ backdrop-filter: blur(var(--demo-subpanel-blur, 12px));
59
+ -webkit-backdrop-filter: blur(var(--demo-subpanel-blur, 12px));
60
+ }
61
+
62
+ .drop.active {
63
+ border-color: var(--demo-drop-active-border, var(--demo-accent2, #0284c7));
64
+ background: var(--demo-drop-active-bg, rgba(255, 255, 255, 0.4));
65
+ box-shadow: var(--demo-drop-active-shadow, inset 0 0 0 1px rgba(255, 255, 255, 0.8));
66
+ }
67
+
68
+ .drop.selected {
69
+ border-style: solid;
70
+ border-color: var(--demo-accent2, #0284c7);
71
+ background: var(--demo-subpanel-bg, rgba(255, 255, 255, 0.35));
72
+ }
73
+
74
+ .drop strong {
75
+ display: block;
76
+ margin-bottom: 0.5rem;
77
+ font-size: 1.1rem;
78
+ font-weight: 600;
79
+ text-transform: uppercase;
80
+ letter-spacing: 0.05em;
81
+ color: var(--demo-text, #0f172a);
82
+ }
83
+
84
+ .drop p {
85
+ margin: 0.2rem 0;
86
+ color: var(--demo-muted, #475569);
87
+ font-family: var(--demo-font-mono, monospace);
88
+ font-size: 0.85rem;
89
+ word-break: break-all;
90
+ }
91
+
92
+ .drop p.file-selected {
93
+ color: var(--demo-text, #0f172a);
94
+ font-weight: 600;
95
+ }
96
+
97
+ .picker {
98
+ display: inline-flex;
99
+ margin-top: 1rem;
100
+ background: var(--demo-picker-bg, rgba(255, 255, 255, 0.5));
101
+ color: var(--demo-picker-text, var(--demo-text, #0f172a));
102
+ border: 1px solid var(--demo-picker-border, rgba(255, 255, 255, 0.6));
103
+ border-radius: var(--demo-subpanel-radius, 8px);
104
+ padding: 0.5rem 1rem;
105
+ cursor: pointer;
106
+ text-transform: uppercase;
107
+ font-weight: 600;
108
+ font-size: 0.85rem;
109
+ letter-spacing: 0.05em;
110
+ transition: all 0.2s;
111
+ backdrop-filter: blur(var(--demo-subpanel-blur, 12px));
112
+ -webkit-backdrop-filter: blur(var(--demo-subpanel-blur, 12px));
113
+ }
114
+
115
+ .picker:hover {
116
+ background: var(--demo-picker-hover-bg, rgba(255, 255, 255, 0.8));
117
+ border-color: var(--demo-picker-hover-border, rgba(255, 255, 255, 0.9));
118
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
119
+ }
120
+
121
+ .picker input {
122
+ display: none;
123
+ }
124
+ </style>
@@ -1,53 +1,107 @@
1
- <form class="launcher">
1
+ <form class="launcher" @submit.prevent="onLaunch">
2
2
  <section class="sdk">
3
3
  <CSdkInfo ...sdkInfo />
4
4
  </section>
5
- <section class="drop" :class.active="dragging" @dragover.prevent="dragging = true" @dragleave="dragging = false"
6
- @drop.prevent="onDrop">
7
- <strong>Drop your ROM/asset file here</strong>
8
- <p>{{ assetHint }}</p>
9
- <label class="picker">
10
- Pick file
11
- <input type="file" :accept="accept" @change="onPick" />
12
- </label>
13
- </section>
5
+
6
+ <div class="drop-grid" :class.single="!showBIOS">
7
+ <CDropArea
8
+ :if="showBIOS"
9
+ :prompt="requiresBIOS ? 'Drop BIOS File (Required)' : 'Drop BIOS File (Optional)'"
10
+ :asset-hint="biosAssetHint"
11
+ :accept="biosAccept"
12
+ :button-text="'Pick BIOS'"
13
+ :file-name="biosFile?.name"
14
+ @select="onBiosSelect"
15
+ />
16
+ <CDropArea
17
+ :prompt="'Drop ROM File'"
18
+ :asset-hint="romAssetHint"
19
+ :accept="romAccept"
20
+ :button-text="'Pick ROM'"
21
+ :file-name="romFile?.name"
22
+ @select="onRomSelect"
23
+ />
24
+ </div>
25
+
26
+ <button type="submit" class="launch-btn" :disabled="!canLaunch">
27
+ Launch Engine
28
+ </button>
29
+
14
30
  <div class="status">{{ status }}</div>
15
31
  </form>
16
32
 
17
33
  <script :setup="{
18
34
  CSdkInfo,
35
+ CDropArea,
19
36
  sdkInfo,
20
- onFile,
21
- accept,
22
- assetHint = 'or choose one from your computer',
37
+ onLaunchFiles,
38
+ onSaveBios,
39
+ onSaveRom,
40
+ initialRomFile = null,
41
+ initialBiosFile = null,
42
+ romAccept,
43
+ romAssetHint = 'or choose ROM file from computer',
44
+ biosAccept,
45
+ biosAssetHint = 'optional BIOS firmware file',
46
+ requiresBIOS = false,
47
+ showBIOS = true,
23
48
  }">
24
- let status = "Ready";
25
- let dragging = false;
49
+ let romFile = initialRomFile;
50
+ let biosFile = initialBiosFile;
26
51
 
27
- const onFileList = async (files) => {
28
- const file = files.item(0);
29
- if (!file) {
30
- return;
31
- }
32
- try {
33
- status = `Loading ${file.name}...`;
34
- await onFile(file);
35
- } catch (error) {
36
- const message = error instanceof Error ? error.message : String(error);
37
- status = `Failed to load file: ${message}`;
52
+ let canLaunch = Boolean(romFile && (!requiresBIOS || biosFile));
53
+ let status = "";
54
+
55
+ const updateStatus = () => {
56
+ canLaunch = Boolean(romFile && (!requiresBIOS || biosFile));
57
+ if (requiresBIOS && !biosFile && !romFile) {
58
+ status = "Ready - Please select ROM and BIOS files to launch.";
59
+ } else if (requiresBIOS && !biosFile) {
60
+ status = "BIOS file is required to launch.";
61
+ } else if (!romFile) {
62
+ status = "Ready - Please select a ROM file to launch.";
63
+ } else {
64
+ status = `Ready to launch! (${romFile.name}${biosFile ? ' + BIOS' : ''})`;
38
65
  }
39
66
  };
40
67
 
41
- const onDrop = async (event) => {
42
- dragging = false;
43
- if (event.dataTransfer?.files) {
44
- await onFileList(event.dataTransfer.files);
45
- }
68
+ updateStatus();
69
+
70
+ const extractFile = (arg) => {
71
+ if (!arg) return null;
72
+ if (arg instanceof File) return arg;
73
+ if (arg.detail instanceof File) return arg.detail;
74
+ if (arg.detail) return extractFile(arg.detail);
75
+ return null;
46
76
  };
47
77
 
48
- const onPick = async (event) => {
49
- if (event.target.files) {
50
- await onFileList(event.target.files);
78
+ const onRomSelect = (arg) => {
79
+ const file = extractFile(arg);
80
+ if (!file) return;
81
+ romFile = file;
82
+ updateStatus();
83
+ onSaveRom?.(file);
84
+ };
85
+
86
+ const onBiosSelect = (arg) => {
87
+ const file = extractFile(arg);
88
+ if (!file) return;
89
+ biosFile = file;
90
+ updateStatus();
91
+ onSaveBios?.(file);
92
+ };
93
+
94
+ const onLaunch = async () => {
95
+ if (!canLaunch) {
96
+ updateStatus();
97
+ return;
98
+ }
99
+ try {
100
+ status = `Launching with ${romFile.name}...`;
101
+ await onLaunchFiles({ rom: romFile, bios: biosFile });
102
+ } catch (error) {
103
+ const message = error instanceof Error ? error.message : String(error);
104
+ status = `Failed to load engine: ${message}`;
51
105
  }
52
106
  };
53
107
  </script>
@@ -89,66 +143,47 @@
89
143
  box-shadow: var(--demo-subpanel-shadow, inset 0 0 0 1px rgba(255, 255, 255, 0.6));
90
144
  }
91
145
 
92
- .drop {
93
- border: 2px dashed var(--demo-drop-border, rgba(255, 255, 255, 0.6));
94
- border-radius: var(--demo-subpanel-radius, 8px);
95
- padding: 2.5rem 1.5rem;
96
- text-align: center;
97
- transition: all 250ms ease;
98
- background: var(--demo-drop-bg, rgba(255, 255, 255, 0.15));
99
- backdrop-filter: blur(var(--demo-subpanel-blur, 12px));
100
- -webkit-backdrop-filter: blur(var(--demo-subpanel-blur, 12px));
146
+ .drop-grid {
147
+ display: grid;
148
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
149
+ gap: 1rem;
101
150
  }
102
151
 
103
- .drop.active {
104
- border-color: var(--demo-drop-active-border, var(--demo-accent2, #0284c7));
105
- background: var(--demo-drop-active-bg, rgba(255, 255, 255, 0.4));
106
- box-shadow: var(--demo-drop-active-shadow, inset 0 0 0 1px rgba(255, 255, 255, 0.8));
152
+ .drop-grid.single {
153
+ grid-template-columns: 1fr;
107
154
  }
108
155
 
109
- .drop strong {
110
- display: block;
111
- margin-bottom: 0.5rem;
112
- font-size: 1.2rem;
113
- font-weight: 600;
156
+ .launch-btn {
157
+ width: 100%;
158
+ padding: 0.9rem 1.5rem;
159
+ font-size: 1.1rem;
160
+ font-weight: 700;
161
+ font-family: var(--demo-font-sans, sans-serif);
114
162
  text-transform: uppercase;
115
- letter-spacing: 0.05em;
116
- color: var(--demo-text, #0f172a);
117
- }
118
-
119
- .drop p {
120
- margin: 0.2rem 0;
121
- color: var(--demo-muted, #475569);
122
- font-family: var(--demo-font-mono, monospace);
123
- font-size: 0.9rem;
124
- }
125
-
126
- .picker {
127
- display: inline-flex;
128
- margin-top: 1.2rem;
129
- background: var(--demo-picker-bg, rgba(255, 255, 255, 0.5));
130
- color: var(--demo-picker-text, var(--demo-text, #0f172a));
131
- border: 1px solid var(--demo-picker-border, rgba(255, 255, 255, 0.6));
163
+ letter-spacing: 0.08em;
164
+ color: var(--demo-launch-text, #ffffff);
165
+ background: var(--demo-launch-bg, var(--demo-accent, #ff007f));
166
+ border: none;
132
167
  border-radius: var(--demo-subpanel-radius, 8px);
133
- padding: 0.6rem 1.2rem;
134
168
  cursor: pointer;
135
- text-transform: uppercase;
136
- font-weight: 600;
137
- font-size: 0.9rem;
138
- letter-spacing: 0.05em;
139
- transition: all 0.2s;
140
- backdrop-filter: blur(var(--demo-subpanel-blur, 12px));
141
- -webkit-backdrop-filter: blur(var(--demo-subpanel-blur, 12px));
169
+ transition: all 0.2s ease;
170
+ box-shadow: var(--demo-accent-glow, 0 0 15px rgba(255, 0, 127, 0.3));
171
+ }
172
+
173
+ .launch-btn:hover:not(:disabled) {
174
+ filter: brightness(1.1);
175
+ transform: translateY(-1px);
142
176
  }
143
177
 
144
- .picker:hover {
145
- background: var(--demo-picker-hover-bg, rgba(255, 255, 255, 0.8));
146
- border-color: var(--demo-picker-hover-border, rgba(255, 255, 255, 0.9));
147
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
178
+ .launch-btn:active:not(:disabled) {
179
+ transform: translateY(0);
148
180
  }
149
181
 
150
- .picker input {
151
- display: none;
182
+ .launch-btn:disabled {
183
+ opacity: 0.5;
184
+ cursor: not-allowed;
185
+ box-shadow: none;
186
+ background: var(--demo-muted, #475569);
152
187
  }
153
188
 
154
189
  .status {
package/demo/demo.js CHANGED
@@ -1,13 +1,20 @@
1
1
  import { getSdk } from "./sdk.js";
2
2
  import { checksumAlgorithms, computeChecksums } from "./checksums.js";
3
+ import { loadStoredAsset, saveStoredAsset } from "./storage.js";
3
4
  import { $, $create, Component79 } from "https://jgermade.github.io/jq79/jq79.js";
4
5
 
6
+ // Safe URL parameter resolution from script URL & page URL
7
+ const pageUrl = new URL(location.href);
8
+ const scriptUrl = new URL(import.meta.url, location.href);
9
+ const biosParam = scriptUrl.searchParams.get("bios") ?? pageUrl.searchParams.get("bios");
10
+
5
11
  // Fetch component HTML templates using import.meta.url for resilient subpath resolution
6
12
  const fetchComponent = (path) => Component79.fetch(new URL(path, import.meta.url).href);
7
13
 
8
14
  // Fetch all components at once to leverage http2 request multiplexing.
9
- const [CLauncher, CSdkInfo, CFileInfo] = await Promise.all([
15
+ const [CLauncher, CDropArea, CSdkInfo, CFileInfo] = await Promise.all([
10
16
  fetchComponent("./components/launcher.html"),
17
+ fetchComponent("./components/drop-area.html"),
11
18
  fetchComponent("./components/sdk-info.html"),
12
19
  fetchComponent("./components/file-info.html"),
13
20
  ]);
@@ -23,15 +30,39 @@ if (sdk.manifest?.name) {
23
30
  document.title = `${sdk.manifest.name} - Demo`;
24
31
  }
25
32
 
26
- // Compute asset accept filter and hint text from manifest
27
- const acceptedExts = sdk.manifest?.assets?.flatMap((a) => a.accept ?? []) ?? [];
28
- const acceptAttr = acceptedExts.length > 0 ? acceptedExts.join(",") : undefined;
29
- const assetHint = acceptedExts.length > 0
30
- ? `Supported files: ${acceptedExts.join(", ")}`
31
- : "or choose one from your computer";
33
+ // Compute asset accept filters and hints from manifest
34
+ const romSpec = sdk.manifest?.assets?.find((a) => a.key === "rom") ?? sdk.manifest?.assets?.[0];
35
+ const biosSpec = sdk.manifest?.assets?.find((a) => a.key === "bios") ?? sdk.manifest?.assets?.[1];
36
+
37
+ // Determine BIOS visibility and requirement rules
38
+ const hasBiosSpec = Boolean(biosSpec);
39
+ const showBIOS = biosParam !== "false" && (hasBiosSpec || biosParam === "required" || biosParam === "true" || biosParam === "optional");
40
+ const requiresBIOS = biosParam === "required" || (biosParam !== "optional" && Boolean(biosSpec?.required));
41
+
42
+ // Load previously stored ROM & BIOS from OPFS if available
43
+ const [storedRom, storedBios] = await Promise.all([
44
+ loadStoredAsset("rom"),
45
+ showBIOS ? loadStoredAsset("bios") : null,
46
+ ]);
47
+
48
+ const romAccept = romSpec?.accept?.join(",");
49
+ const romAssetHint = romSpec?.accept ? `Supported: ${romSpec.accept.join(", ")}` : "or choose ROM file from computer";
32
50
 
33
- const bootWithFile = async (file) => {
34
- const bytes = await file.arrayBuffer();
51
+ const biosAccept = biosSpec?.accept?.join(",");
52
+ const biosAssetHint = biosSpec?.description ?? (biosSpec?.accept ? `Supported: ${biosSpec.accept.join(", ")}` : (requiresBIOS ? "required BIOS firmware file" : "optional BIOS firmware file"));
53
+
54
+ async function toArrayBuffer(input) {
55
+ if (!input) return null;
56
+ if (input instanceof ArrayBuffer) return input;
57
+ if (ArrayBuffer.isView(input)) return input.buffer;
58
+ if (typeof input.arrayBuffer === "function") return await input.arrayBuffer();
59
+ if (input.detail) return await toArrayBuffer(input.detail);
60
+ throw new TypeError("Provided input is not a File, Blob, or ArrayBuffer.");
61
+ }
62
+
63
+ const bootWithFiles = async ({ rom, bios }) => {
64
+ const romBytes = await toArrayBuffer(rom);
65
+ const biosBytes = await toArrayBuffer(bios);
35
66
 
36
67
  mainEl.classList.add("running");
37
68
 
@@ -43,9 +74,10 @@ const bootWithFile = async (file) => {
43
74
 
44
75
  mainEl.replaceChildren(runtimeEl);
45
76
 
77
+ const primaryFile = rom || bios;
46
78
  const fileInfoProps = {
47
- fileName: file.name,
48
- size: `${bytes.byteLength.toLocaleString()} bytes`,
79
+ fileName: primaryFile.name,
80
+ size: `${primaryFile.size.toLocaleString()} bytes`,
49
81
  };
50
82
  const fileInfoPlaceholder = CFileInfo
51
83
  .renderShadow({
@@ -54,15 +86,23 @@ const bootWithFile = async (file) => {
54
86
  })
55
87
  .mount(fileInfoEl);
56
88
 
57
- const assetKey = sdk.manifest?.assets?.[0]?.key ?? "rom";
89
+ const assets = {};
90
+ if (romBytes && romSpec) {
91
+ assets[romSpec.key] = romBytes;
92
+ }
93
+ if (biosBytes && biosSpec) {
94
+ assets[biosSpec.key] = biosBytes;
95
+ }
96
+ if (Object.keys(assets).length === 0 && romBytes) {
97
+ assets.rom = romBytes;
98
+ }
99
+
58
100
  const instance = await sdk.load({
59
101
  attachTo: runtimeEl,
60
- assets: {
61
- [assetKey]: bytes,
62
- },
102
+ assets,
63
103
  options: {
64
- fileName: file.name,
65
- byteLength: bytes.byteLength,
104
+ fileName: primaryFile.name,
105
+ byteLength: primaryFile.size,
66
106
  },
67
107
  onEvent(event) {
68
108
  if (event.type === "error") {
@@ -73,7 +113,7 @@ const bootWithFile = async (file) => {
73
113
 
74
114
  instance.start();
75
115
 
76
- const checksums = await computeChecksums(bytes);
116
+ const checksums = await computeChecksums(romBytes || biosBytes);
77
117
  fileInfoPlaceholder.destroy();
78
118
 
79
119
  CFileInfo
@@ -84,6 +124,7 @@ const bootWithFile = async (file) => {
84
124
  CLauncher
85
125
  .render({
86
126
  CSdkInfo,
127
+ CDropArea,
87
128
  sdkInfo: {
88
129
  name: sdk.manifest.name,
89
130
  id: sdk.manifest.id,
@@ -91,10 +132,18 @@ CLauncher
91
132
  version: sdk.manifest.version,
92
133
  baseWidth: sdk.manifest.video?.baseWidth ?? 320,
93
134
  baseHeight: sdk.manifest.video?.baseHeight ?? 240,
94
- assetKey: sdk.manifest.assets?.[0]?.key ?? "rom",
135
+ assetKey: romSpec?.key ?? "rom",
95
136
  },
96
- onFile: bootWithFile,
97
- accept: acceptAttr,
98
- assetHint,
137
+ initialRomFile: storedRom,
138
+ initialBiosFile: storedBios,
139
+ onSaveRom: (file) => saveStoredAsset("rom", file),
140
+ onSaveBios: (file) => saveStoredAsset("bios", file),
141
+ onLaunchFiles: bootWithFiles,
142
+ romAccept,
143
+ romAssetHint,
144
+ biosAccept,
145
+ biosAssetHint,
146
+ requiresBIOS,
147
+ showBIOS,
99
148
  })
100
149
  .mount(mainEl);
package/demo/demo.sdk.js CHANGED
@@ -14,7 +14,14 @@ export function createDummySdk() {
14
14
  mountPath: "/rom.bin",
15
15
  required: true,
16
16
  accept: [".rom", ".bin", ".zip", ".dat"],
17
- description: "Any file works in this demo; data is not executed.",
17
+ description: "Any game ROM file works in this demo.",
18
+ },
19
+ {
20
+ key: "bios",
21
+ mountPath: "/bios.bin",
22
+ required: false,
23
+ accept: [".bin", ".rom", ".sys"],
24
+ description: "Optional system BIOS firmware file.",
18
25
  },
19
26
  ],
20
27
  input: "keyboard-default",
@@ -45,6 +52,19 @@ export function createDummySdk() {
45
52
  throw error;
46
53
  }
47
54
 
55
+ // Copy loaded ROM and BIOS assets to MEMFS (In-Memory Virtual File System)
56
+ const MEMFS = new Map();
57
+ if (config.assets) {
58
+ for (const assetSpec of manifest.assets) {
59
+ const data = config.assets[assetSpec.key];
60
+ if (data) {
61
+ const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
62
+ MEMFS.set(assetSpec.mountPath, bytes);
63
+ console.log(`[MEMFS] Copied asset '${assetSpec.key}' (${bytes.byteLength} bytes) to ${assetSpec.mountPath}`);
64
+ }
65
+ }
66
+ }
67
+
48
68
  let raf = 0;
49
69
  let running = false;
50
70
  let t0 = performance.now();
@@ -90,7 +110,6 @@ export function createDummySdk() {
90
110
  }
91
111
 
92
112
  // Dark background inside the canvas to contrast with the light UI components
93
- // ctx.fillStyle = "#0f172a";
94
113
  ctx.fillStyle = "#060910ff";
95
114
  ctx.fillRect(0, 0, width, height);
96
115
 
@@ -100,18 +119,15 @@ export function createDummySdk() {
100
119
  for (let x = 0; x < drops.length; x++) {
101
120
  let drop = drops[x];
102
121
 
103
- // Change characters dynamically
104
122
  if (Math.random() > 0.4) {
105
123
  let randIdx = Math.floor(Math.random() * drop.chars.length);
106
124
  drop.chars[randIdx] = String.fromCharCode(0x30A0 + Math.random() * 96);
107
125
  }
108
126
 
109
- // Move the drop (slower)
110
127
  drop.y += drop.speed * (dt * 12);
111
128
 
112
129
  let startY = Math.floor(drop.y);
113
130
 
114
- // Draw the column
115
131
  for (let i = 0; i < drop.length; i++) {
116
132
  let charY = startY - i;
117
133
  if (charY < 0 || charY >= maxRows) continue;
@@ -120,10 +136,8 @@ export function createDummySdk() {
120
136
  let alpha = drop.opacity * (1 - (i / drop.length));
121
137
 
122
138
  if (i === 0) {
123
- // Head of the drop (bright cyan-white)
124
139
  ctx.fillStyle = `rgba(220, 255, 255, ${drop.opacity})`;
125
140
  } else {
126
- // Tail of the drop (bright colors for dark background)
127
141
  const hue = ((time / 15) + (x * 10)) % 360;
128
142
  ctx.fillStyle = `hsla(${hue}, 80%, 65%, ${alpha})`;
129
143
  }
@@ -131,7 +145,6 @@ export function createDummySdk() {
131
145
  ctx.fillText(text, x * fontSize + fontSize / 2, charY * fontSize);
132
146
  }
133
147
 
134
- // Reset drop when fully off-screen
135
148
  if ((drop.y - drop.length) * fontSize > height) {
136
149
  drop.y = 0;
137
150
  drop.speed = Math.random() * 1.5 + 0.5;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * OPFS Asset Storage Helper for Engine Demo
3
+ * Persists ROM and BIOS files in the Origin Private File System (OPFS) across page reloads.
4
+ */
5
+
6
+ const DIR_NAME = "wasm-gaming-demo";
7
+
8
+ async function getStorageDir() {
9
+ if (typeof navigator === "undefined" || !navigator.storage?.getDirectory) {
10
+ return null;
11
+ }
12
+ try {
13
+ const root = await navigator.storage.getDirectory();
14
+ return await root.getDirectoryHandle(DIR_NAME, { create: true });
15
+ } catch (err) {
16
+ console.warn("OPFS storage not accessible:", err);
17
+ return null;
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Save an asset File into OPFS.
23
+ */
24
+ export async function saveStoredAsset(key, file) {
25
+ const dir = await getStorageDir();
26
+ if (!dir || !file) return;
27
+ try {
28
+ const fileHandle = await dir.getFileHandle(`${key}.bin`, { create: true });
29
+ const writable = await fileHandle.createWritable();
30
+ const bytes = await file.arrayBuffer();
31
+ await writable.write(bytes);
32
+ await writable.close();
33
+
34
+ // Store file metadata
35
+ const metaHandle = await dir.getFileHandle(`${key}.json`, { create: true });
36
+ const metaWritable = await metaHandle.createWritable();
37
+ await metaWritable.write(
38
+ JSON.stringify({
39
+ name: file.name,
40
+ size: file.size || bytes.byteLength,
41
+ type: file.type || "application/octet-stream",
42
+ lastModified: file.lastModified || Date.now(),
43
+ })
44
+ );
45
+ await metaWritable.close();
46
+ } catch (err) {
47
+ console.warn(`Failed to save ${key} in OPFS:`, err);
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Retrieve a stored asset File from OPFS.
53
+ */
54
+ export async function loadStoredAsset(key) {
55
+ const dir = await getStorageDir();
56
+ if (!dir) return null;
57
+ try {
58
+ const fileHandle = await dir.getFileHandle(`${key}.bin`);
59
+ const metaHandle = await dir.getFileHandle(`${key}.json`);
60
+
61
+ const fileObj = await fileHandle.getFile();
62
+ const metaText = await (await metaHandle.getFile()).text();
63
+ const meta = JSON.parse(metaText);
64
+
65
+ const bytes = await fileObj.arrayBuffer();
66
+ return new File([bytes], meta.name, {
67
+ type: meta.type,
68
+ lastModified: meta.lastModified,
69
+ });
70
+ } catch {
71
+ // File or meta doesn't exist yet
72
+ return null;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Clear a stored asset from OPFS.
78
+ */
79
+ export async function clearStoredAsset(key) {
80
+ const dir = await getStorageDir();
81
+ if (!dir) return;
82
+ try {
83
+ await dir.removeEntry(`${key}.bin`);
84
+ } catch {}
85
+ try {
86
+ await dir.removeEntry(`${key}.json`);
87
+ } catch {}
88
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wasm-gaming/engine-specs",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "The wasm-gaming engine contract: the single shared interface every WASM game/emulator engine and the host app conform to. TypeScript types + JSON Schema + a Zod runtime validator.",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",