@@ -0,0 +1,25 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import Addons from '~main/modules/addons';
|
||||
import { AddonDataSchema } from '~common/schemas';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
export const addonsRouter = createTRPCRouter({
|
||||
verify: publicProcedure.mutation(() => {
|
||||
Addons.verify();
|
||||
}),
|
||||
update: publicProcedure
|
||||
.input(z.object({ toUpdate: z.array(z.string()).optional() }))
|
||||
.mutation(({ input }) => Addons.update(input.toUpdate)),
|
||||
install: publicProcedure
|
||||
.input(AddonDataSchema)
|
||||
.mutation(({ input }) => Addons.install(input)),
|
||||
remove: publicProcedure
|
||||
.input(z.object({ toDelete: z.array(z.string()) }))
|
||||
.mutation(({ input }) => Addons.remove(input.toDelete)),
|
||||
checkGitUrl: publicProcedure
|
||||
.input(z.string())
|
||||
.query(({ input }) => Addons.checkGitUrl(input)),
|
||||
observe: publicProcedure.subscription(() => Addons.observe())
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import { app, dialog, shell } from 'electron';
|
||||
import Logger from 'electron-log/main';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { mainWindow } from '~main/index';
|
||||
import Preferences from '~main/modules/preferences';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
export const generalRouter = createTRPCRouter({
|
||||
appVersion: publicProcedure.query(() => app.getVersion()),
|
||||
quit: publicProcedure.mutation(() => app.quit()),
|
||||
minimize: publicProcedure.mutation(() => mainWindow?.minimize()),
|
||||
openLink: publicProcedure
|
||||
.input(z.string().url())
|
||||
.mutation(({ input }) => shell.openExternal(input)),
|
||||
openInstallFolder: publicProcedure.mutation(() => {
|
||||
const dir = Preferences.data.clientDir;
|
||||
if (dir) shell.openPath(dir);
|
||||
}),
|
||||
openLogFile: publicProcedure.mutation(() => {
|
||||
const file = Logger.transports.file.getFile().path;
|
||||
shell.openPath(file);
|
||||
}),
|
||||
filePicker: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
title: z.string().optional(),
|
||||
message: z.string().optional(),
|
||||
filters: z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
extensions: z.array(z.string())
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
properties: z
|
||||
.array(
|
||||
z.enum([
|
||||
'openDirectory',
|
||||
'openFile',
|
||||
'multiSelections',
|
||||
'showHiddenFiles',
|
||||
'createDirectory',
|
||||
'promptToCreate',
|
||||
'noResolveAliases',
|
||||
'treatPackageAsDirectory',
|
||||
'dontAddToRecent'
|
||||
])
|
||||
)
|
||||
.optional()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
if (!mainWindow) return { canceled: true } as const;
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog(
|
||||
mainWindow,
|
||||
input
|
||||
);
|
||||
|
||||
return canceled
|
||||
? ({ canceled: true } as const)
|
||||
: ({
|
||||
canceled: false,
|
||||
path: filePaths as [string, ...string[]]
|
||||
} as const);
|
||||
})
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
import path from 'path';
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { inject } from 'dll-inject';
|
||||
import Logger from 'electron-log/main';
|
||||
|
||||
import Preferences from '~main/modules/preferences';
|
||||
import Mods from '~main/modules/mods';
|
||||
import { mainWindow } from '~main/index';
|
||||
import { isGameRunning } from '~main/modules/updater';
|
||||
import { patchConfig } from '~main/modules/patcher';
|
||||
import { minimizeToTray, restoreFromTray } from '~main/modules/tray';
|
||||
import { getMod } from '~common/mods';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
const ensureChainloaderTweak = async (clientDir: string): Promise<boolean> => {
|
||||
if (Preferences.data.config.vanillaFixes) return true;
|
||||
|
||||
const installedMods = Mods.status.mods.filter(r => r.installedVersion);
|
||||
const anyDependsOnVf = installedMods.some(r =>
|
||||
getMod(r.id)?.requires?.includes('vanillaFixes')
|
||||
);
|
||||
|
||||
let dllsTxtHasEntries = false;
|
||||
const dllsPath = path.join(clientDir, 'dlls.txt');
|
||||
if (await fs.pathExists(dllsPath)) {
|
||||
const raw = await fs.readFile(dllsPath, 'utf8');
|
||||
dllsTxtHasEntries = raw
|
||||
.split(/\r?\n/)
|
||||
.some(l => l.trim() && !l.trim().startsWith('#'));
|
||||
}
|
||||
|
||||
if (!anyDependsOnVf && !dllsTxtHasEntries) return false;
|
||||
|
||||
Logger.info(
|
||||
`Auto-enabling vanillaFixes Tweak (chainloader required): ${
|
||||
anyDependsOnVf ? 'a dependent mod is installed' : ''
|
||||
}${anyDependsOnVf && dllsTxtHasEntries ? ' + ' : ''}${
|
||||
dllsTxtHasEntries ? 'dlls.txt has user entries' : ''
|
||||
}.`
|
||||
);
|
||||
Preferences.data = {
|
||||
config: { ...Preferences.data.config, vanillaFixes: true }
|
||||
};
|
||||
return true;
|
||||
};
|
||||
|
||||
export const launcherRouter = createTRPCRouter({
|
||||
start: publicProcedure.mutation(async () => {
|
||||
const { cleanWdb, minimizeToTrayOnPlay, config, clientDir } =
|
||||
Preferences.data;
|
||||
if (!clientDir) return false;
|
||||
|
||||
const clientPath = path.join(clientDir, 'WoW.exe');
|
||||
Logger.log(`Launching ${clientPath}...`);
|
||||
if (await isGameRunning(clientPath)) return false;
|
||||
|
||||
if (cleanWdb) {
|
||||
Logger.log('Cleaning up WDB...');
|
||||
await fs.remove(path.join(clientPath, 'WDB'));
|
||||
}
|
||||
|
||||
Logger.log('Checking Config.wtf...');
|
||||
await patchConfig();
|
||||
|
||||
Logger.log('Launching WoW...');
|
||||
const process = spawn(clientPath, { detached: !minimizeToTrayOnPlay });
|
||||
|
||||
const wantChainloader = await ensureChainloaderTweak(clientDir);
|
||||
if (wantChainloader) {
|
||||
Logger.log('Injecting VanillaFixes...');
|
||||
const vfPath = path.join(clientDir, 'VfPatcher.dll');
|
||||
|
||||
if (!(await fs.pathExists(vfPath))) {
|
||||
Logger.warn(
|
||||
`VfPatcher.dll missing at ${vfPath} — chainloader needed but ` +
|
||||
'the vanillaFixes mod is not installed. Skipping inject; ' +
|
||||
'dlls.txt entries and dependent mods will not load. Install ' +
|
||||
"vanillaFixes from the Mods tab to fix."
|
||||
);
|
||||
} else {
|
||||
const status = inject('WoW.exe', vfPath);
|
||||
if (status) {
|
||||
Logger.error(`Injecting failed with error code ${status}...`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!minimizeToTrayOnPlay) {
|
||||
mainWindow?.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
minimizeToTray();
|
||||
process.on('exit', () => {
|
||||
Logger.log('WoW stopped');
|
||||
restoreFromTray();
|
||||
});
|
||||
return true;
|
||||
})
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import Mods from '~main/modules/mods';
|
||||
import { ModIdSchema } from '~common/mods';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
export const modsRouter = createTRPCRouter({
|
||||
list: publicProcedure.query(() => Mods.status),
|
||||
verify: publicProcedure.mutation(() => Mods.verify()),
|
||||
toggle: publicProcedure
|
||||
.input(z.object({ id: ModIdSchema, enabled: z.boolean() }))
|
||||
.mutation(({ input }) => Mods.toggle(input.id, input.enabled)),
|
||||
setIgnoreUpdates: publicProcedure
|
||||
.input(z.object({ id: ModIdSchema, ignore: z.boolean() }))
|
||||
.mutation(({ input }) => Mods.setIgnoreUpdates(input.id, input.ignore)),
|
||||
applyAll: publicProcedure.mutation(() => Mods.applyAll()),
|
||||
observe: publicProcedure.subscription(() => Mods.observe())
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import fetch from 'node-fetch';
|
||||
import Logger from 'electron-log/main';
|
||||
|
||||
import { NewsFeedSchema, type NewsItem } from '~common/schemas';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
const FETCH_TIMEOUT_MS = 8_000;
|
||||
|
||||
const fetchNews = async (): Promise<NewsItem[]> => {
|
||||
const url = `${import.meta.env.MAIN_VITE_SERVER_URL || 'https://octowow.st'}/news.json`;
|
||||
const controller = new AbortController();
|
||||
const t = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
||||
try {
|
||||
const res = await fetch(url, { signal: controller.signal });
|
||||
if (!res.ok) throw Error(`HTTP ${res.status}`);
|
||||
const parsed = NewsFeedSchema.safeParse(await res.json());
|
||||
if (!parsed.success) {
|
||||
Logger.error('News feed failed schema validation', parsed.error.flatten());
|
||||
throw Error('Malformed news feed');
|
||||
}
|
||||
return parsed.data.items;
|
||||
} finally {
|
||||
clearTimeout(t);
|
||||
}
|
||||
};
|
||||
|
||||
export const newsRouter = createTRPCRouter({
|
||||
list: publicProcedure.query(async () => {
|
||||
try {
|
||||
return await fetchNews();
|
||||
} catch (e) {
|
||||
Logger.error('Failed to fetch news', e);
|
||||
throw e;
|
||||
}
|
||||
})
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { patchConfig, patchExecutable } from '~main/modules/patcher';
|
||||
import Preferences from '~main/modules/preferences';
|
||||
import { getClientVersion } from '~main/utils';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
export const patcherRouter = createTRPCRouter({
|
||||
apply: publicProcedure.mutation(async () => {
|
||||
await patchExecutable();
|
||||
await patchConfig();
|
||||
Preferences.data = { version: await getClientVersion() };
|
||||
})
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { PreferencesSchema } from '~common/schemas';
|
||||
import Preferences from '~main/modules/preferences';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
export const preferencesRouter = createTRPCRouter({
|
||||
get: publicProcedure.output(PreferencesSchema).query(() => Preferences.data),
|
||||
set: publicProcedure
|
||||
.input(PreferencesSchema.partial())
|
||||
.mutation(async ({ input }) => {
|
||||
Preferences.data = input;
|
||||
return Preferences.data;
|
||||
}),
|
||||
isValidClientDir: publicProcedure
|
||||
.input(z.string().optional())
|
||||
.query(({ input }) => Preferences.isValidClientDir(input))
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import SelfUpdater from '~main/modules/selfUpdater';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
export const selfUpdaterRouter = createTRPCRouter({
|
||||
observe: publicProcedure.subscription(() => SelfUpdater.observe()),
|
||||
install: publicProcedure.mutation(() => SelfUpdater.triggerInstall())
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import Updater from '~main/modules/updater';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
export const updaterRouter = createTRPCRouter({
|
||||
verify: publicProcedure.mutation(() => Updater.verify()),
|
||||
update: publicProcedure
|
||||
.input(z.boolean().optional())
|
||||
.mutation(async ({ input }) => Updater.update(input)),
|
||||
observe: publicProcedure.subscription(() => Updater.observe())
|
||||
});
|
||||
Reference in New Issue
Block a user