homegames-client 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -674
- package/README.md +7 -4
- package/THIRD_PARTY_NOTICES.md +5 -0
- package/dist/THIRD_PARTY_NOTICES.txt +258 -0
- package/dist/homegames-client.js +260 -1
- package/notices/homepad-0.2.1.txt +24 -0
- package/notices/qrcode-generator-2.0.4.txt +27 -0
- package/package.json +15 -9
- package/src/game-loader.js +19 -1
- package/src/game-view.js +7 -1
- package/src/index.js +9 -3
- package/src/input.js +33 -10
- package/src/local-game-chrome.js +2 -2
- package/src/local-game-loader.js +1 -0
- package/src/local-session.js +7 -6
- package/src/portable-project.js +4 -0
- package/src/rtc-copy-protocol.js +3 -1
- package/src/rtc-session.js +1 -0
- package/src/squish-map.js +2 -5
- package/tools/license-notices-plugin.cjs +57 -0
- package/webpack.config.js +2 -0
- package/src/codecs/squish-142.js +0 -17
- package/src/codecs/squish-143.js +0 -17
- package/src/codecs/squish-145.js +0 -17
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Homepad
|
|
2
|
+
Source: https://github.com/homegamesio/homepad
|
|
3
|
+
|
|
4
|
+
MIT License
|
|
5
|
+
|
|
6
|
+
Copyright (c) 2026 Joseph Garcia and Homegames contributors
|
|
7
|
+
|
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
in the Software without restriction, including without limitation the rights
|
|
11
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
furnished to do so, subject to the following conditions:
|
|
14
|
+
|
|
15
|
+
The above copyright notice and this permission notice shall be included in all
|
|
16
|
+
copies or substantial portions of the Software.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
+
SOFTWARE.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
QR Code Generator for JavaScript
|
|
2
|
+
Source: https://github.com/kazuhikoarase/qrcode-generator
|
|
3
|
+
The distributed source header grants MIT; the npm package omits a standalone license file.
|
|
4
|
+
|
|
5
|
+
MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2009 Kazuhiko Arase
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
The word QR Code is a registered trademark of DENSO WAVE INCORPORATED.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homegames-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Embeddable browser client for Homegames",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"browser": "dist/homegames-client.js",
|
|
@@ -10,24 +10,30 @@
|
|
|
10
10
|
"prepack": "npm run build"
|
|
11
11
|
},
|
|
12
12
|
"author": "Joseph Garcia",
|
|
13
|
-
"license": "
|
|
13
|
+
"license": "MIT",
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"buffer": "^6.0.3",
|
|
16
|
-
"homegames-common": "1.7.
|
|
17
|
-
"homepad": "0.2.
|
|
16
|
+
"homegames-common": "1.7.3",
|
|
17
|
+
"homepad": "0.2.1",
|
|
18
18
|
"qrcode-generator": "^2.0.4",
|
|
19
|
-
"squish-
|
|
20
|
-
"squish-143": "npm:squishjs@1.4.3",
|
|
21
|
-
"squish-145": "npm:squishjs@1.4.5",
|
|
22
|
-
"squish-146": "npm:squishjs@1.4.6"
|
|
19
|
+
"squish-150": "npm:squishjs@1.5.0"
|
|
23
20
|
},
|
|
24
21
|
"devDependencies": {
|
|
25
22
|
"webpack": "^5.65.0",
|
|
26
23
|
"webpack-cli": "^4.3.1"
|
|
27
24
|
},
|
|
25
|
+
"overrides": {
|
|
26
|
+
"homepad": "0.2.1",
|
|
27
|
+
"homegames-common": {
|
|
28
|
+
"ws": "7.5.13"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
28
31
|
"files": [
|
|
29
32
|
"src",
|
|
30
33
|
"dist",
|
|
31
|
-
"
|
|
34
|
+
"tools/license-notices-plugin.cjs",
|
|
35
|
+
"notices",
|
|
36
|
+
"webpack.config.js",
|
|
37
|
+
"THIRD_PARTY_NOTICES.md"
|
|
32
38
|
]
|
|
33
39
|
}
|
package/src/game-loader.js
CHANGED
|
@@ -30,6 +30,7 @@ const loadGameClass = ({ files, entryPoint }) => {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
const moduleCache = {};
|
|
33
|
+
const versions = new Set();
|
|
33
34
|
|
|
34
35
|
const loadModule = (path) => {
|
|
35
36
|
if (moduleCache[path]) return moduleCache[path].exports;
|
|
@@ -39,6 +40,8 @@ const loadGameClass = ({ files, entryPoint }) => {
|
|
|
39
40
|
const requireShim = (request) => {
|
|
40
41
|
const squishMatch = /^squish-(\d+)$/.exec(request);
|
|
41
42
|
if (squishMatch) {
|
|
43
|
+
versions.add(squishMatch[1]);
|
|
44
|
+
if (versions.size > 1) throw new Error('Mixed Squish runtimes are unsupported');
|
|
42
45
|
const squishModule = squishMap[squishMatch[1]];
|
|
43
46
|
if (squishModule) return squishModule;
|
|
44
47
|
throw new Error('Unsupported Squish runtime: ' + request);
|
|
@@ -57,6 +60,15 @@ const loadGameClass = ({ files, entryPoint }) => {
|
|
|
57
60
|
moduleCache[path] = module;
|
|
58
61
|
|
|
59
62
|
const source = normalized[path];
|
|
63
|
+
if (path.endsWith('.json')) {
|
|
64
|
+
try {
|
|
65
|
+
module.exports = JSON.parse(source);
|
|
66
|
+
return module.exports;
|
|
67
|
+
} catch (err) {
|
|
68
|
+
delete moduleCache[path];
|
|
69
|
+
throw new Error('Invalid JSON module ' + path + ': ' + err.message);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
60
72
|
const fn = new Function('module', 'exports', 'require', '__filename', '__dirname', source);
|
|
61
73
|
try {
|
|
62
74
|
fn(module, module.exports, requireShim, path, dir);
|
|
@@ -73,7 +85,13 @@ const loadGameClass = ({ files, entryPoint }) => {
|
|
|
73
85
|
throw new Error('Entry point "' + entryPoint + '" not found in game source map');
|
|
74
86
|
}
|
|
75
87
|
|
|
76
|
-
|
|
88
|
+
const GameClass = loadModule(entry);
|
|
89
|
+
if (versions.size) {
|
|
90
|
+
const { normalizeMetadata, bindRuntimeMetadata } = require('homegames-common/runtime-metadata');
|
|
91
|
+
const metadata = typeof GameClass.metadata === 'function' ? GameClass.metadata() : {};
|
|
92
|
+
bindRuntimeMetadata(GameClass, normalizeMetadata(metadata, { runtime: [...versions][0], legacy: true }));
|
|
93
|
+
}
|
|
94
|
+
return GameClass;
|
|
77
95
|
};
|
|
78
96
|
|
|
79
97
|
module.exports = { loadGameClass };
|
package/src/game-view.js
CHANGED
|
@@ -11,7 +11,13 @@ class GameView {
|
|
|
11
11
|
container.innerHTML = '';
|
|
12
12
|
Object.assign(container.style, { position: 'relative', overflow: 'hidden', background: '#000' });
|
|
13
13
|
this.canvas = document.createElement('canvas');
|
|
14
|
-
Object.assign(this.canvas.style, {
|
|
14
|
+
Object.assign(this.canvas.style, {
|
|
15
|
+
display: 'block', margin: '0 auto', cursor: 'default',
|
|
16
|
+
// Keep iOS tap highlights and long-press selection off the game surface.
|
|
17
|
+
webkitTapHighlightColor: 'transparent',
|
|
18
|
+
webkitTouchCallout: 'none',
|
|
19
|
+
webkitUserSelect: 'none', userSelect: 'none',
|
|
20
|
+
});
|
|
15
21
|
this.cursor = null;
|
|
16
22
|
this.cursorOverride = null;
|
|
17
23
|
this._cursorRaster = null;
|
package/src/index.js
CHANGED
|
@@ -111,7 +111,7 @@ class HomegamesClient {
|
|
|
111
111
|
onVisibility: this._shareOptions.room ? visibility => this._getRtcHost().setVisibility(visibility) : null,
|
|
112
112
|
onOpen: () => this._localSession?.inputHandler?.detach(),
|
|
113
113
|
onClose: () => this._localSession?.inputHandler?.attach(),
|
|
114
|
-
} : null, { onExports: () => this.viewExports(),
|
|
114
|
+
} : null, { author: game.author, onExports: () => this.viewExports(),
|
|
115
115
|
onOpen: () => this.setInputEnabled(false), onClose: () => this.setInputEnabled(true) });
|
|
116
116
|
this._configureToolbar(chrome.toolbar);
|
|
117
117
|
return chrome;
|
|
@@ -120,9 +120,15 @@ class HomegamesClient {
|
|
|
120
120
|
playerName: this.playerName,
|
|
121
121
|
gamepad: this.gamepadOptions, platform: opts.platform, saveStore: this.saveStore,
|
|
122
122
|
onSaveData: this.onSaveData, onError: this.onError, onReady: id => {
|
|
123
|
+
const solo = this._localSession?.maxPlayers === 1;
|
|
124
|
+
if (this._shareOptions) this._localChrome?.toolbar?.setAction('share', {
|
|
125
|
+
label: solo ? 'Invite (solo)' : 'Invite', disabled: solo,
|
|
126
|
+
title: solo ? 'This is a solo game. Use Share a copy to send it to a friend.' : 'Invite friends to play',
|
|
127
|
+
run: () => this.showShare(),
|
|
128
|
+
});
|
|
123
129
|
this.onReady(id);
|
|
124
|
-
if (this._shareOptions?.autoStart) {
|
|
125
|
-
this._getRtcHost().share().catch(err => this._localChrome?.setStatus?.(`Not available to join: ${err.message}. Use
|
|
130
|
+
if (this._shareOptions?.autoStart && !solo) {
|
|
131
|
+
this._getRtcHost().share().catch(err => this._localChrome?.setStatus?.(`Not available to join: ${err.message}. Use Invite to retry.`));
|
|
126
132
|
}
|
|
127
133
|
},
|
|
128
134
|
}),
|
package/src/input.js
CHANGED
|
@@ -106,6 +106,7 @@ class InputHandler {
|
|
|
106
106
|
this._pressClickSent = false;
|
|
107
107
|
this.mousePos = this._hasPointerLock() ? this._canvasCenter() : [e.clientX, e.clientY];
|
|
108
108
|
this._lastClientPos = this.mousePos.slice();
|
|
109
|
+
this._pressOrigin = this.mousePos.slice();
|
|
109
110
|
this.assetManager.unlockAudio();
|
|
110
111
|
const captureGesture = this._canCapturePointer() && !this.spectating &&
|
|
111
112
|
e.target === this.canvas && e.button === 0 && !e.sourceCapabilities?.firesTouchEvents &&
|
|
@@ -178,6 +179,11 @@ class InputHandler {
|
|
|
178
179
|
return;
|
|
179
180
|
}
|
|
180
181
|
e.preventDefault();
|
|
182
|
+
// Claiming a canvas touch suppresses the synthesized mouse click
|
|
183
|
+
// that would normally focus an embedded game. Treat this direct
|
|
184
|
+
// gesture as input focus so the first tap can reach the game too.
|
|
185
|
+
// Page-control touches return above; blur still releases input.
|
|
186
|
+
this._focused = true;
|
|
181
187
|
this._touchActive = true;
|
|
182
188
|
clearTimeout(this.clickStopper); this.clickStopper = null;
|
|
183
189
|
this.assetManager.liveMedia?.hold('pointer');
|
|
@@ -185,6 +191,7 @@ class InputHandler {
|
|
|
185
191
|
this._pressClickSent = false;
|
|
186
192
|
this.mousePos = [e.touches[0].clientX + window.scrollX, e.touches[0].clientY + window.scrollY];
|
|
187
193
|
this._lastClientPos = [e.touches[0].clientX, e.touches[0].clientY];
|
|
194
|
+
this._pressOrigin = this.mousePos.slice();
|
|
188
195
|
this.assetManager.unlockAudio();
|
|
189
196
|
};
|
|
190
197
|
|
|
@@ -229,7 +236,7 @@ class InputHandler {
|
|
|
229
236
|
// report the release at the last known position so the server can
|
|
230
237
|
// close out the press (drag-end / offClick).
|
|
231
238
|
if (this.mouseDown && this._lastClientPos) {
|
|
232
|
-
this._sendMouseUp(this._lastClientPos[0], this._lastClientPos[1]);
|
|
239
|
+
this._sendMouseUp(this._lastClientPos[0], this._lastClientPos[1], true);
|
|
233
240
|
}
|
|
234
241
|
this.mouseDown = false;
|
|
235
242
|
};
|
|
@@ -408,8 +415,7 @@ class InputHandler {
|
|
|
408
415
|
// click contract those games' interactions were built on.
|
|
409
416
|
if (this.mouseDown && !this.clickStopper) {
|
|
410
417
|
if (!this._pressClickSent) {
|
|
411
|
-
this.
|
|
412
|
-
this._handleClick(clickInfo, true);
|
|
418
|
+
this._sendPendingClick(clickInfo);
|
|
413
419
|
} else if (this._hasPointerLock()) {
|
|
414
420
|
// Captured presses fire once; relative look is sent separately.
|
|
415
421
|
} else if (this._usesSingleClickContract()) {
|
|
@@ -476,7 +482,7 @@ class InputHandler {
|
|
|
476
482
|
_releasePointerPress() {
|
|
477
483
|
this._pointerLockOnRelease = false;
|
|
478
484
|
this.assetManager.liveMedia?.release('pointer');
|
|
479
|
-
if (this.mouseDown && this._lastClientPos) this._sendMouseUp(...this._lastClientPos);
|
|
485
|
+
if (this.mouseDown && this._lastClientPos) this._sendMouseUp(...this._lastClientPos, true);
|
|
480
486
|
this.mouseDown = false;
|
|
481
487
|
this._pressClickSent = false;
|
|
482
488
|
this.mousePos = null;
|
|
@@ -524,7 +530,7 @@ class InputHandler {
|
|
|
524
530
|
event.key === 'Escape' || event.key === 'Shift';
|
|
525
531
|
}
|
|
526
532
|
|
|
527
|
-
_sendMouseUp(clientX, clientY) {
|
|
533
|
+
_sendMouseUp(clientX, clientY, cancelled = false) {
|
|
528
534
|
const rect = this.canvas.getBoundingClientRect();
|
|
529
535
|
const clickX = (clientX - rect.left) / rect.width * 100;
|
|
530
536
|
const clickY = (clientY - rect.top) / rect.height * 100;
|
|
@@ -533,12 +539,21 @@ class InputHandler {
|
|
|
533
539
|
// (drag-end / offClick), even ones that finish outside the canvas.
|
|
534
540
|
const x = Math.min(99.99, Math.max(0, clickX));
|
|
535
541
|
const y = Math.min(99.99, Math.max(0, clickY));
|
|
536
|
-
|
|
542
|
+
// Preserve the last movement even when the whole drag happened between
|
|
543
|
+
// animation frames or inside the 30ms repeat throttle.
|
|
544
|
+
const origin = this._pressOrigin;
|
|
545
|
+
if (!cancelled && origin && this._pressClickSent && !this._hasPointerLock() &&
|
|
546
|
+
(Math.abs(clientX - origin[0]) > .5 || Math.abs(clientY - origin[1]) > .5)) {
|
|
547
|
+
this._sendMouseMove(clientX, clientY);
|
|
548
|
+
}
|
|
549
|
+
this._pressOrigin = null;
|
|
550
|
+
// End the press on blur/touchcancel without committing a drag-and-drop.
|
|
551
|
+
this.send(JSON.stringify({ type: 'mouseup', data: { x, y, ...(cancelled ? { cancelled: true } : {}) } }));
|
|
537
552
|
}
|
|
538
553
|
|
|
539
554
|
// Whether the game's squish version uses the single-click contract
|
|
540
555
|
// (onClick once per press, mousemove while held, mouseup on release).
|
|
541
|
-
// Modern version strings are three digits ('
|
|
556
|
+
// Modern version strings are three digits ('150', '144'); anything else
|
|
542
557
|
// ('0767' / '1000'-style) predates drag support entirely — both get the
|
|
543
558
|
// legacy repeated-click behavior.
|
|
544
559
|
_usesSingleClickContract() {
|
|
@@ -572,7 +587,15 @@ class InputHandler {
|
|
|
572
587
|
_sendPendingClick(clickInfo) {
|
|
573
588
|
if (!this.mouseDown || this._pressClickSent || this.spectating || !this._focused) return;
|
|
574
589
|
this._pressClickSent = true;
|
|
590
|
+
// A pointer can move before the first animation frame. Its press still
|
|
591
|
+
// belongs to the original object, not whichever object it moved over.
|
|
592
|
+
const latest = this.mousePos;
|
|
593
|
+
if (this._pressOrigin) {
|
|
594
|
+
this.mousePos = this._pressOrigin.slice();
|
|
595
|
+
clickInfo = this._canClick(this.mousePos[0], this.mousePos[1]);
|
|
596
|
+
}
|
|
575
597
|
this._handleClick(clickInfo, true);
|
|
598
|
+
if (this._attached) this.mousePos = latest;
|
|
576
599
|
}
|
|
577
600
|
|
|
578
601
|
_promptTextInput(nodeId) {
|
|
@@ -639,9 +662,9 @@ class InputHandler {
|
|
|
639
662
|
_promptFileUpload(nodeId) {
|
|
640
663
|
const inputEl = document.createElement('input');
|
|
641
664
|
inputEl.type = 'file';
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
inputEl.accept = 'image/*,audio/*,video
|
|
665
|
+
// Games also import portable creations such as level packs. The node
|
|
666
|
+
// has no per-input accept field; each game still validates the bytes.
|
|
667
|
+
inputEl.accept = 'image/*,audio/*,video/*,application/json,.json';
|
|
645
668
|
inputEl.style.display = 'none';
|
|
646
669
|
document.body.appendChild(inputEl);
|
|
647
670
|
const cleanupInput = () => {
|
package/src/local-game-chrome.js
CHANGED
|
@@ -7,7 +7,7 @@ const createLocalChrome = (containerId, title, onHome, share = null, options = {
|
|
|
7
7
|
container.innerHTML = '';
|
|
8
8
|
const releaseStyles = useFrameStyles();
|
|
9
9
|
const frame = document.createElement('div'); frame.className = 'hg-session-frame';
|
|
10
|
-
const toolbar = createPlayerToolbar(window, { title, home: onHome, status: options.status || 'Playing locally',
|
|
10
|
+
const toolbar = createPlayerToolbar(window, { title, author: options.author, home: onHome, status: options.status || 'Playing locally',
|
|
11
11
|
onOpen: options.onOpen || share?.onOpen, onClose: options.onClose || share?.onClose });
|
|
12
12
|
toolbar.bar.classList.add('hg-session-bar');
|
|
13
13
|
if (options.onExports) toolbar.setAction('exports', { label: 'View exports', run: options.onExports });
|
|
@@ -17,7 +17,7 @@ const createLocalChrome = (containerId, title, onHome, share = null, options = {
|
|
|
17
17
|
let dialog;
|
|
18
18
|
if (share) {
|
|
19
19
|
dialog = require('./share-dialog')({ container: frame, ...share });
|
|
20
|
-
toolbar.setAction('share', { label: '
|
|
20
|
+
toolbar.setAction('share', { label: 'Invite', run: () => dialog.show() });
|
|
21
21
|
if (share.onVisibility) {
|
|
22
22
|
let privateGame = share.private;
|
|
23
23
|
const update = () => toolbar.setAction('privacy', { label: privateGame ? 'Make session public' : 'Make session private', run: async () => {
|
package/src/local-game-loader.js
CHANGED
|
@@ -15,6 +15,7 @@ const loadLocalGame = async ({ manifestUrl, gameKey, versionId, baseUrl } = {},
|
|
|
15
15
|
}
|
|
16
16
|
return {
|
|
17
17
|
name: manifest.name,
|
|
18
|
+
author: manifest.author || manifest.createdBy,
|
|
18
19
|
multiplayer: manifest.multiplayer === true,
|
|
19
20
|
files: manifest.files,
|
|
20
21
|
entryPoint: manifest.entryPoint,
|
package/src/local-session.js
CHANGED
|
@@ -154,9 +154,12 @@ class LocalSession {
|
|
|
154
154
|
return;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
157
|
+
// Loaders bind the statically resolved runtime; legacy class-only embeds
|
|
158
|
+
// retain their metadata/default compatibility path.
|
|
159
|
+
let metadata;
|
|
160
|
+
try { metadata = require('homegames-common/runtime-metadata').runtimeMetadata(this.gameClass); }
|
|
161
|
+
catch (error) { this.stop(); this.onError(error); return; }
|
|
162
|
+
this.maxPlayers = metadata.maxPlayers;
|
|
160
163
|
this.pointerLockEnabled = metadata.pointerLock === true;
|
|
161
164
|
const squishVersion = metadata.squishVersion || DEFAULT_VERSION;
|
|
162
165
|
let squishModule = squishMap[squishVersion];
|
|
@@ -236,8 +239,7 @@ class LocalSession {
|
|
|
236
239
|
});
|
|
237
240
|
// --- Instantiate game + squisher ---
|
|
238
241
|
try {
|
|
239
|
-
// Same option bag child_game_server passes
|
|
240
|
-
// stubbed in local mode. addAsset and saveGame/saveData are real:
|
|
242
|
+
// Same option bag child_game_server passes. addAsset and saveGame/saveData are real:
|
|
241
243
|
// uploads render locally and saves persist in browser storage.
|
|
242
244
|
this.game = new this.gameClass({
|
|
243
245
|
createExport: this.userExportsStore.createExport,
|
|
@@ -249,7 +251,6 @@ class LocalSession {
|
|
|
249
251
|
&& this.setCursor(playerId, cursor),
|
|
250
252
|
saveGame,
|
|
251
253
|
saveData,
|
|
252
|
-
services: {},
|
|
253
254
|
});
|
|
254
255
|
// The runtime owns ticking; Squish owns scene serialization.
|
|
255
256
|
// Frameless: no custom layers, scale 1.
|
package/src/portable-project.js
CHANGED
|
@@ -9,9 +9,12 @@ const saves = require('homegames-common/portable-saves');
|
|
|
9
9
|
const portableClient = require('homegames-common/portable-client');
|
|
10
10
|
|
|
11
11
|
module.exports = {
|
|
12
|
+
...require('homegames-common/studio-debug'),
|
|
13
|
+
...require('homegames-common/studio-handoff'),
|
|
12
14
|
create: files => project.createProject(files, options),
|
|
13
15
|
importArchive: bytes => project.importArchive(bytes, options),
|
|
14
16
|
exportArchive: project.exportArchive,
|
|
17
|
+
withGameId: project.withGameId,
|
|
15
18
|
normalizePath: project.normalizePath,
|
|
16
19
|
limits: project.LIMITS,
|
|
17
20
|
buildIsolatedHtml: html.buildIsolatedHtml,
|
|
@@ -36,5 +39,6 @@ module.exports = {
|
|
|
36
39
|
readHtmlArchive: html.readHtmlArchive,
|
|
37
40
|
htmlBytesLimit: html.htmlBytesLimit,
|
|
38
41
|
checkSyntax: project.checkSyntax,
|
|
42
|
+
thumbnailPath: project.thumbnailPath,
|
|
39
43
|
prepareTrustedLocalGame: (value, settings) => prepareTrustedLocalGame(value, squishMap, settings),
|
|
40
44
|
};
|
package/src/rtc-copy-protocol.js
CHANGED
|
@@ -64,7 +64,9 @@ class Receiver {
|
|
|
64
64
|
if (msg.type !== 'copy-complete' || this.state !== 'receiving' || this.offset !== this.metadata.bytes) throw new Error('Incomplete game copy');
|
|
65
65
|
this.state = 'validating';
|
|
66
66
|
const bytes = this.bytes, metadata = this.metadata;
|
|
67
|
-
|
|
67
|
+
const actualDigest = await digest(bytes);
|
|
68
|
+
if (this.state === 'closed') return;
|
|
69
|
+
if (actualDigest !== metadata.sha256) throw new Error('The game copy was damaged during transfer. Try again.');
|
|
68
70
|
const project = await this.importArchive(bytes);
|
|
69
71
|
if (project.revision !== metadata.revision || project.manifest.gameId !== metadata.gameId) throw new Error('The received game does not match the offered copy');
|
|
70
72
|
if (this.state === 'closed') return;
|
package/src/rtc-session.js
CHANGED
|
@@ -112,6 +112,7 @@ class RtcHost {
|
|
|
112
112
|
session.onStop = () => this.stop();
|
|
113
113
|
}
|
|
114
114
|
share() {
|
|
115
|
+
if (this.session.maxPlayers === 1) return Promise.reject(new Error('This is a solo game. Use Share a copy to send it to a friend.'));
|
|
115
116
|
if (this.link) return Promise.resolve(this.link);
|
|
116
117
|
if (this.pending) return this.pending;
|
|
117
118
|
if (!this.session.game) return Promise.reject(new Error('The game is still loading'));
|
package/src/squish-map.js
CHANGED
|
@@ -4,12 +4,9 @@
|
|
|
4
4
|
// dependency. Requires must stay static so webpack can bundle them.
|
|
5
5
|
|
|
6
6
|
const squishMap = {
|
|
7
|
-
'
|
|
8
|
-
'143': require('./codecs/squish-143'),
|
|
9
|
-
'145': require('./codecs/squish-145'),
|
|
10
|
-
'146': require('squish-146')
|
|
7
|
+
'150': require('squish-150')
|
|
11
8
|
};
|
|
12
9
|
|
|
13
|
-
const DEFAULT_VERSION = '
|
|
10
|
+
const DEFAULT_VERSION = '150';
|
|
14
11
|
|
|
15
12
|
module.exports = { squishMap, DEFAULT_VERSION };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
// Discover notices from the modules webpack actually considered, including
|
|
7
|
+
// concatenated modules. Embed full texts so a standalone HTML export does not
|
|
8
|
+
// depend on a sidecar file that is only available on the original website.
|
|
9
|
+
class LicenseNoticesPlugin {
|
|
10
|
+
apply(compiler) {
|
|
11
|
+
const { Compilation, sources } = compiler.webpack;
|
|
12
|
+
compiler.hooks.thisCompilation.tap('HomegamesLicenseNotices', compilation => {
|
|
13
|
+
compilation.hooks.processAssets.tap({ name: 'HomegamesLicenseNotices',
|
|
14
|
+
stage: Compilation.PROCESS_ASSETS_STAGE_REPORT }, () => {
|
|
15
|
+
const roots = new Set([path.resolve(__dirname, '..')]);
|
|
16
|
+
const visited = new Set();
|
|
17
|
+
const visit = module => {
|
|
18
|
+
if (visited.has(module)) return;
|
|
19
|
+
visited.add(module);
|
|
20
|
+
if (module.resource) {
|
|
21
|
+
let dir = path.dirname(module.resource.split('?')[0]);
|
|
22
|
+
while (!fs.existsSync(path.join(dir, 'package.json'))) {
|
|
23
|
+
const parent = path.dirname(dir);
|
|
24
|
+
if (parent === dir) return;
|
|
25
|
+
dir = parent;
|
|
26
|
+
}
|
|
27
|
+
roots.add(dir);
|
|
28
|
+
}
|
|
29
|
+
if (module.modules) for (const child of module.modules) visit(child);
|
|
30
|
+
};
|
|
31
|
+
for (const module of compilation.modules) visit(module);
|
|
32
|
+
const notices = [...roots].map(root => {
|
|
33
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
34
|
+
const files = fs.readdirSync(root).filter(name => /^(licen[cs]e|copying|notice)(\..*)?$/i.test(name)
|
|
35
|
+
&& fs.statSync(path.join(root, name)).isFile()).sort();
|
|
36
|
+
const texts = files.map(name => fs.readFileSync(path.join(root, name), 'utf8').trim());
|
|
37
|
+
if (!texts.length) {
|
|
38
|
+
const supplied = path.resolve(__dirname, `../notices/${pkg.name}-${pkg.version}.txt`);
|
|
39
|
+
if (!fs.existsSync(supplied)) throw new Error(`Missing license text for bundled ${pkg.name}@${pkg.version}`);
|
|
40
|
+
texts.push(fs.readFileSync(supplied, 'utf8').trim());
|
|
41
|
+
}
|
|
42
|
+
return { key: `${pkg.name}@${pkg.version}`, text: `${pkg.name}@${pkg.version} (${pkg.license || 'see notice'})\n\n${[...new Set(texts)].join('\n\n')}` };
|
|
43
|
+
}).sort((a, b) => a.key.localeCompare(b.key));
|
|
44
|
+
const text = 'Homegames runtime license notices\n\n'
|
|
45
|
+
+ [...new Set(notices.map(item => item.text))].join('\n\n----------------------------------------\n\n') + '\n';
|
|
46
|
+
compilation.emitAsset('THIRD_PARTY_NOTICES.txt', new sources.RawSource(text));
|
|
47
|
+
for (const asset of compilation.getAssets()) {
|
|
48
|
+
if (!asset.name.endsWith('.js')) continue;
|
|
49
|
+
compilation.updateAsset(asset.name, new sources.ConcatSource(
|
|
50
|
+
`/*! ${text.replace(/\*\//g, '* /')} */\n`, asset.source));
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = LicenseNoticesPlugin;
|
package/webpack.config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const webpack = require('webpack');
|
|
3
|
+
const LicenseNoticesPlugin = require('./tools/license-notices-plugin.cjs');
|
|
3
4
|
|
|
4
5
|
module.exports = {
|
|
5
6
|
entry: path.join(__dirname, 'src', 'index.js'),
|
|
@@ -24,6 +25,7 @@ module.exports = {
|
|
|
24
25
|
}
|
|
25
26
|
},
|
|
26
27
|
plugins: [
|
|
28
|
+
new LicenseNoticesPlugin(),
|
|
27
29
|
// The squish Squisher (used by LocalSession) builds asset bundles with
|
|
28
30
|
// Buffer, which browsers don't have. Resolve the polyfill absolutely —
|
|
29
31
|
// squish is file-linked from outside this package, so relative
|
package/src/codecs/squish-142.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
// Versioned scene API; lifecycle and asset I/O belong to the current host.
|
|
2
|
-
const codec = require('squish-142/src/squish');
|
|
3
|
-
const { Squisher, Asset } = require('squish-146');
|
|
4
|
-
module.exports = { ...codec, Asset,
|
|
5
|
-
Squisher: class extends Squisher { constructor(options) { super({...options, encodeNode:codec.squish, decodeNode:codec.unsquish}); } },
|
|
6
|
-
Game: require('squish-142/src/Game'),
|
|
7
|
-
ViewableGame: require('squish-142/src/ViewableGame'),
|
|
8
|
-
Colors: require('squish-142/src/Colors'),
|
|
9
|
-
Shapes: require('squish-142/src/Shapes'),
|
|
10
|
-
subtypes: require('squish-142/src/subtypes'),
|
|
11
|
-
ShapeUtils: require('squish-142/src/util/shapes'),
|
|
12
|
-
ViewUtils: require('squish-142/src/util/views'),
|
|
13
|
-
GeometryUtils: require('squish-142/src/util/geometry'),
|
|
14
|
-
Physics: require('squish-142/src/physics'),
|
|
15
|
-
...require('squish-142/src/GameNode'),
|
|
16
|
-
TerrainGenerator: require('squish-142/src/terrain').terrainGenerator,
|
|
17
|
-
};
|
package/src/codecs/squish-143.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
// Versioned scene API; lifecycle and asset I/O belong to the current host.
|
|
2
|
-
const codec = require('squish-143/src/squish');
|
|
3
|
-
const { Squisher, Asset } = require('squish-146');
|
|
4
|
-
module.exports = { ...codec, Asset,
|
|
5
|
-
Squisher: class extends Squisher { constructor(options) { super({...options, encodeNode:codec.squish, decodeNode:codec.unsquish}); } },
|
|
6
|
-
Game: require('squish-143/src/Game'),
|
|
7
|
-
ViewableGame: require('squish-143/src/ViewableGame'),
|
|
8
|
-
Colors: require('squish-143/src/Colors'),
|
|
9
|
-
Shapes: require('squish-143/src/Shapes'),
|
|
10
|
-
subtypes: require('squish-143/src/subtypes'),
|
|
11
|
-
ShapeUtils: require('squish-143/src/util/shapes'),
|
|
12
|
-
ViewUtils: require('squish-143/src/util/views'),
|
|
13
|
-
GeometryUtils: require('squish-143/src/util/geometry'),
|
|
14
|
-
Physics: require('squish-143/src/physics'),
|
|
15
|
-
...require('squish-143/src/GameNode'),
|
|
16
|
-
TerrainGenerator: require('squish-143/src/terrain').terrainGenerator,
|
|
17
|
-
};
|
package/src/codecs/squish-145.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
// Versioned scene API; lifecycle and asset I/O belong to the current host.
|
|
2
|
-
const codec = require('squish-145/src/squish');
|
|
3
|
-
const { Squisher, Asset } = require('squish-146');
|
|
4
|
-
module.exports = { ...codec, Asset,
|
|
5
|
-
Squisher: class extends Squisher { constructor(options) { super({...options, encodeNode:codec.squish, decodeNode:codec.unsquish}); } },
|
|
6
|
-
Game: require('squish-145/src/Game'),
|
|
7
|
-
ViewableGame: require('squish-145/src/ViewableGame'),
|
|
8
|
-
Colors: require('squish-145/src/Colors'),
|
|
9
|
-
Shapes: require('squish-145/src/Shapes'),
|
|
10
|
-
subtypes: require('squish-145/src/subtypes'),
|
|
11
|
-
ShapeUtils: require('squish-145/src/util/shapes'),
|
|
12
|
-
ViewUtils: require('squish-145/src/util/views'),
|
|
13
|
-
GeometryUtils: require('squish-145/src/util/geometry'),
|
|
14
|
-
Physics: require('squish-145/src/physics'),
|
|
15
|
-
...require('squish-145/src/GameNode'),
|
|
16
|
-
TerrainGenerator: require('squish-145/src/terrain').terrainGenerator,
|
|
17
|
-
};
|