cleanup comments, update translations, bump to 0.4.1

This commit is contained in:
olcxja 2026-10-05 15:42:44 +02:00
commit 7d8ab87eee
28 changed files with 412 additions and 1114 deletions

View file

@ -8,9 +8,8 @@ const { prefixPath, isPrefixRel } = require('./prefixes')
const REGISTRY = path.join(DIRS.DATA, 'apps.json')
const SETTINGS = path.join(DIRS.DATA, 'settings.json')
// Playtime lives in its own tiny file holding ONLY a number of seconds
// (files/data/appdata/<id>/playtime.txt) so the running game can update it
// every ~30s without rewriting the whole apps.json registry on each tick.
// Playtime lives in its own tiny file (files/data/appdata/<id>/playtime.txt),
// updated every ~30s without touching apps.json.
function playtimeFile(id) {
return path.join(DIRS.DATA, 'appdata', id, 'playtime.txt')
}
@ -32,9 +31,7 @@ function writePlaySeconds(id, seconds) {
function loadGames() {
const games = readJson(REGISTRY, [])
for (const g of games) {
// stored relative to FILES (portable) -> resolve to absolute for use.
// Prefix imports resolve against their own prefix instead (never the
// launcher-wide default).
// portable paths resolve against FILES, prefix imports against their prefix
const usePrefix = g.importSource === 'prefix' && g.prefix ? prefixPath(g.prefix) : null
for (const k of ['path', 'exePath', 'workdir']) {
if (!g[k]) continue
@ -47,9 +44,7 @@ function loadGames() {
return games
}
// Paths of anything living inside FILES/ are stored RELATIVE to FILES so the
// whole launcher folder can be moved (portable mode). Absolute paths outside
// FILES (apps left in place) are kept as-is.
// Paths inside FILES/ are stored FILES-relative (portable); others as-is.
function portableGame(g) {
const out = { ...g }
const pfx = out.importSource === 'prefix' && out.prefix ? prefixPath(out.prefix) : null
@ -57,8 +52,7 @@ function portableGame(g) {
const v = out[k]
if (!v) continue
if (pfx && k !== 'iconSource' && !isPrefixRel(v)) {
// prefix game: store relative to the PREFIX ROOT (keeps the drive_c
// segment, e.g. "drive_c/Games/..." or "pfx/drive_c/Games/...")
// prefix game: store relative to the prefix root ("drive_c/...")
const rel = path.relative(pfx, resolvePortablePath(v))
if (rel && !rel.startsWith('..') && !path.isAbsolute(rel)) out[k] = rel
} else if (!(pfx && isPrefixRel(v))) {
@ -76,28 +70,21 @@ function getGame(id) {
return loadGames().find(g => g.id === id) || null
}
// Which prefix a game's runs land in: an app without one follows the launcher
// default. The single place for that fallback (runner.buildEnv and the in-use
// guards both use it, so a rename/delete can never blame the wrong folder).
// Which prefix a game's runs land in (launcher default when unset).
function prefixNameForGame(game) {
if (game && game.prefix) return game.prefix
const defs = readJson(path.join(DIRS.DATA, 'prefixes.json'), { default: 'Main', prefixes: [] })
return defs.default || 'Main'
}
// Apps whose runs land in this prefix - used to keep a prefix that any app
// still references from being deleted out from under it.
// Apps whose runs land in this prefix (in-use guard).
function gamesUsingPrefix(name) {
if (!name) return []
return loadGames().filter(g => prefixNameForGame(g) === name)
}
// In-use guards for registry edits: a prefix folder being renamed or deleted
// (or a wine build deleted) while a game runs in it would leave that process
// without its registry/files - the folder move/removal has to wait until the
// game stops. `running` is the games service's list of live runs
// ([{ name, prefixes, wine }]); prefixes lists every folder a run holds (its
// snapshot plus a new one it was pointed at mid-run).
// In-use guards: rename/delete of a prefix or build waits until no live run
// uses it. `running` is the games service's list of live runs.
function appsUsingPrefix(name, running) {
if (!name) return []
return (running || []).filter(r => (r.prefixes || []).includes(name))
@ -107,15 +94,13 @@ function isPrefixInUse(name, running) {
return appsUsingPrefix(name, running).length > 0
}
// A run with no explicit wine falls back to System Wine, which cannot be
// deleted anyway - only an exact match on a registered build counts.
// Only an exact match on a registered build counts.
function isWineInUse(id, running) {
if (!id || id === 'system') return false
return (running || []).some(r => r.wine === id)
}
// Switching the default prefix would silently move the next launch of every
// app that has no prefix of its own - so it waits while any of them runs.
// Switching the default prefix waits while any default-running app is live.
function gamesFollowingDefault(running) {
return (running || []).filter(r => {
const game = getGame(r.id)
@ -134,16 +119,13 @@ function updateGame(id, patch) {
const games = loadGames()
const g = games.find(x => x.id === id)
if (!g) return null
// prefix imports are pinned: their paths are stored relative to their
// prefix, so NOTHING may move them to another prefix or flip their
// import source (the patch fields are simply ignored)
// prefix imports are pinned; their patch fields are ignored
if (g.importSource === 'prefix' && patch) {
patch = { ...patch }
delete patch.prefix
delete patch.importSource
}
// playSeconds goes to its own tiny file; the registry just mirrors it for
// anything reading apps.json directly (loadGames prefers the file anyway).
// playSeconds lives in its own file; the registry mirrors it
if (patch.playSeconds !== undefined) writePlaySeconds(id, patch.playSeconds)
Object.assign(g, patch)
saveGames(games)
@ -154,9 +136,7 @@ function clearPlaytime(id) {
return updateGame(id, { playSeconds: 0, lastPlayed: 0 })
}
// A portable app's folder is removed along with its registry entry - measured
// first (staging) so the renderer can show a bar while the tree is walked,
// instead of freezing on one rmSync of a multi-GB game.
// Delete a portable app's folder with measured progress.
async function removeGame(id, onProgress) {
const games = loadGames()
const idx = games.findIndex(g => g.id === id)
@ -171,9 +151,7 @@ async function removeGame(id, onProgress) {
const total = await treeSize(g.path)
await removeTree(g.path, p => emit('remove', { files: p.files, bytes: p.bytes, totalFiles: total.files, totalBytes: total.bytes }))
}
// prefix imports live inside the prefix: deleting launcher files would
// nuke the user's prefix (installed DLLs, saves of every other app), so
// only the registry entry is dropped
// prefix imports: drop only the registry entry, never prefix files
games.splice(idx, 1)
saveGames(games)
return true
@ -189,9 +167,7 @@ function saveSettings(patch) {
return s
}
// ID is the current timestamp (ms); while a game with that id or a folder
// with that name exists, keep incrementing - so ids and import folder names
// can never collide.
// Timestamp id, incremented while it collides.
function newGameId() {
let id = Date.now().toString()
const taken = () => getGame(id) || fs.existsSync(path.join(DIRS.APPS, id))
@ -211,8 +187,7 @@ function newGame({ id, name, exePath, workdir, mode }) {
prefix: '',
args: '',
dllOverrides: [],
// Proton-only switch: keep UMU (the default path) or fall back to the
// build's steam.exe launcher for this app (see runner.buildEnv)
// Proton-only: UMU by default, or the build's steam.exe launcher
disableUmu: false,
env: {},
iconSource: '',
@ -222,9 +197,7 @@ function newGame({ id, name, exePath, workdir, mode }) {
}
}
// The registry array IS the library order, so a drag & drop reorder rewrites
// it whole. Ids the caller did not send keep their relative order at the end:
// a renderer that raced an import must never drop an entry.
// Reorder the registry array; unsent ids keep their order at the end.
function reorderGames(ids) {
const games = loadGames()
const rest = new Map(games.map(g => [g.id, g]))
@ -240,10 +213,8 @@ function reorderGames(ids) {
return out
}
// Follows a prefix rename through the app registry. Raw JSON on purpose:
// loadGames()/saveGames() would resolve and re-relativize every path against
// a prefix folder that was just renamed. The stored paths of prefix imports
// are relative to the prefix root, so only the name reference changes.
// Follows a prefix rename through the registry. Raw JSON: the normal
// load/save would re-relativize paths against the renamed folder.
function renamePrefixRefs(oldName, newName) {
const games = readJson(REGISTRY, [])
let updated = 0