Initial commit
Build check / build (push) Has been cancelled

This commit is contained in:
2026-05-07 20:06:01 -07:00
commit ec0557204c
110 changed files with 18550 additions and 0 deletions
+25
View File
@@ -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())
});
+69
View File
@@ -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);
})
});
+104
View File
@@ -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;
})
});
+19
View File
@@ -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())
});
+37
View File
@@ -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;
}
})
});
+13
View File
@@ -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() };
})
});
+19
View File
@@ -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))
});
+8
View File
@@ -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())
});
+13
View File
@@ -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())
});