mirror of
https://github.com/k1tbyte/Wand-Enhancer.git
synced 2026-08-28 22:01:17 +00:00
314 lines
7.8 KiB
JavaScript
Vendored
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)
|
|
)
|
|
}
|
|
}
|