Move Warcraft platform detection to the main process

Phase 1 of the UI-decoupling migration: adds WarcraftController and
WarcraftPlatform (win/mac/linux) in app/, wires them up via
registerControllers(), and replaces the renderer's
warcraft.service.{impl,win,mac,linux}.ts with a thin
warcraft-api.service.ts over IPC.

Also included: CurseForge download auth headers, a couple of TS lint
fixes, a globrex/minimist import fix, CLAUDE.md plus commit/patch-notes
skills, and the 2.23.0-beta.1 version bump.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jliddev
2026-07-19 17:32:58 -05:00
parent 389557d21e
commit 21ec46ef91
26 changed files with 2509 additions and 2229 deletions

View File

@@ -0,0 +1,62 @@
---
name: commit
description: "Commit whatever is currently staged, with a message drafted from the staged diff and this repo's commit style."
---
# Commit Staged Changes
Commit **only what's already staged** — never stage anything yourself (`git add`, `git add -A`,
`git add .`) unless the user explicitly asks for that in this same turn. If the user wants
different files included, tell them to stage them (or ask which files) rather than guessing.
## Step 1 — Look at what's staged
Run these in parallel:
- `git status` — see staged vs. unstaged/untracked, so you don't confuse the two
- `git diff --staged` — the actual content to summarize
- `git log --oneline -10` — recent commit style to match
If `git diff --staged` is empty, stop and tell the user nothing is staged. Don't stage
anything for them, don't commit an empty change.
## Step 2 — Sanity-check what's staged
Before drafting a message:
- If any staged file looks like it could hold secrets (`.env`, `credentials.json`, `*.pem`,
API keys, tokens) even from an innocuous-looking name, open it and check before proceeding.
Warn the user and ask before committing if it looks sensitive.
- If the staged diff mixes clearly unrelated changes (e.g. an unrelated formatting pass bundled
with a real fix), flag it to the user instead of silently writing one message that papers over
two different changes.
## Step 3 — Draft the message
Follow this repo's convention from `CLAUDE.md`:
- Present tense, imperative mood ("Add raiderio provider", not "Added raiderio provider")
- Subject line under 72 characters
- Focus on *why*, not a restatement of the diff — 1-2 sentences is enough; only add a body if the
reasoning isn't obvious from the subject alone
- Match the tone/format of the last few `git log` entries rather than inventing a new style
## Step 4 — Commit
```
git commit -m "$(cat <<'EOF'
<subject>
<optional body>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
EOF
)"
```
Then run `git status` to confirm the commit succeeded and the working tree is what's expected.
If a pre-commit hook fails: fix the underlying issue, re-stage the fix, and create a **new**
commit — never `--amend` (the failed commit never happened, so amend would rewrite the prior
commit instead) and never `--no-verify` to skip the hook.
## Step 5 — Hand off
Report the commit hash and subject line. Don't push — that's a separate, explicit ask.

View File

@@ -0,0 +1,93 @@
---
name: patch-notes
description: "Generate an in-app patch notes entry for the current wowup-electron version, if one isn't already present in CHANGELOGS."
---
# Generate Patch Notes
WowUp's in-app "What's New" dialog is driven by the `CHANGELOGS` array at the top of
[`wowup-electron/src/app/services/wowup/patch-notes.service.ts`](../../../wowup-electron/src/app/services/wowup/patch-notes.service.ts).
Each entry is `{ Version: string, html: string }`, newest first. Do **not** touch
`wowup-electron/src/assets/changelog.json` — that's a legacy file for pre-2.1.0 releases and is no longer
extended.
## Step 1 — Determine the target version
Read `version` from `wowup-electron/package.json`. Strip any pre-release/build suffix
(`-beta.1`, `-alpha.2`, etc.) — every existing `CHANGELOGS` entry uses a bare `X.Y.Z`, never a
pre-release tag.
## Step 2 — Check if it already exists
Search the `CHANGELOGS` array for an entry whose `Version` matches. If found, **stop** — tell the
user the entry already exists (quote it) and do not modify the file.
## Step 3 — Find the source material
The current top entry of `CHANGELOGS` is the previous released version. Find its git tag with
`git tag --list "v<prevVersion>*" --sort=-v:refname` (tags follow `v2.22.1`, or
`v2.22.1-beta.N` if no stable tag was ever cut). Use the most relevant tag as the base and diff
to `HEAD`:
```
git log --oneline <baseTag>..HEAD -- wowup-electron/src wowup-electron/app
```
If no matching tag exists at all, ask the user for the right base ref instead of guessing.
If that tag *does* exist but the range is empty (`HEAD` is the tagged commit, i.e. everything
interesting is still uncommitted — this happens mid-release), fall back to
`git diff <baseTag> -- wowup-electron/src wowup-electron/app` and `git status` instead of
stopping. Don't ask the user to commit first; just work from the working tree.
## Step 4 — Draft the entry
Read the actual commits/diffs (not just messages) for anything non-obvious. Group into
whichever of these sections apply — skip empty ones, keep existing entries' ordering:
1. `Features` (new user-facing capability)
2. `Changes` (behavior/UX tweaks, non-breaking)
3. `Fixes` (bug fixes)
Write each bullet as a short, user-facing sentence (not a commit message) — no ticket numbers,
no internal file paths, no "refactor"/"chore" noise. Skip commits/diff hunks that are purely
internal (lint, tests, CI, dependency bumps, config/build tooling, the ongoing UI-decoupling
migration) unless they fix a user-visible bug. Match the plain style of the two most recent
entries (`2.22.1`, `2.22.0`) — no inline `style=` attributes, just:
```html
<h4>Features</h4>
<ul>
<li>...</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>...</li>
</ul>
```
**If nothing user-facing survives that filter** (everything found is internal refactor, tooling,
or config), don't stop and ask what to write. Fall back to a minimal default entry instead —
this mirrors how past releases with a quiet cycle were actually written (see `2.22.1`'s own
"Various bug fixes"):
```html
<h4>Fixes</h4>
<ul>
<li>Various bug fixes and improvements</li>
</ul>
```
Flag clearly in the hand-off (Step 6) that the default was used and why, so it's obvious this is
a placeholder rather than a researched entry.
## Step 5 — Insert
Add the new `{ Version, html }` object as the **first** element of the `CHANGELOGS` array
(right after `const CHANGELOGS: ChangeLog[] = [`), preserving the descending-version order of
every entry after it.
## Step 6 — Hand off
Show the user the diff. This is a first draft of customer-facing release copy — tell them to
proofread wording/ordering before it ships, don't present it as final.

2
.vscode/tasks.json vendored
View File

