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.

83 lines
2.3 KiB
Go

// Resident hero policy (engine memory):
// - After the last WebSocket disconnect, the hero stays in Engine.movements; the world keeps ticking.
// - Cold start: ListHeroesForEngineBootstrap loads rows with ws_disconnected_at set (cap 500 default in main).
// - Full hero row is saved every offlineDisconnectedFullSaveInterval while heroSubscriber reports false.
package game
import (
"context"
"log/slog"
"time"
"github.com/denisovdennis/autohero/internal/model"
"github.com/denisovdennis/autohero/internal/storage"
)
// BootstrapResidentHeroes loads heroes whose WebSocket session had ended before this process started,
// catches up wall time using the same batch path as server-downtime recovery, then registers them
// in the engine so movement and combat continue without a live subscriber.
func BootstrapResidentHeroes(ctx context.Context, e *Engine, heroStore *storage.HeroStore, sim *OfflineSimulator, limit int, logger *slog.Logger) {
if e == nil || heroStore == nil || sim == nil {
return
}
heroes, err := heroStore.ListHeroesForEngineBootstrap(ctx, limit)
if err != nil {
if logger != nil {
logger.Error("engine bootstrap: list heroes", "error", err)
}
return
}
now := time.Now()
for _, h := range heroes {
if h == nil {
continue
}
e.mu.Lock()
_, already := e.movements[h.ID]
rg := e.roadGraph
e.mu.Unlock()
if already || rg == nil {
continue
}
e.mergeTownSessionFromRedis(h)
if err := sim.SimulateHeroAt(ctx, h, now, true); err != nil {
if logger != nil {
logger.Error("engine bootstrap: catch-up sim", "hero_id", h.ID, "error", err)
}
continue
}
e.mu.Lock()
if e.roadGraph == nil {
e.mu.Unlock()
return
}
if _, taken := e.movements[h.ID]; taken {
e.mu.Unlock()
continue
}
hm := NewHeroMovement(h, e.roadGraph, now)
e.movements[h.ID] = hm
hm.MarkTownPausePersisted(hm.townPausePersistSignature())
hm.SyncToHero()
if hm.State == model.StateFighting {
if _, exists := e.combats[h.ID]; !exists {
en := PickEnemyForLevel(h.Level)
if en.Slug != "" {
e.startCombatLocked(hm.Hero, &en)
} else {
hm.State = model.StateWalking
hm.Hero.State = model.StateWalking
}
}
}
e.mu.Unlock()
if logger != nil {
logger.Info("engine bootstrap: resident hero registered", "hero_id", h.ID)
}
}
}