A turn-by-turn GPS navigator for Forza Horizon, built as a native Android app. The game streams its telemetry over UDP ("Data Out" / Car Dash format); ForzaDrive listens on the local network, plots your car on a live map of the in-game world, computes real road routes with Dijkstra, and speaks driving instructions out loud — on your phone and on your car's screen via native Android Auto rendering.
The live navigator: in-game world map with destination search, your car's position and the bottom action bar. "ESPERANDO FORZA" means it's listening for UDP telemetry from the game (the HUD and routing light up once the game starts streaming).
- UDP telemetry parsing — decodes 324-byte Car Dash packets (little-endian
ByteBuffer) into position, speed, RPM, gear, pedals, G-forces, tire slip and wheel speeds at full packet rate (default port20440). - Live map with heading-up view — MapGenie tile rendering in Compose
Canvas, with the whole map rotating around the car (yaw-driven) so the road ahead always points up. Pan/pinch gestures, auto-center, and over-zoom past the native tile level. - Real pathfinding — Dijkstra over a road graph extracted from the game map (
app/src/main/assets/fh6_labsgg_roads.json), with per-surface edge costs (asphalt / dirt / dirt-snow) and three routing modes (balanced, avoid dirt, prefer dirt). Routes re-snap and recalculate when you leave them. - Spanish voice guidance — Android TextToSpeech (
es-ES) with deduplicated turn instructions ("En 200 metros gira a la derecha"), off-road recovery directions, and speed-trap warnings. - Native Android Auto — a real
CarAppServicewithNavigationTemplate+SurfaceCallback: the map, route and HUD are drawn directly on the car screen'sSurfacewithCanvas, not a mirrored phone UI. - Speed-adaptive zoom — on Android Auto the zoom level eases out automatically as you go faster (≈17.4 in town, down to ≈15.8 at top speed), with manual override buttons.
- 2-point affine calibration — game world coordinates (X, Z) are mapped to map pixel space through a two-point affine transform that the user can recalibrate in-app, so the projection survives map/tile changes.
- Full HUD — speed, RPM bar with redline, gear, throttle/brake/clutch/handbrake bars, G-force circle, current speed limit, lap timer with per-lap max/avg speed.
- Session recording — telemetry sessions persist to a Room database and can be exported; custom POIs are stored and routable like built-in destinations.
[Forza Horizon] ──UDP :20440──► UdpListenerService (foreground, connectedDevice)
│ TelemetryParser (324-byte Car Dash)
▼
TelemetryManager ── StateFlow<TelemetryState>
│ + calibration & settings
┌─────────────────┴──────────────────┐
▼ ▼
Phone (Compose) Android Auto (Car App)
MainScreen + ForzaMapView ForzaNavigationScreen
│ │
└────────► NavigationManager ◄───────┘
Dijkstra route • voice TTS • laps • speed traps
Package layout under app/src/main/java/com/example/:
| Package | Contents |
|---|---|
telemetry/ |
UdpListenerService (foreground service with socket watchdog and status notification), TelemetryParser (binary packet decoding), TelemetryState, and the TelemetryManager singleton that exposes telemetry, port/tile-URL settings and calibration points as StateFlows. |
map/ |
ForzaMapView (Compose canvas map: tile fetching with LruCache, heading-up rotation, gestures, route/POI drawing), NavigationManager (road graph loading, Dijkstra shortest path, route snapping/slicing, turn instructions, VoiceGuidanceManager TTS, lap detector, speed traps), CoordinateCalibration (game → map pixel affine transform). |
car/ |
ForzaCarAppService + ForzaSession (Car App Library entry point with host validation) and ForzaNavigationScreen (NavigationTemplate + SurfaceCallback drawing map, route and HUD straight onto the car display, with speed-adaptive zoom). |
data/ |
Room persistence: ForzaDatabase, SessionDao/SessionEntity, SessionRecorder (auto-records driving sessions from telemetry) and SessionExporter, plus custom POI storage. |
ui/ |
MainScreen (map + HUD + destination search), SettingsScreen (port, tile server, routing mode, calibration), SavedSessionsPanel, ContributePanel, and the Material 3 theme/. |
- Kotlin (100%), coroutines +
StateFlow - Jetpack Compose + Material 3 for the phone UI
- Android for Cars App Library (
androidx.car.app, navigation templates + projected surface) - Room (KSP) for session/POI persistence
- Coil, OkHttp/Retrofit/Moshi for tile loading and networking
- TextToSpeech for voice guidance (Spanish)
- Robolectric + Roborazzi for unit and screenshot tests
- Gradle (Kotlin DSL) with version catalog, Secrets Gradle Plugin (
.envconvention)
- Android Studio (or a JDK 17+ command-line setup), Android SDK 36
- An Android device/emulator with minSdk 29 (Android 10)
- Forza Horizon running on PC or Xbox, on the same network as the phone
./gradlew assembleDebugThe APK is generated in app/build/outputs/apk/debug/. A debug keystore is expected at the project root (debug.keystore, not committed — Android Studio's default debug keystore also works if you adjust signingConfigs).
- In Forza, open Settings → HUD and Gameplay → Data Out and enable it.
- Set the IP address to your phone's local IP and the port to
20440(changeable in the app's Settings screen). - Launch ForzaDrive — the foreground notification switches to "Forza conectado" when packets arrive, and your car appears on the map.
Copy .env.example to .env if you want to provide a GEMINI_API_KEY (read by the Secrets Gradle Plugin; the core navigator works without it).
Telemetry. Forza's "Data Out" feature broadcasts a fixed-layout binary struct every frame. ForzaDrive's parser reads the 324-byte Car Dash layout with a little-endian ByteBuffer: race state and timestamp at offset 0–7, engine RPMs at 8–19, acceleration at 20–31, yaw/pitch/roll at 56–67, world position at 244–255, speed and velocity at 256–271, and the byte-packed controls (throttle, brake, gear, steering) near the end of the packet. A watchdog marks telemetry inactive after 3 seconds without packets.
Calibration. The in-game world uses meters on an X/Z plane; the map tiles use WebMercator pixel coordinates. Two reference points (known game position ↔ known map pixel) define an affine transform pixel = m·game + b per axis. Because the transform is just two slopes and two offsets, users can recalibrate it themselves from the app if the default mapping drifts — and route geometry is converted back from map space to game space through the inverse transform.
This is a hobby project and ships with an honest, documented bug backlog in docs/bugs/ — 10 issues with severity and effort estimates, including:
- Tile cache growth/eviction issues under long navigation sessions (BUG-03, BUG-04 — partially mitigated with an
LruCacheon the phone map and a bounded cache on Android Auto) - UDP service lifecycle concerns (BUG-02 — a
connectedDeviceforeground service has since been added; seedocs/mejoras/MEJORA-01) TelemetryManagerbeing a global singleton without lifecycle awareness (BUG-01)- Calibration precision loss from
Double→Floatround-trips (BUG-07) ALLOW_ALL_HOSTS_VALIDATORfor Android Auto in debug builds (BUG-09)- Several POI coordinates are approximate; only two reference POIs are surveyed precisely (BUG-10)
See docs/README.md for the full table.
docs/README.md— full technical documentation (Spanish): stack, architecture diagram, bug/improvement tablesdocs/bugs/— 10 documented bugs with analysis and proposed fixesdocs/mejoras/— 8 planned improvements (interactive calibration, lap detection, GPX export…)docs/vision_futuro/— longer-term ideas (Windows companion app, OBS streaming overlay, multiplayer dashboard), with prototypes undertools/
MIT © 2026 Gerard Alvear
ForzaDrive is a fan project. It is not affiliated with, endorsed by, or connected to Microsoft, Playground Games, or the Forza franchise. Map tiles are served by MapGenie.
