Lots of content

This commit is contained in:
2026-11-03 12:41:09 -05:00
parent 6ebe95b208
commit d34388bf2c
126 changed files with 28076 additions and 1027 deletions
+234 -131
View File
@@ -1,39 +1,100 @@
#!/usr/bin/env node
// Original MIT-licensed launcher. Node built-ins only; never invokes a shell.
import { spawn } from 'node:child_process';
import { randomInt, randomUUID } from 'node:crypto';
import { constants } from 'node:fs';
import { createHash, randomInt, randomUUID } from 'node:crypto';
import * as fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { generateAssets } from './assets.mjs';
import { pathToFileURL } from 'node:url';
import { backupWorld, restoreWorld } from './backup.mjs';
import { absolute, atomicDirectory, copyTree, ensureDirectory, exists, safePath, withLauncherLock } from './files.mjs';
import { packageGame, SOURCE, verifyPackage } from './package.mjs';
import { coordinates, engineExecutable, environmentSettings, fixtureSettings, graphicsSettings, prepareCourse, runtimeHome, snapshotBeforePlay, validateEngine } from './launcher.mjs';
import { isTerrainSetting, terrainSettings } from './terrain.mjs';
const source = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const source = SOURCE;
const command = process.argv[2];
const excluded = new Set(['scripts', 'tests', '.git']);
const marker = '.last-hearth-launcher-managed';
const TEST_TIMEOUT = 90_000;
const PLAY_TIMEOUT = 8 * 60 * 60 * 1000;
const EMBEDDED_MARKER = '.last-hearth-embedded-session.json';
async function exists(filename) {
try { await fs.access(filename, constants.F_OK); return true; }
catch (error) { if (error.code === 'ENOENT') return false; throw error; }
// JSON escapes are not Lua escapes (notably \uXXXX). Encode UTF-8 bytes with
// fixed-width decimal escapes, including quotes, backslashes and control bytes.
const luaString = value => '"' + [...Buffer.from(value)].map(byte => `\\${String(byte).padStart(3, '0')}`).join('') + '"';
async function pinPackage(game, gameId, verified, requireCourse = false) {
const manifest = verified ?? await verifyPackage(game);
const release = JSON.parse(await fs.readFile(path.join(game, 'release.json'), 'utf8'));
const digest = createHash('sha256').update(JSON.stringify(manifest)).digest('hex');
const token = randomUUID();
const marker = `LAST_HEARTH_PACKAGE_LOADED ${token} ${release.version} ${digest}`;
const init = path.join(game, 'mods/hearth/init.lua');
const original = await fs.readFile(init);
// Only the private runtime copy is instrumented. The exact payload was verified
// above; path checks run BEFORE its code and the marker follows mod loading.
// A stale installed game or a worldmod overriding hearth cannot emit this token.
const normalize = value => value.replaceAll('\\', '/').replace(/\/+$/, '');
await fs.writeFile(init, Buffer.concat([Buffer.from([
'do',
' local function normalized(p) return (p:gsub("\\\\", "/"):gsub("/+$", "")) end',
' local info = core.get_game_info()',
` assert(info.id == ${luaString(gameId)} and normalized(info.path) == ${luaString(normalize(game))}, "Launcher game path/id mismatch")`,
` assert(normalized(core.get_modpath("hearth")) == ${luaString(normalize(path.join(game, 'mods/hearth')))}, "Launcher hearth path mismatch")`,
' core.register_on_mods_loaded(function()',
...(requireCourse ? [
' assert(hearth.course and type(hearth.course.report) == "function", "Course module must be wired by the game bootstrap")',
' assert(hearth.storage:get_string("m1_course_world_nonce") == core.settings:get("hearth_m1_course_nonce"), "Course must enroll before clock/players on first load")',
] : []),
` core.log("action", ${luaString(marker)})`,
' end)',
'end', '',
].join('\n')), original]));
return marker;
}
// copyFile avoids fs.cp's realpath failures on Windows SMB shares. Refuse
// symlinks/junctions rather than following them out of the selected source.
async function copyTree(from, to, skip = new Set()) {
const stat = await fs.lstat(from);
if (stat.isSymbolicLink()) throw new Error(`Refusing symbolic link: ${from}`);
if (stat.isDirectory()) {
await fs.mkdir(to, { recursive: true });
for (const entry of await fs.readdir(from)) {
if (!skip.has(entry)) await copyTree(path.join(from, entry), path.join(to, entry), skip);
}
} else if (stat.isFile()) {
await fs.copyFile(from, to);
} else {
throw new Error(`Not a regular file or directory: ${from}`);
async function embeddedOwner(world, runtime) {
const game = await safePath(path.join(world, 'game'));
if (!await exists(game)) return undefined;
const marker = await safePath(path.join(game, EMBEDDED_MARKER));
if (!(await fs.lstat(game)).isDirectory() || !await exists(marker) || !(await fs.lstat(marker)).isFile() ||
(await fs.stat(marker)).size > 4096) throw new Error('Refusing to overwrite an unmanaged embedded world game.');
const owner = JSON.parse(await fs.readFile(marker, 'utf8'));
if (owner.format !== 1 || owner.owner !== 'last-hearth-runner' || owner.world !== world || owner.runtime !== runtime ||
typeof owner.token !== 'string' || !/^[0-9a-f-]{36}$/.test(owner.token)) {
throw new Error('Refusing an unrecognized embedded world game marker.');
}
return owner;
}
async function playGame(world, runtime, packaged, action) {
// Embedded games take precedence in findWorldSubgame(), including the GUI
// client, which otherwise ignores --gameid for an existing world. Never alias
// or rewrite world.mt: its logical last_hearth identity survives even SIGKILL.
const text = await fs.readFile(await safePath(path.join(world, 'world.mt')), 'utf8');
const ids = [...text.matchAll(/^\s*gameid\s*=\s*([^\r\n]*?)\s*$/gm)];
if (ids.length !== 1 || ids[0][1] !== 'last_hearth') throw new Error('Play requires logical world gameid last_hearth.');
const game = path.join(world, 'game');
if (await embeddedOwner(world, runtime)) {
// A hard-killed launcher can leave its private package behind. Snapshot was
// taken already; preserve rather than delete it (including any added files).
await fs.rename(game, path.join(runtime, `preserved-embedded-game-${randomUUID()}`));
}
const token = randomUUID();
let verified;
await atomicDirectory(game, async directory => {
await copyTree(packaged, directory, { intoEmpty: true });
verified = await verifyPackage(directory);
await fs.writeFile(path.join(directory, EMBEDDED_MARKER), JSON.stringify({
format: 1, owner: 'last-hearth-runner', token, world, runtime,
}), { flag: 'wx', mode: 0o600 });
});
try {
// Keep ownership present continuously, including if instrumentation is
// interrupted. Only this verified private copy gains launcher-only files.
const marker = await pinPackage(game, 'last_hearth', verified);
await action(marker);
} finally {
if ((await embeddedOwner(world, runtime))?.token !== token) throw new Error('Embedded game ownership changed; cleanup refused.');
await fs.rm(game, { recursive: true, force: true });
}
}
@@ -41,7 +102,7 @@ async function luaFiles(directory, prefix = '') {
const result = [];
for (const entry of await fs.readdir(directory, { withFileTypes: true })) {
if (entry.isSymbolicLink()) throw new Error(`Refusing symbolic link: ${entry.name}`);
if (entry.name === '.git' || entry.name === 'scripts') continue;
if (['.git', 'scripts', 'dist', 'node_modules'].includes(entry.name) || entry.name.startsWith('.nfs')) continue;
const relative = prefix + entry.name;
if (entry.isDirectory()) result.push(...await luaFiles(path.join(directory, entry.name), relative + '/'));
else if (entry.isFile() && entry.name.endsWith('.lua')) result.push(relative);
@@ -49,36 +110,21 @@ async function luaFiles(directory, prefix = '') {
return result.sort();
}
function coordinates() {
const latitude = process.env.HEARTH_LATITUDE;
const longitude = process.env.HEARTH_LONGITUDE;
if (latitude === undefined && longitude === undefined) return [];
if (latitude === undefined || longitude === undefined) {
throw new Error('Provide both HEARTH_LATITUDE and HEARTH_LONGITUDE, or neither.');
}
const parse = (value, limit, name) => {
if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(value)) throw new Error(`Invalid ${name}.`);
const number = Number(value);
if (!Number.isFinite(number) || Math.abs(number) > limit) throw new Error(`${name} out of range.`);
return number;
};
return [
`hearth_latitude = ${parse(latitude, 90, 'latitude')}`,
`hearth_longitude = ${parse(longitude, 180, 'longitude')}`,
];
}
function run(executable, args, cwd, timeout, successMarker) {
function run(executable, args, cwd, timeout, successMarker, games) {
return new Promise((resolve, reject) => {
const child = spawn(executable, args, {
cwd, shell: false, windowsHide: command !== 'play', stdio: ['ignore', 'pipe', 'pipe'],
cwd, shell: false, windowsHide: !['play', 'course'].includes(command), stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, ...(games ? { LUANTI_GAME_PATH: games, MINETEST_GAME_PATH: '' } : {}) },
});
let tail = '', timedOut = false, interrupted = false;
const pending = new Set([successMarker].flat().filter(Boolean));
let timedOut = false, interrupted = false;
const capture = stream => {
let tail = '';
stream.setEncoding('utf8');
stream.on('data', text => {
// Bound memory even if the engine emits excessive diagnostics.
tail = (tail + text).slice(-1024 * 1024);
for (const marker of pending) if (tail.includes(marker)) pending.delete(marker);
process.stdout.write(text);
});
};
@@ -99,60 +145,44 @@ function run(executable, args, cwd, timeout, successMarker) {
if (timedOut) reject(new Error(`Process exceeded ${timeout / 1000}s timeout.`));
else if (interrupted) reject(new Error('Interrupted; child process stopped.'));
else if (code !== 0) reject(new Error(`Process failed (exit ${code}, signal ${signal || 'none'}).`));
else if (successMarker && !tail.includes(successMarker)) reject(new Error('Engine exited without the required success marker.'));
else if (pending.size) reject(new Error('Engine exited without the required package-load/test success marker.'));
else resolve();
});
});
}
async function stageGame(engine) {
for (const required of ['game.conf', 'mods/hearth/init.lua', 'mods/hearth/model.lua', 'mods/hearth/runtime.lua', 'mods/hearth/content.lua']) {
if (!await exists(path.join(source, required))) throw new Error(`Game is incomplete: missing ${required}.`);
export async function writeWorld(world, config, location, extra = [], environment = 'production', gameId = 'last_hearth', env = process.env) {
const graphics = graphicsSettings(env);
const settings = environmentSettings(environment, [...location, ...extra]);
const graphicsKeys = new Set(graphics.map(line => line.split('=')[0].trim()));
if (settings.some(line => graphicsKeys.has(line.split('=')[0].trim()))) {
throw new Error('Graphics settings are launcher-managed; use HEARTH_VIEW_DISTANCE.');
}
const games = path.join(engine, 'games');
await fs.mkdir(games, { recursive: true });
const destination = path.join(games, 'last_hearth');
if (await exists(destination)) {
if ((await fs.lstat(destination)).isSymbolicLink() || !await exists(path.join(destination, marker))) {
throw new Error(`Refusing to overwrite an unmanaged game: ${destination}`);
}
}
const stage = await fs.mkdtemp(path.join(games, '.last-hearth-stage-'));
const backup = path.join(games, `.last-hearth-backup-${randomUUID()}`);
let backedUp = false;
try {
await copyTree(source, stage, excluded);
await generateAssets(stage, source);
await fs.writeFile(path.join(stage, marker), 'Managed copy; edit the repository, not this directory.\n');
if (await exists(destination)) { await fs.rename(destination, backup); backedUp = true; }
try { await fs.rename(stage, destination); }
catch (error) { if (backedUp) await fs.rename(backup, destination); throw error; }
if (backedUp) await fs.rm(backup, { recursive: true, force: true });
} finally {
await fs.rm(stage, { recursive: true, force: true });
}
}
async function writeWorld(world, config, location, extra = []) {
await fs.mkdir(world, { recursive: true });
await ensureDirectory(world);
const worldFile = path.join(world, 'world.mt');
if (!await exists(worldFile)) {
// Decide before writing world.mt. Saved mapgen parameters are engine-owned;
// even a legacy/orphan map_meta.txt opts out of creation-time overrides.
const worldExists = await exists(worldFile);
const newWorld = !worldExists && !await exists(path.join(world, 'map_meta.txt'));
if (!worldExists) {
await fs.writeFile(worldFile, [
'gameid = last_hearth', 'backend = sqlite3', 'player_backend = sqlite3',
`gameid = ${gameId}`, 'backend = sqlite3', 'player_backend = sqlite3',
'auth_backend = sqlite3', 'mod_storage_backend = sqlite3', 'load_mod_hearth = true', '',
].join('\n'), { flag: 'wx' });
}
await safePath(config);
await fs.writeFile(config, [
'enable_damage = true', 'enable_pvp = false', 'creative_mode = false',
'bind_address = 127.0.0.1', 'server_announce = false', 'enable_ipv6 = false',
'time_speed = 0', 'movement_speed_walk = 4', 'movement_speed_fast = 6',
'movement_speed_jump = 6.5', 'secure.enable_security = true',
'mg_name = v7', 'fixed_map_seed = 731104',
...location, ...extra, '',
].join('\n'), { mode: 0o600 });
...graphics,
...terrainSettings(newWorld),
...settings.filter(line => newWorld || !isTerrainSetting(line)), '',
].join('\n'), { flag: 'wx', mode: 0o600 });
}
async function engineTest(mode, runtime, engine, executable, location) {
async function engineTest(mode, runtime, games, executable, location, gameId, packageMarker, options = {}) {
const temporary = await fs.mkdtemp(path.join(runtime, `${mode}-`));
try {
const world = path.join(temporary, 'world');
@@ -178,11 +208,13 @@ async function engineTest(mode, runtime, engine, executable, location) {
await writeWorld(world, config, testLocation, [
'load_mod_hearth_smoke = true', `hearth_launcher_mode = ${mode}`,
`hearth_smoke_token = ${token}`, 'max_users = 0',
]);
...(Number.isSafeInteger(options.seed) && options.seed >= 0 ? [`fixed_map_seed = ${options.seed}`] : []),
...await fixtureSettings(options.fixture === false ? undefined : process.env.HEARTH_FIXTURE, 'staging'),
], 'staging', gameId);
await run(executable, [
'--server', '--world', world, '--gameid', 'last_hearth', '--config', config,
'--server', '--world', world, '--gameid', gameId, '--config', config,
'--port', String(randomInt(49152, 65536)), '--logfile', path.join(temporary, 'engine.log'),
], engine, TEST_TIMEOUT, `LAST_HEARTH_${mode.toUpperCase()}_PASS ${token}`);
], runtime, TEST_TIMEOUT, [packageMarker, `LAST_HEARTH_${mode.toUpperCase()}_PASS ${token}`], games);
} finally {
await fs.rm(temporary, { recursive: true, force: true });
}
@@ -191,13 +223,14 @@ async function engineTest(mode, runtime, engine, executable, location) {
async function standaloneTests(runtime) {
const candidates = process.env.HEARTH_LUA
? [process.env.HEARTH_LUA]
: ['luajit.exe', 'lua.exe', 'bin/luajit.exe', 'bin/lua.exe'].map(name => path.join(runtime, name));
: (process.platform === 'win32' ? ['luajit.exe', 'lua.exe', 'bin/luajit.exe', 'bin/lua.exe']
: ['luajit', 'lua', 'bin/luajit', 'bin/lua']).map(name => path.join(runtime, name));
let lua;
for (const candidate of candidates) {
if (!path.isAbsolute(candidate) || path.extname(candidate).toLowerCase() !== '.exe') {
throw new Error('HEARTH_LUA must be an absolute path to a local Lua executable (.exe).');
if (!path.isAbsolute(candidate)) {
throw new Error('HEARTH_LUA must be an absolute path to a local Lua executable.');
}
if (await exists(candidate)) { lua = candidate; break; }
if (await exists(candidate)) { lua = await safePath(candidate); break; }
}
if (!lua) throw new Error('Standalone Lua unavailable: engine checks passed, but unit tests were NOT run. Install Lua 5.1+/LuaJIT locally and set HEARTH_LUA to its absolute executable path.');
const specs = (await fs.readdir(path.join(source, 'tests'))).filter(name => name.endsWith('_spec.lua')).sort();
@@ -210,48 +243,118 @@ async function standaloneTests(runtime) {
}
}
async function main() {
if (!['check', 'smoke', 'play', 'test'].includes(command) || process.argv.length !== 3) {
throw new Error('Usage: node scripts/run.mjs <check|smoke|play|test>');
async function launchCourse(stage, runtime, games, packaged, gameId, executable, headless) {
// No original world, snapshot, restored copy, or inherited location/fixture.
// Every invocation gets a new stage/course, marker and UUID, removed on exit.
const { world, nonce, settings } = await prepareCourse(stage, { environment: headless ? 'staging' : 'local' });
const extra = [...settings];
if (headless) {
const probe = await safePath(path.join(source, 'tests/course_engine.lua'));
if (!await exists(probe)) throw new Error('test:course requires the main-owned tests/course_engine.lua native probe; no fake player or exported build API is assumed.');
const mod = path.join(world, 'worldmods/hearth_course_probe');
await fs.mkdir(mod, { recursive: true });
await fs.copyFile(probe, path.join(mod, 'init.lua'));
await fs.writeFile(path.join(mod, 'mod.conf'), 'name = hearth_course_probe\ndepends = hearth\n', { flag: 'wx' });
// Probe contract: native API/enrollment checks, request_shutdown on success,
// and LAST_HEARTH_COURSE_PASS <hearth_smoke_token>. Generation requires live
// /course consent; course.report is the only exported API, not course.build.
extra.push('load_mod_hearth_course_probe = true', 'hearth_launcher_mode = course',
`hearth_smoke_token = ${nonce}`, 'max_users = 0');
}
if (process.platform !== 'win32' || !process.env.LOCALAPPDATA || !path.isAbsolute(process.env.LOCALAPPDATA)) {
throw new Error('This launcher requires Windows and an absolute LOCALAPPDATA path.');
}
const location = coordinates();
const runtime = path.join(process.env.LOCALAPPDATA, 'last-hearth-runtime');
const engine = path.join(runtime, 'luanti-5.18.0-win64');
const executable = path.join(engine, 'bin', 'luanti.exe');
if (!await exists(executable)) throw new Error(`Official Luanti 5.18.0 runtime not found: ${executable}`);
const lockPath = path.join(runtime, 'launcher.lock');
let lock;
try { lock = await fs.open(lockPath, 'wx', 0o600); }
catch (error) {
if (error.code === 'EEXIST') throw new Error('Another launcher may be active. If it crashed, close its engine before removing LOCALAPPDATA/last-hearth-runtime/launcher.lock.');
throw error;
}
try {
await lock.writeFile(`${process.pid}\n`);
await stageGame(engine);
if (command === 'play') {
const world = path.join(runtime, 'worlds', 'last_hearth');
const config = path.join(runtime, 'last-hearth-play.conf');
await writeWorld(world, config, location);
await run(executable, [
'--go', '--world', world, '--gameid', 'last_hearth', '--config', config,
'--port', String(randomInt(49152, 65536)), '--logfile', path.join(runtime, 'last-hearth-play.log'),
], engine, PLAY_TIMEOUT);
} else if (command === 'test') {
await engineTest('check', runtime, engine, executable, location);
await engineTest('smoke', runtime, engine, executable, location);
if (process.env.HEARTH_LUA) await standaloneTests(runtime);
console.log('LAST_HEARTH_TEST_PASS: real-engine integration and isolated model/content suites completed.');
} else {
await engineTest(command, runtime, engine, executable, location);
}
} finally {
await lock.close();
await fs.unlink(lockPath);
const config = path.join(stage, 'course.conf');
await writeWorld(world, config, [], extra, headless ? 'staging' : 'local', gameId);
const packageMarker = await pinPackage(packaged, gameId, undefined, true);
if (!headless) {
console.log('Disposable summer course: use /course after arrival to generate and receive the kit. /course retry resets kit/needs/timer; /course report shows server step intervals and Lua callback wall duration, NOT FPS or CPU profiling. No automatic opponents. Original play world is not opened.');
}
await run(executable, [
headless ? '--server' : '--go', '--world', world, '--gameid', gameId, '--config', config,
'--port', String(randomInt(49152, 65536)), '--logfile', path.join(stage, 'course.log'),
], runtime, headless ? TEST_TIMEOUT : PLAY_TIMEOUT,
headless ? [packageMarker, `LAST_HEARTH_COURSE_PASS ${nonce}`] : packageMarker, games);
}
main().catch(error => { console.error(`Last Hearth: ${error.message}`); process.exitCode = 1; });
async function main() {
if (Number(process.versions.node.split('.')[0]) < 22) throw new Error('Node.js 22+ is required.');
const args = process.argv.slice(3);
if (command === 'package' && args.length <= 1) {
const destination = args.length ? absolute(args[0]) : path.join(source, 'dist/last_hearth');
if (!args.length) await ensureDirectory(path.dirname(destination));
await packageGame(destination);
console.log('LAST_HEARTH_PACKAGE_PASS: verified game directory and SHA256 manifest.');
return;
}
if (command === 'verify' && args.length === 1) {
await verifyPackage(absolute(args[0]));
console.log('LAST_HEARTH_PACKAGE_VERIFY_PASS');
return;
}
if (['backup', 'restore'].includes(command) && args.length === 3 && args[2] === '--offline') {
const options = { offline: true, runtime: runtimeHome() };
await (command === 'backup' ? backupWorld : restoreWorld)(absolute(args[0]), absolute(args[1]), options);
console.log(`LAST_HEARTH_${command.toUpperCase()}_PASS`);
return;
}
if (!['check', 'smoke', 'play', 'test', 'test:seeds', 'course', 'test:course'].includes(command) || args.length) {
throw new Error('Usage: node scripts/run.mjs <check|smoke|play|test|test:seeds|course|test:course|package [absolute-new-folder]|verify <absolute-package>|backup <absolute-world> <absolute-new-backup> --offline|restore <absolute-backup> <absolute-new-world> --offline>. OFFLINE: stop ALL external engines; lock checks cannot prove remote idleness. Backups contain private data: use a protected/encrypted volume.');
}
const course = ['course', 'test:course'].includes(command);
// Reject malformed graphics input before engine execution, staging or backups.
graphicsSettings();
const location = course || command === 'test:seeds' ? [] : coordinates();
const runtime = runtimeHome();
const executable = engineExecutable(process.env, process.platform, runtime);
// Fail before staging or touching worlds when the engine is absent/wrong.
await validateEngine(executable);
await withLauncherLock(runtime, async lease => {
// Never install into or overwrite the user's engine/game directories.
const stage = await fs.mkdtemp(path.join(runtime, 'session-'));
try {
const games = path.join(stage, 'games');
await fs.mkdir(games);
// Installed/share games precede LUANTI_GAME_PATH; never reuse their ID.
const gameId = `last_hearth_session_${randomUUID().replaceAll('-', '')}`;
const packaged = path.join(games, gameId);
await packageGame(packaged);
if (course) {
await launchCourse(stage, runtime, games, packaged, gameId, executable, command === 'test:course');
} else if (command === 'play') {
const fixture = await fixtureSettings(process.env.HEARTH_FIXTURE, 'local');
const world = path.join(runtime, 'worlds', 'last_hearth');
await embeddedOwner(world, runtime); // Refuse unmanaged content before touching the world.
if (await snapshotBeforePlay(runtime, lease)) {
console.log('LAST_HEARTH_PRELAUNCH_BACKUP_PASS: full snapshot retained outside the game package.');
}
const config = path.join(stage, 'play.conf');
await writeWorld(world, config, location, fixture, 'local');
await playGame(world, runtime, packaged, packageMarker => run(executable, [
'--go', '--world', world, '--gameid', gameId, '--config', config,
'--port', String(randomInt(49152, 65536)), '--logfile', path.join(stage, 'play.log'),
], runtime, PLAY_TIMEOUT, packageMarker, games));
} else if (command === 'test:seeds') {
const seeds = [731104, 19027, 2026, 987654321, 271828, 314159, 42, 8675309];
const packageMarker = await pinPackage(packaged, gameId);
await engineTest('check', runtime, games, executable, location, gameId, packageMarker);
for (const seed of seeds) {
await engineTest('seed', runtime, games, executable, location, gameId, packageMarker,
{seed, fixture: false});
}
console.log(`LAST_HEARTH_SEED_TEST_PASS: ${seeds.length} fixed seeds, required starting resources and safe arrival surfaces.`);
} else {
const packageMarker = await pinPackage(packaged, gameId);
if (command === 'test') {
await engineTest('check', runtime, games, executable, location, gameId, packageMarker);
await engineTest('smoke', runtime, games, executable, location, gameId, packageMarker);
if (process.env.HEARTH_LUA) await standaloneTests(runtime);
console.log('LAST_HEARTH_TEST_PASS: real-engine integration and isolated model/content suites completed.');
} else {
await engineTest(command, runtime, games, executable, location, gameId, packageMarker);
}
}
} finally { await fs.rm(stage, { recursive: true, force: true }); }
});
}
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
main().catch(error => { console.error(`Last Hearth: ${error.message}`); process.exitCode = 1; });
}