@@ -110,7 +110,7 @@
"endsPattern": "^.*Terminal will be reused by tasks, press any key to close it.*"
}
},
"dependsOn": ["Build.Lib"]
"dependsOn": ["Build.Lib", "Build.Main.OW.Simple"]
},
{
"label": "Build.Renderer",

83
CLAUDE.md Normal file
View File

@@ -0,0 +1,83 @@
# WowUp Monorepo — Developer Architecture & Style Guide
WowUp is a World of Warcraft addon manager. This monorepo separates platform targets, core business logic, and shared TypeScript libraries.
---
## 📂 Repository Structure
| Package | Tech Stack | Purpose |
| :--- | :--- | :--- |
| [`wowup-electron/`](file:///d:/GitHub/WowUp-Dev/wowup-electron) | Electron 40 + Angular 17 | The main desktop client application (Active/Primary). |
| [`wowup-lib/`](file:///d:/GitHub/WowUp-Dev/wowup-lib) | TypeScript + Parcel | Shared core library (`wowup-lib-core`) consumed by the Electron app. |
| [`v3/`](file:///d:/GitHub/WowUp-Dev/v3) | TypeScript | Next-generation client version (In early development). |
---
## 🏛️ Baseline Architecture (Coupled UI, mid-migration)
The application still runs a **coupled renderer architecture** where Node capabilities are fully integrated into the browser process, but a UI-decoupling migration (below) is actively moving domain logic into the main process one vertical slice at a time:
- **Node Integration Enabled**: `nodeIntegration: true` and `contextIsolation: false` are configured in `app/main.ts`. The Angular UI imports Node native APIs (`fs`, `path`, etc.) directly.
- **Business Logic in Renderer (not yet migrated)**:
- Addon Providers (`src/app/addon-providers/`) execute all HTTP calls and metadata scraping in the browser context.
- Warcraft installation CRUD, TOC parsing, addon scan/sync, and install/remove (`src/app/services/warcraft/`, `src/app/services/addons/`) still run on the Angular side.
- **Main Process (`app/`)**: Was a thin window manager routed through the legacy [`app/ipc-events.ts`](file:///d:/GitHub/WowUp-Dev/wowup-electron/app/ipc-events.ts) registry; now also hosts a growing set of **Controllers** (`app/controllers/`, implementing `IpcController`, registered in `app/controllers/index.ts`) and main-process **services** (`app/services/`) for logic that has already been migrated — e.g. Warcraft platform/executable detection (`WarcraftController`).
- **Renderer API wrappers**: Migrated domains get a thin Angular service under `src/app/services/api/` (e.g. `warcraft-api.service.ts`) that just wraps `ipcRenderer.invoke()` for the new IPC channels — no business logic.
---
## 🚀 UI Decoupling Plan (in progress)
Goal: move business logic out of the Angular renderer into the Electron main process so a future UI swap (e.g. React) only has to replace thin API-wrapper services. Each domain becomes a Controller in `app/controllers/` exposed via `ipcMain.handle()`; the renderer gets a thin API service in `src/app/services/api/`; shared request/response types will live in `src/common/api/contracts/`.
Full phase-by-phase checklist: [`wowup-electron/UI_DECOUPLING_PLAN.md`](file:///d:/GitHub/WowUp-Dev/wowup-electron/UI_DECOUPLING_PLAN.md) — keep that file, not this section, as the source of truth for what's done vs. pending; update its checkboxes as phases complete.
Status snapshot:
1. **Phase 1 — Warcraft Platform Detection**: ✅ done. `WarcraftPlatform` (`app/services/warcraft/`) + `WarcraftController` + `warcraft-api.service.ts`; old `warcraft.service.{impl,win,mac,linux}.ts` deleted from the renderer.
2. **Phase 2 — Warcraft Installation CRUD**: 🚧 in progress.
3. **Phases 36 — TOC parsing/addon folder listing, addon scan/sync, install/remove pipeline, `ipc-events.ts` cleanup**: ⏳ not started.
Note: there is no `Procedure<I, O>` / `EventChannel<P>` primitive layer — the actual pattern is the simpler `IpcController` interface + per-channel `ipcMain.handle()` calls, wired up in `registerControllers()`.
---
## 🛠️ Operations & Execution Playbook
### Build Order Requirement
Because `wowup-electron` depends on the local `wowup-lib-core` package, **`wowup-lib` must be compiled first**.
```bash
# 1. Compile the shared library
cd wowup-lib
npm install
npm run build
# 2. Setup the Electron workspace
cd ../wowup-electron
npm install
npm run build:lib # Syncs and links the shared library locally
# 3. Serve the application
npm start # Serve default flavor (wago)
npm run ow:start # Serve Overwolf flavor (ow)
```
### Common Commands (wowup-electron/)
- **Lint Code**: `npm run lint`
- **Run Tests**: `npm test`
- **Format Code**: `npm run pretty`
---
## 📝 Branching & Style Guidelines
- **Branching Policy**:
- Primary development branch: `master`.
- Release branches: `release/X.Y.Z`.
- Target all PRs at `master` unless explicitly working on a specific release.
- **Code Formatting**:
- TypeScript strictly throughout the workspace.
- Formatted via Prettier (`npx prettier --write .`).
- **Commit Messages**:
- Present tense, imperative mood, under 72 characters (e.g. *"Add raiderio provider"* not *"Added raiderio provider"*).

View File

@@ -3,16 +3,30 @@ import * as Store from "electron-store";
import { AddonController } from "./addon.controller";
import { IpcController } from "./ipc-controller";
import { WarcraftController } from "./warcraft/warcraft.controller";
import { WarcraftPlatformWin } from "../services/warcraft/warcraft-platform.win";
import { WarcraftPlatformMac } from "../services/warcraft/warcraft-platform.mac";
import { WarcraftPlatformLinux } from "../services/warcraft/warcraft-platform.linux";
import { WarcraftPlatform } from "../services/warcraft/warcraft-platform.service";
import * as platform from "../platform";
export interface ControllerDeps {
window: BrowserWindow;
addonStore: Store;
preferenceStore: Store;
}
function getPlatformImpl(): WarcraftPlatform {
if (platform.isWin) return new WarcraftPlatformWin();
if (platform.isMac) return new WarcraftPlatformMac();
if (platform.isLinux) return new WarcraftPlatformLinux();
throw new Error("Unsupported platform");
}
export function registerControllers(deps: ControllerDeps): void {
const controllers: IpcController[] = [
new AddonController(deps.addonStore),
// Add new controllers here as domains grow
new WarcraftController(getPlatformImpl(), deps.preferenceStore),
];
for (const controller of controllers) {

View File

@@ -0,0 +1,57 @@
import { ipcMain } from "electron";
import * as Store from "electron-store";
import {
BLIZZARD_AGENT_PATH_KEY,
IPC_WARCRAFT_GET_BLIZZARD_AGENT_PATH,
IPC_WARCRAFT_GET_CLIENT_TYPE_FOR_BINARY,
IPC_WARCRAFT_GET_EXECUTABLE_EXTENSION,
IPC_WARCRAFT_GET_EXECUTABLE_NAME,
IPC_WARCRAFT_GET_INSTALLED_PRODUCTS,
IPC_WARCRAFT_IS_WOW_APPLICATION,
} from "../../../src/common/constants";
import { InstalledProduct, WowClientType } from "wowup-lib-core";
import { IpcController } from "../ipc-controller";
import { decodeProducts, WarcraftPlatform } from "../../services/warcraft/warcraft-platform.service";
export class WarcraftController implements IpcController {
public constructor(
private readonly platform: WarcraftPlatform,
private readonly prefStore: Store,
) {}
public register(): void {
ipcMain.handle(IPC_WARCRAFT_GET_BLIZZARD_AGENT_PATH, () => this.getBlizzardAgentPath());
ipcMain.handle(IPC_WARCRAFT_GET_INSTALLED_PRODUCTS, (_evt, agentPath: string) => this.getInstalledProducts(agentPath));
ipcMain.handle(IPC_WARCRAFT_GET_EXECUTABLE_NAME, (_evt, clientType: WowClientType) => this.platform.getExecutableName(clientType));
ipcMain.handle(IPC_WARCRAFT_GET_CLIENT_TYPE_FOR_BINARY, (_evt, binaryPath: string) => this.platform.getClientType(binaryPath));
ipcMain.handle(IPC_WARCRAFT_IS_WOW_APPLICATION, (_evt, appName: string) => this.platform.isWowApplication(appName));
ipcMain.handle(IPC_WARCRAFT_GET_EXECUTABLE_EXTENSION, () => this.platform.getExecutableExtension());
}
public async getBlizzardAgentPath(): Promise<string> {
const stored = this.prefStore.get(BLIZZARD_AGENT_PATH_KEY) as string | undefined;
if (stored) {
return stored;
}
const agentPath = await this.platform.getBlizzardAgentPath();
if (agentPath) {
this.prefStore.set(BLIZZARD_AGENT_PATH_KEY, agentPath);
}
return agentPath;
}
public async getInstalledProducts(agentPath: string): Promise<Map<WowClientType, InstalledProduct>> {
const decoded = await decodeProducts(agentPath);
const resolved = this.platform.resolveProducts(decoded, agentPath);
const result = new Map<WowClientType, InstalledProduct>();
for (const product of resolved) {
result.set(product.clientType, product);
}
return result;
}
}

View File

@@ -12,7 +12,7 @@ import {
systemPreferences,
} from "electron";
import * as log from "electron-log/main";
import * as globrex from "globrex";
import globrex = require("globrex");
import * as _ from "lodash";
import { nanoid } from "nanoid";
import * as nodeDiskInfo from "node-disk-info";
@@ -110,7 +110,6 @@ import { createTray } from "./system-tray";
import { WowUpFolderScanner } from "./wowup-folder-scanner";
import * as push from "./push";
import { GetDirectoryTreeRequest } from "../src/common/models/ipc-request";
import { ProductDb } from "../src/common/wowup/product-db";
import { restoreWindow } from "./window-state";
import { firstValueFrom, from, mergeMap, toArray } from "rxjs";
import { CurseFolderScanner } from "./curse-folder-scanner";
@@ -475,15 +474,6 @@ export function initializeIpcHandlers(window: BrowserWindow): void {
return await fsp.readFile(filePath);
});
handle("decode-product-db", async (evt, filePath: string) => {
const productDbData = await fsp.readFile(filePath);
const productDb = ProductDb.decode(productDbData);
setTimeout(() => {
console.log("productDb", JSON.stringify(productDb));
},1);
return productDb;
});
handle(IPC_WRITE_FILE_CHANNEL, async (evt, filePath: string, contents: string) => {
return await fsp.writeFile(filePath, contents, { encoding: "utf-8", mode: DEFAULT_FILE_MODE });

View File

@@ -325,7 +325,7 @@ function createWindow(): BrowserWindow {
initializeIpcHandlers(win);
initializeStoreIpcHandlers();
registerControllers({ window: win, addonStore: getAddonStore() });
registerControllers({ window: win, addonStore: getAddonStore(), preferenceStore: getPreferenceStore() });
if (AppEnv.buildFlavor === "wago") {
wagoHandler.initialize(win);

View File

@@ -1,15 +1,16 @@
import * as fsp from "fs/promises";
import * as os from "os";
import * as path from "path";
import { WowClientType } from "wowup-lib-core";
import { InstalledProduct } from "wowup-lib-core";
import {
WOW_ANNIVERSARY_FOLDER,
WOW_CLASSIC_ERA_FOLDER,
WOW_CLASSIC_ERA_PTR_FOLDER,
WOW_RETAIL_XPTR_FOLDER,
} from "../../../common/constants";
import { ElectronService } from "../electron/electron.service";
import { FileService } from "../files/file.service";
import { WarcraftServiceImpl } from "./warcraft.service.impl";
} from "../../../src/common/constants";
import { InstalledProduct, WowClientType } from "wowup-lib-core";
import { WarcraftPlatform } from "./warcraft-platform.service";
const WOW_RETAIL_NAME = "Wow.exe";
const WOW_RETAIL_PTR_NAME = "WowT.exe";
@@ -28,22 +29,12 @@ const WOW_APP_NAMES = [
];
const LUTRIS_CONFIG_PATH = "/.config/lutris/system.yml";
// Search in this order until products are found on one.
// All WoW products can be found under any or all of them,
// since each of them are essentially just Battle.net
// launchers with a different install path.
const LUTRIS_WOW_DIRS = ["battlenet/drive_c", "world-of-warcraft/drive_c", "world-of-warcraft-classic/drive_c"];
// BLIZZARD STRINGS
const WINDOWS_BLIZZARD_AGENT_PATH = "ProgramData/Battle.net/Agent";
const BLIZZARD_PRODUCT_DB_NAME = "product.db";
export class WarcraftServiceLinux implements WarcraftServiceImpl {
public constructor(
private _electronService: ElectronService,
private _fileService: FileService,
) {}
export class WarcraftPlatformLinux implements WarcraftPlatform {
public getExecutableExtension(): string {
return "exe";
}
@@ -52,9 +43,6 @@ export class WarcraftServiceLinux implements WarcraftServiceImpl {
return WOW_APP_NAMES.includes(appName);
}
/**
* On Linux players could be using Lutris to install the Battle.net launcher or WoW
*/
public async getBlizzardAgentPath(): Promise<string> {
try {
const lutrisLibraryPath = await this.getLutrisWowPath();
@@ -63,9 +51,7 @@ export class WarcraftServiceLinux implements WarcraftServiceImpl {
}
const agentPath = path.join(lutrisLibraryPath, WINDOWS_BLIZZARD_AGENT_PATH, BLIZZARD_PRODUCT_DB_NAME);
const agentPathExists = await this._fileService.pathExists(agentPath);
if (agentPathExists) {
if (await pathExists(agentPath)) {
console.log(`Found WoW products at ${agentPath}`);
return agentPath;
}
@@ -76,59 +62,6 @@ export class WarcraftServiceLinux implements WarcraftServiceImpl {
return "";
}
public resolveProducts(decodedProducts: InstalledProduct[], agentPath: string): InstalledProduct[] {
const resolvedProducts: InstalledProduct[] = [];
const agentPathPrefixRegex = new RegExp(`(.*drive_c)`);
for (const product of decodedProducts) {
console.log(`location: ${location.toString()} agentPath: ${agentPath}`);
const regexResults = agentPathPrefixRegex.exec(agentPath);
if (regexResults === null) {
console.warn("No agentPath match found");
continue;
}
const agentPathPrefix = regexResults[1].trim();
resolvedProducts.push({
...product,
location: path.join(agentPathPrefix, product.location.substr(3)),
} as InstalledProduct);
}
return resolvedProducts;
}
public async getLutrisWowPath(): Promise<string> {
const homeDir = await this._fileService.getHomeDir();
const resolvedPath = path.join(homeDir, LUTRIS_CONFIG_PATH);
try {
const lutrisConfigExists = await this._fileService.pathExists(resolvedPath);
if (lutrisConfigExists) {
const lutrisConfig = await this._fileService.readFile(resolvedPath);
const libraryPathRegex = new RegExp(`game_path: (.*)`);
const regexResults = libraryPathRegex.exec(lutrisConfig);
if (regexResults === null) {
throw new Error("No matching game_path found");
}
const libraryPath = regexResults[1].trim();
const libraryPathExists = await this._fileService.pathExists(libraryPath);
if (libraryPathExists) {
for (const wowDir of LUTRIS_WOW_DIRS) {
const productPath = path.join(libraryPath, wowDir);
const productPathExists = await this._fileService.pathExists(productPath);
if (productPathExists) {
console.log(`Found WoW product in Lutris library at ${productPath}`);
return productPath;
}
}
}
}
throw new Error();
} catch (e) {
console.error("Failed to search for Lutris library location", e);
}
return "";
}
public getExecutableName(clientType: WowClientType): string {
switch (clientType) {
case WowClientType.Retail:
@@ -166,17 +99,13 @@ export class WarcraftServiceLinux implements WarcraftServiceImpl {
return WowClientType.Classic;
}
case WOW_RETAIL_PTR_NAME:
if (binaryPath.toLowerCase().includes(WOW_RETAIL_XPTR_FOLDER)) {
return WowClientType.RetailXPtr;
} else {
return WowClientType.RetailPtr;
}
return binaryPath.toLowerCase().includes(WOW_RETAIL_XPTR_FOLDER)
? WowClientType.RetailXPtr
: WowClientType.RetailPtr;
case WOW_CLASSIC_PTR_NAME:
if (binaryPath.toLowerCase().includes(WOW_CLASSIC_ERA_PTR_FOLDER)) {
return WowClientType.ClassicEraPtr;
} else {
return WowClientType.ClassicPtr;
}
return binaryPath.toLowerCase().includes(WOW_CLASSIC_ERA_PTR_FOLDER)
? WowClientType.ClassicEraPtr
: WowClientType.ClassicPtr;
case WOW_RETAIL_BETA_NAME:
return WowClientType.Beta;
case WOW_CLASSIC_BETA_NAME:
@@ -185,4 +114,61 @@ export class WarcraftServiceLinux implements WarcraftServiceImpl {
return WowClientType.None;
}
}
public resolveProducts(decodedProducts: InstalledProduct[], agentPath: string): InstalledProduct[] {
const agentPathPrefixRegex = /.*drive_c/;
const resolvedProducts: InstalledProduct[] = [];
for (const product of decodedProducts) {
const match = agentPathPrefixRegex.exec(agentPath);
if (match === null) {
console.warn("No agentPath match found");
continue;
}
const agentPathPrefix = match[0].trim();
resolvedProducts.push({
...product,
location: path.join(agentPathPrefix, product.location.substring(3)),
});
}
return resolvedProducts;
}
private async getLutrisWowPath(): Promise<string> {
const resolvedPath = path.join(os.homedir(), LUTRIS_CONFIG_PATH);
try {
if (!(await pathExists(resolvedPath))) {
throw new Error("Lutris config not found");
}
const lutrisConfig = await fsp.readFile(resolvedPath, "utf-8");
const match = /game_path: (.*)/.exec(lutrisConfig);
if (match === null) {
throw new Error("No matching game_path found");
}
const libraryPath = match[1].trim();
if (!(await pathExists(libraryPath))) {
throw new Error("Lutris library path does not exist");
}
for (const wowDir of LUTRIS_WOW_DIRS) {
const productPath = path.join(libraryPath, wowDir);
if (await pathExists(productPath)) {
console.log(`Found WoW product in Lutris library at ${productPath}`);
return productPath;
}
}
} catch (e) {
console.error("Failed to search for Lutris library location", e);
}
return "";
}
}
async function pathExists(filePath: string): Promise<boolean> {
return fsp.access(filePath).then(() => true).catch(() => false);
}

View File

@@ -1,15 +1,15 @@
import * as fsp from "fs/promises";
import * as path from "path";
import { WowClientType } from "wowup-lib-core";
import { InstalledProduct } from "wowup-lib-core";
import {
WOW_ANNIVERSARY_FOLDER,
WOW_CLASSIC_ERA_FOLDER,
WOW_CLASSIC_ERA_PTR_FOLDER,
WOW_RETAIL_XPTR_FOLDER,
} from "../../../common/constants";
import { FileService } from "../files/file.service";
import { WarcraftServiceImpl } from "./warcraft.service.impl";
} from "../../../src/common/constants";
import { InstalledProduct, WowClientType } from "wowup-lib-core";
import { WarcraftPlatform } from "./warcraft-platform.service";
const WOW_RETAIL_NAME = "World of Warcraft.app";
const WOW_RETAIL_PTR_NAME = "World of Warcraft Test.app";
@@ -30,9 +30,7 @@ const WOW_APP_NAMES = [
const BLIZZARD_AGENT_PATH = "/Users/Shared/Battle.net/Agent";
const BLIZZARD_PRODUCT_DB_NAME = "product.db";
export class WarcraftServiceMac implements WarcraftServiceImpl {
public constructor(private _fileService: FileService) {}
export class WarcraftPlatformMac implements WarcraftPlatform {
public getExecutableExtension(): string {
return "app";
}
@@ -41,13 +39,9 @@ export class WarcraftServiceMac implements WarcraftServiceImpl {
return WOW_APP_NAMES.includes(appName);
}
/**
* Attempt to figure out where the blizzard agent was installed at
*/
public async getBlizzardAgentPath(): Promise<string> {
const agentPath = path.join(BLIZZARD_AGENT_PATH, BLIZZARD_PRODUCT_DB_NAME);
const exists = await this._fileService.pathExists(agentPath);
return exists ? agentPath : "";
return (await pathExists(agentPath)) ? agentPath : "";
}
public getExecutableName(clientType: WowClientType): string {
@@ -60,7 +54,7 @@ export class WarcraftServiceMac implements WarcraftServiceImpl {
return WOW_CLASSIC_NAME;
case WowClientType.RetailPtr:
case WowClientType.RetailXPtr:
return WOW_RETAIL_NAME;
return WOW_RETAIL_PTR_NAME;
case WowClientType.ClassicPtr:
case WowClientType.ClassicEraPtr:
return WOW_CLASSIC_PTR_NAME;
@@ -87,17 +81,13 @@ export class WarcraftServiceMac implements WarcraftServiceImpl {
return WowClientType.Classic;
}
case WOW_RETAIL_PTR_NAME:
if (binaryPath.toLowerCase().includes(WOW_RETAIL_XPTR_FOLDER)) {
return WowClientType.RetailXPtr;
} else {
return WowClientType.RetailPtr;
}
return binaryPath.toLowerCase().includes(WOW_RETAIL_XPTR_FOLDER)
? WowClientType.RetailXPtr
: WowClientType.RetailPtr;
case WOW_CLASSIC_PTR_NAME:
if (binaryPath.toLowerCase().includes(WOW_CLASSIC_ERA_PTR_FOLDER)) {
return WowClientType.ClassicEraPtr;
} else {
return WowClientType.ClassicPtr;
}
return binaryPath.toLowerCase().includes(WOW_CLASSIC_ERA_PTR_FOLDER)
? WowClientType.ClassicEraPtr
: WowClientType.ClassicPtr;
case WOW_RETAIL_BETA_NAME:
return WowClientType.Beta;
case WOW_CLASSIC_BETA_NAME:
@@ -111,3 +101,7 @@ export class WarcraftServiceMac implements WarcraftServiceImpl {
return decodedProducts;
}
}
async function pathExists(filePath: string): Promise<boolean> {
return fsp.access(filePath).then(() => true).catch(() => false);
}

View File

@@ -0,0 +1,85 @@
import * as fsp from "fs/promises";
import * as log from "electron-log/main";
import { InstalledProduct, WowClientType } from "wowup-lib-core";
import {
WOW_ANNIVERSARY_FOLDER,
WOW_BETA_FOLDER,
WOW_CLASSIC_BETA_FOLDER,
WOW_CLASSIC_ERA_FOLDER,
WOW_CLASSIC_ERA_PTR_FOLDER,
WOW_CLASSIC_FOLDER,
WOW_CLASSIC_PTR_FOLDER,
WOW_RETAIL_FOLDER,
WOW_RETAIL_PTR_FOLDER,
WOW_RETAIL_XPTR_FOLDER,
} from "../../../src/common/constants";
import { ProductDb } from "../../../src/common/wowup/product-db";
export interface WarcraftPlatform {
getExecutableExtension(): string;
isWowApplication(appName: string): boolean;
getBlizzardAgentPath(): Promise<string>;
getExecutableName(clientType: WowClientType): string;
getClientType(binaryPath: string): WowClientType;
resolveProducts(decodedProducts: InstalledProduct[], agentPath: string): InstalledProduct[];
}
function getClientTypeForFolderName(folderName: string): WowClientType {
switch (folderName) {
case WOW_RETAIL_FOLDER:
return WowClientType.Retail;
case WOW_RETAIL_PTR_FOLDER:
return WowClientType.RetailPtr;
case WOW_RETAIL_XPTR_FOLDER:
return WowClientType.RetailXPtr;
case WOW_CLASSIC_ERA_FOLDER:
return WowClientType.ClassicEra;
case WOW_CLASSIC_FOLDER:
return WowClientType.Classic;
case WOW_CLASSIC_PTR_FOLDER:
return WowClientType.ClassicPtr;
case WOW_BETA_FOLDER:
return WowClientType.Beta;
case WOW_CLASSIC_BETA_FOLDER:
return WowClientType.ClassicBeta;
case WOW_CLASSIC_ERA_PTR_FOLDER:
return WowClientType.ClassicEraPtr;
case WOW_ANNIVERSARY_FOLDER:
return WowClientType.Anniversary;
default:
return WowClientType.None;
}
}
export async function decodeProducts(productDbPath: string): Promise<InstalledProduct[]> {
if (!productDbPath) {
return [];
}
try {
const data = await fsp.readFile(productDbPath);
const productDb = ProductDb.decode(data);
log.debug("productDb", JSON.stringify(productDb));
const wowProducts: InstalledProduct[] = productDb.products
.filter((p) => p.family === "wow")
.map((p) => ({
location: p.client.location,
name: p.client.name,
clientType: getClientTypeForFolderName(p.client.name),
}));
return wowProducts.filter((wp) => {
if (wp.clientType === WowClientType.None) {
log.warn("Invalid client type detected", wp);
return false;
}
return true;
});
} catch (e) {
log.error(`Failed to decode product db at ${productDbPath}`, e);
return [];
}
}

View File

@@ -1,16 +1,16 @@
import * as fsp from "fs/promises";
import * as nodeDiskInfo from "node-disk-info";
import * as path from "path";
import { ElectronService } from "../electron/electron.service";
import { FileService } from "../files/file.service";
import { WarcraftServiceImpl } from "./warcraft.service.impl";
import {
IPC_LIST_DISKS_WIN32,
WOW_ANNIVERSARY_FOLDER,
WOW_CLASSIC_ERA_FOLDER,
WOW_CLASSIC_ERA_PTR_FOLDER,
WOW_RETAIL_XPTR_FOLDER,
} from "../../../common/constants";
import { WowClientType } from "wowup-lib-core";
import { InstalledProduct } from "wowup-lib-core";
} from "../../../src/common/constants";
import { InstalledProduct, WowClientType } from "wowup-lib-core";
import { WarcraftPlatform } from "./warcraft-platform.service";
const WOW_RETAIL_NAME = "Wow.exe";
const WOW_RETAIL_PTR_NAME = "WowT.exe";
@@ -44,39 +44,29 @@ const WOW_APP_NAMES_ARM64 = [
WOW_CLASSIC_BETA_NAME_ARM64,
];
// BLIZZARD STRINGS
const WINDOWS_BLIZZARD_AGENT_PATH = "ProgramData/Battle.net/Agent";
const BLIZZARD_PRODUCT_DB_NAME = "product.db";
export class WarcraftServiceWin implements WarcraftServiceImpl {
public constructor(
private _electronService: ElectronService,
private _fileService: FileService,
) {}
export class WarcraftPlatformWin implements WarcraftPlatform {
private get isArm64(): boolean {
return process.arch === "arm64";
}
public getExecutableExtension(): string {
return "exe";
}
public isWowApplication(appName: string): boolean {
const nameList = this._electronService.isArm64 ? WOW_APP_NAMES_ARM64 : WOW_APP_NAMES;
const nameList = this.isArm64 ? WOW_APP_NAMES_ARM64 : WOW_APP_NAMES;
return nameList.includes(appName);
}
/**
* Attempt to figure out where the blizzard agent was installed at
*/
public async getBlizzardAgentPath(): Promise<string> {
try {
const diskInfo = await this._electronService.invoke(IPC_LIST_DISKS_WIN32);
console.debug("diskInfo", diskInfo);
const driveNames: string[] = diskInfo.map((i) => i.mounted);
for (const name of driveNames) {
const agentPath = path.join(name, WINDOWS_BLIZZARD_AGENT_PATH, BLIZZARD_PRODUCT_DB_NAME);
const exists = await this._fileService.pathExists(agentPath);
if (exists) {
const diskInfos = await nodeDiskInfo.getDiskInfo();
for (const disk of diskInfos) {
const agentPath = path.join(disk.mounted, WINDOWS_BLIZZARD_AGENT_PATH, BLIZZARD_PRODUCT_DB_NAME);
if (await pathExists(agentPath)) {
console.log(`Found products at ${agentPath}`);
return agentPath;
}
@@ -91,21 +81,21 @@ export class WarcraftServiceWin implements WarcraftServiceImpl {
public getExecutableName(clientType: WowClientType): string {
switch (clientType) {
case WowClientType.Retail:
return this.getRetailName();
return this.isArm64 ? WOW_RETAIL_NAME_ARM64 : WOW_RETAIL_NAME;
case WowClientType.ClassicEra:
case WowClientType.Classic:
case WowClientType.Anniversary:
return this.getClassicName();
return this.isArm64 ? WOW_CLASSIC_NAME_ARM64 : WOW_CLASSIC_NAME;
case WowClientType.RetailPtr:
case WowClientType.RetailXPtr:
return this.getRetailPtrName();
return this.isArm64 ? WOW_RETAIL_PTR_NAME_ARM64 : WOW_RETAIL_PTR_NAME;
case WowClientType.ClassicPtr:
case WowClientType.ClassicEraPtr:
return this.getClassicPtrName();
return this.isArm64 ? WOW_CLASSIC_PTR_NAME_ARM64 : WOW_CLASSIC_PTR_NAME;
case WowClientType.Beta:
return this.getRetailBetaName();
return this.isArm64 ? WOW_RETAIL_BETA_NAME_ARM64 : WOW_RETAIL_BETA_NAME;
case WowClientType.ClassicBeta:
return this.getClassicBetaName();
return this.isArm64 ? WOW_CLASSIC_BETA_NAME_ARM64 : WOW_CLASSIC_BETA_NAME;
default:
return "";
}
@@ -113,69 +103,61 @@ export class WarcraftServiceWin implements WarcraftServiceImpl {
public getClientType(binaryPath: string): WowClientType {
const binaryName = path.basename(binaryPath);
let clientType: WowClientType = WowClientType.None;
switch (binaryName) {
case WOW_RETAIL_NAME:
case WOW_RETAIL_NAME_ARM64:
return WowClientType.Retail;
clientType = WowClientType.Retail;
break;
case WOW_CLASSIC_NAME:
case WOW_CLASSIC_NAME_ARM64:
if (binaryPath.toLowerCase().includes(WOW_CLASSIC_ERA_FOLDER)) {
return WowClientType.ClassicEra;
clientType = WowClientType.ClassicEra;
} else if (binaryPath.toLowerCase().includes(WOW_ANNIVERSARY_FOLDER)) {
return WowClientType.Anniversary;
clientType = WowClientType.Anniversary;
} else {
return WowClientType.Classic;
clientType = WowClientType.Classic;
}
break;
case WOW_RETAIL_PTR_NAME:
case WOW_RETAIL_PTR_NAME_ARM64:
if (binaryPath.toLowerCase().includes(WOW_RETAIL_XPTR_FOLDER)) {
return WowClientType.RetailXPtr;
} else {
return WowClientType.RetailPtr;
}
clientType = binaryPath.toLowerCase().includes(WOW_RETAIL_XPTR_FOLDER)
? WowClientType.RetailXPtr
: WowClientType.RetailPtr;
break;
case WOW_CLASSIC_PTR_NAME:
case WOW_CLASSIC_PTR_NAME_ARM64:
if (binaryPath.toLowerCase().includes(WOW_CLASSIC_ERA_PTR_FOLDER)) {
return WowClientType.ClassicEraPtr;
} else {
return WowClientType.ClassicPtr;
}
clientType = binaryPath.toLowerCase().includes(WOW_CLASSIC_ERA_PTR_FOLDER)
? WowClientType.ClassicEraPtr
: WowClientType.ClassicPtr;
break;
case WOW_RETAIL_BETA_NAME:
case WOW_RETAIL_BETA_NAME_ARM64:
return WowClientType.Beta;
clientType = WowClientType.Beta;
break;
case WOW_CLASSIC_BETA_NAME:
case WOW_CLASSIC_BETA_NAME_ARM64:
return WowClientType.ClassicBeta;
clientType = WowClientType.ClassicBeta;
break;
default:
return WowClientType.None;
clientType = WowClientType.None;
}
if (clientType === WowClientType.None) {
console.warn(`Unknown client type for binary path: ${binaryPath}`);
}
return clientType;
}
public resolveProducts(decodedProducts: InstalledProduct[]): InstalledProduct[] {
return decodedProducts;
}
private getRetailName(): string {
return this._electronService.isArm64 ? WOW_RETAIL_NAME_ARM64 : WOW_RETAIL_NAME;
}
private getClassicName(): string {
return this._electronService.isArm64 ? WOW_CLASSIC_NAME_ARM64 : WOW_CLASSIC_NAME;
}
private getRetailPtrName(): string {
return this._electronService.isArm64 ? WOW_RETAIL_PTR_NAME_ARM64 : WOW_RETAIL_PTR_NAME;
}
private getClassicPtrName(): string {
return this._electronService.isArm64 ? WOW_CLASSIC_PTR_NAME_ARM64 : WOW_CLASSIC_PTR_NAME;
}
private getRetailBetaName(): string {
return this._electronService.isArm64 ? WOW_RETAIL_BETA_NAME_ARM64 : WOW_RETAIL_BETA_NAME;
}
private getClassicBetaName(): string {
return this._electronService.isArm64 ? WOW_CLASSIC_BETA_NAME_ARM64 : WOW_CLASSIC_BETA_NAME;
}
}
async function pathExists(filePath: string): Promise<boolean> {
return fsp
.access(filePath)
.then(() => true)
.catch(() => false);
}

View File

@@ -0,0 +1,3 @@
{
"extends": "../tsconfig.serve.json"
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "wowup",
"productName": "WowUp",
"version": "2.22.1-beta.3",
"name": "wowup-cf",
"productName": "WowUpCf",
"version": "2.23.0-beta.1",
"private": true,
"description": "World of Warcraft addon updater",
"keywords": [
@@ -11,7 +11,7 @@
"homepage": "https://wowup.io",
"repository": {
"type": "git",
"url": "https://github.com/WowUp/WowUp.git"
"url": "https://github.com/WowUp/WowUp.CF.git"
},
"author": {
"name": "WowUp LLC",
@@ -73,9 +73,9 @@
"dependencies": {
"adm-zip": "0.5.12",
"auto-launch": "5.0.6",
"electron-log": "5.4.3",
"electron-log": "5.4.4",
"electron-store": "8.2.0",
"electron-updater": "6.8.3",
"electron-updater": "6.8.9",
"globrex": "0.1.2",
"handlebars": "4.7.8",
"lodash": "4.17.21",
@@ -87,8 +87,9 @@
"rxjs": "7.8.1",
"win-ca": "3.5.1",
"wowup-lib-core": "file:../wowup-lib",
"yauzl": "2.10.0"
"yauzl": "3.4.0"
},
"overrides": {},
"devDependencies": {
"@angular-builders/custom-webpack": "17.0.2",
"@angular-devkit/build-angular": "17.3.4",
@@ -119,8 +120,8 @@
"@microsoft/applicationinsights-web": "3.0.5",
"@ngx-translate/core": "15.0.0",
"@ngx-translate/http-loader": "8.0.0",
"@overwolf/ow-electron": "37.10.3",
"@overwolf/ow-electron-builder": "26.0.12",
"@overwolf/ow-electron": "39.8.10",
"@overwolf/ow-electron-builder": "26.9.0",
"@types/adm-zip": "0.5.1",
"@types/flat": "5.0.2",
"@types/globrex": "0.1.2",
@@ -142,11 +143,11 @@
"chai": "4.3.8",
"core-js": "3.38.1",
"cross-env": "7.0.3",
"curseforge-v2": "1.4.0",
"curseforge-v2": "1.5.0",
"del": "7.1.0",
"dotenv": "16.4.5",
"electron": "40.6.0",
"electron-builder": "26.8.1",
"electron": "43.1.1",
"electron-builder": "26.15.3",
"electron-notarize": "1.2.2",
"electron-reload": "2.0.0-alpha.1",
"eslint": "^8.57.0",

View File

@@ -14,6 +14,7 @@ import {
AddonSearchResultFile,
AddonWarningType,
AdPageOptions,
DownloadAuth,
GetAllBatchResult,
GetAllResult,
getEnumName,
@@ -95,6 +96,7 @@ const GAME_TYPE_LISTS = [
export class CurseAddonProvider extends AddonProvider {
private readonly _circuitBreaker: CircuitBreakerWrapper;
private readonly _cf2Client: cfv2.CFV2Client;
private readonly _cf2ApiKey = AppConfig.curseforge.apiKey;
public readonly name = ADDON_PROVIDER_CURSEFORGE;
public readonly forceIgnore = false;
@@ -120,7 +122,7 @@ export class CurseAddonProvider extends AddonProvider {
);
this._cf2Client = new cfv2.CFV2Client({
apiKey: AppConfig.curseforge.apiKey,
apiKey: this._cf2ApiKey,
});
}
@@ -488,6 +490,14 @@ export class CurseAddonProvider extends AddonProvider {
};
}
public override getDownloadAuth(): Promise<DownloadAuth | undefined> {
return Promise.resolve({
headers: {
"X-Api-Key": this._cf2ApiKey,
},
});
}
private isCfFileCompatible(clientType: WowClientType, file: cfv2.CF2File): boolean {
if (Array.isArray(file.sortableGameVersions) && file.sortableGameVersions.length > 0) {
const gameVersionTypeId = this.getGameVersionTypeId(clientType);

View File

@@ -1,5 +1,5 @@
import { dirname } from "path";
import { BehaviorSubject, from, of, Subscription } from "rxjs";
import { BehaviorSubject, from, Observable, of, Subscription } from "rxjs";
import { filter, map, switchMap } from "rxjs/operators";
import { Component, Input, OnDestroy, OnInit } from "@angular/core";
@@ -43,11 +43,10 @@ export class WowClientOptionsComponent implements OnInit, OnDestroy {
public selectedAddonChannelType: AddonChannelType = AddonChannelType.Stable;
public editMode$ = this._editModeSrc.asObservable();
public isBusy$ = this._isBusySrc.asObservable();
public installationCount$ = this._warcraftInstallationService.wowInstallations$.pipe(
map((installations) => installations.length),
);
public installationCount$: Observable<number>;
public clientAutoUpdate = false;
public executableName = "";
public set isBusy(enabled: boolean) {
this._isBusySrc.next(enabled);
@@ -63,14 +62,6 @@ export class WowClientOptionsComponent implements OnInit, OnDestroy {
}
}
public get executableName(): string {
if (!this.installation) {
return "";
}
return this._warcraftService.getExecutableName(this.installation.clientType);
}
public get wowLogoImage(): string {
if (!this.installation) {
return "";
@@ -106,6 +97,7 @@ export class WowClientOptionsComponent implements OnInit, OnDestroy {
private _electronService: ElectronService,
) {
this.addonChannelInfos = this.getAddonChannelInfos();
this.installationCount$ = this._warcraftInstallationService.wowInstallations$.pipe(map((i) => i.length));
const editingSub = this._sessionService.editingWowInstallationId$
.pipe(filter((installationId) => this.installationId !== installationId))
@@ -122,6 +114,11 @@ export class WowClientOptionsComponent implements OnInit, OnDestroy {
throw new Error(`Failed to find installation: ${this.installationId}`);
}
this._warcraftService
.getExecutableName(this.installation.clientType)
.then((name) => (this.executableName = name))
.catch(console.error);
this.resetInstallationModel();
this.selectedAddonChannelType = this.installation.defaultAddonChannelType;
@@ -231,6 +228,8 @@ export class WowClientOptionsComponent implements OnInit, OnDestroy {
if (this.installation) {
return from(this._warcraftInstallationService.removeWowInstallation(this.installation));
}
return of(undefined);
}),
)
.subscribe();

View File

@@ -43,7 +43,7 @@ export interface InstallQueueItem {
const IGNORED_FOLDER_NAMES = ["__MACOSX"];
const ADDON_PROVIDER_TOC_EXTERNAL_ID_MAP = {
const ADDON_PROVIDER_TOC_EXTERNAL_ID_MAP: Record<string, keyof Toc> = {
[ADDON_PROVIDER_WOWINTERFACE]: "wowInterfaceId",
[ADDON_PROVIDER_TUKUI]: "tukUiProjectId",
[ADDON_PROVIDER_WAGO]: "wagoAddonId",
@@ -123,6 +123,7 @@ export class AddonInstallService {
try {
const downloadAuth = await addonProvider.getDownloadAuth();
console.debug(`Download auth for ${addon.name}:`, downloadAuth);
let retryCt = 0;
while (downloadedFilePath.length === 0) {
@@ -479,7 +480,7 @@ export class AddonInstallService {
// Remove external ids that are not valid that we may have saved previously
_.remove(
newAddon.externalIds ?? [],
(extId) => !this._addonProviderService.getProvider(extId.providerName)?.isValidAddonId(extId.id) ?? false,
(extId) => !this._addonProviderService.getProvider(extId.providerName)?.isValidAddonId(extId.id),
);
await this.saveAddon(newAddon);

View File

@@ -89,7 +89,7 @@ export interface AddonActionEvent {
const IGNORED_FOLDER_NAMES = ["__MACOSX"];
const ADDON_PROVIDER_TOC_EXTERNAL_ID_MAP = {
const ADDON_PROVIDER_TOC_EXTERNAL_ID_MAP: Record<string, keyof Toc> = {
[ADDON_PROVIDER_WOWINTERFACE]: "wowInterfaceId",
[ADDON_PROVIDER_TUKUI]: "tukUiProjectId",
[ADDON_PROVIDER_CURSEFORGE]: "curseProjectId",

View File

@@ -0,0 +1,44 @@
import { Injectable } from "@angular/core";
import {
IPC_WARCRAFT_GET_BLIZZARD_AGENT_PATH,
IPC_WARCRAFT_GET_CLIENT_TYPE_FOR_BINARY,
IPC_WARCRAFT_GET_EXECUTABLE_EXTENSION,
IPC_WARCRAFT_GET_EXECUTABLE_NAME,
IPC_WARCRAFT_GET_INSTALLED_PRODUCTS,
IPC_WARCRAFT_IS_WOW_APPLICATION,
} from "../../../common/constants";
import { InstalledProduct, WowClientType } from "wowup-lib-core";
import { ElectronService } from "../electron/electron.service";
@Injectable({
providedIn: "root",
})
export class WarcraftApiService {
public constructor(private readonly _electronService: ElectronService) {}
public getBlizzardAgentPath(): Promise<string> {
return this._electronService.invoke(IPC_WARCRAFT_GET_BLIZZARD_AGENT_PATH);
}
public getInstalledProducts(agentPath: string): Promise<Map<WowClientType, InstalledProduct>> {
return this._electronService.invoke(IPC_WARCRAFT_GET_INSTALLED_PRODUCTS, agentPath);
}
public getExecutableName(clientType: WowClientType): Promise<string> {
return this._electronService.invoke(IPC_WARCRAFT_GET_EXECUTABLE_NAME, clientType);
}
public getClientTypeForBinary(binaryPath: string): Promise<WowClientType> {
return this._electronService.invoke(IPC_WARCRAFT_GET_CLIENT_TYPE_FOR_BINARY, binaryPath);
}
public isWowApplication(appName: string): Promise<boolean> {
return this._electronService.invoke(IPC_WARCRAFT_IS_WOW_APPLICATION, appName);
}
public getExecutableExtension(): Promise<string> {
return this._electronService.invoke(IPC_WARCRAFT_GET_EXECUTABLE_EXTENSION);
}
}

View File

@@ -3,7 +3,7 @@
import { IpcRendererEvent, OpenDialogOptions, OpenDialogReturnValue, OpenExternalOptions, Settings } from "electron";
import { LoginItemSettings } from "electron/main";
import { find } from "lodash";
import * as minimist from "minimist";
import minimist from "minimist";
import { BehaviorSubject, ReplaySubject, Subject } from "rxjs";
import { v4 as uuidv4 } from "uuid";

View File

@@ -160,7 +160,7 @@ export class WarcraftInstallationService {
public async selectWowClientPath(): Promise<string> {
const selectionName = this._translateService.instant("COMMON.WOW_EXE_SELECTION_NAME");
const extensionFilter = this._warcraftService.getExecutableExtension();
const extensionFilter = await this._warcraftService.getExecutableExtension();
let dialogResult: Electron.OpenDialogReturnValue;
if (extensionFilter) {
dialogResult = await this._electronService.showOpenDialog({
@@ -217,7 +217,7 @@ export class WarcraftInstallationService {
}
public async createWowInstallationForPath(applicationPath: string): Promise<WowInstallation> {
const clientType = this._warcraftService.getClientTypeForBinary(applicationPath);
const clientType = await this._warcraftService.getClientTypeForBinary(applicationPath);
const typeName = getEnumName(WowClientType, clientType);
const currentInstallations = await this.getWowInstallationsByClientType(clientType);
@@ -260,7 +260,7 @@ export class WarcraftInstallationService {
: DEFAULT_NAME_TOKEN;
const displayName = await this.getDisplayName(label, typeName);
const fullProductPath = this.getFullProductPath(product.location, product.clientType);
const fullProductPath = await this.getFullProductPath(product.location, product.clientType);
if (currentInstallations.some((inst) => inst.location === fullProductPath)) {
continue;
@@ -292,7 +292,6 @@ export class WarcraftInstallationService {
const defaultName: string = await firstValueFrom(
this._translateService.get(`COMMON.CLIENT_TYPES.${typeName.toUpperCase()}`),
);
console.debug("getDisplayName", defaultName, label, typeName);
const finalLabel = label.replace(DEFAULT_NAME_TOKEN, defaultName);
if (finalLabel.includes(DEFAULT_NAME_TOKEN)) {
@@ -302,9 +301,9 @@ export class WarcraftInstallationService {
return finalLabel;
}
private getFullProductPath(location: string, clientType: WowClientType): string {
private async getFullProductPath(location: string, clientType: WowClientType): Promise<string> {
const clientFolderName = getWowClientFolderName(clientType);
const executableName = this._warcraftService.getExecutableName(clientType);
const executableName = await this._warcraftService.getExecutableName(clientType);
return path.join(location, clientFolderName, executableName);
}
}

View File

@@ -1,11 +0,0 @@
import { WowClientType } from "wowup-lib-core";
import { InstalledProduct } from "wowup-lib-core";
export interface WarcraftServiceImpl {
getExecutableExtension(): string;
isWowApplication(appName: string): boolean;
getBlizzardAgentPath(): Promise<string>;
getExecutableName(clientType: WowClientType): string;
getClientType(binaryPath: string): WowClientType;
resolveProducts(decodedProducts: InstalledProduct[], agentPath: string): InstalledProduct[];
}

View File

@@ -4,18 +4,12 @@ import { filter, map } from "rxjs/operators";
import { Injectable } from "@angular/core";
import { ElectronService } from "../electron/electron.service";
import * as constants from "../../../common/constants";
import { SelectItem } from "../../models/wowup/select-item";
import { getEnumName, getEnumList } from "wowup-lib-core";
import { FileService } from "../files/file.service";
import { PreferenceStorageService } from "../storage/preference-storage.service";
import { TocService } from "../toc/toc.service";
import { WarcraftServiceImpl } from "./warcraft.service.impl";
import { WarcraftServiceLinux } from "./warcraft.service.linux";
import { WarcraftServiceMac } from "./warcraft.service.mac";
import { WarcraftServiceWin } from "./warcraft.service.win";
import { ProductDb } from "../../../common/wowup/product-db";
import { WarcraftApiService } from "../api/warcraft-api.service";
import { AddonFolder, Toc, WowClientType } from "wowup-lib-core";
import { InstalledProduct, WowInstallation } from "wowup-lib-core";
@@ -23,7 +17,6 @@ import { InstalledProduct, WowInstallation } from "wowup-lib-core";
providedIn: "root",
})
export class WarcraftService {
private readonly _impl: WarcraftServiceImpl;
private readonly _productsSrc = new BehaviorSubject<InstalledProduct[]>([]);
private readonly _installedClientTypesSrc = new BehaviorSubject<WowClientType[] | undefined>(undefined);
private readonly _allClientTypes = getEnumList<WowClientType>(WowClientType).filter(
@@ -34,7 +27,6 @@ export class WarcraftService {
public readonly productsReady$ = this.products$.pipe(filter((products) => Array.isArray(products)));
public readonly installedClientTypes$ = this._installedClientTypesSrc.asObservable();
// Map the client types so that we can localize them
public installedClientTypesSelectItems$ = this._installedClientTypesSrc.pipe(
filter((clientTypes) => clientTypes !== undefined),
map((clientTypes) => {
@@ -53,20 +45,17 @@ export class WarcraftService {
);
public constructor(
private _electronService: ElectronService,
private _fileService: FileService,
private _preferenceStorageService: PreferenceStorageService,
private _tocService: TocService,
) {
this._impl = this.getImplementation();
private readonly _warcraftApiService: WarcraftApiService,
private readonly _fileService: FileService,
private readonly _tocService: TocService,
) {}
public getExecutableName(clientType: WowClientType): Promise<string> {
return this._warcraftApiService.getExecutableName(clientType);
}
public getExecutableName(clientType: WowClientType): string {
return this._impl.getExecutableName(clientType);
}
public getExecutableExtension(): string {
return this._impl.getExecutableExtension();
public getExecutableExtension(): Promise<string> {
return this._warcraftApiService.getExecutableExtension();
}
public async isWowApplication(appPath: string): Promise<boolean> {
@@ -76,7 +65,7 @@ export class WarcraftService {
}
const fileName = path.basename(appPath);
return this._impl.isWowApplication(fileName);
return this._warcraftApiService.isWowApplication(fileName);
}
public getAllClientTypes(): WowClientType[] {
@@ -91,19 +80,8 @@ export class WarcraftService {
return clientLocation?.location ?? "";
}
/**
* Scan the local blizzard product db for install WoW instances
*/
public async getInstalledProducts(blizzardAgentPath: string): Promise<Map<WowClientType, InstalledProduct>> {
const decodedProducts = await this.decodeProducts(blizzardAgentPath);
const resolvedProducts = this._impl.resolveProducts(decodedProducts, blizzardAgentPath);
const dictionary = new Map<WowClientType, InstalledProduct>();
for (const product of resolvedProducts) {
dictionary.set(product.clientType, product);
}
return dictionary;
public getInstalledProducts(blizzardAgentPath: string): Promise<Map<WowClientType, InstalledProduct>> {
return this._warcraftApiService.getInstalledProducts(blizzardAgentPath);
}
public getAddonFolderPath(installation: WowInstallation): string {
@@ -119,7 +97,6 @@ export class WarcraftService {
const addonFolderPath = this.getAddonFolderPath(installation);
// Folder may not exist if no addons have been installed
const addonFolderExists = await this._fileService.pathExists(addonFolderPath);
if (!addonFolderExists) {
return addonFolders;
@@ -129,8 +106,7 @@ export class WarcraftService {
const dirPaths = directories.map((dir) => path.join(addonFolderPath, dir));
const dirStats = await this._fileService.statFiles(dirPaths);
for (let i = 0; i < directories.length; i += 1) {
const dir = directories[i];
for (const dir of directories) {
const addonFolder = await this.getAddonFolder(addonFolderPath, dir);
if (!addonFolder) {
console.warn(`Failed to get addonFolder, no toc found: ${dir}`);
@@ -138,9 +114,7 @@ export class WarcraftService {
}
addonFolder.fileStats = dirStats[path.join(addonFolderPath, dir)];
if (addonFolder) {
addonFolders.push(addonFolder);
}
addonFolders.push(addonFolder);
}
return addonFolders;
@@ -174,24 +148,15 @@ export class WarcraftService {
}
}
public async getBlizzardAgentPath(): Promise<string> {
const storedAgentPath = await this._preferenceStorageService.getAsync(constants.BLIZZARD_AGENT_PATH_KEY);
if (storedAgentPath) {
return storedAgentPath;
}
const agentPath = await this._impl.getBlizzardAgentPath();
await this._preferenceStorageService.setAsync(constants.BLIZZARD_AGENT_PATH_KEY, agentPath);
return agentPath;
public getBlizzardAgentPath(): Promise<string> {
return this._warcraftApiService.getBlizzardAgentPath();
}
public getClientTypeForBinary(binaryPath: string): WowClientType {
return this._impl.getClientType(binaryPath);
public getClientTypeForBinary(binaryPath: string): Promise<WowClientType> {
return this._warcraftApiService.getClientTypeForBinary(binaryPath);
}
/**
* Get the old style preference key for a WoW client type
* @deprecated
*/
public getLegacyClientLocationKey(clientType: WowClientType): string {
@@ -212,82 +177,4 @@ export class WarcraftService {
throw new Error(`Failed to get client location key: ${clientType}, ${getEnumName(WowClientType, clientType)}`);
}
}
private async decodeProducts(productDbPath: string) {
if (!productDbPath) {
return [];
}
try {
const productDb = await this._electronService.invoke<ProductDb>("decode-product-db", productDbPath);
console.log("productDb", productDb);
let wowProducts: InstalledProduct[] = productDb.products
.filter((p) => p.family === "wow")
.map((p) => ({
location: p.client.location,
name: p.client.name,
clientType: this.getClientTypeForFolderName(p.client.name),
}));
wowProducts = wowProducts.filter((wp) => {
const hasClientType = wp.clientType != WowClientType.None;
if (!hasClientType) {
console.warn("Invalid client type detected", wp);
}
return hasClientType;
});
console.log("wowProducts", wowProducts);
return wowProducts;
} catch (e) {
console.error(`failed to decode product db at ${productDbPath}`);
console.error(e);
return [];
}
}
private getImplementation(): WarcraftServiceImpl {
if (this._electronService.isWin) {
return new WarcraftServiceWin(this._electronService, this._fileService);
}
if (this._electronService.isMac) {
return new WarcraftServiceMac(this._fileService);
}
if (this._electronService.isLinux) {
return new WarcraftServiceLinux(this._electronService, this._fileService);
}
throw new Error("No warcraft service implementation found");
}
private getClientTypeForFolderName(folderName: string): WowClientType {
switch (folderName) {
case constants.WOW_RETAIL_FOLDER:
return WowClientType.Retail;
case constants.WOW_RETAIL_PTR_FOLDER:
return WowClientType.RetailPtr;
case constants.WOW_RETAIL_XPTR_FOLDER:
return WowClientType.RetailXPtr;
case constants.WOW_CLASSIC_ERA_FOLDER:
return WowClientType.ClassicEra;
case constants.WOW_CLASSIC_FOLDER:
return WowClientType.Classic;
case constants.WOW_CLASSIC_PTR_FOLDER:
return WowClientType.ClassicPtr;
case constants.WOW_BETA_FOLDER:
return WowClientType.Beta;
case constants.WOW_CLASSIC_BETA_FOLDER:
return WowClientType.ClassicBeta;
case constants.WOW_CLASSIC_ERA_PTR_FOLDER:
return WowClientType.ClassicEraPtr;
case constants.WOW_ANNIVERSARY_FOLDER:
return WowClientType.Anniversary;
default:
return WowClientType.None;
}
}
}

View File

@@ -15,6 +15,16 @@ export class PatchNotesService {
}
const CHANGELOGS: ChangeLog[] = [
{
Version: "2.23.0",
html: `
<h4>Fixes</h4>
<ul>
<li>Add support for CurseForge download authentication</li>
<li>Various bug fixes and improvements</li>
</ul>
`,
},
{
Version: "2.22.1",
html: `

View File

@@ -110,6 +110,14 @@ export const IPC_PUSH_UNREGISTER = "push-unregister";
export const IPC_PUSH_SUBSCRIBE = "push-subscribe";
export const IPC_PUSH_NOTIFICATION = "push-notification";
// WARCRAFT CONTROLLER
export const IPC_WARCRAFT_GET_BLIZZARD_AGENT_PATH = "warcraft-get-blizzard-agent-path";
export const IPC_WARCRAFT_GET_INSTALLED_PRODUCTS = "warcraft-get-installed-products";
export const IPC_WARCRAFT_GET_EXECUTABLE_NAME = "warcraft-get-executable-name";
export const IPC_WARCRAFT_GET_CLIENT_TYPE_FOR_BINARY = "warcraft-get-client-type-for-binary";
export const IPC_WARCRAFT_IS_WOW_APPLICATION = "warcraft-is-wow-application";
export const IPC_WARCRAFT_GET_EXECUTABLE_EXTENSION = "warcraft-get-executable-extension";
// IPC STORAGE
export const IPC_STORE_GET_OBJECT = "store-get-object";
export const IPC_STORE_GET_OBJECT_SYNC = "store-get-object-sync";