mirror of
https://github.com/k1tbyte/Wand-Enhancer.git
synced 2026-08-28 17:01:04 +00:00
13759b1db6
- 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
422 lines
10 KiB
JavaScript
422 lines
10 KiB
JavaScript
import {
|
|
BIND_CHANNEL,
|
|
BOOTSTRAP_LOG_THROTTLE_ATTEMPTS,
|
|
COMMAND_REQUEST_CHANNEL,
|
|
FOLLOW_UP_SYNC_DELAY_MS,
|
|
GLOBAL_FLAG,
|
|
MAX_BOOTSTRAP_ATTEMPTS,
|
|
OPTIONAL_SERVICES_RETRY_INTERVAL_MS,
|
|
RETRY_DELAY_MS,
|
|
SYNC_CHANNEL,
|
|
SYNC_INTERVAL_MS,
|
|
} from "./constants.js"
|
|
import {
|
|
createIdleGameSession,
|
|
createIdleTrainerStatus,
|
|
installGameStatusSubscriptions,
|
|
} from "./game-status.js"
|
|
import {
|
|
buildSnapshot,
|
|
makeInstalledAppsSignature,
|
|
refreshUnavailableTitles,
|
|
resolveInstalledData,
|
|
} from "./installed-data.js"
|
|
import { createLogger } from "./logger.js"
|
|
import { handleRemoteCommandRequest } from "./remote-commands.js"
|
|
import {
|
|
getAppRoot,
|
|
getAureliaContainer,
|
|
getRequire,
|
|
getWebpackRequire,
|
|
hasAppRoot,
|
|
isRecord,
|
|
summarizeAureliaSubtree,
|
|
} from "./runtime.js"
|
|
import {
|
|
getInstalledAppsService,
|
|
getStoreRef,
|
|
hasMissingOptionalServices,
|
|
resolveOptionalServices,
|
|
} from "./services.js"
|
|
|
|
export function installInstalledAppsSync(WandEnhancer) {
|
|
if (globalThis[GLOBAL_FLAG]) {
|
|
return
|
|
}
|
|
|
|
globalThis[GLOBAL_FLAG] = true
|
|
|
|
const state = createState(WandEnhancer)
|
|
state.resolveRemoteCommandServices = () => resolveRemoteCommandServices(state)
|
|
state.queueSync = (force = false) => queueSync(state, force)
|
|
state.queueFollowUpSync = () => queueFollowUpSync(state)
|
|
|
|
state.log(
|
|
"info",
|
|
"Script loaded.",
|
|
`logFile=${globalThis.__wandInstalledAppsSyncLogFile || "console-only"}`
|
|
)
|
|
retryBootstrap(state)
|
|
}
|
|
|
|
function createState(WandEnhancer) {
|
|
return {
|
|
WandEnhancer,
|
|
log: createLogger(WandEnhancer),
|
|
lastSignature: null,
|
|
lastGameStatusSignature: null,
|
|
refreshTimer: null,
|
|
followUpSyncTimer: null,
|
|
pollTimer: null,
|
|
optionalServicesTimer: null,
|
|
bootstrapAttempts: 0,
|
|
bridgeBound: false,
|
|
refreshPatched: false,
|
|
installedAppsService: null,
|
|
gameLifecycleService: null,
|
|
trainerVisibilityService: null,
|
|
unavailableTitlesService: null,
|
|
storeRef: null,
|
|
ipcRenderer: null,
|
|
lastBootstrapReason: null,
|
|
gameLifecycleSubscriptionsInstalled: false,
|
|
trainerVisibilitySubscriptionInstalled: false,
|
|
trainerEndedSubscriptionInstalled: false,
|
|
unavailableTitlesFetchKey: null,
|
|
unavailableTitlesFetchPromise: null,
|
|
unavailableTitlesById: {},
|
|
trainerApiService: null,
|
|
trainerService: null,
|
|
trainerLaunchRequestCtor: null,
|
|
commandListenerInstalled: false,
|
|
missingOptionalServiceWarnings: new Set(),
|
|
currentGameSession: createIdleGameSession(),
|
|
currentRunningTrainer: createIdleTrainerStatus(),
|
|
resolveRemoteCommandServices: null,
|
|
queueSync: null,
|
|
queueFollowUpSync: null,
|
|
}
|
|
}
|
|
|
|
function setBootstrapReason(state, reason) {
|
|
if (
|
|
reason === state.lastBootstrapReason &&
|
|
state.bootstrapAttempts % BOOTSTRAP_LOG_THROTTLE_ATTEMPTS !== 0
|
|
) {
|
|
return
|
|
}
|
|
|
|
state.lastBootstrapReason = reason
|
|
state.log(
|
|
"info",
|
|
`Bootstrap waiting: ${reason}.`,
|
|
`attempt=${state.bootstrapAttempts + 1}/${MAX_BOOTSTRAP_ATTEMPTS}`
|
|
)
|
|
}
|
|
|
|
function bindBridge(state) {
|
|
if (state.bridgeBound || !state.ipcRenderer) {
|
|
return
|
|
}
|
|
|
|
state.bridgeBound = true
|
|
|
|
if (!state.commandListenerInstalled) {
|
|
state.ipcRenderer.on(COMMAND_REQUEST_CHANNEL, (event, request) =>
|
|
handleRemoteCommandRequest(state, event, request)
|
|
)
|
|
state.commandListenerInstalled = true
|
|
state.log("info", "Bridge remote command handler installed.")
|
|
}
|
|
|
|
try {
|
|
void state.ipcRenderer.invoke(BIND_CHANNEL)
|
|
state.log("info", "Bridge set-value handler bind requested.")
|
|
} catch (error) {
|
|
state.log("warn", "Bridge bind failed.", error?.stack || String(error))
|
|
}
|
|
}
|
|
|
|
async function syncInstalledApps(state, force = false) {
|
|
if (!state.ipcRenderer || (!state.installedAppsService && !state.storeRef)) {
|
|
return false
|
|
}
|
|
|
|
const data = resolveInstalledData(state)
|
|
if (!data) {
|
|
if (state.installedAppsService) {
|
|
state.log(
|
|
"warn",
|
|
"Service resolved but installedApps is empty/undefined. Store fallback also unavailable."
|
|
)
|
|
}
|
|
return false
|
|
}
|
|
|
|
if (Object.keys(data.rawInstalledApps).length > 0) {
|
|
await refreshUnavailableTitles(state, data.rawInstalledApps, force)
|
|
}
|
|
|
|
const snapshot = buildSnapshot(state)
|
|
if (!snapshot) {
|
|
state.log("warn", "Snapshot build returned nothing.")
|
|
return false
|
|
}
|
|
|
|
const signature = makeInstalledAppsSignature(snapshot)
|
|
if (!force && signature === state.lastSignature) {
|
|
return false
|
|
}
|
|
|
|
state.lastSignature = signature
|
|
|
|
try {
|
|
await state.ipcRenderer.invoke(SYNC_CHANNEL, snapshot)
|
|
state.log(
|
|
"info",
|
|
"Installed apps snapshot sent.",
|
|
`apps=${snapshot.apps.length}, catalogGames=${snapshot.diagnostics.catalogGames}, rawInstalledApps=${snapshot.diagnostics.rawInstalledApps}`
|
|
)
|
|
return true
|
|
} catch (error) {
|
|
state.log(
|
|
"error",
|
|
"Installed apps snapshot IPC failed.",
|
|
error?.stack || String(error)
|
|
)
|
|
return false
|
|
}
|
|
}
|
|
|
|
function queueSync(state, force = false) {
|
|
if (state.refreshTimer) {
|
|
clearTimeout(state.refreshTimer)
|
|
}
|
|
|
|
state.refreshTimer = setTimeout(() => {
|
|
state.refreshTimer = null
|
|
void syncInstalledApps(state, force)
|
|
}, 0)
|
|
}
|
|
|
|
function queueFollowUpSync(state) {
|
|
if (state.followUpSyncTimer) {
|
|
clearTimeout(state.followUpSyncTimer)
|
|
}
|
|
|
|
state.followUpSyncTimer = setTimeout(() => {
|
|
state.followUpSyncTimer = null
|
|
void syncInstalledApps(state, true)
|
|
}, FOLLOW_UP_SYNC_DELAY_MS)
|
|
}
|
|
|
|
function patchRefreshApps(state) {
|
|
if (
|
|
!state.installedAppsService ||
|
|
state.refreshPatched ||
|
|
typeof state.installedAppsService.refreshApps !== "function"
|
|
) {
|
|
return
|
|
}
|
|
|
|
const originalRefreshApps = state.installedAppsService.refreshApps.bind(
|
|
state.installedAppsService
|
|
)
|
|
state.refreshPatched = true
|
|
state.log("info", "refreshApps hook installed.")
|
|
|
|
state.installedAppsService.refreshApps = async (...args) => {
|
|
const result = await originalRefreshApps(...args)
|
|
state.log("info", "refreshApps completed; queueing installed apps sync.")
|
|
queueSync(state, true)
|
|
queueFollowUpSync(state)
|
|
return result
|
|
}
|
|
}
|
|
|
|
function resolveRemoteCommandServices(state) {
|
|
const container = getAureliaContainer()
|
|
const webpackRequire = getWebpackRequire()
|
|
if (!container || !webpackRequire) {
|
|
return false
|
|
}
|
|
|
|
resolveRuntimeServices(state, container, webpackRequire)
|
|
return true
|
|
}
|
|
|
|
function resolveRuntimeServices(state, container, webpackRequire) {
|
|
resolveOptionalServices(state, container, webpackRequire)
|
|
installGameStatusSubscriptions(state)
|
|
|
|
if (!hasMissingOptionalServices(state)) {
|
|
stopOptionalServicesRetry(state)
|
|
}
|
|
}
|
|
|
|
function stopOptionalServicesRetry(state) {
|
|
if (!state.optionalServicesTimer) {
|
|
return
|
|
}
|
|
|
|
clearInterval(state.optionalServicesTimer)
|
|
state.optionalServicesTimer = null
|
|
}
|
|
|
|
function startOptionalServicesRetry(state) {
|
|
if (state.optionalServicesTimer || !hasMissingOptionalServices(state)) {
|
|
return
|
|
}
|
|
|
|
state.optionalServicesTimer = setInterval(() => {
|
|
const container = getAureliaContainer()
|
|
const webpackRequire = getWebpackRequire()
|
|
if (container && webpackRequire) {
|
|
resolveRuntimeServices(state, container, webpackRequire)
|
|
}
|
|
}, OPTIONAL_SERVICES_RETRY_INTERVAL_MS)
|
|
|
|
state.log(
|
|
"info",
|
|
"Optional service retry timer started.",
|
|
`${OPTIONAL_SERVICES_RETRY_INTERVAL_MS}ms`
|
|
)
|
|
}
|
|
|
|
function bootstrap(state) {
|
|
if (!hasAppRoot()) {
|
|
setBootstrapReason(state, "app root not ready")
|
|
return false
|
|
}
|
|
|
|
if (!state.ipcRenderer && !resolveIpcRenderer(state)) {
|
|
return false
|
|
}
|
|
|
|
const webpackRequire = getWebpackRequire()
|
|
if (!webpackRequire) {
|
|
setBootstrapReason(state, "webpack runtime not ready")
|
|
return false
|
|
}
|
|
|
|
const container = getAureliaContainer()
|
|
if (!container) {
|
|
logMissingContainer(state)
|
|
setBootstrapReason(state, "Aurelia container not ready")
|
|
return false
|
|
}
|
|
|
|
state.log("info", "Aurelia container resolved.")
|
|
|
|
if (!state.storeRef) {
|
|
state.storeRef = getStoreRef(state, container, webpackRequire)
|
|
if (!state.storeRef) {
|
|
state.log(
|
|
"warn",
|
|
"Store reference unavailable; unsupported installed titles will be missing."
|
|
)
|
|
}
|
|
}
|
|
|
|
state.installedAppsService = getInstalledAppsService(
|
|
state,
|
|
container,
|
|
webpackRequire
|
|
)
|
|
if (!state.installedAppsService) {
|
|
setBootstrapReason(state, "installed apps service not ready")
|
|
return false
|
|
}
|
|
|
|
resolveRuntimeServices(state, container, webpackRequire)
|
|
startOptionalServicesRetry(state)
|
|
|
|
if (!isRecord(state.installedAppsService.installedApps)) {
|
|
state.log(
|
|
"info",
|
|
"Service instance has no installedApps data yet; reading from store until refreshApps populates it."
|
|
)
|
|
if (!state.storeRef) {
|
|
state.log("warn", "Store fallback also unavailable; will retry on poll.")
|
|
}
|
|
}
|
|
|
|
bindBridge(state)
|
|
patchRefreshApps(state)
|
|
queueSync(state, true)
|
|
queueFollowUpSync(state)
|
|
startPollTimer(state)
|
|
|
|
state.log("info", "Installed apps sync ready.")
|
|
return true
|
|
}
|
|
|
|
function resolveIpcRenderer(state) {
|
|
const electron = getRequire()?.("electron")
|
|
if (!electron?.ipcRenderer) {
|
|
setBootstrapReason(state, "electron ipcRenderer not ready")
|
|
return false
|
|
}
|
|
|
|
state.ipcRenderer = electron.ipcRenderer
|
|
state.log("info", "Electron ipcRenderer resolved.")
|
|
return true
|
|
}
|
|
|
|
function startPollTimer(state) {
|
|
if (state.pollTimer) {
|
|
return
|
|
}
|
|
|
|
state.pollTimer = setInterval(() => {
|
|
const container = getAureliaContainer()
|
|
const webpackRequire = getWebpackRequire()
|
|
if (container && webpackRequire) {
|
|
resolveRuntimeServices(state, container, webpackRequire)
|
|
}
|
|
void syncInstalledApps(state)
|
|
}, SYNC_INTERVAL_MS)
|
|
|
|
state.log(
|
|
"info",
|
|
"Installed apps poll timer started.",
|
|
`${SYNC_INTERVAL_MS}ms`
|
|
)
|
|
}
|
|
|
|
function logMissingContainer(state) {
|
|
if (state.bootstrapAttempts % 10 !== 0) {
|
|
return
|
|
}
|
|
|
|
const root = getAppRoot()
|
|
const aureliaKeys = root
|
|
? Object.getOwnPropertyNames(root)
|
|
.filter((key) => key.startsWith("__") || key === "au")
|
|
.join(", ")
|
|
: "root=null"
|
|
state.log(
|
|
"warn",
|
|
"Aurelia container not found.",
|
|
`rootProps=${aureliaKeys || "(none)"}, subtree=${summarizeAureliaSubtree(root)}`
|
|
)
|
|
}
|
|
|
|
function retryBootstrap(state) {
|
|
if (bootstrap(state)) {
|
|
return
|
|
}
|
|
|
|
state.bootstrapAttempts += 1
|
|
if (state.bootstrapAttempts < MAX_BOOTSTRAP_ATTEMPTS) {
|
|
setTimeout(() => retryBootstrap(state), RETRY_DELAY_MS)
|
|
return
|
|
}
|
|
|
|
state.log(
|
|
"error",
|
|
"Installed apps sync bootstrap exhausted.",
|
|
state.lastBootstrapReason || "unknown reason"
|
|
)
|
|
}
|