Files
OctoLauncher/src/main/modules/localePatch.ts
T
OctoWoW fbad749f0c Sync launcher: stop phantom update prompt, forum News panel, hardware-aware render distance
Squashed sync from upstream. Highlights:
- Updater no longer reports already-applied deletes as a pending update on
  every launch (guard the del branch on the target still existing)
- Derive the packaged CSP image origin from the configured server URL
- Forum Announcements panel + News tab; hardware-aware farClip recommendation;
  parchment UI; localization and tweak updates
- Addon source refresh; schema and mod-state fixes

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 16:02:46 -07:00

117 lines
3.1 KiB
TypeScript

import path from 'node:path';
import fs from 'fs-extra';
import {
SFileOpenArchive,
SFileCloseArchive,
SFileHasFile
} from 'stormlib-node';
import { STREAM_FLAG } from 'stormlib-node/dist/enums';
import Logger from 'electron-log/main';
import Preferences from './preferences';
const PREFERRED = 'L';
const LETTERS = 'BCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
const MARKER = 'octolocale.marker';
const patchFile = (dataDir: string, letter: string) =>
path.join(dataDir, `patch-${letter}.mpq`);
const prebuiltFor = (dataDir: string, locale: string) =>
path.join(dataDir, locale, 'patch-L.mpq');
const isOurPatch = (mpqPath: string): boolean => {
if (!fs.existsSync(mpqPath)) return false;
try {
const h = SFileOpenArchive(mpqPath, STREAM_FLAG.READ_ONLY);
try {
return SFileHasFile(h, MARKER);
} finally {
SFileCloseArchive(h);
}
} catch {
return false;
}
};
const usableSlot = (dataDir: string, letter: string): boolean => {
if (fs.existsSync(path.join(dataDir, `patch-${letter}.MPQ`))) return false;
const f = patchFile(dataDir, letter);
return !fs.existsSync(f) || isOurPatch(f);
};
const removeOurPatch = async (dataDir: string) => {
for (const l of LETTERS) {
const f = patchFile(dataDir, l);
if (isOurPatch(f)) await fs.remove(f).catch(() => {});
}
if (Preferences.data.localePatchLetter || Preferences.data.localePatchLocale)
Preferences.data = {
localePatchLetter: undefined,
localePatchLocale: undefined
};
};
export const applyLocalePatch = async (
clientDir: string | undefined,
locale: string | undefined
): Promise<void> => {
if (!clientDir) return;
const dataDir = path.join(clientDir, 'Data');
const nextLocale = !locale || locale === 'enUS' ? undefined : locale;
if (Preferences.data.localePatchLocale !== nextLocale)
await fs.remove(path.join(clientDir, 'WDB')).catch(() => {});
if (!locale || locale === 'enUS') {
await removeOurPatch(dataDir);
return;
}
const source = prebuiltFor(dataDir, locale);
if (!(await fs.pathExists(source))) {
Logger.warn(
`Locale patch: no prebuilt patch-L for ${locale}; leaving UI as-is`
);
return;
}
const tracked = Preferences.data.localePatchLetter;
const letter =
(tracked && usableSlot(dataDir, tracked) ? tracked : undefined) ??
(usableSlot(dataDir, PREFERRED)
? PREFERRED
: LETTERS.find(l => usableSlot(dataDir, l)));
if (!letter) {
Logger.warn('Locale patch: no usable patch slot');
return;
}
const target = patchFile(dataDir, letter);
try {
if (
Preferences.data.localePatchLocale === locale &&
isOurPatch(target) &&
fs.statSync(target).mtimeMs >= fs.statSync(source).mtimeMs
)
return;
} catch {
}
try {
for (const l of LETTERS) {
if (l === letter) continue;
const f = patchFile(dataDir, l);
if (isOurPatch(f)) await fs.remove(f).catch(() => {});
}
await fs.copy(source, target, { overwrite: true });
Preferences.data = { localePatchLetter: letter, localePatchLocale: locale };
Logger.log(
`Locale patch: swapped prebuilt ${locale} -> patch-${letter}.mpq`
);
} catch (e) {
Logger.error('Locale patch: failed to swap in prebuilt patch', e);
}
};