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

@ -17,15 +17,9 @@ function sendToWindow(win, channel, data) {
if (win && !win.isDestroyed()) win.webContents.send(channel, data)
}
// Game-process state: id -> { pid (null while the process is starting), the
// prefix name and wine id the run was resolved to }. Both are frozen here:
// the default prefix/wine can be switched and the app's own config edited
// while it plays, and the in-use guards have to name what the process really
// locked - not what the registry says now. Editing an app's own prefix while
// it runs does not move the process, so the new folder is blocked next to the
// snapshot (a pre-edit prefix does not exist in it yet).
// onGamesChanged fires in the MAIN process whenever the running-games set
// changes (the tray menu rebuilds itself from it).
// Game-process state: id -> { pid, resolved prefix name and wine id }, frozen
// at launch (the in-use guards must name what the process really locked).
// onGamesChanged fires in the main process when the running set changes.
const STARTUP_GRACE_MS = 60000
function createGamesService({ getWindow, onGamesChanged }) {
@ -47,11 +41,7 @@ function createGamesService({ getWindow, onGamesChanged }) {
}
}
// A game is "running" only while its process really is. An entry left behind
// by a launch that never got a pid (the binary could not be executed, so the
// spawn failed) or whose pid is gone (the game crashed, the launcher was
// restarted) would otherwise answer every later Play with "already
// running".
// a stale entry (no pid yet / dead pid) must not answer "already running"
function prune() {
for (const [id, entry] of [...running]) {
if (alive(entry.pid)) continue
@ -70,11 +60,7 @@ function createGamesService({ getWindow, onGamesChanged }) {
prune()
return [...running.keys()]
}
// What the still-running games actually use, for the "is this prefix/wine in
// use?" guards: [{ id, name, prefixes: [snapshot, ...], wine }]. The
// snapshot alone goes stale when a running app is pointed at another prefix
// (the process keeps its folder open); the live name alone misses the folder
// it currently holds. Together they name every folder the run touches.
// snapshot + live name together name every folder a run touches
const runningInfo = () => {
prune()
const out = []
@ -87,9 +73,7 @@ function createGamesService({ getWindow, onGamesChanged }) {
return out
}
// "Open with PLWL" handoff: queue the file for the renderer (picker popup
// with wine version + prefix). Safe to call at any time - if the window is
// not up yet, the renderer picks the file up right after boot.
// "Open with" handoff: queue the file for the renderer.
function openExternally(openPath) {
require('../openwith').setPending(openPath)
sendToWindow(getWindow(), 'openwith:file', openPath)
@ -118,10 +102,7 @@ function createGamesService({ getWindow, onGamesChanged }) {
return { ok: false, error: 'wine-not-found' }
}
if (running.has(id)) {
// Opening a document in an app that is already running is a HANDOFF,
// exactly like Windows: a second process receives the file and the app's
// own single-instance logic takes it over. It is not a tracked run -
// playtime, the tray and the kill switch belong to the first process.
// a document into a running app is a handoff, not a tracked run
if (!openFile) return { ok: false, error: 'already-running' }
try {
await runGame({ game, wine, systemWine: wine.type === 'system', openFile, handoff: true })
@ -134,8 +115,7 @@ function createGamesService({ getWindow, onGamesChanged }) {
startedAt.set(id, Date.now())
try {
const res = await runGame({ game, wine, systemWine: wine.type === 'system', onExit: () => handleExit(id), openFile })
// stop was requested while the process was still starting: kill it and
// leave nothing in the running set
// stop requested while still starting: kill and clear
if (killRequests.has(id)) {
killRequests.delete(id)
if (res.pid) killProcessGroup(res.pid)
@ -145,8 +125,7 @@ function createGamesService({ getWindow, onGamesChanged }) {
notifyChanged()
return { ok: true, pid: null, logFile: res.logFile || null }
}
// no pid means the process was not started after all - never keep such
// an entry, it would block the next launch with "already running"
// no pid: never keep the entry, it would block the next launch
if (!res.pid) {
running.delete(id)
startedAt.delete(id)
@ -164,8 +143,7 @@ function createGamesService({ getWindow, onGamesChanged }) {
}
}
// Stop always clears the state: whether the process was running, still
// starting (kill request for as soon as it has a pid) or already gone.
// Stop always clears the state.
async function kill(id) {
const entry = running.get(id)
if (entry) killProcessGroup(entry.pid)
@ -194,8 +172,7 @@ function createGamesService({ getWindow, onGamesChanged }) {
}
}
// subscribe to game exits (used by the tray/headless mode to e.g. quit when
// the game that auto-started the launcher has finished)
// subscribe to game exits (tray/headless auto-quit)
function setOnGameExit(fn) {
onGameExit = typeof fn === 'function' ? fn : null
}
@ -204,8 +181,7 @@ function createGamesService({ getWindow, onGamesChanged }) {
return importGame(srcDir, mode, data => sendToWindow(win, 'import:progress', data))
}
// prefix import: nothing is copied, the game just gets registered with
// paths relative to the prefix (wine sees it as a normal C:\ install)
// prefix import: register in place, nothing copied
async function importFromPrefix(win, prefix, srcDir) {
return importGameFromPrefix(prefix, srcDir)
}