homegames-client 0.1.2 → 0.2.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.
@@ -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.1.2",
3
+ "version": "0.2.1",
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": "GPL-3.0",
13
+ "license": "MIT",
14
14
  "dependencies": {
15
15
  "buffer": "^6.0.3",
16
- "homegames-common": "1.7.2",
16
+ "homegames-common": "1.7.3",
17
17
  "homepad": "0.2.1",
18
18
  "qrcode-generator": "^2.0.4",
19
- "squish-142": "npm:squishjs@1.4.2",
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
- "webpack.config.js"
34
+ "tools/license-notices-plugin.cjs",
35
+ "notices",
36
+ "webpack.config.js",
37
+ "THIRD_PARTY_NOTICES.md"
32
38
  ]
33
39
  }
@@ -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);
@@ -82,7 +85,13 @@ const loadGameClass = ({ files, entryPoint }) => {
82
85
  throw new Error('Entry point "' + entryPoint + '" not found in game source map');
83
86
  }
84
87
 
85
- return loadModule(entry);
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;
86
95
  };
87
96
 
88
97
  module.exports = { loadGameClass };
package/src/index.js CHANGED
@@ -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 Share to retry.`));
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
@@ -183,6 +183,13 @@ class InputHandler {
183
183
  // that would normally focus an embedded game. Treat this direct
184
184
  // gesture as input focus so the first tap can reach the game too.
185
185
  // Page-control touches return above; blur still releases input.
186
+ // preventDefault also suppresses native focus transfer. Move DOM
187
+ // focus into the canvas so a later keyboard press cannot activate
188
+ // the embedding toolbar's previously focused button.
189
+ if (typeof this.canvas.focus === 'function') {
190
+ if (this.canvas.tabIndex < 0) this.canvas.tabIndex = -1;
191
+ this.canvas.focus({ preventScroll: true });
192
+ }
186
193
  this._focused = true;
187
194
  this._touchActive = true;
188
195
  clearTimeout(this.clickStopper); this.clickStopper = null;
@@ -553,7 +560,7 @@ class InputHandler {
553
560
 
554
561
  // Whether the game's squish version uses the single-click contract
555
562
  // (onClick once per press, mousemove while held, mouseup on release).
556
- // Modern version strings are three digits ('142', '144'); anything else
563
+ // Modern version strings are three digits ('150', '144'); anything else
557
564
  // ('0767' / '1000'-style) predates drag support entirely — both get the
558
565
  // legacy repeated-click behavior.
559
566
  _usesSingleClickContract() {
@@ -662,9 +669,9 @@ class InputHandler {
662
669
  _promptFileUpload(nodeId) {
663
670
  const inputEl = document.createElement('input');
664
671
  inputEl.type = 'file';
665
- // The platform understands image, audio and video assets; the squished node
666
- // carries no per-node accept info, so this is the global filter.
667
- inputEl.accept = 'image/*,audio/*,video/*';
672
+ // Games also import portable creations such as level packs. The node
673
+ // has no per-input accept field; each game still validates the bytes.
674
+ inputEl.accept = 'image/*,audio/*,video/*,application/json,.json';
668
675
  inputEl.style.display = 'none';
669
676
  document.body.appendChild(inputEl);
670
677
  const cleanupInput = () => {
@@ -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: 'Share', run: () => dialog.show() });
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 () => {
@@ -154,9 +154,12 @@ class LocalSession {
154
154
  return;
155
155
  }
156
156
 
157
- // --- Resolve squish version from game metadata ---
158
- const metadata = (typeof this.gameClass.metadata === 'function') ? this.gameClass.metadata() : {};
159
- this.maxPlayers = Math.min(255, metadata.maxPlayers || 64);
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; platform services are
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.
@@ -14,6 +14,7 @@ module.exports = {
14
14
  create: files => project.createProject(files, options),
15
15
  importArchive: bytes => project.importArchive(bytes, options),
16
16
  exportArchive: project.exportArchive,
17
+ withGameId: project.withGameId,
17
18
  normalizePath: project.normalizePath,
18
19
  limits: project.LIMITS,
19
20
  buildIsolatedHtml: html.buildIsolatedHtml,
@@ -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
- if (await digest(bytes) !== metadata.sha256) throw new Error('The game copy was damaged during transfer. Try again.');
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;
@@ -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
- '142': require('./codecs/squish-142'),
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 = '142';
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
@@ -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
- };
@@ -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
- };
@@ -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
- };