forked from OctoWoW/OctoLauncher
OctoLauncher 1.3.6
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "octo-launcher",
|
||||
"version": "1.3.5",
|
||||
"version": "1.3.6",
|
||||
"description": "An Electron application for launching and updating the OctoWoW client",
|
||||
"author": "OctoWoW",
|
||||
"copyright": "Copyright © 2026 OctoWoW",
|
||||
|
||||
@@ -9,6 +9,7 @@ import icon from '~build/icon.png?asset';
|
||||
import { PreferencesSchema } from '~common/schemas';
|
||||
|
||||
import { appRouter } from './api/root';
|
||||
import { stopSyncing, stopSeeding } from './modules/aria2';
|
||||
import Preferences from './modules/preferences';
|
||||
import Updater from './modules/updater';
|
||||
import Addons from './modules/addons';
|
||||
@@ -201,6 +202,8 @@ if (!gotSingleInstanceLock) {
|
||||
|
||||
let settingsFlushed = false;
|
||||
app.on('before-quit', event => {
|
||||
stopSyncing();
|
||||
stopSeeding();
|
||||
if (settingsFlushed) return;
|
||||
settingsFlushed = true;
|
||||
event.preventDefault();
|
||||
|
||||
@@ -84,6 +84,15 @@ const parseProgress = (
|
||||
};
|
||||
};
|
||||
|
||||
let syncChild: ChildProcess | undefined;
|
||||
|
||||
// aria2's --stop-with-process is unreliable on Windows; kill the download
|
||||
// child explicitly on quit or it keeps running headless
|
||||
export const stopSyncing = (): void => {
|
||||
syncChild?.kill();
|
||||
syncChild = undefined;
|
||||
};
|
||||
|
||||
export const syncClient = (opts: SyncOpts): Promise<void> =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
let child: ChildProcess | undefined;
|
||||
@@ -120,6 +129,7 @@ export const syncClient = (opts: SyncOpts): Promise<void> =>
|
||||
];
|
||||
Logger.log(`aria2c ${args.join(' ')}`);
|
||||
child = spawn(bin(), args, { windowsHide: true });
|
||||
syncChild = child;
|
||||
|
||||
const onLine = (buf: Buffer) => {
|
||||
for (const line of buf.toString().split(/\r?\n/)) {
|
||||
@@ -136,6 +146,7 @@ export const syncClient = (opts: SyncOpts): Promise<void> =>
|
||||
|
||||
child.on('error', reject);
|
||||
child.on('close', code => {
|
||||
if (syncChild === child) syncChild = undefined;
|
||||
if (code === 0) resolve();
|
||||
else reject(new Error(`aria2c exited with code ${code}`));
|
||||
});
|
||||
@@ -341,6 +352,7 @@ export const torrentDownloadSelection = async (
|
||||
const files = torrent?.info?.files ?? [];
|
||||
if (!files.length) return null;
|
||||
const need: number[] = [];
|
||||
let missing = false;
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const f = files[i];
|
||||
if (!f.path?.length || typeof f.length !== 'number') return null;
|
||||
@@ -348,6 +360,7 @@ export const torrentDownloadSelection = async (
|
||||
const dest = path.join(clientDir, ...f.path);
|
||||
const st = await fs.stat(dest).catch(() => null);
|
||||
if (!st) {
|
||||
missing = true;
|
||||
need.push(i + 1);
|
||||
continue;
|
||||
}
|
||||
@@ -357,6 +370,10 @@ export const torrentDownloadSelection = async (
|
||||
need.push(i + 1);
|
||||
}
|
||||
}
|
||||
// a deleted file poisons the resume state (its pieces are marked
|
||||
// done, so aria2 skips them forever); partial files keep it so an
|
||||
// interrupted download resumes instead of restarting
|
||||
if (missing) await clearTorrentResumeState().catch(() => undefined);
|
||||
return need;
|
||||
} catch (e) {
|
||||
Logger.warn('Torrent selection computation failed', e);
|
||||
|
||||
+47
-13
@@ -58,6 +58,20 @@ const KNOWN_DLLS = new Set(
|
||||
const AV_ERROR =
|
||||
'Windows Defender blocked this download. Use "Allow through antivirus" and apply again.';
|
||||
|
||||
// pinned dxvk-gplasync v2.7.1-1 x32 d3d9.dll (same build the client ships)
|
||||
const DXVK_DLL_SHA256 =
|
||||
'a2cd6841e102f37189527c118ec416fa5071ac4d3120762973d9a0c6c5fd067e';
|
||||
|
||||
const fileSha256 = async (p: string): Promise<string | null> => {
|
||||
try {
|
||||
return createHash('sha256')
|
||||
.update(await fs.readFile(p))
|
||||
.digest('hex');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const looksLikeAvBlock = (msg: string) =>
|
||||
/windows defender|virus|potentially unwanted/i.test(msg);
|
||||
|
||||
@@ -278,6 +292,7 @@ class ModsClass extends Observable<ModsStatus> {
|
||||
}
|
||||
|
||||
const missing: string[] = [];
|
||||
let dxvkRepair = false;
|
||||
for (const m of MODS) {
|
||||
// disabled mods: leave dlls.txt and installed state untouched
|
||||
if (m.disabled) continue;
|
||||
@@ -288,30 +303,41 @@ class ModsClass extends Observable<ModsStatus> {
|
||||
// torrent mode: DLLs ship in the client; a missing file goes to `missing`, not dirty
|
||||
if (isTorrentMode()) {
|
||||
const enabled = state?.enabled ?? DEFAULT_ENABLED_MODS.includes(m.id);
|
||||
// dxvk loads by file presence, not dlls.txt; disabled parks
|
||||
// d3d9.dll as .off, enabled restores it
|
||||
// dxvk loads by file presence and torrent piece spillover can
|
||||
// corrupt it; hash-verify every state: park/restore verified
|
||||
// copies only, delete junk, re-download the pin when needed
|
||||
if (m.id === 'dxvk' && clientDir) {
|
||||
const live = path.join(clientDir, 'd3d9.dll');
|
||||
const off = path.join(clientDir, 'd3d9.dll.off');
|
||||
const liveStat = await fs.stat(live).catch(() => null);
|
||||
if (!enabled && liveStat && liveStat.size === 0) {
|
||||
// 0-byte aria2 stub, never park it over a real copy
|
||||
await fs.remove(live).catch(() => undefined);
|
||||
} else if (!enabled && liveStat) {
|
||||
await fs.remove(off).catch(() => undefined);
|
||||
const liveSha = await fileSha256(live);
|
||||
if (!enabled) {
|
||||
if (
|
||||
liveSha === DXVK_DLL_SHA256 &&
|
||||
!(await fs.pathExists(off))
|
||||
) {
|
||||
await fs
|
||||
.move(live, off)
|
||||
.then(() => Logger.info('dxvk disabled: parked d3d9.dll'))
|
||||
.catch(e => Logger.warn('Could not park d3d9.dll', e));
|
||||
} else if (
|
||||
enabled &&
|
||||
!(await fs.pathExists(live)) &&
|
||||
(await fs.pathExists(off))
|
||||
) {
|
||||
} else if (liveSha !== null) {
|
||||
await fs.remove(live).catch(() => undefined);
|
||||
}
|
||||
} else if (liveSha !== DXVK_DLL_SHA256) {
|
||||
if (liveSha !== null) {
|
||||
Logger.warn('dxvk: d3d9.dll failed verification; replacing');
|
||||
await fs.remove(live).catch(() => undefined);
|
||||
}
|
||||
const offSha = await fileSha256(off);
|
||||
if (offSha === DXVK_DLL_SHA256) {
|
||||
await fs
|
||||
.move(off, live)
|
||||
.then(() => Logger.info('dxvk enabled: restored d3d9.dll'))
|
||||
.catch(e => Logger.warn('Could not restore d3d9.dll', e));
|
||||
} else {
|
||||
if (offSha !== null)
|
||||
await fs.remove(off).catch(() => undefined);
|
||||
dxvkRepair = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
const files = modTargetFiles(m);
|
||||
@@ -371,6 +397,14 @@ class ModsClass extends Observable<ModsStatus> {
|
||||
});
|
||||
}
|
||||
|
||||
if (dxvkRepair) {
|
||||
const dm = getMod('dxvk');
|
||||
if (dm)
|
||||
await this.#install(dm).catch(e =>
|
||||
Logger.warn('dxvk repair download failed', e)
|
||||
);
|
||||
}
|
||||
|
||||
this._value = {
|
||||
...this._value,
|
||||
state: 'idle',
|
||||
|
||||
@@ -456,9 +456,6 @@ class UpdaterClass extends Observable<UpdaterStatus> {
|
||||
}
|
||||
});
|
||||
if (!(await torrentTreeIntact(clientPath, url))) {
|
||||
// stale resume state makes aria2 skip pieces it thinks are
|
||||
// done; drop it so the next attempt starts from the real files
|
||||
await clearTorrentResumeState().catch(() => undefined);
|
||||
await healRealmlist(clientPath).catch(() => undefined);
|
||||
this.status = {
|
||||
state: 'updateAvailable',
|
||||
@@ -474,15 +471,12 @@ class UpdaterClass extends Observable<UpdaterStatus> {
|
||||
);
|
||||
if (removed.length)
|
||||
Logger.log(`Removed stale archives: ${removed.join(', ')}`);
|
||||
// a clean sync restores d3d9.dll; re-park it if dxvk is off
|
||||
if (Preferences.data.mods?.dxvk?.enabled === false) {
|
||||
const live = path.join(clientPath, 'd3d9.dll');
|
||||
const off = path.join(clientPath, 'd3d9.dll.off');
|
||||
if (await fs.pathExists(live)) {
|
||||
await fs.remove(off).catch(() => undefined);
|
||||
await fs.move(live, off).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
// a sync can recreate d3d9.dll (clean pass, or piece spillover);
|
||||
// drop it while dxvk is off, the mods verify owns park/restore
|
||||
if (Preferences.data.mods?.dxvk?.enabled === false)
|
||||
await fs
|
||||
.remove(path.join(clientPath, 'd3d9.dll'))
|
||||
.catch(() => undefined);
|
||||
await this.#reconcileClientPatch(clientPath);
|
||||
await this.#reconcileRaidVisuals(clientPath);
|
||||
Preferences.data = {
|
||||
|
||||
Reference in New Issue
Block a user