@wasm-gaming/engine-specs 0.2.6 → 0.3.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/README.md +63 -55
- package/demo/app.html +227 -0
- package/demo/canvas-renderer.js +153 -0
- package/demo/components/asset-picker.html +137 -0
- package/demo/components/call-log.html +33 -0
- package/demo/components/engine-header.html +78 -0
- package/demo/components/lifecycle-bar.html +69 -0
- package/demo/components/property-table.html +119 -0
- package/demo/demo.css +26 -130
- package/demo/demo.sdk.js +405 -203
- package/demo/index.html +7 -56
- package/demo/tsconfig.json +17 -0
- package/dist/engine-specs.d.ts +99 -0
- package/dist/engine-specs.d.ts.map +1 -0
- package/dist/engine-specs.js +3 -0
- package/dist/engine-specs.js.map +1 -0
- package/package.json +8 -12
- package/src/engine-specs.ts +110 -0
- package/demo/README.md +0 -155
- package/demo/checksums.js +0 -101
- package/demo/components/drop-area.html +0 -171
- package/demo/components/esc-menu.html +0 -540
- package/demo/components/launcher.html +0 -220
- package/demo/components/sdk-info.html +0 -54
- package/demo/demo.js +0 -622
- package/demo/sdk.js +0 -19
- package/demo/storage.js +0 -88
- package/dist/engine.d.ts +0 -78
- package/dist/engine.d.ts.map +0 -1
- package/dist/engine.js +0 -8
- package/dist/engine.js.map +0 -1
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -9
- package/dist/index.js.map +0 -1
- package/dist/manifest.d.ts +0 -69
- package/dist/manifest.d.ts.map +0 -1
- package/dist/manifest.js +0 -8
- package/dist/manifest.js.map +0 -1
- package/dist/validate.d.ts +0 -10
- package/dist/validate.d.ts.map +0 -1
- package/dist/validate.js +0 -79
- package/dist/validate.js.map +0 -1
- package/schema/engine-manifest.schema.json +0 -74
- package/src/engine.ts +0 -88
- package/src/index.ts +0 -32
- package/src/manifest.ts +0 -82
- package/src/validate.ts +0 -92
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"noEmit": true,
|
|
5
|
+
"declaration": false,
|
|
6
|
+
"declarationMap": false,
|
|
7
|
+
"sourceMap": false,
|
|
8
|
+
"rootDir": "..",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"checkJs": true
|
|
11
|
+
},
|
|
12
|
+
"include": [
|
|
13
|
+
"../src",
|
|
14
|
+
"demo.sdk.js",
|
|
15
|
+
"canvas-renderer.js"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export interface PropertyDefinition<Value = unknown> {
|
|
2
|
+
value: Value;
|
|
3
|
+
default?: Value;
|
|
4
|
+
description?: string;
|
|
5
|
+
live?: boolean;
|
|
6
|
+
enum?: readonly Value[];
|
|
7
|
+
allowedTypes?: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
export type PropertiesDefinition<T> = {
|
|
10
|
+
[K in keyof T]-?: PropertyDefinition<T[K]>;
|
|
11
|
+
};
|
|
12
|
+
export interface EngineConfig {
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
export interface EngineOptions {
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
export interface EngineAssets {
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
export interface EngineStorage {
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Members are `object`, not the four interfaces above, on purpose: a named
|
|
26
|
+
* `interface MyEngineConfig { … }` has no implicit index signature, so
|
|
27
|
+
* constraining to a type that has one would reject exactly what a closed-type
|
|
28
|
+
* engine will write. Each engine can close its own config/options/assets/storage
|
|
29
|
+
* shapes without an index signature and still satisfy this constraint.
|
|
30
|
+
*/
|
|
31
|
+
export interface EnginePayloads {
|
|
32
|
+
config: object;
|
|
33
|
+
options: object;
|
|
34
|
+
assets: object;
|
|
35
|
+
storage: object;
|
|
36
|
+
}
|
|
37
|
+
export interface DefaultPayloads extends EnginePayloads {
|
|
38
|
+
config: EngineConfig;
|
|
39
|
+
options: EngineOptions;
|
|
40
|
+
assets: EngineAssets;
|
|
41
|
+
storage: EngineStorage;
|
|
42
|
+
}
|
|
43
|
+
export interface EngineInit<P extends EnginePayloads = DefaultPayloads> {
|
|
44
|
+
config?: Partial<P['config']>;
|
|
45
|
+
options?: Partial<P['options']>;
|
|
46
|
+
assets?: Partial<P['assets']>;
|
|
47
|
+
storage?: Partial<P['storage']>;
|
|
48
|
+
/**
|
|
49
|
+
* Not a `Partial`, unlike the four above: the payloads are patches merged onto a
|
|
50
|
+
* cumulative state, a mount target is a single value that replaces.
|
|
51
|
+
*/
|
|
52
|
+
mount?: HTMLElement;
|
|
53
|
+
}
|
|
54
|
+
export interface EngineSurface<TSelf, P extends EnginePayloads = DefaultPayloads> {
|
|
55
|
+
config(): Promise<PropertiesDefinition<P['config']>>;
|
|
56
|
+
config(patch: Partial<P['config']>): TSelf;
|
|
57
|
+
options(): Promise<PropertiesDefinition<P['options']>>;
|
|
58
|
+
options(patch: Partial<P['options']>): TSelf;
|
|
59
|
+
assets(): Promise<PropertiesDefinition<P['assets']>>;
|
|
60
|
+
assets(patch: Partial<P['assets']>): TSelf;
|
|
61
|
+
storage(): Promise<PropertiesDefinition<P['storage']>>;
|
|
62
|
+
storage(patch: Partial<P['storage']>): TSelf;
|
|
63
|
+
mount(target: HTMLElement): TSelf;
|
|
64
|
+
}
|
|
65
|
+
export interface EnginePlay<P extends EnginePayloads = DefaultPayloads> extends EngineSurface<PromisePlay<P>, P> {
|
|
66
|
+
restart(options?: Partial<P['options']>): PromisePlay<P>;
|
|
67
|
+
destroy(): Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Structural, not nominal: an engine does NOT need to subclass the real
|
|
71
|
+
* `Promise` to satisfy this. Implementing `then`/`catch`/`finally` with the
|
|
72
|
+
* right signatures — typically by delegating to an internal real `Promise` —
|
|
73
|
+
* is enough. Note that `extends Promise<T>` also pulls in
|
|
74
|
+
* `[Symbol.toStringTag]: string` via lib.es2015.symbol.wellknown, so an
|
|
75
|
+
* implementer needs that member too, not just the three methods.
|
|
76
|
+
*/
|
|
77
|
+
export interface PromisePlay<P extends EnginePayloads = DefaultPayloads> extends Promise<EnginePlay<P>>, EngineSurface<PromisePlay<P>, P> {
|
|
78
|
+
restart(options?: Partial<P['options']>): PromisePlay<P>;
|
|
79
|
+
destroy(): Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
export interface EngineSDK<P extends EnginePayloads = DefaultPayloads> extends EngineSurface<EngineSDK<P>, P> {
|
|
82
|
+
boot(init?: EngineInit<P>): PromisePlay<P>;
|
|
83
|
+
start(options?: Partial<P['options']>): PromisePlay<P>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Identity lives on the constructor, not on an instance: a launcher listing engines
|
|
87
|
+
* reads `Rsdkv4SDK.id` from the imported class, without constructing anything.
|
|
88
|
+
*/
|
|
89
|
+
export interface EngineSDKConstructor<P extends EnginePayloads = DefaultPayloads> {
|
|
90
|
+
/** Stable engine id, e.g. "rsdkv4" | "fbneo" | "blastem" | "jgenesis" | "smw". */
|
|
91
|
+
readonly id: string;
|
|
92
|
+
/** SemVer of this engine build/contract implementation. */
|
|
93
|
+
readonly version: string;
|
|
94
|
+
/** Human-readable name. */
|
|
95
|
+
readonly name?: string;
|
|
96
|
+
readonly description?: string;
|
|
97
|
+
new (init?: EngineInit<P>): EngineSDK<P>;
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=engine-specs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine-specs.d.ts","sourceRoot":"","sources":["../src/engine-specs.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine-specs.js","sourceRoot":"","sources":["../src/engine-specs.ts"],"names":[],"mappings":"AAAA,gDAAgD"}
|
package/package.json
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wasm-gaming/engine-specs",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "The wasm-gaming engine contract: the single shared
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "The wasm-gaming engine contract: the single shared JS API every WASM game/emulator engine and the host app conform to. TypeScript types, no runtime dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"types": "./dist/
|
|
6
|
+
"types": "./dist/engine-specs.d.ts",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": {
|
|
9
|
-
"types": "./dist/
|
|
10
|
-
"import": "./dist/
|
|
11
|
-
}
|
|
12
|
-
"./schema/engine-manifest.json": "./schema/engine-manifest.schema.json"
|
|
9
|
+
"types": "./dist/engine-specs.d.ts",
|
|
10
|
+
"import": "./dist/engine-specs.js"
|
|
11
|
+
}
|
|
13
12
|
},
|
|
14
13
|
"files": [
|
|
15
14
|
"dist/",
|
|
16
15
|
"demo/",
|
|
17
|
-
"src/"
|
|
18
|
-
"schema/"
|
|
16
|
+
"src/"
|
|
19
17
|
],
|
|
20
18
|
"publishConfig": {
|
|
21
19
|
"access": "public",
|
|
@@ -23,14 +21,12 @@
|
|
|
23
21
|
"provenance": true
|
|
24
22
|
},
|
|
25
23
|
"devDependencies": {
|
|
24
|
+
"typedoc": "^0.28.20",
|
|
26
25
|
"typescript": "^5.5.0"
|
|
27
26
|
},
|
|
28
27
|
"license": "MIT",
|
|
29
28
|
"repository": {
|
|
30
29
|
"type": "git",
|
|
31
30
|
"url": "git+https://github.com/wasm-gaming/engine-specs.git"
|
|
32
|
-
},
|
|
33
|
-
"dependencies": {
|
|
34
|
-
"zod": "^4.4.3"
|
|
35
31
|
}
|
|
36
32
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// ---- Introspectable property definitions ----
|
|
2
|
+
|
|
3
|
+
export interface PropertyDefinition<Value = unknown> {
|
|
4
|
+
value: Value;
|
|
5
|
+
default?: Value;
|
|
6
|
+
description?: string;
|
|
7
|
+
live?: boolean; // Absent means `false`. `true` = changing it applies to current play session.
|
|
8
|
+
enum?: readonly Value[];
|
|
9
|
+
allowedTypes?: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type PropertiesDefinition<T> = { [K in keyof T]-?: PropertyDefinition<T[K]> };
|
|
13
|
+
|
|
14
|
+
// ---- Default open payloads, for an engine that doesn't close its types ----
|
|
15
|
+
|
|
16
|
+
export interface EngineConfig { [key: string]: unknown }
|
|
17
|
+
export interface EngineOptions { [key: string]: unknown }
|
|
18
|
+
export interface EngineAssets { [key: string]: unknown }
|
|
19
|
+
export interface EngineStorage { [key: string]: unknown }
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Members are `object`, not the four interfaces above, on purpose: a named
|
|
23
|
+
* `interface MyEngineConfig { … }` has no implicit index signature, so
|
|
24
|
+
* constraining to a type that has one would reject exactly what a closed-type
|
|
25
|
+
* engine will write. Each engine can close its own config/options/assets/storage
|
|
26
|
+
* shapes without an index signature and still satisfy this constraint.
|
|
27
|
+
*/
|
|
28
|
+
export interface EnginePayloads {
|
|
29
|
+
config: object;
|
|
30
|
+
options: object;
|
|
31
|
+
assets: object;
|
|
32
|
+
storage: object;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface DefaultPayloads extends EnginePayloads {
|
|
36
|
+
config: EngineConfig;
|
|
37
|
+
options: EngineOptions;
|
|
38
|
+
assets: EngineAssets;
|
|
39
|
+
storage: EngineStorage;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface EngineInit<P extends EnginePayloads = DefaultPayloads> {
|
|
43
|
+
config?: Partial<P['config']>;
|
|
44
|
+
options?: Partial<P['options']>;
|
|
45
|
+
assets?: Partial<P['assets']>;
|
|
46
|
+
storage?: Partial<P['storage']>;
|
|
47
|
+
/**
|
|
48
|
+
* Not a `Partial`, unlike the four above: the payloads are patches merged onto a
|
|
49
|
+
* cumulative state, a mount target is a single value that replaces.
|
|
50
|
+
*/
|
|
51
|
+
mount?: HTMLElement;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---- The shared fluent surface: the members every carrier exposes ----
|
|
55
|
+
|
|
56
|
+
export interface EngineSurface<TSelf, P extends EnginePayloads = DefaultPayloads> {
|
|
57
|
+
config(): Promise<PropertiesDefinition<P['config']>>;
|
|
58
|
+
config(patch: Partial<P['config']>): TSelf;
|
|
59
|
+
options(): Promise<PropertiesDefinition<P['options']>>;
|
|
60
|
+
options(patch: Partial<P['options']>): TSelf;
|
|
61
|
+
assets(): Promise<PropertiesDefinition<P['assets']>>;
|
|
62
|
+
assets(patch: Partial<P['assets']>): TSelf;
|
|
63
|
+
storage(): Promise<PropertiesDefinition<P['storage']>>;
|
|
64
|
+
storage(patch: Partial<P['storage']>): TSelf;
|
|
65
|
+
mount(target: HTMLElement): TSelf;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---- Public contract each WASM engine must implement ----
|
|
69
|
+
|
|
70
|
+
export interface EnginePlay<P extends EnginePayloads = DefaultPayloads>
|
|
71
|
+
extends EngineSurface<PromisePlay<P>, P> {
|
|
72
|
+
restart(options?: Partial<P['options']>): PromisePlay<P>;
|
|
73
|
+
destroy(): Promise<void>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Structural, not nominal: an engine does NOT need to subclass the real
|
|
78
|
+
* `Promise` to satisfy this. Implementing `then`/`catch`/`finally` with the
|
|
79
|
+
* right signatures — typically by delegating to an internal real `Promise` —
|
|
80
|
+
* is enough. Note that `extends Promise<T>` also pulls in
|
|
81
|
+
* `[Symbol.toStringTag]: string` via lib.es2015.symbol.wellknown, so an
|
|
82
|
+
* implementer needs that member too, not just the three methods.
|
|
83
|
+
*/
|
|
84
|
+
export interface PromisePlay<P extends EnginePayloads = DefaultPayloads>
|
|
85
|
+
extends Promise<EnginePlay<P>>, EngineSurface<PromisePlay<P>, P> {
|
|
86
|
+
restart(options?: Partial<P['options']>): PromisePlay<P>;
|
|
87
|
+
destroy(): Promise<void>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface EngineSDK<P extends EnginePayloads = DefaultPayloads>
|
|
91
|
+
extends EngineSurface<EngineSDK<P>, P> {
|
|
92
|
+
boot(init?: EngineInit<P>): PromisePlay<P>;
|
|
93
|
+
start(options?: Partial<P['options']>): PromisePlay<P>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Identity lives on the constructor, not on an instance: a launcher listing engines
|
|
98
|
+
* reads `Rsdkv4SDK.id` from the imported class, without constructing anything.
|
|
99
|
+
*/
|
|
100
|
+
export interface EngineSDKConstructor<P extends EnginePayloads = DefaultPayloads> {
|
|
101
|
+
/** Stable engine id, e.g. "rsdkv4" | "fbneo" | "blastem" | "jgenesis" | "smw". */
|
|
102
|
+
readonly id: string;
|
|
103
|
+
/** SemVer of this engine build/contract implementation. */
|
|
104
|
+
readonly version: string;
|
|
105
|
+
/** Human-readable name. */
|
|
106
|
+
readonly name?: string;
|
|
107
|
+
readonly description?: string;
|
|
108
|
+
|
|
109
|
+
new (init?: EngineInit<P>): EngineSDK<P>;
|
|
110
|
+
}
|
package/demo/README.md
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
# Themable Engine Demo Template
|
|
2
|
-
|
|
3
|
-
This directory contains a standalone, themable web interface template for hosting interactive WASM gaming engine demos.
|
|
4
|
-
|
|
5
|
-
## 🚀 How to use in another engine repository
|
|
6
|
-
|
|
7
|
-
1. **Copy the `demo/` folder** into your engine repository.
|
|
8
|
-
|
|
9
|
-
2. **Initialize via ES module script**:
|
|
10
|
-
```html
|
|
11
|
-
<script type="module">
|
|
12
|
-
import demo from './demo.js'
|
|
13
|
-
import sdk from './demo.sdk.js' // Or your engine SDK import
|
|
14
|
-
|
|
15
|
-
demo
|
|
16
|
-
.init({
|
|
17
|
-
messages: {
|
|
18
|
-
title: 'My Custom Engine Demo',
|
|
19
|
-
},
|
|
20
|
-
bios: false, // 'required' | 'optional' | false
|
|
21
|
-
escMenu: {
|
|
22
|
-
enabled: true,
|
|
23
|
-
title: 'Game Pause Menu',
|
|
24
|
-
items: [
|
|
25
|
-
{ label: 'Toggle Fullscreen', action: (instance) => { document.documentElement.requestFullscreen?.(); } }
|
|
26
|
-
],
|
|
27
|
-
},
|
|
28
|
-
})
|
|
29
|
-
.on('file', ({ type, action, file }) => {
|
|
30
|
-
if (file) {
|
|
31
|
-
console.log(`[Demo] Selected ${type} file: ${file.name}`)
|
|
32
|
-
}
|
|
33
|
-
})
|
|
34
|
-
.on('launch', async (e) => {
|
|
35
|
-
const assets = {}
|
|
36
|
-
|
|
37
|
-
// if rom loaded
|
|
38
|
-
if (demo.hasFile('rom')) {
|
|
39
|
-
assets.rom = await demo.loadToMEMFS('rom')
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// if bios required and loaded
|
|
43
|
-
if (demo.hasFile('bios')) {
|
|
44
|
-
assets.bios = await demo.loadToMEMFS('bios')
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const instance = await sdk.load({
|
|
48
|
-
attachTo: e.attachTo,
|
|
49
|
-
assets,
|
|
50
|
-
options: { fileName: e.fileName, byteLength: e.byteLength },
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
instance.start()
|
|
54
|
-
return instance
|
|
55
|
-
})
|
|
56
|
-
</script>
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
---
|
|
60
|
-
|
|
61
|
-
## 🎮 Component79 ESC Menu
|
|
62
|
-
|
|
63
|
-
The demo includes an interactive, customizable in-game ESC pause menu component built with **Component79**.
|
|
64
|
-
|
|
65
|
-
### ESC Menu Options (`escMenu`)
|
|
66
|
-
|
|
67
|
-
| Option | Type | Default | Description |
|
|
68
|
-
|---|---|---|---|
|
|
69
|
-
| `enabled` | `boolean` | `true` | Enable/disable in-game ESC menu overlay. |
|
|
70
|
-
| `title` | `string` | `"Game Menu"` | Title displayed at top of ESC menu modal. |
|
|
71
|
-
| `shortcutKey` | `string` | `"Escape"` | Keyboard key that toggles the menu overlay. |
|
|
72
|
-
| `showTriggerButton` | `boolean` | `true` | Display floating top-right "ESC Menu" trigger badge for mouse/touch access. |
|
|
73
|
-
| `items` | `Array<{label: string, action: function}>` | `[]` | Custom action buttons added to ESC menu. |
|
|
74
|
-
| `onReset` | `function` | `instance.reset()` | Callback when Reset Game button is pressed. |
|
|
75
|
-
| `onExit` | `function` | `instance.destroy()` | Callback when Exit to Launcher button is pressed. |
|
|
76
|
-
| `onSaveState` | `function` | `instance.saveState()` | Callback when Save State button is pressed. |
|
|
77
|
-
| `onLoadState` | `function` | `instance.loadState()` | Callback when Load State button is pressed. |
|
|
78
|
-
|
|
79
|
-
---
|
|
80
|
-
|
|
81
|
-
## 🎨 Customizing & Theming
|
|
82
|
-
|
|
83
|
-
The demo visual design system is fully controlled via **CSS Custom Properties** defined in `demo.css`.
|
|
84
|
-
|
|
85
|
-
### Overriding CSS Variables
|
|
86
|
-
|
|
87
|
-
To apply your custom branding, override any of the `:root` variables in `demo.css` or in your own custom stylesheet:
|
|
88
|
-
|
|
89
|
-
```css
|
|
90
|
-
:root {
|
|
91
|
-
/* Brand colors */
|
|
92
|
-
--demo-bg-a: #1e1b4b;
|
|
93
|
-
--demo-bg-b: #0f172a;
|
|
94
|
-
--demo-text: #f8fafc;
|
|
95
|
-
--demo-muted: #94a3b8;
|
|
96
|
-
|
|
97
|
-
/* Accent highlights */
|
|
98
|
-
--demo-accent: #00ffcc;
|
|
99
|
-
--demo-accent-rgb: 0, 255, 204;
|
|
100
|
-
--demo-accent2: #38bdf8;
|
|
101
|
-
--demo-accent2-rgb: 56, 189, 248;
|
|
102
|
-
|
|
103
|
-
/* Glassmorphism panels */
|
|
104
|
-
--demo-panel-bg: rgba(15, 23, 42, 0.75);
|
|
105
|
-
--demo-panel-border: rgba(255, 255, 255, 0.1);
|
|
106
|
-
--demo-subpanel-bg: rgba(30, 41, 59, 0.6);
|
|
107
|
-
--demo-subpanel-border: rgba(255, 255, 255, 0.1);
|
|
108
|
-
|
|
109
|
-
/* ESC Menu styling */
|
|
110
|
-
--demo-esc-bg: rgba(6, 9, 16, 0.85);
|
|
111
|
-
--demo-esc-card-bg: rgba(15, 23, 42, 0.9);
|
|
112
|
-
--demo-esc-text: #f8fafc;
|
|
113
|
-
}
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
### Full Token Reference
|
|
117
|
-
|
|
118
|
-
| Variable | Description | Default |
|
|
119
|
-
|---|---|---|
|
|
120
|
-
| `--demo-bg-a` | Gradient background stop 1 | `#d8b4fe` |
|
|
121
|
-
| `--demo-bg-b` | Gradient background stop 2 | `#7dd3fc` |
|
|
122
|
-
| `--demo-text` | Primary text color | `#0f172a` |
|
|
123
|
-
| `--demo-muted` | Secondary/muted text color | `#475569` |
|
|
124
|
-
| `--demo-accent` | Primary accent color | `#ff007f` |
|
|
125
|
-
| `--demo-accent-rgb` | Primary accent RGB channels | `255, 0, 127` |
|
|
126
|
-
| `--demo-accent2` | Secondary accent color | `#0284c7` |
|
|
127
|
-
| `--demo-accent2-rgb` | Secondary accent RGB channels | `2, 132, 199` |
|
|
128
|
-
| `--demo-panel-bg` | Main card/launcher background | `rgba(255, 255, 255, 0.25)` |
|
|
129
|
-
| `--demo-panel-border` | Main card border color | `rgba(255, 255, 255, 0.4)` |
|
|
130
|
-
| `--demo-panel-radius` | Main card corner rounding | `12px` |
|
|
131
|
-
| `--demo-panel-blur` | Backdrop glass blur effect | `24px` |
|
|
132
|
-
| `--demo-subpanel-bg` | Inner info section background | `rgba(255, 255, 255, 0.35)` |
|
|
133
|
-
| `--demo-drop-bg` | Dropzone background | `rgba(255, 255, 255, 0.15)` |
|
|
134
|
-
| `--demo-drop-active-bg` | Drag-over active background | `rgba(255, 255, 255, 0.4)` |
|
|
135
|
-
| `--demo-picker-bg` | Pick File button background | `rgba(255, 255, 255, 0.5)` |
|
|
136
|
-
| `--demo-pill-bg` | Metadata badge background | `rgba(var(--demo-accent-rgb), 0.1)` |
|
|
137
|
-
| `--demo-file-info-bg` | Canvas overlay panel background | `rgba(255, 255, 255, 0.7)` |
|
|
138
|
-
| `--demo-esc-bg` | ESC menu overlay background | `rgba(6, 9, 16, 0.82)` |
|
|
139
|
-
| `--demo-esc-card-bg` | ESC menu card background | `rgba(15, 23, 42, 0.88)` |
|
|
140
|
-
| `--demo-esc-text` | ESC menu text color | `#f8fafc` |
|
|
141
|
-
| `--demo-font-sans` | Primary sans-serif font family | `"Outfit", system-ui, sans-serif` |
|
|
142
|
-
| `--demo-font-mono` | Monospace code font family | `"Space Mono", monospace` |
|
|
143
|
-
|
|
144
|
-
---
|
|
145
|
-
|
|
146
|
-
## 🛠 Features
|
|
147
|
-
|
|
148
|
-
- **Component79 ESC Menu**: Customizable pause menu overlay with keydown listeners and touch/mouse badges.
|
|
149
|
-
- **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.
|
|
150
|
-
- **Split Asset Drop Zones**: Independent drop/picker areas for optional **BIOS** firmware and game **ROM** assets.
|
|
151
|
-
- **Dedicated Launch Trigger**: Prominent launch button that activates once required game assets are selected.
|
|
152
|
-
- **Automated Manifest Integration**: Page title, SDK info, asset key mapping, and file type (`accept`) filters are automatically loaded from `sdk.manifest`.
|
|
153
|
-
- **Checksum Calculation**: Computes MD5, SHA-1, and SHA-256 hashes of loaded files asynchronously in the background.
|
|
154
|
-
- **Shadow DOM Isolation**: UI components are isolated using Shadow DOM to prevent style leakage.
|
|
155
|
-
- **Subpath Hosting Ready**: Uses `import.meta.url` for component templates to work seamlessly on subpaths (e.g. GitHub Pages).
|
package/demo/checksums.js
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
const CRC_TABLE = Uint32Array.from({ length: 256 }, (_, n) => {
|
|
2
|
-
let c = n;
|
|
3
|
-
for (let k = 0; k < 8; k += 1) {
|
|
4
|
-
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
5
|
-
}
|
|
6
|
-
return c >>> 0;
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
const crc32 = (bytes) => {
|
|
10
|
-
let crc = 0xffffffff;
|
|
11
|
-
for (const byte of bytes) {
|
|
12
|
-
crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
|
13
|
-
}
|
|
14
|
-
return ((crc ^ 0xffffffff) >>> 0).toString(16).padStart(8, "0");
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const toHex = (buffer) =>
|
|
18
|
-
Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
19
|
-
|
|
20
|
-
// RFC 1321. Hand-rolled because crypto.subtle does not offer MD5.
|
|
21
|
-
const MD5_K = Uint32Array.from({ length: 64 }, (_, i) =>
|
|
22
|
-
Math.floor(Math.abs(Math.sin(i + 1)) * 2 ** 32));
|
|
23
|
-
|
|
24
|
-
const MD5_S = [
|
|
25
|
-
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
|
|
26
|
-
5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
|
|
27
|
-
4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
|
|
28
|
-
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
|
|
29
|
-
];
|
|
30
|
-
|
|
31
|
-
const md5 = (bytes) => {
|
|
32
|
-
const paddedLength = (((bytes.length + 8) >> 6) + 1) << 6;
|
|
33
|
-
const padded = new Uint8Array(paddedLength);
|
|
34
|
-
padded.set(bytes);
|
|
35
|
-
padded[bytes.length] = 0x80;
|
|
36
|
-
const view = new DataView(padded.buffer);
|
|
37
|
-
view.setUint32(paddedLength - 8, (bytes.length * 8) >>> 0, true);
|
|
38
|
-
view.setUint32(paddedLength - 4, Math.floor(bytes.length / 0x20000000), true);
|
|
39
|
-
|
|
40
|
-
let [h0, h1, h2, h3] = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476];
|
|
41
|
-
const words = new Uint32Array(16);
|
|
42
|
-
|
|
43
|
-
for (let offset = 0; offset < paddedLength; offset += 64) {
|
|
44
|
-
for (let j = 0; j < 16; j += 1) {
|
|
45
|
-
words[j] = view.getUint32(offset + j * 4, true);
|
|
46
|
-
}
|
|
47
|
-
let [a, b, c, d] = [h0, h1, h2, h3];
|
|
48
|
-
for (let i = 0; i < 64; i += 1) {
|
|
49
|
-
let f;
|
|
50
|
-
let g;
|
|
51
|
-
if (i < 16) {
|
|
52
|
-
f = (b & c) | (~b & d);
|
|
53
|
-
g = i;
|
|
54
|
-
} else if (i < 32) {
|
|
55
|
-
f = (d & b) | (~d & c);
|
|
56
|
-
g = (5 * i + 1) % 16;
|
|
57
|
-
} else if (i < 48) {
|
|
58
|
-
f = b ^ c ^ d;
|
|
59
|
-
g = (3 * i + 5) % 16;
|
|
60
|
-
} else {
|
|
61
|
-
f = c ^ (b | ~d);
|
|
62
|
-
g = (7 * i) % 16;
|
|
63
|
-
}
|
|
64
|
-
const sum = (a + f + MD5_K[i] + words[g]) >>> 0;
|
|
65
|
-
const rotated = (sum << MD5_S[i]) | (sum >>> (32 - MD5_S[i]));
|
|
66
|
-
[a, b, c, d] = [d, (b + rotated) >>> 0, b, c];
|
|
67
|
-
}
|
|
68
|
-
h0 = (h0 + a) >>> 0;
|
|
69
|
-
h1 = (h1 + b) >>> 0;
|
|
70
|
-
h2 = (h2 + c) >>> 0;
|
|
71
|
-
h3 = (h3 + d) >>> 0;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const digest = new DataView(new ArrayBuffer(16));
|
|
75
|
-
digest.setUint32(0, h0, true);
|
|
76
|
-
digest.setUint32(4, h1, true);
|
|
77
|
-
digest.setUint32(8, h2, true);
|
|
78
|
-
digest.setUint32(12, h3, true);
|
|
79
|
-
return toHex(digest.buffer);
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const sha = async (algorithm, buffer) => {
|
|
83
|
-
// crypto.subtle only exists in secure contexts (https or localhost).
|
|
84
|
-
if (!globalThis.crypto?.subtle) {
|
|
85
|
-
return "unavailable (requires a secure context)";
|
|
86
|
-
}
|
|
87
|
-
return toHex(await crypto.subtle.digest(algorithm, buffer));
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
export const checksumAlgorithms = ["CRC32", "MD5", "SHA-1", "SHA-256"];
|
|
91
|
-
|
|
92
|
-
export async function computeChecksums(buffer) {
|
|
93
|
-
const bytes = new Uint8Array(buffer);
|
|
94
|
-
const [sha1, sha256] = await Promise.all([sha("SHA-1", buffer), sha("SHA-256", buffer)]);
|
|
95
|
-
return [
|
|
96
|
-
{ label: "CRC32", value: crc32(bytes) },
|
|
97
|
-
{ label: "MD5", value: md5(bytes) },
|
|
98
|
-
{ label: "SHA-1", value: sha1 },
|
|
99
|
-
{ label: "SHA-256", value: sha256 },
|
|
100
|
-
];
|
|
101
|
-
}
|