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...

4.3 KiB

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.