package storage import ( "context" "encoding/json" "errors" "fmt" "time" "github.com/redis/go-redis/v9" "github.com/denisovdennis/autohero/internal/model" ) const heroTownSessionKeyFmt = "autohero:v1:hero:%d:town_session" // HeroTownSessionRedis is the last persisted in-town NPC tour snapshot (reconnect / crash recovery). type HeroTownSessionRedis struct { SavedAtUnixNano int64 `json:"savedAtUnixNano"` State model.GameState `json:"state"` CurrentTownID int64 `json:"currentTownId,omitempty"` PositionX float64 `json:"positionX"` PositionY float64 `json:"positionY"` TownPause *model.TownPausePersisted `json:"townPause,omitempty"` } // TownSessionStore mirrors in-town hero state to Redis for faster/stale-DB-safe reconnect. type TownSessionStore struct { rdb *redis.Client } // NewTownSessionStore returns a store backed by Redis, or nil if rdb is nil. func NewTownSessionStore(rdb *redis.Client) *TownSessionStore { if rdb == nil { return nil } return &TownSessionStore{rdb: rdb} } func (s *TownSessionStore) key(heroID int64) string { return fmt.Sprintf(heroTownSessionKeyFmt, heroID) } // Save stores the hero's in-town session. Caller must set hero.TownPause (e.g. after SyncToHero). func (s *TownSessionStore) Save(ctx context.Context, heroID int64, h *model.Hero) error { if s == nil || s.rdb == nil || h == nil { return nil } if h.State != model.StateInTown { return nil } var townID int64 if h.CurrentTownID != nil { townID = *h.CurrentTownID } payload := HeroTownSessionRedis{ SavedAtUnixNano: time.Now().UnixNano(), State: h.State, CurrentTownID: townID, PositionX: h.PositionX, PositionY: h.PositionY, TownPause: h.TownPause, } b, err := json.Marshal(payload) if err != nil { return err } return s.rdb.Set(ctx, s.key(heroID), b, 72*time.Hour).Err() } // Delete removes the in-town session key (hero left town or state no longer in_town). func (s *TownSessionStore) Delete(ctx context.Context, heroID int64) error { if s == nil || s.rdb == nil { return nil } return s.rdb.Del(ctx, s.key(heroID)).Err() } // Load returns the stored session, or (nil, nil) if missing. func (s *TownSessionStore) Load(ctx context.Context, heroID int64) (*HeroTownSessionRedis, error) { if s == nil || s.rdb == nil { return nil, nil } b, err := s.rdb.Get(ctx, s.key(heroID)).Bytes() if err != nil { if errors.Is(err, redis.Nil) { return nil, nil } return nil, err } var p HeroTownSessionRedis if err := json.Unmarshal(b, &p); err != nil { return nil, err } return &p, nil }