Files
Wand-Enhancer-k1tbyte/web-panel/bridge/scripts/default/installed-apps-sync/game-status.js
T
kitbyte f2e88e9247 fix(renderer-scripts): await the bridge bind and retry it on poll
ipcRenderer.invoke rejects asynchronously, so marking the bridge bound before
awaiting left set-value dead for the session while the log reported success.

Compare installed-app snapshots structurally instead of by an explicit field
list, which had already drifted from the bridge copy and hid location changes.
2026-08-29 16:56:51 +03:00

326 lines
8.4 KiB
JavaScript
Vendored

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 { invokeIpc, 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)
void invokeIpc(state, TRAINER_SNAPSHOT_CHANNEL, null, "Trainer snapshot clear")
}
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
const sent = await invokeIpc(
state,
GAME_STATUS_CHANNEL,
snapshot,
"Game status snapshot",
"error"
)
if (sent) {
state.log(
"info",
"Game status snapshot sent.",
`session=${snapshot.session.state}/${snapshot.session.event}, trainer=${snapshot.trainer.state}/${snapshot.trainer.event}`
)
}
return sent
}
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("|")
}