mirror of
https://github.com/luanslimadev/Wand-Enhancer.git
synced 2026-08-28 17:01:05 +00:00
a0b3968d33
Reorganize the panel around product capabilities and integrate the remote i18n feature from origin/master into the new structure. - Restructure src into capability features (app, trainer, library, remote-session, appearance, shared); move the bridge to bridge/src. - Add lingui-based UI localization and wrap remaining user-facing strings (Trans / msg macros); fix the 'END В·' mojibake. - Port WeMod cheat-metadata i18n: capture the access token from the snapshot's renderer in the bridge and fetch localized trainer_meta in remote-session.i18n; guarantee snapshot sync on token failure. - Wire vitest to the app's lingui/preact pipeline (mergeConfig).
135 lines
3.8 KiB
TypeScript
135 lines
3.8 KiB
TypeScript
import { WEB_CONTRACT } from '../../protocol/contract';
|
|
import {
|
|
type HelloMessage,
|
|
type IncomingMessage,
|
|
type OutgoingMessage,
|
|
PROTOCOL_VERSION,
|
|
type RemoteCommandMessage,
|
|
type SetValueMessage,
|
|
} from '../../protocol/messages';
|
|
import { isIncomingMessage } from '../../protocol/validation';
|
|
|
|
type SocketHandlers = {
|
|
onConnecting: () => void;
|
|
onTransportOpen: () => void;
|
|
onMessage: (message: IncomingMessage) => void;
|
|
onClose: () => void;
|
|
onError: (message: string) => void;
|
|
};
|
|
|
|
let requestSequence = 0;
|
|
|
|
export class RemoteSessionClient {
|
|
private socket: WebSocket | null = null;
|
|
private intentionalDisconnect = false;
|
|
|
|
constructor(
|
|
private readonly url: string,
|
|
private readonly handlers: SocketHandlers,
|
|
) {}
|
|
|
|
connect(pairingToken?: string): void {
|
|
this.disconnect();
|
|
this.intentionalDisconnect = false;
|
|
this.handlers.onConnecting();
|
|
|
|
const socket = new WebSocket(this.url);
|
|
this.socket = socket;
|
|
|
|
socket.addEventListener('open', () => {
|
|
this.handlers.onTransportOpen();
|
|
this.send(this.createHelloMessage(pairingToken));
|
|
});
|
|
socket.addEventListener('message', (event) => this.handleMessage(event));
|
|
socket.addEventListener('close', () => {
|
|
if (this.socket === socket) {
|
|
this.socket = null;
|
|
}
|
|
if (!this.intentionalDisconnect) {
|
|
this.handlers.onClose();
|
|
}
|
|
});
|
|
socket.addEventListener('error', () => this.handlers.onError('WebSocket connection failed.'));
|
|
}
|
|
|
|
disconnect(): void {
|
|
this.intentionalDisconnect = true;
|
|
this.socket?.close();
|
|
this.socket = null;
|
|
}
|
|
|
|
isOpen(): boolean {
|
|
return Boolean(this.socket && this.socket.readyState === WebSocket.OPEN);
|
|
}
|
|
|
|
setValue(trainerId: string, target: string, value: unknown, cheatId?: string): string | null {
|
|
const requestId = createRequestId(`set_${target}`);
|
|
const message: SetValueMessage = {
|
|
type: 'set_value',
|
|
version: PROTOCOL_VERSION,
|
|
requestId,
|
|
payload: { trainerId, target, value, cheatId },
|
|
};
|
|
return this.send(message) ? requestId : null;
|
|
}
|
|
|
|
launchGame(gameId: string, titleId?: string): boolean {
|
|
return this.sendCommand('launch', gameId, titleId);
|
|
}
|
|
|
|
stopPlaying(gameId?: string, titleId?: string): boolean {
|
|
return this.sendCommand('stop', gameId, titleId);
|
|
}
|
|
|
|
private send(message: OutgoingMessage): boolean {
|
|
const socket = this.socket;
|
|
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
return false;
|
|
}
|
|
socket.send(JSON.stringify(message));
|
|
return true;
|
|
}
|
|
|
|
private sendCommand(action: 'launch' | 'stop', gameId?: string, titleId?: string): boolean {
|
|
const message: RemoteCommandMessage = {
|
|
type: 'remote_command',
|
|
version: PROTOCOL_VERSION,
|
|
requestId: createRequestId(`command_${action}`),
|
|
payload: { action, gameId, titleId },
|
|
};
|
|
return this.send(message);
|
|
}
|
|
|
|
private createHelloMessage(pairingToken?: string): HelloMessage {
|
|
return {
|
|
type: 'hello',
|
|
version: PROTOCOL_VERSION,
|
|
requestId: createRequestId('hello'),
|
|
payload: {
|
|
client: 'mobile-web',
|
|
clientVersion: WEB_CONTRACT.clientVersion,
|
|
pairingToken,
|
|
capabilities: { supportsDeltaValues: true, supportsTrainerSwitch: true },
|
|
},
|
|
};
|
|
}
|
|
|
|
private handleMessage(event: MessageEvent): void {
|
|
try {
|
|
const parsed = JSON.parse(String(event.data)) as unknown;
|
|
if (!isIncomingMessage(parsed)) {
|
|
this.handlers.onError('Received an invalid protocol message.');
|
|
return;
|
|
}
|
|
this.handlers.onMessage(parsed);
|
|
} catch (error) {
|
|
this.handlers.onError(error instanceof Error ? error.message : 'Failed to parse websocket message.');
|
|
}
|
|
}
|
|
}
|
|
|
|
function createRequestId(prefix: string): string {
|
|
requestSequence += 1;
|
|
return `${prefix}_${Date.now()}_${requestSequence}`;
|
|
}
|