mirror of
https://github.com/k1tbyte/Wand-Enhancer.git
synced 2026-09-04 16:23:17 +00:00
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
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
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) {
|
||||
return buildCommandResponse(request, false, {
|
||||
code: "launch_failed",
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.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) {
|
||||
return buildCommandResponse(request, false, {
|
||||
code: "stop_failed",
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.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)
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user