fix reset on reconnect in town

master
Denis Ranneft 1 month ago
parent 1487031748
commit 39ed3382fc

@ -104,6 +104,7 @@ func main() {
engine.SetSender(hub) // Hub implements game.MessageSender engine.SetSender(hub) // Hub implements game.MessageSender
engine.SetRoadGraph(roadGraph) engine.SetRoadGraph(roadGraph)
engine.SetHeroStore(heroStore) engine.SetHeroStore(heroStore)
engine.SetTownSessionStore(storage.NewTownSessionStore(redisClient))
engine.SetQuestStore(questStore) engine.SetQuestStore(questStore)
engine.SetAdventureLog(func(heroID int64, msg string) { engine.SetAdventureLog(func(heroID int64, msg string) {
logCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second) logCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)

@ -61,6 +61,7 @@ type Engine struct {
roadGraph *RoadGraph roadGraph *RoadGraph
sender MessageSender sender MessageSender
heroStore *storage.HeroStore heroStore *storage.HeroStore
townSession *storage.TownSessionStore
questStore *storage.QuestStore questStore *storage.QuestStore
incomingCh chan IncomingMessage // client commands incomingCh chan IncomingMessage // client commands
mu sync.RWMutex mu sync.RWMutex
@ -224,6 +225,13 @@ func (e *Engine) SetHeroStore(hs *storage.HeroStore) {
e.heroStore = hs e.heroStore = hs
} }
// SetTownSessionStore sets the Redis-backed mirror for in-town NPC tour state (reconnect recovery).
func (e *Engine) SetTownSessionStore(ts *storage.TownSessionStore) {
e.mu.Lock()
defer e.mu.Unlock()
e.townSession = ts
}
// SetQuestStore sets the quest store used for visit_town progress on town arrival. // SetQuestStore sets the quest store used for visit_town progress on town arrival.
func (e *Engine) SetQuestStore(qs *storage.QuestStore) { func (e *Engine) SetQuestStore(qs *storage.QuestStore) {
e.mu.Lock() e.mu.Lock()
@ -547,6 +555,11 @@ func (e *Engine) sendError(heroID int64, code, message string) {
// RegisterHeroMovement creates a HeroMovement for an online hero and sends initial state. // RegisterHeroMovement creates a HeroMovement for an online hero and sends initial state.
// Called when a WS client connects. // Called when a WS client connects.
func (e *Engine) RegisterHeroMovement(hero *model.Hero) { func (e *Engine) RegisterHeroMovement(hero *model.Hero) {
if hero == nil {
return
}
e.mergeTownSessionFromRedis(hero)
e.mu.Lock() e.mu.Lock()
defer e.mu.Unlock() defer e.mu.Unlock()
@ -584,6 +597,7 @@ func (e *Engine) RegisterHeroMovement(hero *model.Hero) {
hm := NewHeroMovement(hero, e.roadGraph, now) hm := NewHeroMovement(hero, e.roadGraph, now)
e.movements[hero.ID] = hm e.movements[hero.ID] = hm
hm.MarkTownPausePersisted(hm.townPausePersistSignature())
hm.SyncToHero() hm.SyncToHero()
e.logger.Info("hero movement registered", e.logger.Info("hero movement registered",
@ -639,6 +653,7 @@ func (e *Engine) HeroSocketDetached(heroID int64, lastConnection bool) {
"hero_id", heroID, "hero_id", heroID,
"last_connection", lastConnection, "last_connection", lastConnection,
) )
e.syncTownSessionRedisFromHero(heroID, heroSnap)
} }
} }
} }
@ -1374,7 +1389,80 @@ func (e *Engine) processMovementTick(now time.Time) {
continue continue
} }
hm.MarkTownPausePersisted(sig) hm.MarkTownPausePersisted(sig)
e.syncTownSessionRedis(heroID, hm)
}
}
}
// mergeTownSessionFromRedis overlays a fresher in-town snapshot when Postgres row is stale (e.g. missed town_pause save).
func (e *Engine) mergeTownSessionFromRedis(hero *model.Hero) {
if e.townSession == nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
snap, err := e.townSession.Load(ctx, hero.ID)
if err != nil {
if e.logger != nil {
e.logger.Warn("town session redis load failed", "hero_id", hero.ID, "error", err)
}
return
}
if snap == nil || snap.State != model.StateInTown || snap.TownPause == nil {
return
}
if snap.CurrentTownID > 0 && hero.CurrentTownID != nil && *hero.CurrentTownID != snap.CurrentTownID {
return
}
if snap.SavedAtUnixNano <= hero.UpdatedAt.UnixNano() {
return
}
hero.State = model.StateInTown
hero.MoveState = string(model.StateInTown)
hero.TownPause = snap.TownPause
hero.PositionX = snap.PositionX
hero.PositionY = snap.PositionY
if snap.CurrentTownID > 0 {
tid := snap.CurrentTownID
if hero.CurrentTownID == nil {
hero.CurrentTownID = new(int64)
}
*hero.CurrentTownID = tid
}
}
func (e *Engine) syncTownSessionRedis(heroID int64, hm *HeroMovement) {
if e.townSession == nil || hm == nil || hm.Hero == nil {
return
}
hm.SyncToHero()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if hm.State == model.StateInTown {
if err := e.townSession.Save(ctx, heroID, hm.Hero); err != nil && e.logger != nil {
e.logger.Warn("town session redis save failed", "hero_id", heroID, "error", err)
}
return
}
if err := e.townSession.Delete(ctx, heroID); err != nil && e.logger != nil {
e.logger.Warn("town session redis delete failed", "hero_id", heroID, "error", err)
}
}
func (e *Engine) syncTownSessionRedisFromHero(heroID int64, h *model.Hero) {
if e.townSession == nil || h == nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if h.State == model.StateInTown {
if err := e.townSession.Save(ctx, heroID, h); err != nil && e.logger != nil {
e.logger.Warn("town session redis save failed", "hero_id", heroID, "error", err)
}
return
} }
if err := e.townSession.Delete(ctx, heroID); err != nil && e.logger != nil {
e.logger.Warn("town session redis delete failed", "hero_id", heroID, "error", err)
} }
} }
@ -1389,6 +1477,7 @@ func (e *Engine) persistHeroAfterTownEnter(h *model.Hero) {
e.logger.Error("persist hero after town enter", "hero_id", h.ID, "error", err) e.logger.Error("persist hero after town enter", "hero_id", h.ID, "error", err)
return return
} }
e.syncTownSessionRedisFromHero(h.ID, h)
e.applyVisitTownQuestProgress(h) e.applyVisitTownQuestProgress(h)
} }

