diff --git a/src/library.js b/src/library.js index 514748c..e80a5a4 100644 --- a/src/library.js +++ b/src/library.js @@ -1,14 +1,41 @@ 'use strict' const path = require('path') +const fs = require('fs') const { DIRS, readJson, writeJson } = require('./paths') const { prefixPath } = 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//playtime.txt) so the running game can update it +// every ~30s without rewriting the whole apps.json registry on each tick. +function playtimeFile(id) { + return path.join(DIRS.DATA, 'appdata', id, 'playtime.txt') +} + +function readPlaySeconds(id) { + try { + const n = parseInt(fs.readFileSync(playtimeFile(id), 'utf8').trim(), 10) + if (Number.isFinite(n) && n >= 0) return n + } catch {} + return null // no file yet -> fall back to the registry value +} + +function writePlaySeconds(id, seconds) { + const file = playtimeFile(id) + fs.mkdirSync(path.dirname(file), { recursive: true }) + fs.writeFileSync(file, String(Math.max(0, Math.round(seconds)))) +} + function loadGames() { - return readJson(REGISTRY, []) + const games = readJson(REGISTRY, []) + for (const g of games) { + const ps = readPlaySeconds(g.id) + if (ps != null) g.playSeconds = ps + } + return games } function saveGames(games) { @@ -30,11 +57,18 @@ function updateGame(id, patch) { const games = loadGames() const g = games.find(x => x.id === id) if (!g) return null + // playSeconds goes to its own tiny file; the registry just mirrors it for + // anything reading apps.json directly (loadGames prefers the file anyway). + if (patch.playSeconds !== undefined) writePlaySeconds(id, patch.playSeconds) Object.assign(g, patch) saveGames(games) return g } +function clearPlaytime(id) { + return updateGame(id, { playSeconds: 0, lastPlayed: 0 }) +} + function removeGame(id) { const games = loadGames() const idx = games.findIndex(g => g.id === id) @@ -86,4 +120,4 @@ function resolvePrefixDir(game) { return prefixPath(defs.default || 'default') } -module.exports = { loadGames, saveGames, getGame, addGame, updateGame, removeGame, loadSettings, saveSettings, newGame, resolvePrefixDir } +module.exports = { loadGames, saveGames, getGame, addGame, updateGame, removeGame, loadSettings, saveSettings, newGame, resolvePrefixDir, clearPlaytime, readPlaySeconds, writePlaySeconds, playtimeFile } diff --git a/src/main.js b/src/main.js index 1414e49..b86f69e 100644 --- a/src/main.js +++ b/src/main.js @@ -110,7 +110,7 @@ ipcMain.handle('games:list', () => library.loadGames()) ipcMain.handle('games:remove', (e, id) => library.removeGame(id)) ipcMain.handle('games:get', (e, id) => library.getGame(id)) ipcMain.handle('games:update', (e, id, patch) => library.updateGame(id, patch)) -ipcMain.handle('games:clearPlaytime', (e, id) => library.updateGame(id, { playSeconds: 0, lastPlayed: 0 })) +ipcMain.handle('games:clearPlaytime', (e, id) => library.clearPlaytime(id)) ipcMain.handle('games:resetIcon', (e, id) => { const game = library.getGame(id) if (!game) return { ok: false } diff --git a/src/runner.js b/src/runner.js index b0070ca..651fdcb 100644 --- a/src/runner.js +++ b/src/runner.js @@ -77,15 +77,16 @@ function parseArgs(args) { const GRACE_MS = 60000 -// Live playtime: update stats while the game is still running (not only on exit). -function startPlaytimeTicker({ game, startedAt, onTick }) { +// Live playtime: while the game runs, update ONLY the tiny playtime file +// (a number of seconds) every 30s - apps.json is left untouched to spare the +// disk. lastPlayed is stamped once at start, final totals are mirrored into +// the registry once on exit. +function startPlaytimeTicker({ game, startedAt }) { + const library = require('./library') + const base = game.playSeconds || 0 const timer = setInterval(() => { const counted = Math.max(0, Math.round((Date.now() - startedAt - GRACE_MS) / 1000)) - require('./library').updateGame(game.id, { - lastPlayed: startedAt, - playSeconds: (game.playSeconds || 0) + counted - }) - if (onTick) onTick() + library.writePlaySeconds(game.id, base + counted) }, 30000) return () => clearInterval(timer) } @@ -147,6 +148,8 @@ function runGame({ game, wine, systemWine, onExit }) { child.unref() const startedAt = Date.now() const stopTicker = startPlaytimeTicker({ game, startedAt }) + // stamp "last played" once at start so it is correct even if we crash later + require('./library').updateGame(game.id, { lastPlayed: startedAt }) child.on('exit', (code, signal) => { stopTicker() ws.end()