# Architecture ## Projects ``` src/ VPinCommander.Core/ Domain models + services. No UI or persistence dependencies. VPinCommander.Data/ EF Core 8 + SQLite persistence (references Core). VPinCommander.App/ WPF desktop app, MVVM via CommunityToolkit.Mvvm (references Core + Data). tests/ VPinCommander.Core.Tests/ xUnit tests for Core services. ``` Dependency rule: `App → Data → Core`. Core never references Data or App, so services stay testable and a future CLI/daemon can reuse them. ## Key concepts - **GameTable** — a playable table file (`.vpx` Visual Pinball X, `.fp` Future Pinball) found on disk. - **Rom** — a PinMAME ROM archive (`.zip`) in a ROM folder. - **MediaAsset** — artwork/audio/video (wheel images, backglass, playfield video, DMD, launch audio…), categorized by extension and folder heuristics. - **ScanRun** — one execution of the inventory scanner, with counts and timing, kept as history. - **FrontEndGame** — a game entry imported from a front-end (PinUP Popper today, PinballX later), carrying the front-end's metadata (ROM, manufacturer, year) and a `MatchStatus` linking it to the scanned inventory. ## Inventory scanning (Milestone 1) `InventoryScanner` (Core) walks user-configured folders and produces a `ScanResult` of tables, ROMs, and media. `InventoryService` (Data) upserts results into SQLite keyed by absolute file path, marking records missing from a scan as `IsMissing` instead of deleting them (so health reports can flag removed files later). Matching between tables ↔ ROMs ↔ media is filename-stem based in M1. Later milestones will parse the table script from the VPX OLE compound file to read the real `cGameName` ROM reference. ## VPX metadata & health A `.vpx` table is an OLE compound file. `VpxMetadataReader` (Data, using OpenMcdf) opens it read-only, walks the BIFF records of the `GameStg\GameData` stream to the `CODE` record, and extracts the PinMAME ROM the script declares (`cGameName = "..."`), plus author/version from the `TableInfo` streams. The scanner runs this for every VPX table, so `GameTable.RomName` reflects what the table actually needs — not a filename guess. `HealthReportBuilder` (Core, pure) turns the stored inventory into findings: tables whose declared ROM is absent and front-end games without table files (errors); outdated tables per the VPS catalog, duplicate tables/ROMs, and files that vanished since a previous scan (warnings); unreferenced ROMs, unassigned/duplicate media, tables without media, VPX tables without a backglass, tables missing PuP-Pack or DOF coverage (gated: only reported when the cabinet uses PuP-Packs/DOF at all), and tables saved with an old Visual Pinball format (info). The Health page feeds it the cached VPS update check and offers severity + category filters. ## Dependencies & content management `DependencyProbe` (Core) runs during every scan and stamps each table with what it has on this cabinet: a `.directb2s` backglass next to the table file, a PuP-Pack folder under `PUPVideos\`, `altcolor`/`altsound` folders under the VPinMAME directory (derived as the parent of each ROM folder), and DOF coverage (`DofConfigReader` parses `directoutputconfig*.ini` rows into a ROM set once per scan). Content operations are conservative by design: `MediaManager.AssignToTableAsync` renames a media file to its table's name (never moves it across folders, never overwrites); `RomManager.QuarantineAsync` moves ROM files to `%APPDATA%\VPinCommander\Quarantine` instead of deleting, so every destructive-looking action is reversible by hand. ## Lifecycle - **Version tracking** — every scan appends `TableVersionChange` rows when a table file appears or changes (size, modified date, or TableInfo version), giving a local history of what got added/updated when. - **Update notifications** — `VpsUpdateChecker` (Data) downloads the community Virtual Pinball Spreadsheet database (`vpsdb.json`, cached 24h under `%APPDATA%\VPinCommander`, stale cache used offline). `UpdateMatcher` (Core, pure) matches tables by normalized title with manufacturer/year tie-breakers from the conventional `Title (Manufacturer Year)` stem, compares the local TableInfo version against the newest VPX release, and deliberately skips ambiguous matches rather than guessing. - **Excel export** — `ExcelExporter` (Data, ClosedXML) writes Tables/ROMs/Media/Front-end games/Health/Version history sheets. - **Backup/restore** — `BackupService` checkpoints the WAL, zips the database + settings; restore validates the zip, keeps a `.pre-restore` copy, and requires an app restart. - **Cloud sync** — `CloudSyncService` layers on the backup service: push writes the sync zip + a manifest (when/which machine) into any folder the user's cloud client already syncs; pull restores from it. No accounts, no server. ## Remote management (client/server) `VPinCommander.Server` hosts an ASP.NET Core minimal API inside the app when "cabinet mode" is enabled: `GET /api/status|tables|roms|media|health`, `POST /api/scan`, `POST /api/import/{popper|pinballx|pinbally}`, and `POST /api/install?fileName=…` (content push: the uploaded file is staged under its original name and run through `ContentInstaller`). All requests require the `X-Api-Key` header (shared key generated in Settings). The server also serves a self-contained browser UI (embedded `WebUi/index.html`, no external dependencies) at `/` — the shell loads without a key, but every API call it makes carries the key the user enters once (kept in browser localStorage). Transport is HTTP on the LAN or optional HTTPS: `ServerCertificate` generates a persistent self-signed certificate, and `CabinetClient` pairs by pinning its SHA-256 fingerprint on first use (stored per cabinet; any later mismatch is rejected). The client side drives the "Remote Cabinets" tab, where any number of cabinets are registered (name/address/key, persisted in settings) and monitored/scanned/imported/pushed-to centrally. Dependency rule extension: `App → Server → Data → Core`. ## Android client `VPinCommander.Mobile` (src/, .NET MAUI, Android-only) is a thin UI over the same `CabinetClient` the desktop uses — cabinet list persisted in app preferences, status/health views, remote scan and import. `UseNativeHttpHandler=false` keeps the managed HTTP stack so certificate-fingerprint pinning behaves identically to the desktop; cleartext HTTP is allowed in the manifest for plain-HTTP LAN cabinets. The project is deliberately not in `VPinCommander.sln` (building it needs the MAUI workload); the `Android` workflow builds and attaches the APK to releases. ## Content installer `ContentInstaller` (Core) powers the one-click Installer page. It classifies user-downloaded files by extension and, for archives, by inspecting entries (table files, `.directb2s`, `.pup`, ROM chip images like `.bin`/`.u##`, altcolor formats, `altsound.csv` + audio, media), then resolves targets from settings: tables/backglasses → first table folder, ROM zips copied whole → first ROM folder, altcolor/altsound → `\altcolor|altsound\` (ROM name from the archive's single root folder, else the file stem), PuP-Packs → `\PUPVideos`. Installation never overwrites existing files and rejects archive entries that would escape the target folder (zip slip). Downloading is deliberately not automated: the community sites are login-gated and ROMs are copyrighted, so the browser downloads and the app organizes. ## Front-end integrations `IFrontEndIntegration` (Core) is the adapter seam; adapters live in Data: - `PopperIntegration` opens Popper's `PUPDatabase.db` read-only with Microsoft.Data.Sqlite. Popper's schema varies across versions, so every column read is presence-checked. - `PinballXIntegration` reads the per-system XML databases under `Databases\\*.xml` and parses `Config\PinballX.ini` for each system's `TablePath`, so extensionless PinballX names can be resolved to real table files (`.vpx`/`.vpt`/`.fp`). These databases have no numeric game ids, so a stable FNV-1a hash of system+name serves as `ExternalId`. - `PinballYIntegration` reuses the same XML parsing (`PinballXmlDatabase` — PinballY is deliberately PinballX-compatible) but reads system names and table paths from PinballY's flat `Settings.txt` (`SystemN = Name`, `SystemN.TablePath`, `SystemN.DatabaseDir`). Imports are wholesale replacements per source. `GameMatcher` (Core, pure logic) then links games to inventory tables — by resolved path, then file name, then name stem — and marks non-VPX/FP games (e.g. Pinball FX) as `NotApplicable`. Matches are recomputed after every inventory scan. In the app, both integrations share one page implementation (`FrontEndPageViewModel` + `FrontEndView`). ## Persistence - SQLite database at `%APPDATA%\VPinCommander\vpincommander.db`. - EF Core migrations (`DatabaseInitializer` runs `Migrate()` at startup). Databases created by pre-migration builds are adopted: the final legacy schema is baselined into the migrations history without data loss; older ones are backed up and recreated once. Add migrations with `dotnet ef migrations add --project src/VPinCommander.Data` (local tool manifest in `.config/`). - App settings (folder paths, preferences) as JSON at `%APPDATA%\VPinCommander\settings.json` via `SettingsService`. ## UI WPF, MVVM. `MainViewModel` owns navigation; each page is a ViewModel + DataTemplate-mapped View (Dashboard, Tables, Settings). Dependency injection via `Microsoft.Extensions.Hosting` generic host, composed in `App.xaml.cs`. ## Future modules (planned seams) - `Integrations/` folder in Core defines interfaces (`IFrontEndIntegration`) that PinUP Popper (SQLite `PUPDatabase.db`) and PinballX (XML databases + ini) adapters will implement. - Dependency resolution, health reports, Excel export, backup/restore, and cloud sync each become services over the same Core model.