homegames-common 1.5.1 → 1.5.3

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/docker-helper.js CHANGED
@@ -101,7 +101,7 @@ const runGameContainer = async ({
101
101
  saveDataPath,
102
102
  assetCachePath = null,
103
103
  imageName = 'homegames-runner',
104
- memoryLimit = '64m',
104
+ memoryLimit = '128m',
105
105
  cpuLimit = '1',
106
106
  gameEntryRelative = null,
107
107
  noFrame = false,
@@ -180,7 +180,7 @@ const runGameContainer = async ({
180
180
  PidsLimit: 64,
181
181
  CapDrop: ['ALL'],
182
182
  Tmpfs: {
183
- '/tmp': 'rw,size=64m',
183
+ '/tmp': 'rw,size=160m',
184
184
  },
185
185
  ExtraHosts: extraHosts,
186
186
  },
package/game-loader.js CHANGED
@@ -44,22 +44,13 @@ const DEFAULT_SQUISH_VERSION = '135';
44
44
  const parseSquishVersion = (codePath) => {
45
45
  const code = fs.readFileSync(codePath, 'utf-8');
46
46
 
47
- console.log('aiaiaiai');
48
47
  const { Parser } = require('acorn');
49
48
  const parsed = Parser.parse(code, { ecmaVersion: 'latest', sourceType: 'script' });
50
- console.log('balls and ass');
51
- console.log(parsed);
52
- console.log(parsed.body);
53
- console.log(parsed.body.map(n => n.type));
54
- console.log(parsed.body.filter(n => n.type === 'ClassDeclaration').superClass);
55
49
 
56
50
  const foundGameClasses = parsed.body.filter(
57
51
  n => n.type === 'ClassDeclaration' && n.superClass && (n.superClass.name === 'Game' || n.superClass.name === 'ViewableGame')
58
52
  );
59
53
 
60
- console.log('found game classes');
61
- console.log(foundGameClasses);
62
-
63
54
  if (foundGameClasses.length !== 1) {
64
55
  throw new Error('Unable to parse squish version');
65
56
  }
@@ -84,7 +84,7 @@ class GameSessionManager {
84
84
  this.dockerImageName = opts.dockerImageName || 'homegames-runner';
85
85
  this.childServerPath = opts.childServerPath || null;
86
86
  this.gracePeriodMs = opts.gracePeriodMs || 30000;
87
- this.lifecycleCheckMs = opts.lifecycleCheckMs || 10000;
87
+ this.lifecycleCheckMs = opts.lifecycleCheckMs || 3000;
88
88
  this.forgejo = opts.forgejo || {};
89
89
  this.saveDataRoot = opts.saveDataRoot || path.join(os.tmpdir(), 'hg-save-data');
90
90
  this.assetCachePath = opts.assetCachePath || null;
@@ -368,6 +368,8 @@ class GameSessionManager {
368
368
  execArgv: ['--max-old-space-size=64'],
369
369
  });
370
370
 
371
+ this._attachForkLogForwarding(child);
372
+
371
373
  child.send(JSON.stringify({
372
374
  key: input.gameKey || path.basename(gamePath, '.js'),
373
375
  squishVersion,
@@ -433,6 +435,38 @@ class GameSessionManager {
433
435
  });
434
436
  }
435
437
 
438
+ // -----------------------------------------------------------------------
439
+ // Forward forked child stdout/stderr to the parent terminal.
440
+ // Docker sessions are excluded — use GET /sessions/:id/logs or docker logs.
441
+ // -----------------------------------------------------------------------
442
+ _attachForkLogForwarding(child) {
443
+ const emitLine = (line) => {
444
+ if (!line) return;
445
+ console.log(`session log: ${line}`);
446
+ };
447
+
448
+ const attachStream = (stream) => {
449
+ if (!stream) return;
450
+ let buffer = '';
451
+ stream.on('data', (chunk) => {
452
+ buffer += chunk.toString();
453
+ const lines = buffer.split('\n');
454
+ buffer = lines.pop() || '';
455
+ for (const line of lines) {
456
+ emitLine(line);
457
+ }
458
+ });
459
+ stream.on('end', () => {
460
+ if (buffer.length > 0) {
461
+ emitLine(buffer);
462
+ }
463
+ });
464
+ };
465
+
466
+ attachStream(child.stdout);
467
+ attachStream(child.stderr);
468
+ }
469
+
436
470
  // -----------------------------------------------------------------------
437
471
  // Find the project root (nearest ancestor with node_modules or package.json)
438
472
  // so we mount enough of the tree for relative requires to work.
@@ -525,10 +559,30 @@ class GameSessionManager {
525
559
 
526
560
  if (s.type === 'docker') {
527
561
  const running = await isContainerRunning(s.containerId);
528
- this.log.info(`Session ${sessionId} lifecycle check: container running = ${running}`);
529
562
  if (!running) {
530
563
  this.log.info(`Session ${sessionId} container exited`);
531
564
  this._cleanupSession(sessionId);
565
+ return;
566
+ }
567
+
568
+ // Check player count — stop container if empty past grace period
569
+ try {
570
+ const healthData = await this._querySessionHealth(s.port);
571
+ const playerCount = healthData && healthData.playerCount !== undefined ? healthData.playerCount : -1;
572
+ if (playerCount === 0) {
573
+ s._emptyTicks++;
574
+ const emptyMs = s._emptyTicks * this.lifecycleCheckMs;
575
+ if (emptyMs >= this.gracePeriodMs) {
576
+ this.log.info(`Session ${sessionId} empty for ${emptyMs}ms, stopping`);
577
+ await stopContainer(s.containerId);
578
+ this._cleanupSession(sessionId);
579
+ return;
580
+ }
581
+ } else if (playerCount > 0) {
582
+ s._emptyTicks = 0;
583
+ }
584
+ } catch (e) {
585
+ // Health check failed — container might be starting up, ignore
532
586
  }
533
587
  } else if (s.type === 'fork') {
534
588
  // For fork sessions, send heartbeat. The child_game_server.js
@@ -559,6 +613,28 @@ class GameSessionManager {
559
613
  this._cleanupSession(sessionId);
560
614
  }
561
615
 
616
+ _querySessionHealth(port) {
617
+ return new Promise((resolve) => {
618
+ const req = http.request({
619
+ hostname: 'localhost',
620
+ port,
621
+ path: '/health',
622
+ method: 'GET',
623
+ timeout: 2000,
624
+ }, (res) => {
625
+ let data = '';
626
+ res.on('data', (chunk) => { data += chunk; });
627
+ res.on('end', () => {
628
+ try { resolve(JSON.parse(data)); }
629
+ catch (e) { resolve(null); }
630
+ });
631
+ });
632
+ req.on('error', () => resolve(null));
633
+ req.on('timeout', () => { req.destroy(); resolve(null); });
634
+ req.end();
635
+ });
636
+ }
637
+
562
638
  _cleanupSession(sessionId) {
563
639
  const session = this.sessions[sessionId];
564
640
  if (!session) return;
package/game-session.js CHANGED
@@ -91,6 +91,7 @@ class GameSession {
91
91
  this.squisher.addListener(() => this._broadcastState());
92
92
 
93
93
  this.gameMetadata = (typeof game.constructor.metadata === 'function') ? game.constructor.metadata() : {};
94
+ this.maxPlayers = this.gameMetadata.maxPlayers || 64;
94
95
  this.aspectRatio = this.gameMetadata.aspectRatio || { x: 16, y: 9 };
95
96
 
96
97
  // Player / spectator maps — values are raw WebSocket objects
@@ -128,6 +129,12 @@ class GameSession {
128
129
  // -----------------------------------------------------------------------
129
130
 
130
131
  addPlayer(playerId, ws, playerOpts = {}) {
132
+ // Reject if the game is at capacity
133
+ if (Object.keys(this.players).length >= this.maxPlayers) {
134
+ try { ws.close(); } catch (e) {}
135
+ throw new Error('Game is full');
136
+ }
137
+
131
138
  // If this ID is already connected, disconnect the old one first
132
139
  if (this.players[playerId]) {
133
140
  this.removePlayer(playerId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homegames-common",
3
- "version": "1.5.1",
3
+ "version": "1.5.3",
4
4
  "description": "Homegames common tools",
5
5
  "main": "index.js",
6
6
  "scripts": {