Files
Wand-Enhancer-k1tbyte/web-panel/bridge/scripts/default/installed-apps-sync/remote-commands.js
T
2026-07-22 00:03:38 +03:00

314 lines
7.8 KiB
JavaScript
Vendored

import {
COMMAND_RESPONSE_CHANNEL,
REMOTE_COMMAND_LAUNCH,
REMOTE_COMMAND_STOP,
REMOTE_COMMAND_TRIGGER,
REMOTE_STOP_EVENT,
} from "./constants.js"
import { clearTrainerSnapshot, syncGameStatus } from "./game-status.js"
import {
compareInstalledAppRecords,
getInstalledVersionsForGame,
resolveInstalledData,
toInstalledAppRecord,
} from "./installed-data.js"
import {
getPreferredLocale,
isRecord,
safeString,
toStringId,
} from "./runtime.js"
export function handleRemoteCommandRequest(state, _event, request) {
void (async () => {
let response
if (request?.action === REMOTE_COMMAND_LAUNCH) {
response = await executeRemoteLaunchCommand(state, request)
} else if (request?.action === REMOTE_COMMAND_STOP) {
response = await executeRemoteStopCommand(state, request)
} else {
response = buildCommandResponse(request, false, {
code: "invalid_command",
message: "Unknown remote command.",
})
}
await sendRemoteCommandResponse(state, response)
})()
}
function buildCommandResponse(request, ok, error = null) {
const response = {
requestId: safeString(request?.requestId),
ok,
action:
request?.action === REMOTE_COMMAND_STOP
? REMOTE_COMMAND_STOP
: REMOTE_COMMAND_LAUNCH,
gameId: toStringId(request?.gameId),
titleId: toStringId(request?.titleId),
}
if (!error) {
return response
}
return {
...response,
error,
}
}
async function executeRemoteLaunchCommand(state, request) {
const gameId = toStringId(request?.gameId)
if (!gameId) {
return buildCommandResponse(request, false, {
code: "invalid_game",
message: "A game id is required to launch a trainer.",
})
}
if (!state.resolveRemoteCommandServices()) {
return buildCommandResponse(request, false, {
code: "bridge_not_ready",
message: "The Wand renderer container is not ready yet.",
})
}
if (!state.trainerService) {
return buildCommandResponse(request, false, {
code: "trainer_service_missing",
message: "The Wand trainer service is not available yet.",
})
}
if (!state.trainerLaunchRequestCtor) {
return buildCommandResponse(request, false, {
code: "trainer_launch_missing",
message:
"The Wand trainer launch request constructor is not available yet.",
})
}
const data = resolveInstalledData(state)
if (!data) {
return buildCommandResponse(request, false, {
code: "installations_missing",
message: "Installed game data is not available yet.",
})
}
const launchInfo = getLaunchInfoForGame(gameId, data)
if (!isRecord(launchInfo.app)) {
return buildCommandResponse(request, false, {
code: "game_not_installed",
message: "Wand could not resolve a preferred installation for this game.",
})
}
const trainerInfo = await resolveTrainerInfoForGame(state, gameId, data)
if (!trainerInfo) {
return buildCommandResponse(request, false, {
code: "trainer_not_found",
message: "Wand could not find a compatible trainer for this game.",
})
}
try {
const launchRequest = new state.trainerLaunchRequestCtor(
trainerInfo,
launchInfo.app,
launchInfo.version,
REMOTE_COMMAND_TRIGGER
)
await state.trainerService.launch(launchRequest)
state.queueSync(true)
state.queueFollowUpSync()
void syncGameStatus(state, true)
return buildCommandResponse(request, true)
} catch (error) {
state.log(
"warn",
"Remote trainer launch failed.",
error?.stack || String(error)
)
return buildCommandResponse(request, false, {
code: "launch_failed",
message: "Failed to launch the trainer.",
})
}
}
async function executeRemoteStopCommand(state, request) {
if (!state.resolveRemoteCommandServices()) {
return buildCommandResponse(request, false, {
code: "bridge_not_ready",
message: "The Wand renderer container is not ready yet.",
})
}
if (
!state.trainerService ||
typeof state.trainerService.endTrainer !== "function"
) {
return buildCommandResponse(request, false, {
code: "trainer_service_missing",
message: "The Wand trainer service is not available yet.",
})
}
if (
!state.trainerService.trainer &&
state.currentRunningTrainer.state !== "running"
) {
return buildCommandResponse(request, false, {
code: "no_active_trainer",
message: "No trainer is running right now.",
})
}
try {
await state.trainerService.endTrainer()
clearTrainerSnapshot(state, REMOTE_STOP_EVENT, true)
return buildCommandResponse(request, true)
} catch (error) {
state.log(
"warn",
"Remote trainer stop failed.",
error?.stack || String(error)
)
return buildCommandResponse(request, false, {
code: "stop_failed",
message: "Failed to stop the running trainer.",
})
}
}
function getLaunchInfoForGame(gameId, data) {
const versions = Array.isArray(data?.installedGameVersions?.[gameId])
? data.installedGameVersions[gameId]
: []
const game = isRecord(data?.catalog?.games?.[gameId])
? data.catalog.games[gameId]
: null
const candidates = []
if (Array.isArray(game?.correlationIds)) {
for (const correlationId of game.correlationIds) {
if (typeof correlationId === "string" && correlationId.trim()) {
candidates.push({ correlationId: correlationId.trim(), version: null })
}
}
}
for (const versionEntry of versions) {
if (
typeof versionEntry?.correlationId === "string" &&
versionEntry.correlationId.trim()
) {
candidates.push({
correlationId: versionEntry.correlationId.trim(),
version: versionEntry.version ?? null,
})
}
}
const rankedCandidates = Array.from(
new Map(
candidates.map((candidate) => [candidate.correlationId, candidate])
).values()
)
.map((candidate) => normalizeLaunchCandidate(candidate, data))
.filter(Boolean)
.sort((left, right) =>
compareInstalledAppRecords(left.normalizedApp, right.normalizedApp)
)
if (!rankedCandidates[0]) {
return { app: null, version: null }
}
return {
app: rankedCandidates[0].app,
version: rankedCandidates[0].version,
}
}
async function resolveTrainerInfoForGame(state, gameId, data) {
if (!state.trainerApiService) {
return null
}
try {
const localTrainer = unwrapTrainerInfo(
await state.trainerApiService.getLatestLocalTrainerForGame(gameId)
)
if (localTrainer) {
return localTrainer
}
} catch (error) {
state.log(
"warn",
"Local trainer lookup failed.",
error?.stack || String(error)
)
}
try {
return unwrapTrainerInfo(
await state.trainerApiService.getMostCompatibleTrainerForGame(
gameId,
getPreferredLocale(),
getInstalledVersionsForGame(gameId, data),
false
)
)
} catch (error) {
state.log(
"warn",
"Compatible trainer lookup failed.",
error?.stack || String(error)
)
return null
}
}
function normalizeLaunchCandidate(candidate, data) {
const app = data?.rawInstalledApps?.[candidate.correlationId]
const normalizedApp = toInstalledAppRecord(candidate.correlationId, app)
if (!normalizedApp || !isRecord(app)) {
return null
}
return {
app,
version: candidate.version,
normalizedApp,
}
}
function unwrapTrainerInfo(value) {
if (isRecord(value?.trainer)) {
return value.trainer
}
return isRecord(value) ? value : null
}
async function sendRemoteCommandResponse(state, response) {
if (!state.ipcRenderer) {
return
}
try {
await state.ipcRenderer.invoke(COMMAND_RESPONSE_CHANNEL, response)
} catch (error) {
state.log(
"warn",
"Remote command response IPC failed.",
error?.stack || String(error)
)
}
}