Files
Wand-Enhancer-k1tbyte/AGENTS.md
T
kitbyte f798714f8d chore(build): enforce lint, type-check and dist invariants in build and CI
Add verify-dist.mjs (node --check on bundles, dev-only payload guard).
Wire lint into build.ps1 and type-check both web and bridge in pnpm build.
Drop noImplicitAny override and switch bridge to Bundler resolution.
Run CI on pull_request and push to master.
2026-08-29 16:55:07 +03:00

11 KiB

INFO ./docs/*

Wand Enhancer Agent Notes

This repository patches the Wand Electron app from a .NET Framework WPF desktop tool. Keep changes narrow and preserve the patch pipeline invariants.

Remote Web Panel

  • The default local remote port is 3223. Keep bridge and frontend constants aligned; C# must not duplicate the presentation URL or port.
  • The embedded panel must stay small because the desktop patcher embeds it and then injects it into Wand's app.asar.
  • Remote tooltip links and every rendered remote-qr-code are redirected by web-panel/bridge/scripts/default/remote-popup-cleanup.js. It reuses Wand's loaded QR renderer through the webpack runtime, keeps the local URL visible as a fallback, and hides the Pro onboarding remote mobile app card. Do not reintroduce C# ASAR patches for the tooltip URL or QR component; a changed UI bundle must not make the whole remote-panel patch fail.
  • Production builds must not include mock data, debug routes, sourcemaps, local fonts, heavy icon libraries, or runtime class helper packages.
  • The Electron bridge is authored as TypeScript under web-panel/bridge/src/, but production runtime must be bundled/minified into web-panel/dist/bridge.cjs by pnpm run build:bridge. Do not copy bridge source into Wand or embed it as ASAR resources.
  • Mock/demo data is dev-only and must be reached through import.meta.env.DEV dynamic imports.
  • Source can use React-compatible imports, but production runtime resolves them to Preact aliases in web-panel/vite.config.ts.
  • UI uses Tailwind CSS and lightweight local primitives under web-panel/src/shared/ui/.
  • Default renderer script sources live in web-panel/bridge/scripts/default/ and are bundled/minified into web-panel/dist/renderer-scripts/ by pnpm run build:bridge. Custom user scripts are selected in the WPF patch modal and copied from PatchConfig.CustomScriptPaths; only existing .js files are accepted. A local renderer-scripts/ folder next to the patcher exe is still copied as an advanced fallback.
  • web-panel/bridge/scripts/default/installed-apps-sync.js resolves Wand's renderer services/store and publishes My Games snapshots through the wand-remote-installed-apps IPC channel. The synced list must mirror Wand's my_games source criteria: catalog games come from installedGameVersions, and extra installed unsupported titles come from correlatedUnavailableTitles whose games[].correlationIds match installedApps.
  • If the injected renderer cannot read a populated correlatedUnavailableTitles slice from the live store, installed-apps-sync.js must fall back to Wand's /v3/unavailable_titles correlation lookup through the renderer API client instead of degrading to raw install entries or an empty My Games list.
  • Installed app snapshots should include game artwork in imageUrl when possible. installed-apps-sync.js must prefer Wand's own client icon CDN shape https://api-cdn.wemod.com/steam_community/<steamAppId>/client_icon/96.webp whenever the matched title/game/version metadata contains a Steam AppID, regardless of install platform. Do not assume steamAppId is a flat property; search nested steam* metadata before falling back to installed Steam sku. If metadata still does not expose the icon, fall back to the rendered Wand sidebar DOM (.sidebar-game-row-image background-image) keyed by titleId parsed from data-tooltip-trigger-for. The web panel GameCover must tolerate broken artwork URLs and fall back to its text cover.
  • The same renderer sync script also forwards lifecycle state through wand-remote-game-status: game-launched / game-ended come from Wand's launch monitor service, and trainer runtime comes from the running-trainer visibility service. The web panel consumes this as the game_status websocket message.
  • When Wand does not emit a game-launched event but a trainer is already active, wand-remote-game-status must synthesize a running session from the running-trainer visibility payload so the remote panel does not show an idle game session next to a running trainer.
  • The websocket hello snapshot must still send cached installed_apps and game_status even when no trainer snapshot is active yet; do not reintroduce a handshake path that returns early after trainer_changed.
  • Remote Play/Stop uses the websocket remote_command message. The bridge forwards it over wand-remote-command / wand-remote-command-response, and installed-apps-sync.js resolves Wand's trainer API + trainer service to launch a trainer for a gameId or end the current trainer.
  • Remote Play must construct Wand's real trainer launch request class (69482.vO) before calling trainerService.launch(...). Passing a plain object launches the game process but breaks Wand's getMetadata(vO)-based trainer state, causing missing status, disappearing play/close buttons, and stuck loading behavior.
  • Pro activation is a C# asar patch (EPatchType.ActivatePro, independent of the remote panel / bridge). It wraps the returned promise of three account-returning service methods so subscription:{period:"yearly",state:"active"} is injected before the response reaches the store: getUserAccount, setAccountWandBrandExperience and setAccountLanguage. A fourth patch (setAccountReducer) rewrites the ACTION_SET_ACCOUNT store reducer so any account write (periodic refreshAccount, push/profile updates, etc.) keeps Pro even when it bypasses those API methods. Pro is am(account) = !!account.subscription (flags/512 are irrelevant). setAccountLanguage is the one the original two patches missed, which is why Pro dropped on language change. setAccountWandBrandExperience does not exist on every build, so it is declared optional through CapabilityHints.

Patch Engine

  • Patches are located structurally, not by shape. A patch anchors on something Wand does not rename between builds — an API endpoint, an IPC channel name, a public method name — and then walks the delimiter structure (Core/Js/JsCursor.cs) to the edit site. Identifiers that do change (#Xe, l.vO, the numeric Remote source) are read out of the located region, never baked into a pattern. A rebuild that only reminifies therefore needs no change here.
  • Never write a regex that spans a whole method body or matches across a bundle. Scope patterns to a located JsFunction via Resolve, where they run against a few hundred characters instead of megabytes.
  • A patch is one PatchEntry in Core/EnhancerConfig.cs with a Locate delegate returning the edits to splice. Return null when the anchor is absent from this file — that means "not my file", not "failure". Throw only when the anchor IS present but the surrounding structure is unrecognisable; that is a genuinely unsupported build and must fail loudly.
  • Injected JavaScript lives in WandEnhancer/Patches/*.js and is embedded as patches/<name>.js. Load it with PatchPayload.Load(name, "key", value, ...), which fills ${key} placeholders in one pass. Do not put payload JS back into C# string literals.
  • Multiple edits from one patch are applied highest-offset-first, so their positions stay valid. Keep them non-overlapping.
  • A patch that only exists on some builds sets CapabilityHints: absent capability logs a skip, a detected-but-unpatchable capability still fails the run.
  • When a build really does restructure something, add a fallback branch inside that patch's Locate rather than a version table — old shapes keep working because the old branch is still there.
  • Verify against real bundles, minified and prettified, before shipping: locating must succeed on both and the patched files must pass node --check.

ASAR Patch Pipeline

  • Preserve and restore both resources/app.asar and resources/app.asar.unpacked backups.
  • Inject web-panel/dist as remote-panel/; it must already contain bridge.cjs and generated default renderer scripts under renderer-scripts/. Selected/local custom renderer scripts are then copied under remote-panel/renderer-scripts.
  • Do not commit extracted .source/ or .sources/ output. Recreate it only for reverse-engineering sessions.
  • AsarSharp.AsarExtractor.ExtractAll must skip unpacked entries when their source path equals the destination (in-place extraction is a self-copy that fails on locked files like TrainerLib_x64.dll) and silently skip unpacked entries whose source is missing on disk (e.g. auxiliary/GameLauncher.exe removed by an installer). Do not reintroduce hard failure on either case.
  • The DevToolsOnF12 patch anchors on the Electron main-process <app>.whenReady().then( site and attaches a before-input-event hook to every BrowserWindow.webContents. Do not patch the renderer keydown listener — the minified ACTION_OPEN_DEV_TOOLS dispatch site is not stable across Wand releases.
  • Cheats can be pinned per game in the web panel via pinned-storage.ts (localStorage key wand-remote.pinned-cheats.v1:<gameId>). Pinned cheats render as a virtual pinned category at the top of the list; their normal category placement is preserved.
  • Custom quick presets are per trainer/game and stored by preset-storage.ts under localStorage key wand-remote.presets.v1:<gameId-or-trainerId>. Presets capture persistent cheat values only; do not include button one-shot cheats in saved presets.
  • All localStorage access in web-panel/src/ MUST go through web-panel/src/shared/storage.ts (loadJson / saveJson / loadStringSet / saveStringSet). Do not reintroduce per-capability try/catch + JSON.parse duplication. Trainer/game storage IDs are derived through the shared getTrainerStorageId(trainer) helper; do not re-implement the gameId → titleId → trainerId → 'global' precedence inline.
  • Shared web protocol version, port, and HTTP/WS paths live in web-panel/protocol/web-contract.json. Bridge-only IPC channels, WS opcodes, and renderer injection delays live in web-panel/bridge/src/constants.ts. Do not redeclare these values inline.
  • UI string-union types follow the E* enum convention from .claude/rules/frontend-conventions.md (currently ECheatType in protocol/messages.ts, EConnectionStatus in remote-session/remote-session.reducer.ts); wire string values must remain on the right-hand side of enum members. Reducer action tags stay as discriminated-union string literals.
  • Cheat input controls live one-per-file under web-panel/src/trainer/controls/; shared SliderTrack / StepButton / ControlInternalProps are in controls/shared.tsx and number formatting helpers in controls/format-number.ts. controls/CheatControl.tsx remains a thin dispatcher keyed by ECheatType.
  • Mobile drawer performance is sensitive to backdrop-filter. Keep drawer panels and nested glass controls blur-free under coarse pointers, and do not add per-row backdrop-blur-* inside drawer lists.

Validation

  • Web panel build: cd web-panel && pnpm run build (runs type-check, Vite build, then build:bridge into dist).
  • Bridge/script syntax checks after build: node --check web-panel/dist/bridge.cjs and node --check web-panel/dist/renderer-scripts/remote-popup-cleanup.js.
  • Production dist should contain only static assets and should not contain mock-instance, Mock Adventure, Simulation, Debug session, mock=1, demo-session, vite.svg, tailwind-merge, class-variance-authority, or clsx.