@driftengine/package 3.61.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 +202 -0
- package/NOTICE +9 -0
- package/README.md +408 -0
- package/bin/drift-package.mjs +82 -0
- package/dist/android.d.ts +18 -0
- package/dist/android.js +207 -0
- package/dist/bootstrap.d.ts +17 -0
- package/dist/bootstrap.js +326 -0
- package/dist/build.d.ts +11 -0
- package/dist/build.js +248 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +343 -0
- package/dist/flags.d.ts +57 -0
- package/dist/flags.js +122 -0
- package/dist/identity.d.ts +51 -0
- package/dist/identity.js +75 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +24 -0
- package/dist/init.d.ts +24 -0
- package/dist/init.js +45 -0
- package/dist/ios.d.ts +27 -0
- package/dist/ios.js +152 -0
- package/dist/macCrossSign.d.ts +34 -0
- package/dist/macCrossSign.js +298 -0
- package/dist/main/consoleForward.d.ts +43 -0
- package/dist/main/consoleForward.js +63 -0
- package/dist/main/csp.d.ts +4 -0
- package/dist/main/csp.js +55 -0
- package/dist/main/entry.d.ts +1 -0
- package/dist/main/entry.js +56 -0
- package/dist/main/ipc.d.ts +28 -0
- package/dist/main/ipc.js +284 -0
- package/dist/main/links.d.ts +1 -0
- package/dist/main/links.js +29 -0
- package/dist/main/menu.d.ts +17 -0
- package/dist/main/menu.js +49 -0
- package/dist/main/serve.d.ts +21 -0
- package/dist/main/serve.js +69 -0
- package/dist/main/shortcuts.d.ts +28 -0
- package/dist/main/shortcuts.js +26 -0
- package/dist/main/splash.d.ts +14 -0
- package/dist/main/splash.js +41 -0
- package/dist/main/splashTiming.d.ts +36 -0
- package/dist/main/splashTiming.js +24 -0
- package/dist/main/window.d.ts +19 -0
- package/dist/main/window.js +297 -0
- package/dist/manifest.d.ts +61 -0
- package/dist/manifest.js +91 -0
- package/dist/mobile/assetCheck.d.ts +1 -0
- package/dist/mobile/assetCheck.js +28 -0
- package/dist/mobile/bridgeShim.d.ts +83 -0
- package/dist/mobile/bridgeShim.js +208 -0
- package/dist/mobile/iosShim.d.ts +42 -0
- package/dist/mobile/iosShim.js +104 -0
- package/dist/mobile/report.d.ts +27 -0
- package/dist/mobile/report.js +122 -0
- package/dist/mobile/versionCode.d.ts +12 -0
- package/dist/mobile/versionCode.js +17 -0
- package/dist/outDir.d.ts +30 -0
- package/dist/outDir.js +32 -0
- package/dist/preload/bridge.d.ts +77 -0
- package/dist/preload/bridge.js +2 -0
- package/dist/preload/entry.d.ts +1 -0
- package/dist/preload/entry.js +77 -0
- package/dist/renderer/fileStore.d.ts +23 -0
- package/dist/renderer/fileStore.js +33 -0
- package/dist/renderer/host.d.ts +45 -0
- package/dist/renderer/host.js +30 -0
- package/dist/renderer/nativeHost.d.ts +83 -0
- package/dist/renderer/nativeHost.js +125 -0
- package/dist/resources.d.ts +71 -0
- package/dist/resources.js +83 -0
- package/dist/runtime.d.ts +38 -0
- package/dist/runtime.js +80 -0
- package/dist/runtimeVersion.d.ts +10 -0
- package/dist/runtimeVersion.js +18 -0
- package/dist/signing.d.ts +49 -0
- package/dist/signing.js +135 -0
- package/dist/stage.d.ts +22 -0
- package/dist/stage.js +138 -0
- package/dist/steam/cloudStore.d.ts +30 -0
- package/dist/steam/cloudStore.js +54 -0
- package/dist/steam/depot.d.ts +29 -0
- package/dist/steam/depot.js +39 -0
- package/dist/steam/main.d.ts +22 -0
- package/dist/steam/main.js +41 -0
- package/dist/templates/buildWindows.d.ts +16 -0
- package/dist/templates/buildWindows.js +161 -0
- package/dist/templates/packageWrapper.d.ts +9 -0
- package/dist/templates/packageWrapper.js +80 -0
- package/dist/toolchain.d.ts +101 -0
- package/dist/toolchain.js +186 -0
- package/package.json +73 -0
- package/src/android.ts +256 -0
- package/src/bootstrap.ts +389 -0
- package/src/build.ts +285 -0
- package/src/cli.ts +367 -0
- package/src/flags.ts +171 -0
- package/src/identity.ts +87 -0
- package/src/index.ts +31 -0
- package/src/init.ts +61 -0
- package/src/ios.ts +185 -0
- package/src/macCrossSign.ts +338 -0
- package/src/main/consoleForward.ts +93 -0
- package/src/main/csp.ts +57 -0
- package/src/main/entry.ts +63 -0
- package/src/main/ipc.ts +311 -0
- package/src/main/links.ts +29 -0
- package/src/main/menu.ts +53 -0
- package/src/main/serve.ts +70 -0
- package/src/main/shortcuts.ts +55 -0
- package/src/main/splash.ts +42 -0
- package/src/main/splashTiming.ts +51 -0
- package/src/main/window.ts +316 -0
- package/src/manifest.ts +157 -0
- package/src/mobile/assetCheck.ts +25 -0
- package/src/mobile/bridgeShim.ts +292 -0
- package/src/mobile/iosShim.ts +148 -0
- package/src/mobile/report.ts +121 -0
- package/src/mobile/versionCode.ts +17 -0
- package/src/outDir.ts +37 -0
- package/src/preload/bridge.ts +73 -0
- package/src/preload/entry.ts +91 -0
- package/src/renderer/fileStore.ts +38 -0
- package/src/renderer/host.ts +96 -0
- package/src/renderer/nativeHost.ts +146 -0
- package/src/resources.ts +150 -0
- package/src/runtime.ts +87 -0
- package/src/runtimeVersion.ts +16 -0
- package/src/signing.ts +206 -0
- package/src/stage.ts +180 -0
- package/src/steam/cloudStore.ts +73 -0
- package/src/steam/depot.ts +69 -0
- package/src/steam/main.ts +69 -0
- package/src/templates/buildWindows.ts +163 -0
- package/src/templates/packageWrapper.ts +82 -0
- package/src/toolchain.ts +210 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { showDeviceReport } from './report.js';
|
|
2
|
+
/**
|
|
3
|
+
* Half a megabyte of bytes a call, which is about seven hundred kilobytes of base64.
|
|
4
|
+
*
|
|
5
|
+
* Large enough that a twenty megabyte clip is forty calls rather than four thousand, and small
|
|
6
|
+
* enough that neither the string nor the argument list is anywhere near a limit.
|
|
7
|
+
*/
|
|
8
|
+
export const SAVE_CHUNK_BYTES = 512 * 1024;
|
|
9
|
+
/**
|
|
10
|
+
* Bytes as base64, built in pieces.
|
|
11
|
+
*
|
|
12
|
+
* `String.fromCharCode(...bytes)` on a whole file overflows the argument list and throws — at a
|
|
13
|
+
* size that depends on the engine, so it works in a test and fails on a real clip.
|
|
14
|
+
*/
|
|
15
|
+
export function base64Of(bytes) {
|
|
16
|
+
let binary = '';
|
|
17
|
+
const step = 8192;
|
|
18
|
+
for (let at = 0; at < bytes.length; at += step) {
|
|
19
|
+
binary += String.fromCharCode(...bytes.subarray(at, at + step));
|
|
20
|
+
}
|
|
21
|
+
return btoa(binary);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* What the platform files a name under.
|
|
25
|
+
*
|
|
26
|
+
* Android sorts a download by its declared type, so a clip saved as `application/octet-stream`
|
|
27
|
+
* lands somewhere a gallery will not look for it. Wrong is worse than unknown here, so anything
|
|
28
|
+
* unrecognised stays octet-stream rather than being guessed at.
|
|
29
|
+
*/
|
|
30
|
+
export function mimeForName(name) {
|
|
31
|
+
const dot = name.lastIndexOf('.');
|
|
32
|
+
switch (dot === -1 ? '' : name.slice(dot + 1).toLowerCase()) {
|
|
33
|
+
case 'webm':
|
|
34
|
+
return 'video/webm';
|
|
35
|
+
case 'mp4':
|
|
36
|
+
return 'video/mp4';
|
|
37
|
+
case 'png':
|
|
38
|
+
return 'image/png';
|
|
39
|
+
case 'jpg':
|
|
40
|
+
case 'jpeg':
|
|
41
|
+
return 'image/jpeg';
|
|
42
|
+
case 'gif':
|
|
43
|
+
return 'image/gif';
|
|
44
|
+
case 'json':
|
|
45
|
+
return 'application/json';
|
|
46
|
+
case 'txt':
|
|
47
|
+
return 'text/plain';
|
|
48
|
+
default:
|
|
49
|
+
return 'application/octet-stream';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function parseJson(raw, fallback) {
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(raw);
|
|
55
|
+
return (parsed ?? fallback);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* A malformed payload is a host bug, and a game that cannot start because its preferences
|
|
59
|
+
did not parse is a worse one. Empty is the same degradation `BrowserStore` makes. */
|
|
60
|
+
return fallback;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Synthesise the key a pause menu is bound to, and report whether the page took it.
|
|
65
|
+
*
|
|
66
|
+
* **A keydown and a keyup**, because a game that tracks held keys and only ever receives the down
|
|
67
|
+
* ends up with Escape held forever — which is a menu that will not close, from a fix meant to stop
|
|
68
|
+
* one closing too easily.
|
|
69
|
+
*
|
|
70
|
+
* Dispatched at the focused element so it bubbles the way a real keystroke does; a game listening
|
|
71
|
+
* on `window`, on `document` or on its canvas all receive it.
|
|
72
|
+
*/
|
|
73
|
+
function pressEscape() {
|
|
74
|
+
const target = globalThis.document ?? null;
|
|
75
|
+
if (target === null)
|
|
76
|
+
return false;
|
|
77
|
+
const node = (target.activeElement ?? target.body);
|
|
78
|
+
if (node === undefined || node === null || typeof node.dispatchEvent !== 'function')
|
|
79
|
+
return false;
|
|
80
|
+
const make = (type) => new KeyboardEvent(type, {
|
|
81
|
+
key: 'Escape',
|
|
82
|
+
code: 'Escape',
|
|
83
|
+
bubbles: true,
|
|
84
|
+
cancelable: true,
|
|
85
|
+
});
|
|
86
|
+
const consumed = !node.dispatchEvent(make('keydown'));
|
|
87
|
+
node.dispatchEvent(make('keyup'));
|
|
88
|
+
return consumed;
|
|
89
|
+
}
|
|
90
|
+
export function installShim(native) {
|
|
91
|
+
const focusHandlers = new Set();
|
|
92
|
+
const quitHandlers = new Set();
|
|
93
|
+
const events = {
|
|
94
|
+
focus: (on) => {
|
|
95
|
+
for (const handler of focusHandlers)
|
|
96
|
+
handler(on);
|
|
97
|
+
},
|
|
98
|
+
quitRequest: () => {
|
|
99
|
+
for (const handler of quitHandlers)
|
|
100
|
+
handler();
|
|
101
|
+
},
|
|
102
|
+
saveBlob: async (url, name) => {
|
|
103
|
+
let bytes;
|
|
104
|
+
try {
|
|
105
|
+
bytes = new Uint8Array(await (await fetch(url)).arrayBuffer());
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
/* A revoked or cross-origin URL. Nothing is opened, so no empty file is left behind. */
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
return saveFile(name, bytes);
|
|
112
|
+
},
|
|
113
|
+
back: () => {
|
|
114
|
+
if (quitHandlers.size > 0) {
|
|
115
|
+
for (const handler of quitHandlers)
|
|
116
|
+
handler();
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
return pressEscape();
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
globalThis.__driftHostEvents = events;
|
|
123
|
+
const saveFile = async (name, bytes) => {
|
|
124
|
+
if (!native.saveBegin(name, mimeForName(name)))
|
|
125
|
+
return false;
|
|
126
|
+
try {
|
|
127
|
+
for (let at = 0; at < bytes.length; at += SAVE_CHUNK_BYTES) {
|
|
128
|
+
if (!native.saveChunk(base64Of(bytes.subarray(at, at + SAVE_CHUNK_BYTES)))) {
|
|
129
|
+
native.saveAbort();
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
native.saveEnd();
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
/* Half a clip on disk looks exactly like a whole one to a file manager, and the person who
|
|
138
|
+
finds it there has no way to know. Abandoned rather than finished. */
|
|
139
|
+
native.saveAbort();
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
const noPicker = () => {
|
|
144
|
+
/*
|
|
145
|
+
* A game does not open files; an editor does, and an editor is not a phone application. The
|
|
146
|
+
* refusal is loud rather than a null, for the same reason `BrowserFileDialogs` throws where a
|
|
147
|
+
* browser has no picker: a cancel and an absent capability are different answers.
|
|
148
|
+
*/
|
|
149
|
+
throw new Error('[driftengine] no file picker on this platform; a desktop shell has one');
|
|
150
|
+
};
|
|
151
|
+
return {
|
|
152
|
+
storeSnapshot: parseJson(native.snapshot(), {}),
|
|
153
|
+
writeKey: (key, value) => native.write(key, value),
|
|
154
|
+
removeKey: (key) => native.remove(key),
|
|
155
|
+
isFullscreen: () => native.isFullscreen(),
|
|
156
|
+
setFullscreen: async (on) => {
|
|
157
|
+
native.setFullscreen(on);
|
|
158
|
+
},
|
|
159
|
+
refreshHz: () => {
|
|
160
|
+
const hz = native.refreshHz();
|
|
161
|
+
/* Zero is what a platform reports when it will not say, and a plausible 60 is exactly what
|
|
162
|
+
the engine's rule forbids inventing. */
|
|
163
|
+
return typeof hz === 'number' && hz > 0 ? hz : null;
|
|
164
|
+
},
|
|
165
|
+
mode: () => (native.mode() === 'windowed' ? 'windowed' : 'fullscreen'),
|
|
166
|
+
setMode: async (mode) => native.setMode(mode),
|
|
167
|
+
size: () => parseJson(native.size(), { width: 0, height: 0 }),
|
|
168
|
+
setSize: async (width, height) => native.setSize(width, height),
|
|
169
|
+
/* Asked of the host rather than answered here, so the refusal and the question stay one
|
|
170
|
+
decision: `HostBridge.java` answers both beside each other. */
|
|
171
|
+
canSetSize: () => native.canSetSize(),
|
|
172
|
+
displays: () => parseJson(native.displays(), []),
|
|
173
|
+
onFocusChange: (handler) => {
|
|
174
|
+
focusHandlers.add(handler);
|
|
175
|
+
return () => focusHandlers.delete(handler);
|
|
176
|
+
},
|
|
177
|
+
onQuitRequest: (handler) => {
|
|
178
|
+
quitHandlers.add(handler);
|
|
179
|
+
return () => quitHandlers.delete(handler);
|
|
180
|
+
},
|
|
181
|
+
canQuit: native.canQuit(),
|
|
182
|
+
requestQuit: () => native.requestQuit(),
|
|
183
|
+
/* No store on a phone: neither platform's own is integrated, and a game reads `available`
|
|
184
|
+
and never calls the rest. */
|
|
185
|
+
platform: {
|
|
186
|
+
available: false,
|
|
187
|
+
unlockAchievement: () => undefined,
|
|
188
|
+
setRichPresence: () => undefined,
|
|
189
|
+
},
|
|
190
|
+
openFile: async () => noPicker(),
|
|
191
|
+
saveFile,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Wire the native surface into the global the engine's `hostBridge()` looks for.
|
|
196
|
+
*
|
|
197
|
+
* Called by the injected shim script, which the host evaluates before the game's own scripts run —
|
|
198
|
+
* `WebViewCompat.addDocumentStartJavaScript` on Android, a user script on iOS. That timing is what
|
|
199
|
+
* makes this the mobile equivalent of the desktop preload rather than something a game has to
|
|
200
|
+
* remember to import.
|
|
201
|
+
*/
|
|
202
|
+
export function attachShim(key) {
|
|
203
|
+
const native = globalThis.__driftHostNative;
|
|
204
|
+
if (native === undefined)
|
|
205
|
+
return;
|
|
206
|
+
globalThis[key] = installShim(native);
|
|
207
|
+
showDeviceReport();
|
|
208
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { DisplayInfo, WindowMode } from '@driftengine/core';
|
|
2
|
+
import type { DriftHostBridge } from '../preload/bridge.ts';
|
|
3
|
+
/**
|
|
4
|
+
* The same bridge again, from a platform with no synchronous return.
|
|
5
|
+
*
|
|
6
|
+
* **This is why iOS needs its own adapter rather than Android's.** `@JavascriptInterface` on
|
|
7
|
+
* Android returns a value; `WKScriptMessageHandler` does not — it is one-way and asynchronous, and
|
|
8
|
+
* there is no synchronous path from native code into JavaScript on this platform at all. The
|
|
9
|
+
* engine's `KeyValueStore` is synchronous by contract, because preferences are read during boot
|
|
10
|
+
* and an await there is a frame drawn with the wrong settings and corrected afterwards, which is a
|
|
11
|
+
* visible flicker on every load.
|
|
12
|
+
*
|
|
13
|
+
* So the split is: **everything that must be read is injected**, as a literal object written into
|
|
14
|
+
* the page before its first line of script runs, and **everything that changes something is a
|
|
15
|
+
* message**. The host keeps that object current — a rotation replaces its `size` — which is why
|
|
16
|
+
* this reads through it on every call rather than copying the values once.
|
|
17
|
+
*
|
|
18
|
+
* The mutations that report a result ride on `WKScriptMessageHandlerWithReply`, which does hand a
|
|
19
|
+
* promise back, and which is why the deployment target is iOS 15 rather than something older.
|
|
20
|
+
*/
|
|
21
|
+
export interface IosState {
|
|
22
|
+
snapshot: Record<string, string>;
|
|
23
|
+
refreshHz: number | null;
|
|
24
|
+
mode: WindowMode;
|
|
25
|
+
size: {
|
|
26
|
+
width: number;
|
|
27
|
+
height: number;
|
|
28
|
+
};
|
|
29
|
+
displays: DisplayInfo[];
|
|
30
|
+
fullscreen: boolean;
|
|
31
|
+
canQuit: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** One message to the host. Resolves with whatever it replied, or rejects if it did not. */
|
|
34
|
+
export type IosCall = (name: string, payload: unknown) => Promise<unknown>;
|
|
35
|
+
export declare function installIosShim(state: IosState, call: IosCall): DriftHostBridge;
|
|
36
|
+
/**
|
|
37
|
+
* Install the bridge from what the host injected.
|
|
38
|
+
*
|
|
39
|
+
* Called by the bundled shim script, which the host evaluates at document start — the same timing
|
|
40
|
+
* as the desktop preload and Android's document-start script.
|
|
41
|
+
*/
|
|
42
|
+
export declare function attachIosShim(key: string): void;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { SAVE_CHUNK_BYTES, base64Of, mimeForName } from './bridgeShim.js';
|
|
2
|
+
import { showDeviceReport } from './report.js';
|
|
3
|
+
export function installIosShim(state, call) {
|
|
4
|
+
const focusHandlers = new Set();
|
|
5
|
+
const quitHandlers = new Set();
|
|
6
|
+
const send = (name, payload) => {
|
|
7
|
+
void call(name, payload).catch(() => undefined);
|
|
8
|
+
};
|
|
9
|
+
const ask = async (name, payload) => {
|
|
10
|
+
try {
|
|
11
|
+
return (await call(name, payload)) === true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
/* The host went away, or the handler is not registered. A game must not see an exception
|
|
15
|
+
from a settings screen. */
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
globalThis.__driftHostEvents = {
|
|
20
|
+
focus: (on) => {
|
|
21
|
+
for (const handler of focusHandlers)
|
|
22
|
+
handler(on);
|
|
23
|
+
},
|
|
24
|
+
quitRequest: () => {
|
|
25
|
+
for (const handler of quitHandlers)
|
|
26
|
+
handler();
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
const noPicker = () => {
|
|
30
|
+
throw new Error('[driftengine] no file picker on this platform; a desktop shell has one');
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
storeSnapshot: state.snapshot,
|
|
34
|
+
writeKey: (key, value) => send('write', { key, value }),
|
|
35
|
+
removeKey: (key) => send('remove', { key }),
|
|
36
|
+
isFullscreen: () => state.fullscreen,
|
|
37
|
+
setFullscreen: async (on) => {
|
|
38
|
+
/* The status bar is the only thing there is to hide, and hiding it is not refused — so this
|
|
39
|
+
is a message rather than a question. */
|
|
40
|
+
send('fullscreen', { on });
|
|
41
|
+
},
|
|
42
|
+
refreshHz: () => state.refreshHz,
|
|
43
|
+
mode: () => state.mode,
|
|
44
|
+
setMode: async (mode) => ask('mode', { mode }),
|
|
45
|
+
size: () => state.size,
|
|
46
|
+
/* Constant rather than a round trip: there is no window on this platform to give a size to. */
|
|
47
|
+
setSize: async () => false,
|
|
48
|
+
/* The question, beside the refusal it must agree with. Both are constants here for the same
|
|
49
|
+
reason, and the day iOS gains a resizable window they move together. */
|
|
50
|
+
canSetSize: () => false,
|
|
51
|
+
displays: () => state.displays,
|
|
52
|
+
onFocusChange: (handler) => {
|
|
53
|
+
focusHandlers.add(handler);
|
|
54
|
+
return () => focusHandlers.delete(handler);
|
|
55
|
+
},
|
|
56
|
+
onQuitRequest: (handler) => {
|
|
57
|
+
quitHandlers.add(handler);
|
|
58
|
+
return () => quitHandlers.delete(handler);
|
|
59
|
+
},
|
|
60
|
+
/** False, always: an iOS application does not exit itself, and Apple asks that it not try. */
|
|
61
|
+
canQuit: false,
|
|
62
|
+
requestQuit: () => {
|
|
63
|
+
console.warn('[driftengine] requestQuit: an iOS application does not exit itself');
|
|
64
|
+
},
|
|
65
|
+
/* No store on a phone: neither platform's own is integrated, and a game reads `available`
|
|
66
|
+
and never calls the rest. */
|
|
67
|
+
platform: {
|
|
68
|
+
available: false,
|
|
69
|
+
unlockAchievement: () => undefined,
|
|
70
|
+
setRichPresence: () => undefined,
|
|
71
|
+
},
|
|
72
|
+
openFile: async () => noPicker(),
|
|
73
|
+
/**
|
|
74
|
+
* Saving goes to the share sheet, which is where a file goes on this platform.
|
|
75
|
+
*
|
|
76
|
+
* The bytes travel base64 in one message rather than in chunks: a reply-capable handler takes
|
|
77
|
+
* a string of whatever size, and iOS has no equivalent of Android's per-call binder limit. The
|
|
78
|
+
* chunk size is still respected for the encode itself, because building one enormous string
|
|
79
|
+
* from a twenty megabyte clip is what overflows an argument list.
|
|
80
|
+
*/
|
|
81
|
+
saveFile: async (name, bytes) => {
|
|
82
|
+
const pieces = [];
|
|
83
|
+
for (let at = 0; at < bytes.length; at += SAVE_CHUNK_BYTES) {
|
|
84
|
+
pieces.push(base64Of(bytes.subarray(at, at + SAVE_CHUNK_BYTES)));
|
|
85
|
+
}
|
|
86
|
+
return ask('save', { name, mime: mimeForName(name), data: pieces });
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Install the bridge from what the host injected.
|
|
92
|
+
*
|
|
93
|
+
* Called by the bundled shim script, which the host evaluates at document start — the same timing
|
|
94
|
+
* as the desktop preload and Android's document-start script.
|
|
95
|
+
*/
|
|
96
|
+
export function attachIosShim(key) {
|
|
97
|
+
const holder = globalThis;
|
|
98
|
+
const state = holder.__driftHostState;
|
|
99
|
+
const handler = holder.webkit?.messageHandlers?.driftHost;
|
|
100
|
+
if (state === undefined || handler === undefined)
|
|
101
|
+
return;
|
|
102
|
+
globalThis[key] = installIosShim(state, async (name, payload) => handler.postMessage({ name, payload }));
|
|
103
|
+
showDeviceReport();
|
|
104
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a device says about itself, collected rather than assumed.
|
|
3
|
+
*
|
|
4
|
+
* **A pinned desktop Chromium cannot see the failures this tier exists to catch.** WebKit enforces
|
|
5
|
+
* a sixteen-byte stride for arrays in the uniform address space where Dawn does not, so the shaders
|
|
6
|
+
* that draw everything compiled on every desktop browser and on no iPhone — and nothing in the
|
|
7
|
+
* pipeline could have known. A mobile build that ships without collecting what devices report
|
|
8
|
+
* throws away the only reason mobile is in this delivery at all.
|
|
9
|
+
*
|
|
10
|
+
* **Every value is what the device said**, and a value that could not be read is the empty string.
|
|
11
|
+
* An invented renderer string is what the capability clamp then matches against, which is how a
|
|
12
|
+
* phone ends up in the wrong tier — the same rule `AGENTS.md` sets for a backend that cannot answer.
|
|
13
|
+
*
|
|
14
|
+
* Reached in a packaged build with `?report=1` on the entry URL. Not a hidden gesture: a tester who
|
|
15
|
+
* has to be told a secret is a tester who reports the wrong thing.
|
|
16
|
+
*/
|
|
17
|
+
export declare function deviceReport(): Record<string, string>;
|
|
18
|
+
/**
|
|
19
|
+
* Draw the report over whatever is on screen, if this build was asked for it.
|
|
20
|
+
*
|
|
21
|
+
* **Over the game rather than instead of it**, so a tester can see that the game is running behind
|
|
22
|
+
* the numbers they are reading — a report on a blank page cannot distinguish a device that failed
|
|
23
|
+
* to draw from one that was never asked to.
|
|
24
|
+
*
|
|
25
|
+
* Does nothing at all unless the entry URL carries `?report=1`, which is the whole of the switch.
|
|
26
|
+
*/
|
|
27
|
+
export declare function showDeviceReport(): void;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a device says about itself, collected rather than assumed.
|
|
3
|
+
*
|
|
4
|
+
* **A pinned desktop Chromium cannot see the failures this tier exists to catch.** WebKit enforces
|
|
5
|
+
* a sixteen-byte stride for arrays in the uniform address space where Dawn does not, so the shaders
|
|
6
|
+
* that draw everything compiled on every desktop browser and on no iPhone — and nothing in the
|
|
7
|
+
* pipeline could have known. A mobile build that ships without collecting what devices report
|
|
8
|
+
* throws away the only reason mobile is in this delivery at all.
|
|
9
|
+
*
|
|
10
|
+
* **Every value is what the device said**, and a value that could not be read is the empty string.
|
|
11
|
+
* An invented renderer string is what the capability clamp then matches against, which is how a
|
|
12
|
+
* phone ends up in the wrong tier — the same rule `AGENTS.md` sets for a backend that cannot answer.
|
|
13
|
+
*
|
|
14
|
+
* Reached in a packaged build with `?report=1` on the entry URL. Not a hidden gesture: a tester who
|
|
15
|
+
* has to be told a secret is a tester who reports the wrong thing.
|
|
16
|
+
*/
|
|
17
|
+
export function deviceReport() {
|
|
18
|
+
const agent = typeof navigator === 'undefined' ? '' : navigator.userAgent;
|
|
19
|
+
return {
|
|
20
|
+
userAgent: agent,
|
|
21
|
+
platform: platformOf(agent),
|
|
22
|
+
engine: engineOf(agent),
|
|
23
|
+
webgpu: globalThis.navigator?.gpu === undefined
|
|
24
|
+
? 'absent'
|
|
25
|
+
: 'present',
|
|
26
|
+
webglRenderer: unmaskedRenderer(),
|
|
27
|
+
secureContext: String(Boolean(globalThis.isSecureContext)),
|
|
28
|
+
host: globalThis.__driftHost === undefined ? 'absent' : 'present',
|
|
29
|
+
pixelRatio: String(globalThis.devicePixelRatio ?? ''),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The part number the driver reports, which is the only string worth having.
|
|
34
|
+
*
|
|
35
|
+
* `WEBGL_debug_renderer_info` is the extension that gives it. Where a browser withholds it — some
|
|
36
|
+
* do, for fingerprinting — the answer is empty rather than the vendor-neutral placeholder, because
|
|
37
|
+
* an empty string is visibly nothing and `WebKit WebGL` is visibly a device.
|
|
38
|
+
*/
|
|
39
|
+
function unmaskedRenderer() {
|
|
40
|
+
try {
|
|
41
|
+
const canvas = document.createElement('canvas');
|
|
42
|
+
const gl = canvas.getContext('webgl2') ?? canvas.getContext('webgl');
|
|
43
|
+
if (gl === null)
|
|
44
|
+
return '';
|
|
45
|
+
const info = gl.getExtension('WEBGL_debug_renderer_info');
|
|
46
|
+
if (info === null)
|
|
47
|
+
return '';
|
|
48
|
+
const value = gl.getParameter(info.UNMASKED_RENDERER_WEBGL);
|
|
49
|
+
return typeof value === 'string' ? value : '';
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** `iOS 18.2` or `Android 14`, from the agent string, or empty where it says neither. */
|
|
56
|
+
function platformOf(agent) {
|
|
57
|
+
const ios = agent.match(/(?:iPhone|iPad|CPU) OS (\d+)[._](\d+)/);
|
|
58
|
+
if (ios !== null)
|
|
59
|
+
return `iOS ${ios[1]}.${ios[2]}`;
|
|
60
|
+
const android = agent.match(/Android (\d+(?:\.\d+)?)/);
|
|
61
|
+
if (android !== null)
|
|
62
|
+
return `Android ${android[1]}`;
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Which browser engine build this is.
|
|
67
|
+
*
|
|
68
|
+
* The number that matters when a shader fails to compile: an Android WebView is updated through
|
|
69
|
+
* the store independently of the OS, so two phones on the same Android version can disagree.
|
|
70
|
+
*/
|
|
71
|
+
function engineOf(agent) {
|
|
72
|
+
const chrome = agent.match(/Chrome\/([\d.]+)/);
|
|
73
|
+
if (chrome !== null)
|
|
74
|
+
return `Chrome ${chrome[1]}`;
|
|
75
|
+
const safari = agent.match(/Version\/([\d.]+).*Safari/);
|
|
76
|
+
if (safari !== null)
|
|
77
|
+
return `Safari ${safari[1]}`;
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
/** The query that turns the table on. Documented rather than secret, for the reason above. */
|
|
81
|
+
const REPORT_QUERY = 'report=1';
|
|
82
|
+
/**
|
|
83
|
+
* Draw the report over whatever is on screen, if this build was asked for it.
|
|
84
|
+
*
|
|
85
|
+
* **Over the game rather than instead of it**, so a tester can see that the game is running behind
|
|
86
|
+
* the numbers they are reading — a report on a blank page cannot distinguish a device that failed
|
|
87
|
+
* to draw from one that was never asked to.
|
|
88
|
+
*
|
|
89
|
+
* Does nothing at all unless the entry URL carries `?report=1`, which is the whole of the switch.
|
|
90
|
+
*/
|
|
91
|
+
export function showDeviceReport() {
|
|
92
|
+
try {
|
|
93
|
+
if (!globalThis.location?.search?.includes(REPORT_QUERY))
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const draw = () => {
|
|
100
|
+
try {
|
|
101
|
+
const panel = document.createElement('pre');
|
|
102
|
+
panel.style.cssText =
|
|
103
|
+
'position:fixed;inset:auto 8px 8px 8px;z-index:2147483647;margin:0;padding:10px;' +
|
|
104
|
+
'background:rgba(11,11,13,.92);color:#ececf0;font:12px/1.5 ui-monospace,monospace;' +
|
|
105
|
+
'border:1px solid #26262d;border-radius:6px;white-space:pre-wrap;max-height:60vh;' +
|
|
106
|
+
'overflow:auto;pointer-events:none';
|
|
107
|
+
panel.textContent = Object.entries(deviceReport())
|
|
108
|
+
.map(([key, value]) => `${key.padEnd(14)} ${value === '' ? '—' : value}`)
|
|
109
|
+
.join('\n');
|
|
110
|
+
document.body?.appendChild(panel);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
/* No DOM yet, or a page that replaced its own body. A diagnostic is never worth a throw. */
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
if (document.readyState === 'loading') {
|
|
117
|
+
document.addEventListener('DOMContentLoaded', draw, { once: true });
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
draw();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The integer Android orders upgrades by, derived from the version name.
|
|
3
|
+
*
|
|
4
|
+
* **Two digits per part**, so `1.4.2` is `10402`. That holds until a part reaches a hundred, which
|
|
5
|
+
* is stated here rather than discovered on the release that stops installing — at which point the
|
|
6
|
+
* scheme has to widen and every prior code has to stay below the new ones.
|
|
7
|
+
*
|
|
8
|
+
* **Never zero.** A project with no `package.json` has no version, `0.0.0` computes to nothing, and
|
|
9
|
+
* Gradle refuses the build with a message about a positive integer rather than about the missing
|
|
10
|
+
* file. One is the floor, and it is a real version code rather than an error.
|
|
11
|
+
*/
|
|
12
|
+
export declare function versionCodeFor(version: string): number;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The integer Android orders upgrades by, derived from the version name.
|
|
3
|
+
*
|
|
4
|
+
* **Two digits per part**, so `1.4.2` is `10402`. That holds until a part reaches a hundred, which
|
|
5
|
+
* is stated here rather than discovered on the release that stops installing — at which point the
|
|
6
|
+
* scheme has to widen and every prior code has to stay below the new ones.
|
|
7
|
+
*
|
|
8
|
+
* **Never zero.** A project with no `package.json` has no version, `0.0.0` computes to nothing, and
|
|
9
|
+
* Gradle refuses the build with a message about a positive integer rather than about the missing
|
|
10
|
+
* file. One is the floor, and it is a real version code rather than an error.
|
|
11
|
+
*/
|
|
12
|
+
export function versionCodeFor(version) {
|
|
13
|
+
const [major = 0, minor = 0, patch = 0] = version
|
|
14
|
+
.split('.')
|
|
15
|
+
.map((part) => Number.parseInt(part, 10) || 0);
|
|
16
|
+
return Math.max(1, major * 10000 + minor * 100 + patch);
|
|
17
|
+
}
|
package/dist/outDir.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Target } from './manifest.ts';
|
|
2
|
+
import type { ResourceOptions } from './resources.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Where a build writes, which used to be `join(cwd, 'out', target)` in four places and nowhere
|
|
5
|
+
* else.
|
|
6
|
+
*
|
|
7
|
+
* **This is a flag and not tidiness, and the reason is a copy that refuses itself.** Node's
|
|
8
|
+
* `fs.cp` will not copy a thing into itself and decides that by walking the destination's
|
|
9
|
+
* ancestors comparing each one's `dev` and `ino` against the source's. On a filesystem that
|
|
10
|
+
* reports those honestly it never fires. On a shared or network folder it can: measured by a
|
|
11
|
+
* consumer on a Windows VM with the project on a drive mapped to a folder on a Linux machine,
|
|
12
|
+
* `drift.package.json` and `out/` both answered `dev=66313 ino=-112686486700016` — the same
|
|
13
|
+
* identity for a file and for a directory — and the first staging copy failed with
|
|
14
|
+
*
|
|
15
|
+
* cannot copy …\drift.package.json to a subdirectory of self …\out\win-x64\stage\…
|
|
16
|
+
*
|
|
17
|
+
* which is a sentence about the one thing that is not happening. A junction at `out/` closes it,
|
|
18
|
+
* because the walk calls `stat` and not `lstat`, and that is a filesystem trick standing in for
|
|
19
|
+
* this flag.
|
|
20
|
+
*
|
|
21
|
+
* **The layout under the root is unchanged**, so `--out=C:\drift-out` gives
|
|
22
|
+
* `C:\drift-out\win-x64\stage` and nothing that reads `out/<target>/…` has to learn a second
|
|
23
|
+
* shape.
|
|
24
|
+
*
|
|
25
|
+
* **A relative root resolves against the invoking directory and deliberately not against
|
|
26
|
+
* `--project=`.** Those two differ exactly when the flag matters — a packager run from somewhere
|
|
27
|
+
* other than the project it is packaging — and resolving against the project would put the output
|
|
28
|
+
* back inside the thing the flag exists to escape.
|
|
29
|
+
*/
|
|
30
|
+
export declare function outFor(cwd: string, target: Target, options: ResourceOptions): string;
|
package/dist/outDir.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { join, resolve } from 'node:path';
|
|
2
|
+
/**
|
|
3
|
+
* Where a build writes, which used to be `join(cwd, 'out', target)` in four places and nowhere
|
|
4
|
+
* else.
|
|
5
|
+
*
|
|
6
|
+
* **This is a flag and not tidiness, and the reason is a copy that refuses itself.** Node's
|
|
7
|
+
* `fs.cp` will not copy a thing into itself and decides that by walking the destination's
|
|
8
|
+
* ancestors comparing each one's `dev` and `ino` against the source's. On a filesystem that
|
|
9
|
+
* reports those honestly it never fires. On a shared or network folder it can: measured by a
|
|
10
|
+
* consumer on a Windows VM with the project on a drive mapped to a folder on a Linux machine,
|
|
11
|
+
* `drift.package.json` and `out/` both answered `dev=66313 ino=-112686486700016` — the same
|
|
12
|
+
* identity for a file and for a directory — and the first staging copy failed with
|
|
13
|
+
*
|
|
14
|
+
* cannot copy …\drift.package.json to a subdirectory of self …\out\win-x64\stage\…
|
|
15
|
+
*
|
|
16
|
+
* which is a sentence about the one thing that is not happening. A junction at `out/` closes it,
|
|
17
|
+
* because the walk calls `stat` and not `lstat`, and that is a filesystem trick standing in for
|
|
18
|
+
* this flag.
|
|
19
|
+
*
|
|
20
|
+
* **The layout under the root is unchanged**, so `--out=C:\drift-out` gives
|
|
21
|
+
* `C:\drift-out\win-x64\stage` and nothing that reads `out/<target>/…` has to learn a second
|
|
22
|
+
* shape.
|
|
23
|
+
*
|
|
24
|
+
* **A relative root resolves against the invoking directory and deliberately not against
|
|
25
|
+
* `--project=`.** Those two differ exactly when the flag matters — a packager run from somewhere
|
|
26
|
+
* other than the project it is packaging — and resolving against the project would put the output
|
|
27
|
+
* back inside the thing the flag exists to escape.
|
|
28
|
+
*/
|
|
29
|
+
export function outFor(cwd, target, options) {
|
|
30
|
+
const root = options.outRoot === undefined ? join(cwd, 'out') : resolve(process.cwd(), options.outRoot);
|
|
31
|
+
return join(root, target);
|
|
32
|
+
}
|