Imported from matasarei/wow-launcher (
AGENTS.md). Install upstream withnpx skills add matasarei/wow-launcher. Copyright stays with the author.
Development rules for this project
Read this before changing anything. Deeper background: docs/INTERNALS.md
(architecture, mechanisms, traps — read it first), docs/BUILD-WRAPPER.md
(build walkthrough), docs/THIRD-PARTY.md (embedded components & licenses).
Ground rules
- The repo is the source of truth. Never edit files inside the built
WoW.appdirectly — change the repo, then rebuild via make:make launcherfor the GUI + scripts only,make buildfor the full bundle (helper scripts likebuild.share make's internals, never run directly). The wrapper is a disposable build artifact. - One change at a time, verified in the real game. After each functional change, stop and have the change verified in the running game before making the next one. Do not push experimental fixes until they are confirmed fixed.
- After rebuilding the GUI, the app must be fully quit (Cmd+Q) and reopened — macOS keeps the old instance running and it will look like the change didn't compile.
- No releases and no public distribution of a built wrapper without the maintainer's explicit approval. A wrapper that has ever had a game installed must never be distributed at all.
Localization
UI strings are localized (assets/lproj/.lproj/Localizable.strings, 8 languages, keys = the English strings). When adding or changing a UI string in main.swift: literals localize automatically, dynamically built strings must go through L()/LF() — and every key needs a row in ALL eight .strings files (a missing key silently falls back to English, which is correct for en but leaves other languages untranslated). Script output (verify labels, installer messages) is deliberately NOT localized — it is the shell-tool protocol. Spot-check a language with: open WoW.app --args -AppleLanguages '(ru)'.
Versioning
The app version lives in one place: CFBundleShortVersionString in
assets/Info.plist. The About pane reads it at runtime from the bundle, so a
bump propagates automatically after make build (which copies Info.plist).
make launcher alone does not copy Info.plist — after a version bump either
run make skeleton launcher or a full make build. Release tags on GitHub
should match it (v<version>).
Updating the pinned upstream artifacts
The wine runtime and the patch payloads are pinned in the Makefile:
RUNTIME_URL/RUNTIME_SHA256 and PAYLOAD_URL/PAYLOAD_SHA256. To move to a
newer WoWSilicon runtime or release:
- Update the URL and its sha256 together (download,
shasum -a 256). - Delete
build/deps/(cached artifacts) and the wrapper, rebuild from scratch, and run the full test matrix below. - Check
share/wowsilicon/runtime-lock.jsoninside the new runtime for component changes, and updatedocs/THIRD-PARTY.mdif components or licenses changed.
Making a release
The in-app updater reads the release, so its shape is a contract: the tag is
v<version> matching CFBundleShortVersionString, and the asset is
WoW-v<version>.zip (one app at the zip's top level, sealed by make sign).
GitHub's own digest is what the download is checked against. A release that
breaks any of that is invisible to wow-update — every user stays where they are.
A releasable wrapper must be freshly built — never a used one. A wrapper
that has run installs accumulates Blizzard-derived files (the game under
games/, and self-populated patch-kit references: Wow.exe.*,
DivxDecoder.dll.*, fonts-client/) plus personal data in the prefix and
logs. The procedure:
-
Bump the version in
assets/Info.plist; commit and push everything. -
Build clean:
rm -rf ~/Applications/WoW.app && make build. -
Verify the bundle is pristine:
Resources/games/is empty;Resources/launcher.confcontains onlyAUTO_RES=1;Resources/patch-kit/contains only the open-source payloads,dlls.txtand thewow-icon-*.bsdifffiles — noWow.exe.*, noDivxDecoder.*, nofonts-client/;Resources/logs/is empty;Resources/home/does not exist (wine's HOME: a used wrapper keeps the game's Windows user profile there);Resources/prefix/drive_c/users/holds onlyPublic— no folder named after whoever built it.
-
Run the test matrix (below) with a scratch copy, then rebuild clean again before zipping — testing dirties the wrapper.
-
Archive and publish (only with the maintainer's explicit go-ahead):
make zip APP=/path/to/pristine/WoW.app # → WoW.zip mv WoW.zip WoW-v<version>.zip gh release create v<version> WoW-v<version>.zip \ --title "WoW Launcher v<version>" --notes-file <notes>Don't commit the zip. To keep the maintainer's working wrapper (and its installed game) untouched, build the release copy at a separate path:
make build APP=/tmp/release/WoW.app.Gatekeeper facts (learned the hard way): the bundle must carry a valid deep ad-hoc seal (
make sign, the last wrapper step) or downloaded copies fail as "damaged"; absolute symlinks in the prefix break codesign (stripped by the prefix target). Even with a valid seal, modern macOS refuses quarantined ad-hoc apps with no "Open Anyway" offered — release notes must include thecurlinstall (no quarantine) and thexattr -dr com.apple.quarantinefallback. Frictionless downloads would require Developer ID signing + notarization (paid Apple account).
Tests
make test runs the hermetic suite (tests/run-tests.sh): fake wrapper +
stub wine + fake clients (empty files for the classic three, minimal real PE
headers via mk_pe where a build number or an architecture matters) — client
profiling and capability gating, install validation and patching per client,
verify step counts (43/29/25 for the classic three, computed for anything else)
and CANFIX/REINSTALL/--fix behavior, launch env/exe selection, and the native
font tool against tests/fixtures/locale-*.MPQ (synthetic fonts in a real MPQ
layout, generated by tests/fixtures/make-fixtures.py — dev-only, needs
fontTools). Seconds to run, no game data needed (compiles the font tool with
swiftc once). Run it after ANY script change; add assertions for new
behavior. What it cannot cover stays manual (below).
make test also runs tests/check-strings.sh (make check-strings alone),
which is the only thing that notices a localization mistake: every
Localizable.strings parses, all eight carry exactly the same key set, every
localized literal in main.swift has a key, and every key is still reachable
from the code. Literals meant to read the same in every language — the app
name, a brand, the licence line — are listed in NEVER_TRANSLATED in that
script. make compile type-checks both Swift sources without assembling a
bundle. make lint runs shellcheck over the scripts at error level (needs
brew install shellcheck); a # shellcheck disable=SCxxxx directive takes a
reason only after a second #, or shellcheck ignores it.
CI (.github/workflows/ci.yml, macOS runners, free while the repo is public)
runs those three, plus make lint on Linux, on every push and pull request,
and checks that a v* tag matches CFBundleShortVersionString. A second
workflow asks monthly whether the RUNTIME_URL/PAYLOAD_URL pins still
resolve and whether the wine runtime is still the bytes RUNTIME_SHA256
claims — they live in someone else's releases, and a retag there breaks
make build for every new user silently.
Manual test matrix (after any runtime/installer/launcher change)
- Fresh
make build, open the app. - Install an enUS 3.3.5a client → auto-verify passes (43 checks) → Play: ~120 FPS, maximized Retina window, Dock shows "WoW", fast exit.
- Replace with a ruRU client → Cyrillic input and rendering work
(fonts extracted + stashed in the kit), including after RU→EN→RU layout
toggles and with a Ukrainian layout (
іmust not render as³); reinstall enUS → Cyrillic still works from the stash. - Both renderers launch (Display → DXVK and MTLd3D), and
X87=sidecarinlauncher.confstill boots the game. - Language packs (3.3.5a/2.4.3): import a pack from another-locale client, switch both ways — window must appear and the game language change; the Language section must be absent for 1.12.
- When touching the installer/verify/launch scripts: also install a 1.12 and a 2.4.3 client — version detection, per-version verify totals (25 / 29), vanilla WDB cleanup and root-level realmlist must all hold.
- When touching
wow-client-profileor the capability gates: run it against a real client and checkBUILD/CONFIDENCE(a stock 3.3.5a must reportBUILD=12340 CONFIDENCE=exact), and confirm the Game pane still offers all four patch levels for it. The hermetic suite covers the shapes; only a real client proves the version-resource read.
Layout crib
| Path | Role |
|---|---|
main.swift |
the entire SwiftUI manager (single file by design) |
scripts/wow-* |
runtime helpers installed into Resources/bin/ by build.sh |
tools/wow-client-fonts.swift |
native MPQ font extraction + CP1251 cmap remap, compiled into Resources/bin/ by build.sh |
Makefile |
wrapper assembly: skeleton, runtime download, payloads, patch-kit, prefix, launcher |
build.sh |
internal helper of make launcher: compiles main.swift + the font tool (swiftc, no Xcode) + installs scripts |
assets/ |
Info.plist, icon, icon bsdiffs |
tests/run-tests.sh |
hermetic script tests (make test) — no game data or wine needed |
tests/check-strings.sh |
localization parity (make check-strings, also part of make test) |
.github/workflows/ |
CI: tests, Swift compile, shellcheck, strings parity, tag/version match; monthly upstream-pin check |
tests/fixtures/ |
synthetic locale-*.MPQ font fixtures + their generator |