Files
Wand-Enhancer/web-panel/bridge/scripts/default/installed-apps-sync/game-status.js
T
kitbyte 13759b1db6 feat: ship 1.0.8.0 release automation and runtime overhaul
- reduce ASAR IO overhead with streamed archive reads, buffered copies, faster relative-path handling and placeholder integrity records
- fix in-place app.asar.unpacked packing/extraction self-copy cases that caused locked-file failures
- tighten JS patch discovery with candidate bundle filters and search hints
- require prebuilt remote-panel dist artifacts and clean up embedded bridge/script packaging
- add unified build entrypoints for PowerShell, cmd and bash and move native CMake output under .tmp
- add release metadata validation, changelog section extraction, pre-commit hook and GitHub Actions validation/release pipelines
- make CHANGELOG the source of truth for release notes and document the tag-driven release flow
- add updater release notes UI with latest/full changelog loading and localize the new update strings
- modularize bridge renderer scripts, add installed apps and game status sync, and support remote launch/stop commands
- centralize bridge protocol, IPC and WebSocket constants and improve LAN IP selection for QR pairing
- refactor remote panel controls/state enums, persist accent color, polish library/session UI and refresh assets
2026-05-06 13:07:09 +03:00

339 lines
8.7 KiB
JavaScript

import {
GAME_ENDED_EVENT,
GAME_LAUNCHED_EVENT,
GAME_STATUS_CHANNEL,
SYNTHETIC_SESSION_IDLE_EVENT,
SYNTHETIC_SESSION_RUNNING_EVENT,
TRAINER_ENDED_EVENT,
TRAINER_SNAPSHOT_CHANNEL,
} from "./constants.js"
import { isRecord, safeString, toStringId } from "./runtime.js"
export function createIdleGameSession() {
return {
state: "idle",
event: "snapshot",
processId: null,
gameId: null,
titleId: null,
titleName: null,
sessionDurationSeconds: null,
startedAt: null,
endedAt: null,
}
}
export function createIdleTrainerStatus() {
return {
state: "idle",
event: "snapshot",
trainerId: null,
displayName: null,
gameId: null,
titleId: null,
}
}
export function installGameStatusSubscriptions(state) {
let installed = false
if (
state.gameLifecycleService &&
!state.gameLifecycleSubscriptionsInstalled
) {
installed = installLifecycleSubscriptions(state) || installed
}
if (
state.trainerVisibilityService &&
!state.trainerVisibilitySubscriptionInstalled
) {
state.currentRunningTrainer = normalizeRunningTrainerStatus(
state.trainerVisibilityService.runningTrainer,
"snapshot"
)
syncGameSessionFromTrainerStatus(state, state.currentRunningTrainer)
installed = installTrainerVisibilitySubscription(state) || installed
state.trainerVisibilitySubscriptionInstalled = true
}
if (
state.trainerService &&
!state.trainerEndedSubscriptionInstalled &&
typeof state.trainerService.onTrainerEnded === "function"
) {
state.trainerService.onTrainerEnded(() => {
clearTrainerSnapshot(state, TRAINER_ENDED_EVENT, true)
})
state.trainerEndedSubscriptionInstalled = true
installed = true
}
if (!installed) {
return
}
state.log(
"info",
"Game status hooks installed.",
`lifecycle=${state.gameLifecycleSubscriptionsInstalled ? "yes" : "no"}, trainer=${state.trainerVisibilitySubscriptionInstalled ? "yes" : "no"}, trainerEnded=${state.trainerEndedSubscriptionInstalled ? "yes" : "no"}`
)
void syncGameStatus(state, true)
}
export function clearTrainerSnapshot(state, reason, clearSession = false) {
state.currentRunningTrainer = {
...createIdleTrainerStatus(),
event: reason,
}
if (clearSession) {
clearGameSession(state, reason)
} else if (
state.currentGameSession.state === "running" &&
isSyntheticGameSessionEvent(state.currentGameSession.event)
) {
syncGameSessionFromTrainerStatus(state, state.currentRunningTrainer)
}
void syncGameStatus(state, true)
if (!state.ipcRenderer) {
return
}
try {
void state.ipcRenderer.invoke(TRAINER_SNAPSHOT_CHANNEL, null)
} catch (error) {
state.log(
"warn",
"Trainer snapshot clear IPC failed.",
error?.stack || String(error)
)
}
}
export async function syncGameStatus(state, force = false) {
if (!state.ipcRenderer) {
return false
}
const snapshot = buildGameStatusSnapshot(state)
const signature = makeGameStatusSignature(snapshot)
if (!force && signature === state.lastGameStatusSignature) {
return false
}
state.lastGameStatusSignature = signature
try {
await state.ipcRenderer.invoke(GAME_STATUS_CHANNEL, snapshot)
state.log(
"info",
"Game status snapshot sent.",
`session=${snapshot.session.state}/${snapshot.session.event}, trainer=${snapshot.trainer.state}/${snapshot.trainer.event}`
)
return true
} catch (error) {
state.log(
"error",
"Game status snapshot IPC failed.",
error?.stack || String(error)
)
return false
}
}
function installLifecycleSubscriptions(state) {
let installed = false
if (typeof state.gameLifecycleService.onGameLaunched === "function") {
state.gameLifecycleService.onGameLaunched((event) => {
state.currentGameSession = {
state: "running",
event: GAME_LAUNCHED_EVENT,
processId:
typeof event?.processId === "number" ? event.processId : null,
gameId: toStringId(event?.gameId),
titleId: toStringId(event?.titleId),
titleName: safeString(event?.titleName),
sessionDurationSeconds: null,
startedAt: new Date().toISOString(),
endedAt: null,
}
void syncGameStatus(state, true)
})
installed = true
}
if (typeof state.gameLifecycleService.onGameEnded === "function") {
state.gameLifecycleService.onGameEnded((event) => {
clearGameSession(
state,
GAME_ENDED_EVENT,
typeof event?.sessionDurationSeconds === "number"
? event.sessionDurationSeconds
: state.currentGameSession.sessionDurationSeconds
)
void syncGameStatus(state, true)
})
installed = true
}
if (installed) {
state.gameLifecycleSubscriptionsInstalled = true
}
return installed
}
function installTrainerVisibilitySubscription(state) {
if (
typeof state.trainerVisibilityService.onRunningTrainerChanged !== "function"
) {
return false
}
state.trainerVisibilityService.onRunningTrainerChanged((runningTrainer) => {
state.currentRunningTrainer = normalizeRunningTrainerStatus(
runningTrainer,
runningTrainer ? "trainer-running" : "trainer-idle"
)
syncGameSessionFromTrainerStatus(state, state.currentRunningTrainer)
void syncGameStatus(state, true)
})
return true
}
function normalizeRunningTrainerStatus(runningTrainer, event = "snapshot") {
const info = isRecord(runningTrainer?.info)
? runningTrainer.info
: isRecord(runningTrainer)
? runningTrainer
: null
if (!info) {
return {
...createIdleTrainerStatus(),
event,
}
}
return {
state: "running",
event,
trainerId: toStringId(info.trainerId) || toStringId(info.id),
displayName: safeString(
info.displayName,
info.gameName,
info.titleName,
info.title,
info.name
),
gameId: toStringId(info.gameId),
titleId: toStringId(info.titleId),
}
}
function syncGameSessionFromTrainerStatus(state, trainerStatus) {
if (trainerStatus?.state === "running") {
if (
state.currentGameSession.state === "running" &&
!isSyntheticGameSessionEvent(state.currentGameSession.event)
) {
return false
}
state.currentGameSession = {
state: "running",
event: SYNTHETIC_SESSION_RUNNING_EVENT,
processId: state.currentGameSession.processId,
gameId: trainerStatus.gameId ?? state.currentGameSession.gameId,
titleId: trainerStatus.titleId ?? state.currentGameSession.titleId,
titleName:
trainerStatus.displayName ?? state.currentGameSession.titleName,
sessionDurationSeconds: null,
startedAt:
state.currentGameSession.state === "running" &&
isSyntheticGameSessionEvent(state.currentGameSession.event)
? state.currentGameSession.startedAt
: new Date().toISOString(),
endedAt: null,
}
return true
}
if (
state.currentGameSession.state !== "running" ||
!isSyntheticGameSessionEvent(state.currentGameSession.event)
) {
return false
}
const startedAt = state.currentGameSession.startedAt
const sessionDurationSeconds = startedAt
? Math.max(
0,
Math.round((Date.now() - new Date(startedAt).getTime()) / 1000)
)
: null
clearGameSession(state, SYNTHETIC_SESSION_IDLE_EVENT, sessionDurationSeconds)
return true
}
function clearGameSession(
state,
event,
sessionDurationSeconds = state.currentGameSession.sessionDurationSeconds
) {
state.currentGameSession = {
state: "idle",
event,
processId: null,
gameId: null,
titleId: null,
titleName: null,
sessionDurationSeconds,
startedAt: state.currentGameSession.startedAt,
endedAt: new Date().toISOString(),
}
}
function isSyntheticGameSessionEvent(event) {
return (
event === SYNTHETIC_SESSION_RUNNING_EVENT ||
event === SYNTHETIC_SESSION_IDLE_EVENT
)
}
function buildGameStatusSnapshot(state) {
return {
instanceId: "wand-game-status",
updatedAt: new Date().toISOString(),
session: { ...state.currentGameSession },
trainer: { ...state.currentRunningTrainer },
}
}
function makeGameStatusSignature(snapshot) {
return [
snapshot.session.state,
snapshot.session.event,
snapshot.session.processId ?? "",
snapshot.session.gameId ?? "",
snapshot.session.titleId ?? "",
snapshot.session.titleName ?? "",
snapshot.session.sessionDurationSeconds ?? "",
snapshot.session.startedAt ?? "",
snapshot.session.endedAt ?? "",
snapshot.trainer.state,
snapshot.trainer.event,
snapshot.trainer.trainerId ?? "",
snapshot.trainer.displayName ?? "",
snapshot.trainer.gameId ?? "",
snapshot.trainer.titleId ?? "",
].join("|")
}