mirror of
https://github.com/WowUp/WowUp.git
synced 2026-07-30 14:57:12 -04:00
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:
62
.claude/skills/commit/SKILL.md
Normal file
62
.claude/skills/commit/SKILL.md
Normal 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.
|
||||
93
.claude/skills/patch-notes/SKILL.md
Normal file
93
.claude/skills/patch-notes/SKILL.md
Normal 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
2
.vscode/tasks.json
vendored
@@ -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
83
CLAUDE.md
Normal 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 3–6 — 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"*).
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
3
wowup-electron/app/tsconfig.json
Normal file
3
wowup-electron/app/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../tsconfig.serve.json"
|
||||
}
|
||||
3691
wowup-electron/package-lock.json
generated
3691
wowup-electron/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
44
wowup-electron/src/app/services/api/warcraft-api.service.ts
Normal file
44
wowup-electron/src/app/services/api/warcraft-api.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: `
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user