mirror of
https://github.com/k1tbyte/Wand-Enhancer.git
synced 2026-08-31 15:01:30 +00:00
f2e88e9247
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.
326 lines
8.4 KiB
JavaScript
Vendored
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("|")
|
|
}
|