|
| 1 | +# Developer Guide |
| 2 | + |
| 3 | +This guide is for contributors and maintainers of **FarmHub**. It covers the architecture, codebase structure, and how to work on the project. |
| 4 | + |
| 5 | +## Quick Start for Developers |
| 6 | + |
| 7 | +### Current Versions |
| 8 | +- **App:** 3.9.0 (`FS25_FarmDashboard_App/FS25_FarmDashboard_App/package.json`) |
| 9 | +- **Mod:** 2.3.0.0 (`FS25_FarmDashboard_Mod/FS25_FarmDashboard_Mod/modDesc.xml`) |
| 10 | + |
| 11 | +### Build the Windows App |
| 12 | + |
| 13 | +```bash |
| 14 | +cd FS25_FarmDashboard_App/FS25_FarmDashboard_App |
| 15 | +npm install |
| 16 | +npm run dist |
| 17 | +``` |
| 18 | + |
| 19 | +The installer goes to `%LOCALAPPDATA%\fs25-farm-dashboard-electron-out\` |
| 20 | + |
| 21 | +### Run Dev Build |
| 22 | + |
| 23 | +```bash |
| 24 | +cd FS25_FarmDashboard_App/FS25_FarmDashboard_App |
| 25 | +npm install |
| 26 | +npm start |
| 27 | +``` |
| 28 | + |
| 29 | +Opens the Electron app in dev mode. |
| 30 | + |
| 31 | +## Architecture |
| 32 | + |
| 33 | +``` |
| 34 | +Game (FS25) |
| 35 | + ↓ |
| 36 | +Mod writes data.json |
| 37 | + ↓ |
| 38 | +Electron (main.js) watches data.json |
| 39 | + ↓ |
| 40 | +Express server :8766 |
| 41 | + ↓ |
| 42 | +Web client (browser) polls /api/* |
| 43 | +``` |
| 44 | + |
| 45 | +### Key Components |
| 46 | + |
| 47 | +| Component | File | Role | |
| 48 | +|-----------|------|------| |
| 49 | +| **Lua Mod** | `FS25_FarmDashboard_Mod/src/FarmDashboard.lua` | Runs in game; writes `data.json` every cycle | |
| 50 | +| **Electron Main** | `FS25_FarmDashboard_App/main.js` | Watches files, manages server, IPC bridge | |
| 51 | +| **Express Server** | `FS25_FarmDashboard_App/main.js` | HTTP/WebSocket on port 8766 | |
| 52 | +| **Data Merger** | `FS25_FarmDashboard_App/dataMerger.js` | Combines Lua data + XML savegame data | |
| 53 | +| **Rules Engine** | `FS25_FarmDashboard_App/web/assests/js/rules-engine.js` | AI field suggestions (browser-side) | |
| 54 | +| **Web Client** | `FS25_FarmDashboard_App/web/assests/js/app.js` | Main dashboard UI | |
| 55 | + |
| 56 | +## Repository Layout |
| 57 | + |
| 58 | +``` |
| 59 | +FarmHub/ |
| 60 | +├── FS25_FarmDashboard_App/ |
| 61 | +│ └── FS25_FarmDashboard_App/ |
| 62 | +│ ├── main.js # Electron + Express |
| 63 | +│ ├── preload.js # IPC bridge |
| 64 | +│ ├── dataMerger.js # Merge Lua + XML |
| 65 | +│ ├── package.json # v3.9.0 |
| 66 | +│ ├── web/ |
| 67 | +│ │ ├── index.html |
| 68 | +│ │ ├── setup.html |
| 69 | +│ │ ├── assests/ |
| 70 | +│ │ │ ├── css/styles.css |
| 71 | +│ │ │ └── js/ |
| 72 | +│ │ │ ├── app.js |
| 73 | +│ │ │ ├── rules-engine.js |
| 74 | +│ │ │ ├── modules/ # Sections |
| 75 | +│ │ │ └── i18n/ # Translations |
| 76 | +│ │ └── locales/ |
| 77 | +│ │ ├── messages/ # Source translations |
| 78 | +│ │ └── translations.json |
| 79 | +│ └── build/ |
| 80 | +│ └── installer.nsh # NSIS hooks |
| 81 | +├── FS25_FarmDashboard_Mod/ |
| 82 | +│ └── FS25_FarmDashboard_Mod/ |
| 83 | +│ ├── modDesc.xml # v2.3.0.0 |
| 84 | +│ ├── icon.png |
| 85 | +│ └── src/ |
| 86 | +│ ├── FarmDashboard.lua # Entry point |
| 87 | +│ ├── FarmDashboardDataCollector.lua |
| 88 | +│ ├── Diagnostics.lua |
| 89 | +│ └── collectors/ # Per-data-type |
| 90 | +├── tools/ |
| 91 | +│ ├── app/ # Electron npm helpers |
| 92 | +│ ├── Zip-FarmDashboardMod.ps1 # Package mod zip |
| 93 | +│ └── Export-ModStoreImages.ps1 # Image extraction |
| 94 | +└── docs/ # Full documentation |
| 95 | +``` |
| 96 | + |
| 97 | +## The Lua Mod |
| 98 | + |
| 99 | +### Mission Hook |
| 100 | + |
| 101 | +`FarmDashboard.lua` registers with `addModEventListener`. On `loadMap`, if `isAuthority()` is true (host/single-player), it adds itself to updateables. |
| 102 | + |
| 103 | +### Staggered Orchestration |
| 104 | + |
| 105 | +`FarmDashboardDataCollector:update(dt)` divides each `collectionCycleMs` into slots — one per enabled module. This prevents lag spikes. |
| 106 | + |
| 107 | +### Collectors |
| 108 | + |
| 109 | +| Collector | Produces | |
| 110 | +|-----------|----------| |
| 111 | +| `AnimalDataCollector.lua` | Animals, health, type | |
| 112 | +| `VehicleDataCollector.lua` | Vehicles, fuel, damage, position | |
| 113 | +| `FieldDataCollector.lua` | Crops, growth, PF nitrogen/pH, windrows, bales, suggestions | |
| 114 | +| `WeatherDataCollector.lua` | Temperature, conditions | |
| 115 | +| `FinanceDataCollector.lua` | Money, loan, asset values | |
| 116 | +| `EconomyDataCollector.lua` | Market prices, selling stations | |
| 117 | +| `ProductionDataCollector.lua` | Production chains, fill levels | |
| 118 | + |
| 119 | +### Output |
| 120 | + |
| 121 | +The mod writes `data.json` to: |
| 122 | + |
| 123 | +``` |
| 124 | +%USERPROFILE%\Documents\My Games\FarmingSimulator2025\modSettings\FS25_FarmDashboard\<savename>\data.json |
| 125 | +``` |
| 126 | + |
| 127 | +This file is updated every `collectionCycleMs` (default 60 seconds). |
| 128 | + |
| 129 | +## The Electron App |
| 130 | + |
| 131 | +### HTTP Server |
| 132 | + |
| 133 | +- Runs on port **8766** (both HTTP and WebSocket) |
| 134 | +- Binds to `127.0.0.1` by default (localhost only) |
| 135 | +- If LAN enabled, binds to `0.0.0.0` and enforces HTTP Basic Auth |
| 136 | + |
| 137 | +### File Watching |
| 138 | + |
| 139 | +`startLocalWatching()` watches `data.json` on local servers. If the file changes, Electron: |
| 140 | +1. Reads the new `data.json` |
| 141 | +2. Also re-reads the savegame XML |
| 142 | +3. Merges both with `dataMerger.mergeData()` |
| 143 | +4. Broadcasts to all connected browsers via WebSocket |
| 144 | + |
| 145 | +### FTP Polling |
| 146 | + |
| 147 | +`startFtpPollingCoordinator()` handles remote (FTP) servers. Configurable: |
| 148 | +- **Interval**: 1–25 minutes |
| 149 | +- **Schedule**: Sync (all at once) or Staggered (spread out) |
| 150 | + |
| 151 | +### IPC Bridge |
| 152 | + |
| 153 | +`preload.js` exposes methods to the web UI via `window.farmDashAPI`. Every method is intentionally listed — no ad-hoc access to Node APIs. |
| 154 | + |
| 155 | +**Key methods:** |
| 156 | +- `saveSettings(cfg)` — Persist server config |
| 157 | +- `saveUiPreferences(prefs)` — Save theme, field clusters, etc. |
| 158 | +- `checkDesktopAppUpdates()` — Trigger `electron-updater` |
| 159 | +- `exportModStoreImages()` — Run mod-image PowerShell pipeline |
| 160 | + |
| 161 | +## Data Merge (`dataMerger.js`) |
| 162 | + |
| 163 | +Combines Lua live data with savegame XML static data. |
| 164 | + |
| 165 | +### Precedence |
| 166 | + |
| 167 | +| Domain | Lua | XML | Both | |
| 168 | +|--------|-----|-----|------| |
| 169 | +| Animals | live counts, fill | — | — | |
| 170 | +| Fields | live agronomy, growth, suggestions | base field rows | merged | |
| 171 | +| Vehicles | live state, fuel | base list, ownership | merged | |
| 172 | +| Economy | live prices | history | merged | |
| 173 | + |
| 174 | +### Anti-Regress |
| 175 | + |
| 176 | +If live data is missing (game just restarted), the merger caches the last known value so the UI doesn't flicker. |
| 177 | + |
| 178 | +### Timestamps |
| 179 | + |
| 180 | +The merged payload includes: |
| 181 | +```json |
| 182 | +{ |
| 183 | + "dataTimestamps": { |
| 184 | + "lastLuaReceivedAt": <epoch_ms>, |
| 185 | + "lastXmlReceivedAt": <epoch_ms>, |
| 186 | + "mergeComputedAt": <epoch_ms>, |
| 187 | + "liveNewerThanXml": <bool> |
| 188 | + } |
| 189 | +} |
| 190 | +``` |
| 191 | + |
| 192 | +Used for the top-bar data-source badge. |
| 193 | + |
| 194 | +## Rules Engine (`rules-engine.js`) |
| 195 | + |
| 196 | +Runs in the browser; no network calls. Provides AI-powered field work suggestions. |
| 197 | + |
| 198 | +### Entry Point |
| 199 | + |
| 200 | +`getLocalFieldSuggestion(field, opts)` — called by `fields.js` for each field. |
| 201 | + |
| 202 | +### Thresholds |
| 203 | + |
| 204 | +| Constant | Value | Meaning | |
| 205 | +|----------|-------|---------| |
| 206 | +| `MIN_WINDROW_LITERS` | 120 | Ignore windrow signal below this | |
| 207 | +| `MIN_WINDROW_AREA` | 0.0005 | Min area fraction to count windrow | |
| 208 | +| PF nitrogen band | < 0.6 × target | "Needs nitrogen" | |
| 209 | + |
| 210 | +### Suggestion Priority |
| 211 | + |
| 212 | +When multiple maintenance actions apply: |
| 213 | +1. **Lime** (highest priority) |
| 214 | +2. **Nitrogen** |
| 215 | +3. **Weeds** |
| 216 | +4. **Rolling** (lowest priority) |
| 217 | + |
| 218 | +## Web Client |
| 219 | + |
| 220 | +### Entry Point |
| 221 | + |
| 222 | +`web/assests/js/app.js` defines `LivestockDashboard` and mixes in modules. Assigned to `window.dashboard`. |
| 223 | + |
| 224 | +### Module Map |
| 225 | + |
| 226 | +| Module | Handles | |
| 227 | +|--------|---------| |
| 228 | +| `navigation.js` | Sidebar, landing page | |
| 229 | +| `apiStorage.js` | Server tabs, `/api/*` calls | |
| 230 | +| `livestock.js` | Livestock section | |
| 231 | +| `vehicles.js` | Vehicles section | |
| 232 | +| `fields.js` | Fields section | |
| 233 | +| `economy.js` | Economy section | |
| 234 | +| `pastures.js` | Pastures section | |
| 235 | +| `productions.js` | Production chains | |
| 236 | +| `theming.js` | Color picker | |
| 237 | +| `i18n/i18n.js` | Translations | |
| 238 | + |
| 239 | +### Polling |
| 240 | + |
| 241 | +`app.js` has a `dashboard.pollInterval` (default 1s). On each tick: |
| 242 | +1. Fetch `/api/data` |
| 243 | +2. Merge with local state |
| 244 | +3. Call `refresh*()` on each module (incremental DOM update) |
| 245 | + |
| 246 | +## Internationalization (i18n) |
| 247 | + |
| 248 | +### Adding a String |
| 249 | + |
| 250 | +1. Add the key to `web/locales/messages/en.json` (source of truth) |
| 251 | +2. Run `npm run i18n:sync` — copies the key to every language |
| 252 | +3. Translate non-English files |
| 253 | +4. Run `npm run i18n:build` — creates `translations.json` |
| 254 | +5. Run `npm run i18n:verify` — confirms full coverage |
| 255 | + |
| 256 | +### Build & Verify |
| 257 | + |
| 258 | +```bash |
| 259 | +npm run i18n:build # Create translations.json |
| 260 | +npm run i18n:verify # Check coverage |
| 261 | +npm run i18n:audit # Find orphans/duplicates |
| 262 | +``` |
| 263 | + |
| 264 | +## Build & Packaging |
| 265 | + |
| 266 | +### npm Scripts |
| 267 | + |
| 268 | +| Script | What | |
| 269 | +|--------|------| |
| 270 | +| `npm start` | Dev launch (`electron .`) | |
| 271 | +| `npm run dist` | Full NSIS installer | |
| 272 | +| `npm run pack` | Unpacked app (for testing) | |
| 273 | +| `npm test` | Run tests | |
| 274 | +| `npm run verify:electron-pack` | CI gate: verify all required files in `package.json` `build.files` | |
| 275 | + |
| 276 | +### CI/CD |
| 277 | + |
| 278 | +GitHub Actions runs on push/PR to `main`, `master`, or `develop`: |
| 279 | +- `npm ci` — clean install |
| 280 | +- `npm test` — unit tests |
| 281 | +- `npm run verify:electron-pack` — file verification |
| 282 | +- `npm run i18n:verify` — translation coverage |
| 283 | + |
| 284 | +### Release Checklist |
| 285 | + |
| 286 | +1. Build mod zip: `.\tools\Zip-FarmDashboardMod.ps1` |
| 287 | +2. Build Windows app: `npm run dist` |
| 288 | +3. Attach `.exe` and mod `.zip` to GitHub Release |
| 289 | +4. Verify auto-update works: Settings → Check for updates |
| 290 | + |
| 291 | +## Debugging Checklist |
| 292 | + |
| 293 | +| Issue | Check | |
| 294 | +|-------|-------| |
| 295 | +| Empty dashboard | Mod enabled + save loaded; `data.json` exists; Settings path correct | |
| 296 | +| "Waiting for field data" | `dataTimestamps.lastLuaReceivedAt` advancing? Watcher fired? | |
| 297 | +| Wrong farm shown | `activeFarmId` in merged payload; check farm dropdown | |
| 298 | +| Merge oddities | `dataMerger.js` precedence table; `liveNewerThanXml` should be `true` when fresh | |
| 299 | +| LAN 401/403 | Check `lanUsername`, `lanPassword`, IP allowlist | |
| 300 | +| FTP not polling | `intervalMinutes` must be 1–25 | |
| 301 | +| `app.asar` locked | `npm run unlock-install`, then `npm run dist` | |
| 302 | + |
| 303 | +## Known Gaps from the Audits |
| 304 | + |
| 305 | +1. **Livestock Statistics / Genetics tabs** — UI buttons not wired (`index.html`, `livestock.js`) |
| 306 | +2. **Electron `parseModConfigXml`** — ignores `debugBaleScan` flag (`main.js`) |
| 307 | +3. **Fields error strip** — no retry button; auto-retries every 5s |
| 308 | +4. **Notification history** — hard-codes English empty state |
| 309 | + |
| 310 | +## Conventions |
| 311 | + |
| 312 | +- Match existing naming, IPC channels, and merge semantics |
| 313 | +- Prefer additive JSON fields; don't reintroduce coordinate dumps |
| 314 | +- New translations → `messages/<code>.json` only; never hand-edit `translations.json` |
| 315 | +- New IPC channels: add in `main.js`, expose in `preload.js`, document in this file |
| 316 | +- Store keys: prefer `electron-store` over `localStorage` for desktop-level config |
| 317 | +- Tests: `npm test` for JS changes; Lua/game behaviour needs manual testing |
| 318 | + |
| 319 | +--- |
| 320 | + |
| 321 | +**Questions?** Check the [full documentation](../docs/README.md) or open a GitHub issue. |
0 commit comments