Pick-up notes for continuing this project (for a future dev / Claude session). As of v1.1.0, 2026-07-20.
A one-click installer + launcher for a GTA V LSPDFR police-mod setup. It turns the
infamous "40 mods, 30 config files" manual install into: download one Setup.exe, pick a
preset, install, and launch through RagePluginHook — with every keybind/setting editable
in-app and written back to the real .ini files.
Stack: .NET 8 (targets net8.0-windows), Avalonia UI (MVVM, CommunityToolkit.Mvvm),
Velopack (packaging + self-update). Build with .NET 10 SDK is fine. Directory.Build.props
enforces TreatWarningsAsErrors + code-style analyzers — keep builds at 0 warnings.
| Repo | Visibility | Role |
|---|---|---|
Jackmod/DISPATCHPRIV |
private | primary source repo (this folder came from here) |
Jackmod/DISPATCH |
public | source mirror + hosts all releases + hosts the mod pack |
Both main branches are kept identical. Workflow used all session:
- branch off
main, commit, push toDISPATCHPRIV, open PR,gh pr merge --squash --delete-branch. git reset --hard origin/main, thengit push public main:main(fast-forward) — where remotepublic=https://github.com/Jackmod/DISPATCH.git.
git identity + gh are already authenticated on the owner's machine (account Jackmod).
Thin installer. The shipped Setup.exe does not bundle the ~2 GB of mods. It carries
a small remote-pack.json (mod → download-URL index) and downloads only the mods the user
ticks, on demand.
- Mods are hosted as assets on the
modpack-v1release ofJackmod/DISPATCH(73 archives, ~1.5 GB), plusremote-pack.jsonitself as an asset. tools/Publish-ModPack.ps1 -Repo Jackmod/DISPATCHuploads/updates the archives and the manifest (resumable, clobbers, BOM-free). Re-run it whenever a mod changes.- Live manifest: on startup the app fetches the hosted
remote-pack.json(seeRemotePackRefresher) into a cache the loader prefers over the bundled copy. So adding or renaming a mod reaches installed copies automatically on next launch — no reinstall. - App self-update:
VelopackAppUpdater(inDispatch.App) checksJackmod/DISPATCHreleases on startup and stages a newer version to apply on next close. Requires releases to carry the Velopack feed (RELEASES+releases.win.json+ full.nupkg), whichvpk upload githubproduces.
Net effect: users never re-download the installer — mods and app versions both flow
automatically. A fat build (bundles the whole pack) still exists via
-p:IncludeModPack=true (the default) but the thin build is what ships.
# Build (must be 0 warnings)
dotnet build Dispatch.slnx -c Release
# Full test suite (583 tests as of v1.1.0)
dotnet test Dispatch.slnx -c Release
# Thin installer build by hand
dotnet publish src/Dispatch.App/Dispatch.App.csproj -c Release -r win-x64 \
--self-contained true -p:IncludeModPack=false -o publish-thin
vpk pack -u Dispatch -v <version> -p publish-thin -e Dispatch.exe -o releases
# Cut a real release (build + pack + publish WITH self-update feed):
./tools/Publish-App.ps1 -Version 1.2.0 # needs dotnet, vpk, gh (authed)
# Update the hosted mod pack (after changing modpack/ archives):
./tools/Publish-ModPack.ps1 -Repo Jackmod/DISPATCHTooling: .NET SDK, vpk (dotnet tool install -g vpk, v1.2.0 used), gh CLI (signed in).
The modpack/ archives are gitignored — a fresh clone has an empty pack; the master copy
lives in dispatch-modpack-backup.zip (owner's Downloads) and on the modpack-v1 release.
- Thin installer + on-demand mod downloads (
RemotePackSource,Acquisition/*). - Full config editing: the scanner classifies every
.inikey as keyboard-bind / controller-bind / plain-setting (folding<bind>Modifiercompanions), so one file feeds the keyboard tab, controller tab, and Plugin Settings. Controls screen reads real.inis on open; edits write back in place (IniScanner,ControlsViewModel,ControlWriter). - Launch via RagePluginHook with real feedback + double-launch guard
(
IGameLauncher→LaunchOutcome,DashboardViewModel.GoOnDuty). - Live manifest (
RemotePackRefresher) and app self-update (VelopackAppUpdater).
Design docs of intent live in the XML-doc comments — the code is heavily commented on why.
- Grammar Police keybind: F4 vs F7 (unresolved — owner's call). The guide's auto-config
(
ConfigCatalogue) writes GP Interface = F4, which is the RagePluginHook console key ("never rebind"). The app'sControlCatalogue.Suggestedscheme resolves it to F7 and a test enforces "Suggested never takes F4". Options: (A) change the auto-config to F7 (clash-free, deviates from guide), (B) add a Controls-screen warning that flags any bind on F4, (C) leave as guide-faithful. Not changed because writing a bad GP bind unattended was riskier than the known caveat. - Code signing:
Setup.exeis unsigned → SmartScreen "unknown publisher" on first run. A cert would remove it. - Reserved-key (F4) warning in Controls, and an Epic-vs-Steam launch hint — minor polish, not done.
- CI:
.github/workflows/ci.ymlbuilds/tests, and itspackagejob builds from a plain checkout — so a CI-built release would have an empty pack. Real releases are cut locally withPublish-App.ps1. Wiring CI to fetch the pack (or use the thin model) is a follow-up.
- PowerShell 5.1 (Windows): non-ASCII chars (em-dash) in a
.ps1mis-parse as ANSI → keep scripts ASCII.Set-Content -Encoding utf8adds a BOM — write manifests with[System.IO.File]::WriteAllText(..., UTF8Encoding($false)); the app strips a leading BOM too. Native-command stderr under$ErrorActionPreference='Stop'becomes terminating — the scripts use'Continue'+ explicit$LASTEXITCODEchecks. gh release uploadglobs file args — filenames with[ ](e.g.[SP ONLY]) fail;Publish-ModPack.ps1sanitizes those. The runtime matcher normalizes names anyway.- GitHub 100 MB/file limit → the pack can't live in the repo tree; it lives on a Release.
- Velopack self-update only works if the release carries the feed files — a bare
Setup.exeis not enough. Usevpk upload github(whatPublish-App.ps1does).