homegames-common 1.7.0 → 1.7.1
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/docs/homegames-context.md +4 -0
- package/game-session.js +15 -16
- package/package.json +1 -1
- package/player-identity.js +31 -0
- package/player-toolbar.js +25 -2
- package/portable-client.js +13 -5
- package/portable-html.js +6 -5
|
@@ -478,6 +478,9 @@ class MyGame extends Game {
|
|
|
478
478
|
// A player joined. info.name is their display name.
|
|
479
479
|
handleNewPlayer({ playerId, info, settings, clientInfo }) {}
|
|
480
480
|
|
|
481
|
+
// A player changed their name in the player menu. Refresh their labels.
|
|
482
|
+
handlePlayerUpdate(playerId, { info, settings }) {}
|
|
483
|
+
|
|
481
484
|
// A player left. Clean up their nodes/state.
|
|
482
485
|
handlePlayerDisconnect(playerId) {}
|
|
483
486
|
|
|
@@ -553,6 +556,7 @@ constructor({ saveGame, saveData }) {
|
|
|
553
556
|
|
|
554
557
|
**Method signatures are exact and important:**
|
|
555
558
|
- `handleNewPlayer` receives a **single object** `{ playerId, info, settings, clientInfo, requestedGame }` — destructure it. `playerId` is a number. `info.name` is the player's name.
|
|
559
|
+
- Players initially get a random two-word name. **More → Your name → Save name** changes it and remembers it in that browser. Local, RTC (including manual connections), and WebSocket sessions call `handlePlayerUpdate(playerId, { info, settings })` for a rename; `info.name` contains the new name and settings are preserved. Update any cached roster or label in this hook. Names are non-unique display text, 1–32 characters, never an identity or authorization key; use `playerId` for session state. Browser preferences stay outside game ZIPs/HTML, so recipients use their own names.
|
|
556
560
|
- `handlePlayerDisconnect(playerId)` receives the **bare id**, not an object. (If a player id reconnects while already connected, the runtime fires `handlePlayerDisconnect` for the old connection and then `handleNewPlayer` again.)
|
|
557
561
|
- `handleKeyDown(playerId, key)` / `handleKeyUp(playerId, key)` — two positional args.
|
|
558
562
|
- A node's `onClick` receives `(playerId, x, y)` — see §9.
|
package/game-session.js
CHANGED
|
@@ -28,17 +28,7 @@ const { squishMap, DEFAULT_SQUISH_VERSION } = require('./game-loader');
|
|
|
28
28
|
const { MAX_SAVE_DATA_BYTES, SAVE_PUSH_OPCODE, SAVE_REQUEST_OPCODE } = require('./save-games');
|
|
29
29
|
const { normalizeCursor, encodeCursor } = require('./cursor');
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
const _NAME_WORDS = [
|
|
33
|
-
'chocolate', 'iguana', 'cardigan', 'enormous', 'gargantuan', 'orangutan',
|
|
34
|
-
'cookies', 'monstera', 'daisy', 'grapefruit', 'blueberry', 'mango',
|
|
35
|
-
'elephant', 'bamboo', 'sapphire', 'papaya', 'waffle', 'turquoise',
|
|
36
|
-
'aquatic', 'goblin', 'funky', 'hotdog', 'elegant', 'cascade', 'euphoria',
|
|
37
|
-
];
|
|
38
|
-
const _generateName = () => {
|
|
39
|
-
const pick = () => _NAME_WORDS[Math.floor(Math.random() * _NAME_WORDS.length)];
|
|
40
|
-
return pick() + ' ' + pick();
|
|
41
|
-
};
|
|
31
|
+
const { generateName: _generateName, initialName, normalizeName } = require('./player-identity');
|
|
42
32
|
|
|
43
33
|
class GameSession {
|
|
44
34
|
constructor(game, squishVersion, opts = {}) {
|
|
@@ -161,7 +151,8 @@ class GameSession {
|
|
|
161
151
|
}
|
|
162
152
|
|
|
163
153
|
this.players[playerId] = ws;
|
|
164
|
-
this.playerInfoMap[playerId] = playerOpts.info || {};
|
|
154
|
+
this.playerInfoMap[playerId] = { ...(playerOpts.info || {}) };
|
|
155
|
+
const preferredName = playerOpts.playerName === undefined ? null : initialName(playerOpts.playerName);
|
|
165
156
|
this.clientInfoMap[playerId] = playerOpts.clientInfo || {};
|
|
166
157
|
this.playerSettingsMap[playerId] = playerOpts.settings || { SOUND: true };
|
|
167
158
|
if (playerOpts.isRemote) this.remotePlayerMap[playerId] = true;
|
|
@@ -183,8 +174,8 @@ class GameSession {
|
|
|
183
174
|
|
|
184
175
|
// Homenames may be unreachable (or have no record yet) — never
|
|
185
176
|
// hand the game a nameless player.
|
|
186
|
-
if (!this.playerInfoMap[playerId].name) {
|
|
187
|
-
this.playerInfoMap[playerId].name = _generateName();
|
|
177
|
+
if (preferredName || !this.playerInfoMap[playerId].name) {
|
|
178
|
+
this.playerInfoMap[playerId].name = preferredName || _generateName();
|
|
188
179
|
}
|
|
189
180
|
|
|
190
181
|
const playerPayload = {
|
|
@@ -244,8 +235,8 @@ class GameSession {
|
|
|
244
235
|
.catch(() => finishAdd());
|
|
245
236
|
}
|
|
246
237
|
} else {
|
|
247
|
-
if (!this.playerInfoMap[playerId].name) {
|
|
248
|
-
this.playerInfoMap[playerId].name = _generateName();
|
|
238
|
+
if (preferredName || !this.playerInfoMap[playerId].name) {
|
|
239
|
+
this.playerInfoMap[playerId].name = preferredName || _generateName();
|
|
249
240
|
}
|
|
250
241
|
|
|
251
242
|
const playerPayload = {
|
|
@@ -384,6 +375,14 @@ class GameSession {
|
|
|
384
375
|
const pid = Number(playerId);
|
|
385
376
|
|
|
386
377
|
try {
|
|
378
|
+
if (input.type === 'player-info') {
|
|
379
|
+
if (!this.players[playerId]) return;
|
|
380
|
+
let name; try { name = normalizeName(input.name); } catch (_) { return; }
|
|
381
|
+
if (this.playerInfoMap[playerId]?.name !== name) this.handlePlayerUpdate(pid, {
|
|
382
|
+
info: { ...this.playerInfoMap[playerId], name }, settings: { ...this.playerSettingsMap[playerId] }
|
|
383
|
+
});
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
387
386
|
if (input.type === 'media' && this.players[playerId] && this.media) {
|
|
388
387
|
if (input.action === 'hello') {
|
|
389
388
|
this.media.join(pid, { live: false, recording: input.recording === 1 || input.recording === 2, audioRecording: input.recording === 2,
|
package/package.json
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Self-contained: the trusted wrapper also embeds this in downloaded HTML.
|
|
4
|
+
function createPlayerIdentity() {
|
|
5
|
+
const normalizeName = value => {
|
|
6
|
+
if (typeof value !== 'string' || /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/.test(value)) throw new Error('Use a name without control characters.');
|
|
7
|
+
const name = value.normalize('NFC').trim().replace(/\s+/gu, ' ');
|
|
8
|
+
if (!name || name.length > 32) throw new Error('Choose a name between 1 and 32 characters.');
|
|
9
|
+
return name;
|
|
10
|
+
};
|
|
11
|
+
const words = ['chocolate', 'iguana', 'cardigan', 'enormous', 'gargantuan', 'orangutan',
|
|
12
|
+
'cookies', 'monstera', 'daisy', 'grapefruit', 'blueberry', 'mango', 'elephant', 'bamboo',
|
|
13
|
+
'sapphire', 'papaya', 'waffle', 'turquoise', 'aquatic', 'goblin', 'funky', 'hotdog', 'elegant', 'cascade', 'euphoria'];
|
|
14
|
+
const generateName = () => Array.from({length: 2}, () => words[Math.floor(Math.random() * words.length)]).join(' ');
|
|
15
|
+
const initialName = value => { try { return normalizeName(value); } catch (_) { return generateName(); } };
|
|
16
|
+
function profile(window) {
|
|
17
|
+
const key = 'homegames-player-name';
|
|
18
|
+
let name;
|
|
19
|
+
try { name = normalizeName(window.localStorage.getItem(key)); } catch (_) { name = generateName(); }
|
|
20
|
+
const set = value => {
|
|
21
|
+
name = normalizeName(value);
|
|
22
|
+
// Names still work when storage is disabled or the frame is opaque.
|
|
23
|
+
try { window.localStorage.setItem(key, name); } catch (_) {}
|
|
24
|
+
return name;
|
|
25
|
+
};
|
|
26
|
+
set(name);
|
|
27
|
+
return { get: () => name, set };
|
|
28
|
+
}
|
|
29
|
+
return { normalizeName, generateName, initialName, profile };
|
|
30
|
+
}
|
|
31
|
+
module.exports = { createPlayerIdentity, ...createPlayerIdentity() };
|
package/player-toolbar.js
CHANGED
|
@@ -31,6 +31,9 @@ function createPlayerToolbar(window, options = {}) {
|
|
|
31
31
|
.hg-player-menu-actions{display:flex;flex-direction:column;gap:6px}.hg-player-menu button{width:100%;min-height:44px;padding:10px 12px;text-align:left}
|
|
32
32
|
.hg-player-menu .hg-player-menu-status{font-size:12px;color:#b7bfb5;overflow-wrap:anywhere;margin:12px 0}
|
|
33
33
|
.hg-player-menu .hg-player-menu-close{text-align:center;margin-top:10px}
|
|
34
|
+
.hg-player-name{margin:0 0 16px;padding-bottom:16px;border-bottom:1px solid #4a5049}
|
|
35
|
+
.hg-player-name label{display:block;margin-bottom:6px}.hg-player-name input{width:100%;min-height:44px;padding:10px;font:inherit;color:inherit;background:#141815;border:1px solid #4a5049;border-radius:8px}
|
|
36
|
+
.hg-player-name button{margin-top:8px}.hg-player-name p{font-size:12px;color:#b7bfb5;margin:8px 0 0;overflow-wrap:anywhere}
|
|
34
37
|
`;
|
|
35
38
|
bar.append(style);
|
|
36
39
|
const home = element(typeof options.home === 'string' ? 'a' : 'button', 'hg-player-icon', '⌂');
|
|
@@ -51,11 +54,26 @@ function createPlayerToolbar(window, options = {}) {
|
|
|
51
54
|
const menu = element('dialog', 'hg-player-menu'); menu.setAttribute('aria-label', 'Game options');
|
|
52
55
|
const menuTitle = element('h2', '', 'Game options'), list = element('div', 'hg-player-menu-actions');
|
|
53
56
|
const menuStatus = element('p', 'hg-player-menu-status');
|
|
57
|
+
const nameForm = element('form', 'hg-player-name'); nameForm.hidden = true;
|
|
58
|
+
const nameLabel = element('label', '', 'Your name'), nameInput = element('input', '');
|
|
59
|
+
nameInput.type = 'text'; nameInput.maxLength = 32; nameInput.required = true; nameInput.autocomplete = 'nickname';
|
|
60
|
+
nameInput.setAttribute('aria-label', 'Your name'); nameLabel.append(nameInput);
|
|
61
|
+
const nameSave = element('button', '', 'Save name'); nameSave.type = 'submit';
|
|
62
|
+
const nameStatus = element('p', ''); nameStatus.setAttribute('role', 'status');
|
|
63
|
+
nameForm.append(nameLabel, nameSave, nameStatus);
|
|
64
|
+
let playerName = '', changeName;
|
|
65
|
+
nameForm.onsubmit = async event => {
|
|
66
|
+
event.preventDefault(); if (!changeName || nameSave.disabled) return;
|
|
67
|
+
nameSave.disabled = true;
|
|
68
|
+
try { playerName = await changeName(nameInput.value); nameInput.value = playerName; nameStatus.textContent = 'Name updated for this session.'; }
|
|
69
|
+
catch (error) { nameStatus.textContent = error.message || String(error); }
|
|
70
|
+
finally { nameSave.disabled = false; }
|
|
71
|
+
};
|
|
54
72
|
const done = element('button', 'hg-player-menu-close', 'Done'); done.type = 'button'; done.onclick = () => menu.close();
|
|
55
|
-
menu.append(menuTitle, list, menuStatus, done); document.body.append(menu);
|
|
73
|
+
menu.append(menuTitle, nameForm, list, menuStatus, done); document.body.append(menu);
|
|
56
74
|
let menuActive = false;
|
|
57
75
|
const resume = () => { if (menuActive) { menuActive = false; options.onClose?.(); } };
|
|
58
|
-
more.onclick = () => { if (!menu.open) { menuActive = true; options.onOpen?.(); menu.showModal(); } };
|
|
76
|
+
more.onclick = () => { if (!menu.open) { nameInput.value = playerName; nameStatus.textContent = ''; menuActive = true; options.onOpen?.(); menu.showModal(); } };
|
|
59
77
|
menu.addEventListener('close', resume);
|
|
60
78
|
menu.addEventListener('click', event => { if (event.target === menu) { const r = menu.getBoundingClientRect(); if (event.clientX < r.left || event.clientX > r.right || event.clientY < r.top || event.clientY > r.bottom) menu.close(); } });
|
|
61
79
|
let disposed = false;
|
|
@@ -63,6 +81,11 @@ function createPlayerToolbar(window, options = {}) {
|
|
|
63
81
|
bar, menu, statusElement: status,
|
|
64
82
|
setTitle(value) { title.textContent = title.title = value || 'Homegames'; },
|
|
65
83
|
setStatus(value) { status.textContent = status.title = menuStatus.textContent = value || ''; },
|
|
84
|
+
setPlayerName(value, onChange) {
|
|
85
|
+
playerName = value; changeName = onChange; nameForm.hidden = !onChange;
|
|
86
|
+
if (!menu.open) nameInput.value = value;
|
|
87
|
+
if (onChange) more.hidden = false;
|
|
88
|
+
},
|
|
66
89
|
setAction(key, value) {
|
|
67
90
|
if (!value) { const old = actions.get(key); if (old) { old.hidden = true; if (old !== share && old !== exportsButton && old !== downloadButton) old.remove(); actions.delete(key); } return; }
|
|
68
91
|
let button = actions.get(key);
|
package/portable-client.js
CHANGED
|
@@ -13,7 +13,7 @@ function attachClientHost(window, frame, recordAudio, exportStorage, createExpor
|
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
15
|
const status = text => { if (options.onPlayerStatus) options.onPlayerStatus(text); else toolbar?.setStatus(text); };
|
|
16
|
-
function command(action) { if (port && !closed) port.postMessage({ type: 'player-control', action }); }
|
|
16
|
+
function command(action, value) { if (port && !closed) port.postMessage({ type: 'player-control', action, value }); }
|
|
17
17
|
async function download(format) {
|
|
18
18
|
try {
|
|
19
19
|
status('Preparing game download…');
|
|
@@ -31,6 +31,10 @@ function attachClientHost(window, frame, recordAudio, exportStorage, createExpor
|
|
|
31
31
|
if (typeof message.title === 'string') toolbar?.setTitle(message.title.slice(0, 200));
|
|
32
32
|
if (typeof message.status === 'string') status(message.status.slice(0, 300));
|
|
33
33
|
if (message.ready) {
|
|
34
|
+
toolbar?.setPlayerName(options.profile?.get() || '', message.playerNames && options.profile ? value => {
|
|
35
|
+
if (closed) throw new Error('This player has closed. Reopen the game to change your name.');
|
|
36
|
+
const name = options.profile.set(value); command('player-name', name); return name;
|
|
37
|
+
} : null);
|
|
34
38
|
for (const [key, label] of [['share', 'Share'], ['copy-share', 'Share a copy'], ['manual-connect', 'Manual connection'], ['exports', 'View exports']]) {
|
|
35
39
|
toolbar?.setAction(key, key !== 'exports' && !message.sharing ? null : { label, run: () => command(key) });
|
|
36
40
|
}
|
|
@@ -71,7 +75,7 @@ function attachClientHost(window, frame, recordAudio, exportStorage, createExpor
|
|
|
71
75
|
else if (!(message.bytes instanceof Uint8Array) || !message.bytes.length || message.bytes.length > (entry.format === 'html' ? 220 : 65) * 1024 * 1024) entry.reject(new Error('Invalid game download'));
|
|
72
76
|
else entry.resolve(message.bytes);
|
|
73
77
|
};
|
|
74
|
-
frame.contentWindow.postMessage({ type: 'homegames-client-port-v1' }, '*', [channel.port2]);
|
|
78
|
+
frame.contentWindow.postMessage({ type: 'homegames-client-port-v1', playerName: options.profile?.get() }, '*', [channel.port2]);
|
|
75
79
|
if (!toolbar && createPlayerToolbar) {
|
|
76
80
|
toolbar = createPlayerToolbar(window, { title: options.title, home: options.home, status: 'Starting game…',
|
|
77
81
|
onOpen: () => command('pause-input'), onClose: () => command('resume-input') });
|
|
@@ -85,18 +89,21 @@ function attachClientHost(window, frame, recordAudio, exportStorage, createExpor
|
|
|
85
89
|
for (const entry of pending.values()) { clearTimeout(entry.timer); entry.reject(new Error('Player closed')); } pending.clear(); } };
|
|
86
90
|
}
|
|
87
91
|
function connectClientSource(window, download, exports = {}, controls = {}) {
|
|
88
|
-
let port, closed = false, sequence = 0, resolveConnect, state;
|
|
92
|
+
let port, closed = false, sequence = 0, resolveConnect, state, playerName;
|
|
89
93
|
const recordings = new Map();
|
|
90
94
|
const connected = new Promise(resolve => { resolveConnect = resolve; });
|
|
91
95
|
const connectTimer = setTimeout(resolveConnect, 15000);
|
|
92
96
|
const listener = event => {
|
|
93
97
|
if (closed || event.source !== window.parent || event.data?.type !== 'homegames-client-port-v1' || !event.ports[0]) return;
|
|
94
|
-
window.removeEventListener('message', listener); port = event.ports[0]; clearTimeout(connectTimer); resolveConnect();
|
|
98
|
+
window.removeEventListener('message', listener); port = event.ports[0]; playerName = event.data.playerName; clearTimeout(connectTimer); resolveConnect();
|
|
95
99
|
if (state) port.postMessage({ type: 'player-state', ...state });
|
|
96
100
|
port.onmessage = async event => {
|
|
97
101
|
const message = event.data;
|
|
98
102
|
if (!closed && message?.type === 'player-control' && Object.hasOwn(controls, message.action)) {
|
|
99
|
-
try {
|
|
103
|
+
try {
|
|
104
|
+
if (message.action === 'player-name') playerName = message.value;
|
|
105
|
+
await controls[message.action](message.value);
|
|
106
|
+
} catch (error) { port.postMessage({type:'player-state', status:error.message}); }
|
|
100
107
|
return;
|
|
101
108
|
}
|
|
102
109
|
if (message?.type === 'audio-result' || message?.type === 'export-result') { const entry = recordings.get(message.id); if (!entry) return; recordings.delete(message.id); clearTimeout(entry.timer); message.error ? entry.reject(new Error(message.error)) : entry.resolve(message.value); return; }
|
|
@@ -127,6 +134,7 @@ function connectClientSource(window, download, exports = {}, controls = {}) {
|
|
|
127
134
|
});
|
|
128
135
|
});
|
|
129
136
|
return { setState(value) { state = { ...state, ...value }; if (!closed) port?.postMessage({ type:'player-state', ...state }); },
|
|
137
|
+
getPlayerName: async () => { await connected; return playerName; },
|
|
130
138
|
recordAudio: info => request('record-audio', {recording:info}),
|
|
131
139
|
recordVideo: info => request('record-video', {recording:info}),
|
|
132
140
|
viewExports: () => request('view-exports'),
|
package/portable-html.js
CHANGED
|
@@ -9,6 +9,7 @@ const { createExportsViewer } = require('./exports-viewer');
|
|
|
9
9
|
const { recordVideo } = require('./video-recording');
|
|
10
10
|
const { createExportStorage } = require('./export-storage');
|
|
11
11
|
const { createPlayerToolbar } = require('./player-toolbar');
|
|
12
|
+
const { createPlayerIdentity } = require('./player-identity');
|
|
12
13
|
const inline = value => JSON.stringify(value).replace(/</g, '\\u003c');
|
|
13
14
|
const escapeHtml = value => String(value).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
14
15
|
const filename = name => {
|
|
@@ -74,15 +75,15 @@ if(format==='zip')return copy.bytes;
|
|
|
74
75
|
return new TextEncoder().encode(await HomegamesClient.PortableProject.buildIsolatedHtml(copy.project,{clientBundleSource:document.scripts[0].textContent,installationId:crypto.randomUUID(),sharing:payload.sharing}));
|
|
75
76
|
},{list:()=>client.listExports(),read:name=>client.readExport(name)}, {
|
|
76
77
|
share:()=>client.showShare(),exports:()=>client.viewExports(),'copy-share':shareCopy,'manual-connect':()=>pairing?.show(),
|
|
77
|
-
'pause-input':()=>client.setInputEnabled(false),'resume-input':()=>client.setInputEnabled(true)
|
|
78
|
+
'pause-input':()=>client.setInputEnabled(false),'resume-input':()=>client.setInputEnabled(true),'player-name':name=>client.setPlayerName(name)
|
|
78
79
|
});
|
|
79
|
-
const ready=status=>clientSource.setState({title:project.metadata.name||'Homegame',status,ready:true,sharing:!!payload.sharing});
|
|
80
|
-
client=new HomegamesClient({containerId:'game',chrome:false,localHome:false,share:payload.sharing,saveStore,platform:clientSource,
|
|
80
|
+
const ready=status=>clientSource.setState({title:project.metadata.name||'Homegame',status,ready:true,sharing:!!payload.sharing,playerNames:typeof client.setPlayerName==='function'});
|
|
81
|
+
client=new HomegamesClient({containerId:'game',chrome:false,localHome:false,share:payload.sharing,saveStore,platform:clientSource,playerName:await clientSource.getPlayerName?.(),
|
|
81
82
|
loadLocalGame:async()=>HomegamesClient.PortableProject.prepareTrustedLocalGame(project,{trusted:true,installationId:payload.installationId}),
|
|
82
83
|
onReady:()=>ready('Playing locally'),onSessionStatus:state,onError:fail});
|
|
83
84
|
pairing=payload.sharing?HomegamesClient.createManualPairing({container:document.body,getSession:()=>client._localSession,
|
|
84
85
|
onOpen:()=>client.setInputEnabled(false),onClose:()=>client.setInputEnabled(true),
|
|
85
|
-
join:transport=>{client.stop();client=new HomegamesClient({containerId:'game',chrome:false,transport,platform:clientSource,onReady:()=>ready('Connected manually to host'),onClose:()=>state('Host disconnected'),onError:fail});client.start();}}):null;
|
|
86
|
+
join:async transport=>{client.stop();client=new HomegamesClient({containerId:'game',chrome:false,transport,platform:clientSource,playerName:await clientSource.getPlayerName?.(),onReady:()=>ready('Connected manually to host'),onClose:()=>state('Host disconnected'),onError:fail});client.start();}}):null;
|
|
86
87
|
window.addEventListener('pagehide',()=>{copyShare?.close();pairing?.dispose();client.stop();clientSource.close();});client.startLocalGame({});
|
|
87
88
|
}catch(error){fail(error);}
|
|
88
89
|
})();
|
|
@@ -115,7 +116,7 @@ const originalHtml='<!doctype html>\\n'+document.documentElement.outerHTML;
|
|
|
115
116
|
let frame,saveHost,clientHost;
|
|
116
117
|
const toolbar=(${createPlayerToolbar.toString()})(window,{title:payload.title,status:'Starting game…',onOpen:()=>clientHost?.command('pause-input'),onClose:()=>clientHost?.command('resume-input')});document.body.prepend(toolbar.bar);
|
|
117
118
|
const showSaveStatus=message=>toolbar.setStatus(message);
|
|
118
|
-
async function mount(){clientHost?.close();if(saveHost)await saveHost.close();frame?.remove();frame=(${createFrame.toString()})(document,payload.document);saveHost=(${attachSaveHost.toString()})(window,frame,{...payload.saves,onStatus:value=>showSaveStatus(value.message)});clientHost=(${attachClientHost.toString()})(window,frame,(${recordAudio.toString()}),(${createExportStorage.toString()})(window,payload.saves),(${createExportsViewer.toString()}),(${recordVideo.toString()}),(${createPlayerToolbar.toString()}),{toolbar,htmlId:'portable-html-copy',zipId:'portable-copy'});document.body.appendChild(frame);}
|
|
119
|
+
async function mount(){clientHost?.close();if(saveHost)await saveHost.close();frame?.remove();frame=(${createFrame.toString()})(document,payload.document);saveHost=(${attachSaveHost.toString()})(window,frame,{...payload.saves,onStatus:value=>showSaveStatus(value.message)});clientHost=(${attachClientHost.toString()})(window,frame,(${recordAudio.toString()}),(${createExportStorage.toString()})(window,payload.saves),(${createExportsViewer.toString()}),(${recordVideo.toString()}),(${createPlayerToolbar.toString()}),{toolbar,htmlId:'portable-html-copy',zipId:'portable-copy',profile:(${createPlayerIdentity.toString()})().profile(window)});document.body.appendChild(frame);}
|
|
119
120
|
const fallback=(bytes,type,name)=>{const url=URL.createObjectURL(new Blob([bytes],{type}));const a=document.createElement('a');a.href=url;a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(url),30000);};
|
|
120
121
|
toolbar.setAction('download-html',{label:'Download game HTML',id:'portable-html-copy',run:()=>fallback(originalHtml,'text/html',payload.name.replace(/\\.zip$/,'.html'))});
|
|
121
122
|
toolbar.setAction('download-zip',{label:'Download game ZIP',id:'portable-copy',run:()=>fallback(Uint8Array.from(atob(payload.archive),c=>c.charCodeAt(0)),'application/zip',payload.name)});
|