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 +2 -2
- package/game-loader.js +0 -9
- package/game-session-manager.js +78 -2
- package/game-session.js +7 -0
- package/package.json +1 -1
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 = '
|
|
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=
|
|
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
|
}
|
package/game-session-manager.js
CHANGED
|
@@ -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 ||
|
|
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);
|