You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
autohero/docs/engine_unified_offline_onli...

59 lines
4.3 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# Unified Engine: One Simulation Path for Online and Offline
This document describes how AutoHero runs **all** gameplay logic (movement cadence, encounters, combat, rewards) in a **single** place: the Go `Engine` (`backend/internal/game/engine.go`). WebSocket is observation and command input only; there is no separate “offline world” loop that advances combat differently while the player is away.
## Authority
| Layer | Role |
|--------|------|
| **Engine** | `processMovementTick`, `processCombatTick`, `startCombatLocked`, `HeroMovement` FSM, persistence hooks |
| **WebSocket Hub** | Delivers envelopes only when the hero has at least one connected client (`SendToHero` no-op otherwise) |
| **PostgreSQL** | Durable hero row; periodic and event-driven saves from the engine |
| **Offline digest** | Aggregated summary for “while you were away” UI; filled only after disconnect grace (see below) |
## Resident heroes
- After the **last** WebSocket disconnect for a hero, `HeroSocketDetached` **does not** remove them from `e.movements` or clear combat. The hero keeps ticking like an online session without a viewer.
- In-memory `Hero.WsDisconnectedAt` is set on disconnect (aligned with `heroes.ws_disconnected_at` in the DB) for digest timing.
- **Cold start:** `ListHeroesForEngineBootstrap` (`backend/internal/storage/hero_store.go`) selects heroes with `ws_disconnected_at IS NOT NULL` and a simulatable `state`. `BootstrapResidentHeroes` (`backend/internal/game/engine_bootstrap.go`) runs a **one-shot** wall-time catch-up via `OfflineSimulator.SimulateHeroAt`, then registers the hero in the engine. Live play after that uses only engine combat.
- **Periodic save without WS:** heroes with no subscriber get a full `heroStore.Save` every `offlineDisconnectedFullSaveInterval` (30s) from the movement tick path (`backend/internal/game/engine.go`).
## Combat and encounters
- **Live progression:** encounters call `startCombatLocked`; resolution uses `e.combats` and `processCombatTick` (same for subscribed and unsubscribed heroes).
- **Batch-only paths** (no second “live” world): `SimulateOneFight` / `simulateHeroTick` remain for **bootstrap after restart** and for **server-downtime gap** recovery when the hero is **not** resident in the engine (`catchUpOfflineGap` in `backend/internal/handler/game.go`). If `HeroHasActiveMovement`, gap catch-up **skips** `SimulateHeroAt` so combat is not simulated twice.
## REST and engine consistency
- `Engine.MergeResidentHeroState` copies the authoritative in-engine hero (after `SyncToHero`) into the handlers hero struct.
- **`GET /api/v1/hero/init`** and **`GET /api/v1/hero`**: if the hero is resident, merge from engine and persist so the client and DB match the single simulation.
## Offline digest
- Helpers: `OfflineDigestGrace`, `OfflineDigestCollecting` (`backend/internal/game/offline.go`).
- The engine applies digest deltas on kill, death (including DoT death path), and auto-revive **only when** `OfflineDigestCollecting(hero.WsDisconnectedAt, now)` is true.
- Batch `simulateHeroTick` uses the same rule when a digest store is wired.
## Key source files
| Area | File |
|------|------|
| Engine loop, combat, movement, digest hooks, auto-revive, disconnected save | `backend/internal/game/engine.go` |
| Bootstrap query | `backend/internal/storage/hero_store.go` (`ListHeroesForEngineBootstrap`) |
| Bootstrap orchestration | `backend/internal/game/engine_bootstrap.go` |
| Batch catch-up + digest helpers | `backend/internal/game/offline.go` |
| Hub send if connected | `backend/internal/handler/ws.go` |
| Init / GetHero merge; gap catch-up guard | `backend/internal/handler/game.go` |
| Wiring, bootstrap before `Engine.Run` | `backend/cmd/server/main.go` |
## Scaling notes
- Bootstrap is capped (e.g. 500 heroes in `main`); not every account is loaded into RAM.
- Long-term, explicit unload policy (TTL + final save) can reduce residency memory without reintroducing a second gameplay simulator.
## Related docs
- [spec-server-authoritative.md](./spec-server-authoritative.md) — WS contract and phases.
- [excursion_attractor_fsm.md](./excursion_attractor_fsm.md) — roadside/adventure attractor excursion FSM and persistence.
- [blueprint_server_authoritative.md](./blueprint_server_authoritative.md) — historical gap analysis and migration context.