@ -134,6 +134,34 @@ type townPausePersistSignature struct {
ExcursionDepthWorldUnits float64 ExcursionDepthWorldUnits float64
ExcursionRoadFreezeWaypoint int ExcursionRoadFreezeWaypoint int
ExcursionRoadFreezeFraction float64 ExcursionRoadFreezeFraction float64
// In-town NPC tour: coarse milestones only (not per-tick x,y during walks).
InTown bool
InTownNextRoll time.Time
InTownLeave time.Time
InTownVisitStarted time.Time
InTownVisitLogs int
InTownNPCWalkTarget int64
InTownNPCWalkStart time.Time
InTownNPCWalkArrive time.Time
InTownPlazaHeal bool
InTownCenterWalkStart time.Time
InTownCenterWalkArrive time.Time
InTownNPCQueueLen int
InTownNPCQueueFP uint64
InTownVisitName string
InTownVisitType string
}
func npcQueueFingerprint(q []int64) uint64 {
const prime64 = 1099511628211
var h uint64 = 1469598103934665603
for _, id := range q {
h ^= uint64(id)
h *= prime64
}
h ^= uint64(len(q)) << 32
return h
} }
// NewHeroMovement creates a HeroMovement for a hero that just connected. // NewHeroMovement creates a HeroMovement for a hero that just connected.
@ -1075,6 +1103,24 @@ func (hm *HeroMovement) townPausePersistSignature() townPausePersistSignature {
sig.ExcursionRoadFreezeWaypoint = s.RoadFreezeWaypoint sig.ExcursionRoadFreezeWaypoint = s.RoadFreezeWaypoint
sig.ExcursionRoadFreezeFraction = s.RoadFreezeFraction sig.ExcursionRoadFreezeFraction = s.RoadFreezeFraction
} }
if hm.State == model.StateInTown {
sig.InTown = true
sig.InTownNextRoll = hm.NextTownNPCRollAt
sig.InTownLeave = hm.TownLeaveAt
sig.InTownVisitStarted = hm.TownVisitStartedAt
sig.InTownVisitLogs = hm.TownVisitLogsEmitted
sig.InTownNPCWalkTarget = hm.TownNPCWalkTargetID
sig.InTownNPCWalkStart = hm.TownNPCWalkStart
sig.InTownNPCWalkArrive = hm.TownNPCWalkArrive
sig.InTownPlazaHeal = hm.TownPlazaHealActive
sig.InTownCenterWalkStart = hm.TownCenterWalkStart
sig.InTownCenterWalkArrive = hm.TownCenterWalkArrive
sig.InTownNPCQueueLen = len(hm.TownNPCQueue)
sig.InTownNPCQueueFP = npcQueueFingerprint(hm.TownNPCQueue)
sig.InTownVisitName = hm.TownVisitNPCName
sig.InTownVisitType = hm.TownVisitNPCType
}
return sig return sig
} }

Loading…
Cancel
Save