OctoLauncher 1.3.1

Manifest-based CDN updater and mod manager for the OctoWoW 1.12.1 client:
launcher-owned realmlist, torrent-backed content sync with bundled aria2c,
antivirus and Defender exclusion handling, hardware-aware render distance,
optional client tweaks and mods, and the in-launcher news feed.
This commit is contained in:
OctoWoW
2026-08-14 01:45:50 +00:00
commit 5dca94a3fc
124 changed files with 24276 additions and 0 deletions
+467
View File
@@ -0,0 +1,467 @@
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 { readPristineWow } from '~main/modules/aria2';
import { enumerateDisplays } from '~main/modules/displays';
const Servers = {
live: {
realmList: 'octowow.st',
patchList: 'octowow.st',
realmName: 'OctoWoW'
},
ptr: {
realmList: import.meta.env.MAIN_VITE_PTR_REALMLIST || 'octowow.st',
patchList: import.meta.env.MAIN_VITE_PTR_REALMLIST || 'octowow.st',
realmName: 'OctoWoW PTR'
}
} as const;
const LOCALES = {
enUS: { tag: 'enUS', index: 0 },
deDE: { tag: 'deDE', index: 3 },
zhCN: { tag: 'zhCN', index: 4 },
ruRU: { tag: 'ruRU', index: 5 },
esES: { tag: 'esES', index: 6 },
ptBR: { tag: 'ptBR', index: 7 }
} as const satisfies Record<
PreferencesSchema['locale'],
{ tag: string; index: number }
>;
const LOCALE_NAMES = [
'enUS',
'koKR',
'frFR',
'deDE',
'zhCN',
'zhTW',
'esES',
'xxYY'
] as const;
const localeNameOffset = (index: number) => 0x45591c - index * 8;
const carrierName = (index: number) => LOCALE_NAMES[index];
type TweakKey =
| { synthetic?: false; key: keyof PreferencesSchema['config'] }
| { synthetic: true; key: string };
type Tweak = TweakKey & {
default?: unknown;
forced?: boolean;
} & (
| {
type: 'bytes';
tweaks: [number, number[], number[]?][];
}
| {
type: 'int8' | 'uint16' | 'float';
offset: number;
value?: number;
}
);
const hex = (bytes: number[]) =>
bytes.map(b => b.toString(16).padStart(2, '0')).join(' ');
export const patchExecutable = async () => {
Logger.log('Patching WoW.exe...');
const { clientDir, config, locale } = Preferences.data;
if (!clientDir) return;
const exePath = path.join(clientDir, 'WoW.exe');
try {
Logger.log('Reading clean WoW.exe base...');
const buffer = await readPristineWow(clientDir);
const loc = LOCALES[locale];
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 },
{
synthetic: true,
key: 'skillUiGateHijack',
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
]
]
]
},
{
synthetic: true,
key: 'octowowUrlAllowlist',
type: 'bytes',
default: true,
forced: true,
tweaks: [
[
0x45ccd8,
[
0x6f, 0x63, 0x74, 0x6f, 0x77, 0x6f, 0x77, 0x2e, 0x73, 0x74, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00
]
]
]
},
{
synthetic: true,
key: 'localeTag',
type: 'bytes',
default: true,
forced: true,
tweaks: [
[
0x1b2115,
[0xb8, ...Buffer.from(carrierName(loc.index), 'latin1').reverse()],
[0xa1, 0xa4, 0xa2, 0xc2, 0x00]
]
]
},
{
synthetic: true,
key: 'localeIndex',
type: 'bytes',
default: true,
forced: true,
tweaks: [
[
0x253c,
[0xbe, loc.index, 0x00, 0x00, 0x00, 0xeb, 0x1f],
[0x33, 0xf6, 0x8b, 0xff, 0x8b, 0x04, 0xb5]
]
]
},
{
synthetic: true,
key: 'localeName',
type: 'bytes',
default: true,
forced: true,
tweaks: [
[
localeNameOffset(loc.index),
[...Buffer.from(loc.tag, 'latin1')],
[...Buffer.from(LOCALE_NAMES[loc.index], 'latin1')]
]
]
}
] satisfies Tweak[];
Tweaks.forEach(t => {
const val = t.synthetic
? t.default
: 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 number), t.offset);
} else if (t.type === 'int8') {
buffer.writeInt8(t.value ?? (val as number), t.offset);
} else if (t.type === 'uint16') {
if (!t.forced && !val) return;
buffer.writeUInt16LE(t.value ?? (val as number), t.offset);
} else if (t.type === 'bytes') {
if (!t.forced && !val) return;
t.tweaks.forEach(
([offset, bytes, expect]: [number, number[], number[]?]) => {
if (expect) {
const current = buffer.subarray(offset, offset + expect.length);
if (current.equals(Buffer.from(bytes))) return;
if (!current.equals(Buffer.from(expect)))
throw new Error(
`"${t.key}" expected [${hex(expect)}] at 0x${offset.toString(
16
)} ` +
`but found [${hex([
...current
])}]; refusing to patch WoW.exe`
);
}
const written = Buffer.from(bytes).copy(buffer, offset);
if (written !== bytes.length)
Logger.error(
`"${t.key}" wrote ${written}/${bytes.length} bytes at ` +
`0x${offset.toString(16)}: past end of file (${
buffer.length
} bytes). ` +
'This tweak is a no-op; the offset is probably a virtual address.'
);
}
);
}
});
await fs.writeFile(exePath, buffer);
Preferences.data = { patchedLocale: locale };
Logger.log(`WoW.exe successfully patched (language: ${locale})`);
} catch (e) {
Logger.error('Failed to patch WoW.exe', e);
throw e instanceof Error ? e : new Error('Failed to patch WoW.exe');
}
};
const repairResolution = async (
clientDir: string,
current: string | undefined,
lastWritten: string | undefined
): Promise<{ gxResolution?: string }> => {
const devices = await enumerateDisplays();
if (!devices?.length) return {};
const pinnedRaw = await fs
.readFile(path.join(clientDir, 'VMMFix_preferred_monitor.txt'), 'utf8')
.then(s => s.trim())
.catch(() => '');
const pinned = pinnedRaw ? Number(pinnedRaw) : NaN;
const target =
devices.find(d => d.index === pinned && d.attached) ??
devices.find(d => d.primary && d.attached);
if (!target?.modes.length) return {};
const native = `${target.width}x${target.height}`;
if (!target.modes.includes(native)) return {};
let owned = !current || current === lastWritten;
if (!owned && lastWritten === undefined && current) {
const width = Number(current.split('x')[0]);
if (Number.isFinite(width) && width * 2 < target.width) {
Logger.warn(
`gxResolution ${current} is far below ${target.deviceName}'s ${native} and predates resolution tracking; treating it as a client fallback`
);
owned = true;
}
}
if (!owned) return {};
if (current === native) return {};
Logger.warn(
`gxResolution ${current ?? '<unset>'} is launcher-owned; correcting to ${
target.deviceName
}'s ${native}`
);
return { gxResolution: native };
};
const applyRealmlist = async (clientDir: string, host: string) => {
const body = `set realmlist "${host}"\n`;
const write = async (target: string) => {
const tmp = `${target}.tmp`;
try {
await fs.writeFile(tmp, body);
await fs.move(tmp, target, { overwrite: true });
} catch (e) {
await fs.remove(tmp).catch(() => undefined);
throw e;
}
};
await write(path.join(clientDir, 'realmlist.wtf'));
const dataDir = path.join(clientDir, 'Data');
if (await fs.pathExists(dataDir))
for (const entry of await fs.readdir(dataDir)) {
const scoped = path.join(dataDir, entry, 'realmlist.wtf');
if (!(await fs.pathExists(scoped))) continue;
try {
await write(scoped);
} catch (e) {
Logger.warn(`Could not rewrite ${scoped}: ${String(e)}`);
}
}
};
export const patchConfig = async (forceTweaks = false) => {
const { clientDir, config, locale } = Preferences.data;
if (!clientDir) return;
const server: keyof typeof Servers = import.meta.env.MAIN_VITE_PTR_REALMLIST
? 'ptr'
: 'live';
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' })
: '';
const configWtf = Object.fromEntries(
raw
.split(/\r?\n/)
.map(l => {
const [, k, v] = l.match(/SET (\w+) "(.*)"/) ?? [];
return !k || v === undefined ? undefined : [k, v];
})
.filter(isNotUndef)
);
const isFirstRun = Object.keys(configWtf).length === 0;
const primaryDisplay = screen.getPrimaryDisplay();
const scale = primaryDisplay.scaleFactor || 1;
const width = Math.round(primaryDisplay.bounds.width * scale);
const height = Math.round(primaryDisplay.bounds.height * scale);
const seededResolution = `${width}x${height}`;
const seed = isFirstRun
? {
scriptMemory: 512000,
gxResolution: seededResolution,
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,
M2UseShaders: 1,
particleDensity: 1,
unitDrawDist: 300,
weatherDensity: 3,
movieSubtitle: 1,
minimapZoom: 0,
minimapInsideZoom: 0,
SoundZoneMusicNoDelay: 1,
gxWindow: 1,
gxMaximize: 1,
gxCursor: 1,
checkAddonVersion: 0,
farClip: config.farClip,
CameraDistanceMax: config.cameraDistance,
patchList: Servers[server].patchList,
realmName: Servers[server].realmName
}
: {};
const owned = {
locale: carrierName(LOCALES[locale].index),
patchList: configWtf['patchList'] ?? Servers[server].patchList,
realmName: configWtf['realmName'] ?? Servers[server].realmName,
hwDetect: 0,
BackgroundSound: config.soundInBackground ? 1 : 0
};
const repaired = await repairResolution(
clientDir,
configWtf['gxResolution'],
Preferences.data.lastWrittenResolution
);
const parsed = {
...seed,
...configWtf,
...repaired,
...owned,
...(forceTweaks
? { farClip: config.farClip, CameraDistanceMax: config.cameraDistance }
: {})
};
const body = Object.entries(parsed)
.filter(v => v[1] !== undefined && v[1] !== null)
.filter(([k]) => !/^realmlist$/i.test(k))
.map(l => `SET ${l[0]} "${l[1]}"`)
.join('\n');
const tmpPath = `${configPath}.tmp`;
await fs.writeFile(tmpPath, body);
await fs.move(tmpPath, configPath, { overwrite: true });
await applyRealmlist(clientDir, Servers[server].realmList);
const chosen =
repaired.gxResolution ?? (isFirstRun ? seededResolution : undefined);
if (chosen && chosen !== Preferences.data.lastWrittenResolution)
Preferences.data = { lastWrittenResolution: chosen };
Logger.log('Config.wtf successfully patched');
};
export const ensureDxvkConf = async (clientDir: string) => {
if (!(await fs.pathExists(path.join(clientDir, 'd3d9.dll')))) return;
const confPath = path.join(clientDir, 'dxvk.conf');
if (await fs.pathExists(confPath)) return;
await fs.writeFile(
confPath,
[
'# Cap the texture memory the 32-bit client believes it has so it cannot',
'# over-commit its address space (the common DXVK out-of-memory crash).',
'd3d9.maxAvailableMemory = 2048',
'd3d9.maxFrameLatency = 1',
'dxvk.numCompilerThreads = 2',
'dxvk.logLevel = none',
''
].join('\n')
);
Logger.log('Wrote dxvk.conf');
};