import path from 'path'; import { screen } from 'electron'; import fs from 'fs-extra'; import Logger from 'electron-log/main'; import Preferences from '~main/modules/preferences'; import { ConfigWtfSchema, type PreferencesSchema } from '~common/schemas'; import { isNotUndef } from '~common/utils'; import { fetchFile } from '~main/modules/updater'; const Servers = { live: { realmList: 'octowow.st', patchList: 'octowow.st', realmName: 'OctoWoW' }, ptr: { realmList: 'octowow.st', patchList: 'octowow.st', realmName: 'OctoWoW PTR' } } as const; type Tweak = { key: keyof PreferencesSchema['config']; default?: unknown; forced?: boolean } & ( | { type: 'bytes'; tweaks: [number, number[]][]; } | { type: 'int8' | 'uint16' | 'float'; offset: number; value?: number; } ); export const patchExecutable = async () => { Logger.log('Patching WoW.exe...'); const { clientDir, config } = Preferences.data; if (!clientDir) return; const exePath = path.join(clientDir, 'WoW.exe'); try { Logger.log('Fetching clean WoW.exe...'); const file = await fetchFile('WoW.exe'); const buffer = Buffer.from(file); const Tweaks = [ { key: 'largeAddress', type: 'uint16', offset: 0x126, value: buffer.readUint16LE(0x126) | 0x20, default: false }, { key: 'farClip', type: 'float', offset: 0x40fed8 }, { key: 'fieldOfView', type: 'float', offset: 0x4089b4, value: (config.fieldOfView ?? 1) * (Math.PI / 180), default: 90 }, { key: 'frillDistance', type: 'float', offset: 0x467958 }, { key: 'soundInBackground', type: 'int8', offset: 0x3a4869, value: config.soundInBackground ? 0x27 : 0x14, default: false }, { key: 'alwaysAutoLoot', type: 'bytes', tweaks: [ [0x0c1ecf, [0x75]], [0x0c2b25, [0x75]] ] }, { key: 'nameplateRange', type: 'float', offset: 0x40c448 }, { key: 'cameraDistance', type: 'float', offset: 0x4089a4 }, { key: 'crossFactionResurrect' as never, type: 'bytes', default: true, tweaks: [ [0x006e5fb8, [0x006e5fb9]], [0x006e62a8, [0x006e62a9]] ] }, { key: 'skillUiGateHijack' as never, type: 'bytes', default: true, forced: true, tweaks: [ [ 0x002ddf90, [ 0x55, 0x8b, 0xec, 0x83, 0xec, 0x08, 0x53, 0x56, 0x57, 0x8b, 0x3d, 0x60, 0xab, 0xce, 0x00, 0x83, 0xff, 0xff, 0x89, 0x55, 0xfc, 0x89, 0x4d, 0xf8, 0x74, 0x79, 0x8b, 0x75, 0x08, 0x8b, 0x15, 0x58, 0xab, 0xce, 0x00, 0x8b, 0xc7, 0x23, 0xc6, 0x8d, 0x04, 0x40, 0x8b, 0x4c, 0x82, 0x08, 0xf6, 0xc1, 0x01, 0x8d, 0x44, 0x82, 0x04, 0x75, 0x04, 0x85, 0xc9, 0x75, 0x05, 0x33, 0xc9, 0x8d, 0x49, 0x00, 0xf6, 0xc1, 0x01, 0x75, 0x4e, 0x85, 0xc9, 0x74, 0x4a, 0x39, 0x31, 0x74, 0x13, 0x8b, 0xc7, 0x23, 0xc6, 0x8d, 0x04, 0x40, 0x8d, 0x04, 0x82, 0x8b, 0x00, 0x03, 0xc1, 0x8b, 0x48, 0x04, 0xeb, 0xe0, 0x8b, 0x59, 0x1c, 0x8b, 0x71, 0x18, 0x33, 0xff, 0x85, 0xdb, 0x7e, 0x27, 0x8d, 0x64, 0x24, 0x00, 0x8b, 0x4e, 0x0c, 0x8b, 0x56, 0x08, 0x6a, 0x00, 0x6a, 0x00, 0x51, 0x8b, 0x4d, 0xf8, 0x52, 0x8b, 0x55, 0xfc, 0xe8, 0xb9, 0xfd, 0xff, 0xff, 0x84, 0xc0, 0x75, 0x13, 0x47, 0x83, 0xc6, 0x20, 0x3b, 0xfb, 0x7c, 0xdd, 0x5f, 0x5e, 0x33, 0xc0, 0x5b, 0x8b, 0xe5, 0x5d, 0xc2, 0x04, 0x00, 0x5f, 0x8b, 0xc6, 0x5e, 0x5b, 0x8b, 0xe5, 0x5d, 0xc2, 0x04, 0x00, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 ] ] ] } ] satisfies Tweak[]; // Apply patches Tweaks.forEach(t => { const val = config[t.key] ?? t.default ?? ConfigWtfSchema.parse({})[t.key]; Logger.log(`Applying "${t.key}" patch with value: ${val}`); if (t.type === 'float') { buffer.writeFloatLE(t.value ?? (val as never), t.offset); } else if (t.type === 'int8') { buffer.writeInt8(t.value ?? (val as never), t.offset); } else if (t.type === 'uint16') { if (!t.forced && !val) return; buffer.writeUInt16LE(t.value ?? (val as never), t.offset); } else if (t.type === 'bytes') { if (!t.forced && !val) return; t.tweaks.forEach(([offset, bytes]) => Buffer.from(bytes).copy(buffer, offset) ); } }); await fs.writeFile(exePath, buffer); Logger.log('WoW.exe successfully patched'); } catch (e) { Logger.error('Failed to patch WoW.exe', e); } }; export const patchConfig = async () => { const { clientDir, server, config } = Preferences.data; if (!clientDir) return; const configPath = path.join(clientDir, 'WTF', 'Config.wtf'); await fs.ensureDir(path.dirname(configPath)); const raw = (await fs.pathExists(configPath)) ? await fs.readFile(configPath, { encoding: 'utf-8' }) : ''; if (raw) await fs.remove(configPath); const configWtf = Object.fromEntries( raw .split('\n') .map(l => { const [_, k, v] = l.match(/SET (\w+) "(.+)"/) ?? []; return !k || !v ? undefined : [k, v]; }) .filter(isNotUndef) ); const primaryDisplay = screen.getPrimaryDisplay(); const { width, height } = primaryDisplay.bounds; const parsed = { scriptMemory: 512000, gxResolution: `${width}x${height}`, gxColorBits: primaryDisplay.colorDepth, gxDepthBits: primaryDisplay.colorDepth, gxRefresh: 60, gxMultisample: 8, gxMultisampleQuality: 0, gxTripleBuffer: 1, anisotropic: 16, frillDensity: 48, fullAlpha: 1, SmallCull: 0.01, DistCull: 888.8, shadowLevel: 0, trilinear: 1, specular: 1, pixelShaders: 1, M2UsePixelShaders: 1, particleDensity: 1, unitDrawDist: 300, weatherDensity: 3, movieSubtitle: 1, minimapZoom: 0, minimapInsideZoom: 0, SoundZoneMusicNoDelay: 1, patchList: configWtf['patchList'] ?? Servers[server].patchList, realmName: configWtf['realmName'] ?? Servers[server].realmName, gxWindow: configWtf['gxWindow'] ?? 1, gxMaximize: configWtf['gxMaximize'] ?? 1, gxCursor: configWtf['gxCursor'] ?? 1, checkAddonVersion: configWtf['checkAddonVersion'] ?? 0, ...configWtf, CameraDistanceMax: config.cameraDistance, farClip: config.farClip, realmList: Servers[server].realmList, hwDetect: 0, M2UseShaders: 1 }; await fs.writeFile( configPath, Object.entries(parsed) .filter(v => v[1] !== undefined && v[1] !== null) .map(l => `SET ${l[0]} "${l[1]}"`) .join('\n') ); Logger.log('Config.wtf successfully patched'); };