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
166 lines
5.7 KiB
JavaScript
166 lines
5.7 KiB
JavaScript
const crypto = require('node:crypto');
|
|
const path = require('node:path');
|
|
|
|
const {
|
|
IPC_CHANNEL,
|
|
REMOTE_COMMAND_REQUEST_CHANNEL,
|
|
REMOTE_COMMAND_RESPONSE_CHANNEL,
|
|
REMOTE_COMMAND_RESPONSE_TIMEOUT_MS,
|
|
REMOTE_GAME_STATUS_CHANNEL,
|
|
REMOTE_INSTALLED_APPS_CHANNEL,
|
|
} = require('./constants.cjs');
|
|
const { writeInstallLog } = require('./logger.cjs');
|
|
const { ensureBridge } = require('./runtime.cjs');
|
|
const { installRendererScripts } = require('./renderer-scripts.cjs');
|
|
const { safeString } = require('./utils.cjs');
|
|
|
|
function installWandRuntime(electron, options = {}) {
|
|
const runtime = ensureBridge(options);
|
|
if (!electron || !electron.ipcMain || !electron.app) {
|
|
throw new Error('Electron main-process API is required to install Wand runtime hooks.');
|
|
}
|
|
|
|
const boundRenderers = globalThis.__wandRemoteBridgeBoundRenderers || new Set();
|
|
const pendingCommandResponses = globalThis.__wandRemoteBridgePendingCommandResponses || new Map();
|
|
globalThis.__wandRemoteBridgeBoundRenderers = boundRenderers;
|
|
globalThis.__wandRemoteBridgePendingCommandResponses = pendingCommandResponses;
|
|
|
|
runtime.setHandler((request) => {
|
|
let delivered = false;
|
|
for (const sender of Array.from(boundRenderers)) {
|
|
try {
|
|
if (!sender || sender.isDestroyed()) {
|
|
boundRenderers.delete(sender);
|
|
continue;
|
|
}
|
|
|
|
sender.send(IPC_CHANNEL.SET_VALUE, request);
|
|
delivered = true;
|
|
} catch (error) {
|
|
boundRenderers.delete(sender);
|
|
writeInstallLog('warn', 'Failed to forward set_value to renderer.', error);
|
|
}
|
|
}
|
|
|
|
return delivered;
|
|
});
|
|
|
|
runtime.setCommandHandler(async (request) => {
|
|
for (const sender of Array.from(boundRenderers)) {
|
|
try {
|
|
if (!sender || sender.isDestroyed()) {
|
|
boundRenderers.delete(sender);
|
|
continue;
|
|
}
|
|
|
|
return await dispatchRemoteCommandToRenderer(sender, request, pendingCommandResponses);
|
|
} catch (error) {
|
|
writeInstallLog('warn', 'Failed to execute remote command in renderer.', error);
|
|
}
|
|
}
|
|
|
|
return buildRendererBridgeMissingResponse(request);
|
|
});
|
|
|
|
installIpcHandlers(electron, runtime, boundRenderers, pendingCommandResponses);
|
|
installRendererScripts(electron, runtime, {
|
|
...options,
|
|
panelRoot: options.panelRoot || path.dirname(__dirname),
|
|
});
|
|
writeInstallLog('info', 'Wand runtime hooks installed.');
|
|
return runtime;
|
|
}
|
|
|
|
function installIpcHandlers(electron, runtime, boundRenderers, pendingCommandResponses) {
|
|
if (globalThis.__wandRemoteBridgeIpcInstalled) {
|
|
return;
|
|
}
|
|
|
|
globalThis.__wandRemoteBridgeIpcInstalled = true;
|
|
electron.ipcMain.handle(IPC_CHANNEL.TRAINER_SNAPSHOT, (_event, snapshot) => {
|
|
runtime.sync(snapshot);
|
|
return true;
|
|
});
|
|
electron.ipcMain.handle(REMOTE_INSTALLED_APPS_CHANNEL, (_event, snapshot) => {
|
|
runtime.syncInstalledApps(snapshot);
|
|
return true;
|
|
});
|
|
electron.ipcMain.handle(REMOTE_GAME_STATUS_CHANNEL, (_event, snapshot) => {
|
|
runtime.syncGameStatus(snapshot);
|
|
return true;
|
|
});
|
|
electron.ipcMain.handle(REMOTE_COMMAND_RESPONSE_CHANNEL, (_event, response) => {
|
|
const requestId = safeString(response?.requestId);
|
|
const pending = requestId ? pendingCommandResponses.get(requestId) : null;
|
|
if (!pending) {
|
|
return false;
|
|
}
|
|
|
|
pending.resolve(response);
|
|
return true;
|
|
});
|
|
electron.ipcMain.handle(IPC_CHANNEL.VALUE_CHANGED, (_event, change) => {
|
|
runtime.valueChanged(change);
|
|
return true;
|
|
});
|
|
electron.ipcMain.handle(IPC_CHANNEL.BIND_HANDLER, (event) => {
|
|
if (event && event.sender) {
|
|
boundRenderers.add(event.sender);
|
|
}
|
|
|
|
return true;
|
|
});
|
|
electron.ipcMain.handle(IPC_CHANNEL.REMOTE_URL, () => runtime.remoteUrl);
|
|
}
|
|
|
|
function dispatchRemoteCommandToRenderer(sender, request, pendingCommandResponses) {
|
|
return new Promise((resolve, reject) => {
|
|
const requestId = `remote_command_${typeof crypto.randomUUID === 'function' ? crypto.randomUUID() : Date.now().toString(36)}`;
|
|
const timer = setTimeout(() => {
|
|
pendingCommandResponses.delete(requestId);
|
|
reject(new Error('Renderer remote command timed out.'));
|
|
}, REMOTE_COMMAND_RESPONSE_TIMEOUT_MS);
|
|
|
|
pendingCommandResponses.set(requestId, {
|
|
resolve: (response) => {
|
|
clearTimeout(timer);
|
|
pendingCommandResponses.delete(requestId);
|
|
resolve(response);
|
|
},
|
|
reject: (error) => {
|
|
clearTimeout(timer);
|
|
pendingCommandResponses.delete(requestId);
|
|
reject(error instanceof Error ? error : new Error(String(error)));
|
|
},
|
|
});
|
|
|
|
try {
|
|
sender.send(REMOTE_COMMAND_REQUEST_CHANNEL, {
|
|
...request,
|
|
requestId,
|
|
});
|
|
} catch (error) {
|
|
clearTimeout(timer);
|
|
pendingCommandResponses.delete(requestId);
|
|
reject(error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function buildRendererBridgeMissingResponse(request) {
|
|
return {
|
|
ok: false,
|
|
action: request?.action === 'stop' ? 'stop' : 'launch',
|
|
gameId: typeof request?.gameId === 'string' ? request.gameId : null,
|
|
titleId: typeof request?.titleId === 'string' ? request.titleId : null,
|
|
error: {
|
|
code: 'bridge_not_ready',
|
|
message: 'The renderer command bridge is not ready yet.',
|
|
},
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
installWandRuntime,
|
|
};
|