diff --git a/gogobee_legacy_migration.md b/gogobee_legacy_migration.md index fed9651..16a5e38 100644 --- a/gogobee_legacy_migration.md +++ b/gogobee_legacy_migration.md @@ -314,9 +314,11 @@ This is also the right shape because zone-boss plumbing (bestiary, mood events, - `player_meta.display_name` column added via column migration in `internal/db/db.go`. - `backfillPlayerMetaDisplayName` runs on every Init: idempotent (`UPDATE ... WHERE display_name = ''` only touches empty rows, so dual-writes layered on top survive re-runs). - `upsertPlayerMetaDisplayName(userID, name)` dual-writes from `createAdvCharacter` (inside the same tx, atomic with the AdvCharacter INSERT) and `saveAdvCharacter` (post-commit, error-logged but non-fatal). -- `loadDisplayName(userID)` reads `player_meta` → falls back to `adventure_characters.display_name` → empty string if neither exists. Helper exists but **readers are NOT flipped yet** — soak week begins now; per-call-site flip happens in a follow-up session once we've confirmed dual-write doesn't drift. +- `loadDisplayName(userID)` reads `player_meta` → falls back to `adventure_characters.display_name` → empty string if neither exists. - Tests: `TestPlayerMetaDisplayNameBackfill_Idempotent`, `TestLoadDisplayName_FallsBackToAdvCharacter`, `TestCreateAdvCharacter_DualWritesDisplayName`. +**Reader flip SHIPPED (2026-05-09).** All `char.DisplayName` / `c.DisplayName` reader sites in `internal/plugin/` were swapped to `loadDisplayName(userID)` (or pass-through equivalents) across: `combat_bridge.go`, `dnd_zone_cmd.go`, `dnd_expedition_combat.go`, `dnd_zone_combat.go`, `adventure_robbie.go`, `adventure_scheduler.go`, `adventure_hospital.go`, `adventure_events.go`, `adventure_render.go`, `adventure_masterwork.go`, `adventure.go`, `adventure_arena.go`. The only remaining `char.DisplayName` references are in `adventure_character.go` itself (scan into struct + `saveAdvCharacter` SQL + dual-write to `player_meta`) and the `player_meta.go` doc comment. `go vet` + `go test ./internal/plugin/...` clean. + **Pre-conditions:** - L1–L4 all shipped. - DisplayName migrated to `player_meta` (see above). diff --git a/internal/plugin/adventure.go b/internal/plugin/adventure.go index cb68cb1..6ce3abf 100644 --- a/internal/plugin/adventure.go +++ b/internal/plugin/adventure.go @@ -699,8 +699,9 @@ func (p *AdventurePlugin) handleAdminRevive(ctx MessageContext, target string) e return p.SendReply(ctx.RoomID, ctx.EventID, "That user has no adventurer.") } + displayName, _ := loadDisplayName(targetID) if char.Alive { - return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("%s is already alive.", char.DisplayName)) + return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("%s is already alive.", displayName)) } char.Alive = true @@ -713,7 +714,7 @@ func (p *AdventurePlugin) handleAdminRevive(ctx MessageContext, target string) e if p.achievements != nil { p.achievements.GrantAchievement(targetID, "adv_revived") } - return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("✅ %s has been revived.", char.DisplayName)) + return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("✅ %s has been revived.", displayName)) } func (p *AdventurePlugin) handleAdminSummary(ctx MessageContext) error { @@ -982,6 +983,8 @@ func (p *AdventurePlugin) resolveRest(ctx MessageContext, char *AdventureCharact logAdvActivity(char.UserID, string(AdvActivityRest), "", "rest", 0, 0, "") + dispName, _ := loadDisplayName(char.UserID) + // Compute reset countdown now := time.Now().UTC() midnight := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, time.UTC) @@ -998,7 +1001,7 @@ func (p *AdventurePlugin) resolveRest(ctx MessageContext, char *AdventureCharact "Morning DM: %02d:00 UTC\n"+ "Evening summary: %02d:00 UTC\n\n"+ "The Arena is always open: `!arena`", - char.DisplayName, hours, minutes, p.morningHour, p.summaryHour) + dispName, hours, minutes, p.morningHour, p.summaryHour) if err := p.SendDM(ctx.Sender, restMsg); err != nil { slog.Error("adventure: failed to send rest DM", "user", ctx.Sender, "err", err) @@ -1210,8 +1213,9 @@ func (p *AdventurePlugin) announceTreasureToRoom(char *AdventureCharacter, def * if gr == "" { return } + displayName, _ := loadDisplayName(char.UserID) announce := advSubstituteFlavor(def.RoomAnnounce, map[string]string{ - "{name}": char.DisplayName, + "{name}": displayName, "{location}": loc.Name, }) p.SendMessage(id.RoomID(gr), announce) @@ -1221,8 +1225,9 @@ func (p *AdventurePlugin) announceTreasureToRoom(char *AdventureCharacter, def * func (p *AdventurePlugin) selectFlavorText(char *AdventureCharacter, result *AdvActionResult) (string, string) { loc := result.Location + displayName, _ := loadDisplayName(char.UserID) vars := map[string]string{ - "{name}": char.DisplayName, + "{name}": displayName, "{location}": loc.Name, "{value}": fmt.Sprintf("%d", result.TotalLootValue), "{xp}": fmt.Sprintf("%d", result.XPGained), diff --git a/internal/plugin/adventure_arena.go b/internal/plugin/adventure_arena.go index f24c05d..6655e24 100644 --- a/internal/plugin/adventure_arena.go +++ b/internal/plugin/adventure_arena.go @@ -205,10 +205,11 @@ type arenaBossNarration struct { // surrounding economic glue. There is no legacy fallback — a boss-flow // error surfaces to the player and aborts the round. func (p *AdventurePlugin) resolveArenaRound(ctx MessageContext, run *ArenaRun, char *AdventureCharacter, equip map[EquipmentSlot]*AdvEquipment, tier *ArenaTier, monster *ArenaMonster) error { + displayName, _ := loadDisplayName(ctx.Sender) intro, phases, outcome, result, err := p.resolveArenaBoss(ctx.Sender, ArenaBossEncounter{ Tier: run.Tier, Round: run.Round, - DisplayName: char.DisplayName, + DisplayName: displayName, }) if err != nil { slog.Error("arena: boss flow failed", "user", ctx.Sender, "err", err) @@ -299,7 +300,8 @@ func (p *AdventurePlugin) handleArenaStats(ctx MessageContext) error { } else { slog.Error("player_meta: arena stats read failed, falling back to AdvCharacter", "user", ctx.Sender, "err", err) } - return p.SendDM(ctx.Sender, renderArenaPersonalStats(char.DisplayName, wins, losses, stats)) + displayName, _ := loadDisplayName(ctx.Sender) + return p.SendDM(ctx.Sender, renderArenaPersonalStats(displayName, wins, losses, stats)) } func (p *AdventurePlugin) handleArenaLeaderboard(ctx MessageContext) error { @@ -401,7 +403,8 @@ func (p *AdventurePlugin) resolveArenaSurvival(ctx MessageContext, run *ArenaRun if dropped := p.arenaRollHelmetDrop(ctx.Sender, run.Tier); dropped != nil { finalMessage += "\n" + renderArenaHelmetDrop(dropped) - p.postArenaDropAnnouncement(char.DisplayName, dropped) + displayName, _ := loadDisplayName(ctx.Sender) + p.postArenaDropAnnouncement(displayName, dropped) } done := p.sendZoneCombatMessages(ctx.Sender, phaseMessages, finalMessage) @@ -498,7 +501,8 @@ func (p *AdventurePlugin) resolveArenaDeath(ctx MessageContext, run *ArenaRun, c if dt.PetRecovered { gr := gamesRoom() if gr != "" { - _ = p.SendMessage(gr, petDitchRecoveryGameRoom(char.DisplayName, char.PetName, true)) + displayName, _ := loadDisplayName(ctx.Sender) + _ = p.SendMessage(gr, petDitchRecoveryGameRoom(displayName, char.PetName, true)) } } @@ -581,8 +585,9 @@ func (p *AdventurePlugin) arenaCompleteSession(userID id.UserID, run *ArenaRun, // Room announcement for T5 gr := gamesRoom() if gr != "" { + displayName, _ := loadDisplayName(userID) announce := fmt.Sprintf("🏆 **%s has conquered the Arena.** Tier 5 streak. €%d earned. That Which Has Always Been has fallen.", - char.DisplayName, run.Earnings) + displayName, run.Earnings) p.SendMessage(id.RoomID(gr), announce) } } @@ -596,9 +601,10 @@ func (p *AdventurePlugin) arenaCompleteSession(userID id.UserID, run *ArenaRun, // Game room announcement gr := gamesRoom() if gr != "" { + displayName, _ := loadDisplayName(userID) p.SendMessage(id.RoomID(gr), fmt.Sprintf( "🏆 %s walked out of the arena with the Gladiator's Helm. Tier %d streak. They had the option to stop. They did not stop.", - char.DisplayName, tiersWon)) + displayName, tiersWon)) } } } diff --git a/internal/plugin/adventure_events.go b/internal/plugin/adventure_events.go index b9f1dd1..818f354 100644 --- a/internal/plugin/adventure_events.go +++ b/internal/plugin/adventure_events.go @@ -142,9 +142,11 @@ func (p *AdventurePlugin) tryTriggerEvent(userID id.UserID) { slog.Info("adventure: mid-day event triggered", "user", userID, "event", event.Key, "id", eventID) + displayName, _ := loadDisplayName(userID) + // DM the player triggerDM := advSubstituteFlavor(event.TriggerDM, map[string]string{ - "{name}": char.DisplayName, + "{name}": displayName, }) if err := p.SendDM(userID, triggerDM); err != nil { slog.Error("adventure: events: failed to send trigger DM", "user", userID, "err", err) @@ -154,7 +156,7 @@ func (p *AdventurePlugin) tryTriggerEvent(userID id.UserID) { gr := gamesRoom() if gr != "" { roomLine := advSubstituteFlavor(advEventRoomTriggerWrapper, map[string]string{ - "{trigger_room_line}": advSubstituteFlavor(event.TriggerRoomLine, map[string]string{"{name}": char.DisplayName}), + "{trigger_room_line}": advSubstituteFlavor(event.TriggerRoomLine, map[string]string{"{name}": displayName}), }) _ = p.SendMessage(id.RoomID(gr), roomLine) } diff --git a/internal/plugin/adventure_hospital.go b/internal/plugin/adventure_hospital.go index b259157..609b282 100644 --- a/internal/plugin/adventure_hospital.go +++ b/internal/plugin/adventure_hospital.go @@ -55,7 +55,8 @@ func (p *AdventurePlugin) handleHospitalCmd(ctx MessageContext) error { // Room announcement gr := gamesRoom() if gr != "" { - p.SendMessage(gr, fmt.Sprintf(hospitalDitchAnnounce, char.DisplayName)) + name, _ := loadDisplayName(char.UserID) + p.SendMessage(gr, fmt.Sprintf(hospitalDitchAnnounce, name)) } return nil } @@ -152,7 +153,8 @@ func (p *AdventurePlugin) resolveHospitalPay(ctx MessageContext, interaction *ad gr := gamesRoom() if gr != "" { - p.SendMessage(gr, fmt.Sprintf(hospitalDitchAnnounce, char.DisplayName)) + name, _ := loadDisplayName(char.UserID) + p.SendMessage(gr, fmt.Sprintf(hospitalDitchAnnounce, name)) } return nil } @@ -191,7 +193,8 @@ func (p *AdventurePlugin) resolveHospitalPay(ctx MessageContext, interaction *ad // Room announcement gr := gamesRoom() if gr != "" { - p.SendMessage(gr, fmt.Sprintf(hospitalDischargeAnnounce, char.DisplayName)) + name, _ := loadDisplayName(char.UserID) + p.SendMessage(gr, fmt.Sprintf(hospitalDischargeAnnounce, name)) } return nil @@ -206,7 +209,8 @@ func (p *AdventurePlugin) resolveHospitalPay(ctx MessageContext, interaction *ad if err == nil { gr := gamesRoom() if gr != "" { - p.SendMessage(gr, fmt.Sprintf(hospitalDitchAnnounce, char.DisplayName)) + name, _ := loadDisplayName(char.UserID) + p.SendMessage(gr, fmt.Sprintf(hospitalDitchAnnounce, name)) } } diff --git a/internal/plugin/adventure_masterwork.go b/internal/plugin/adventure_masterwork.go index b0fc9df..49c946f 100644 --- a/internal/plugin/adventure_masterwork.go +++ b/internal/plugin/adventure_masterwork.go @@ -262,7 +262,8 @@ func (p *AdventurePlugin) checkMasterworkDrop(userID id.UserID, char *AdventureC p.SendDM(userID, sb.String()) // Room announcement (tiered) - p.postMasterworkAnnouncement(char.DisplayName, def, loc.Name) + displayName, _ := loadDisplayName(userID) + p.postMasterworkAnnouncement(displayName, def, loc.Name) } // ── Room Announcements ───────────────────────────────────────────────────── diff --git a/internal/plugin/adventure_render.go b/internal/plugin/adventure_render.go index 46f770b..fbb8101 100644 --- a/internal/plugin/adventure_render.go +++ b/internal/plugin/adventure_render.go @@ -92,7 +92,8 @@ func advSubstituteFlavor(template string, vars map[string]string) string { func renderAdvCharacterSheet(char *AdventureCharacter, equip map[EquipmentSlot]*AdvEquipment, items []AdvItem, treasures []AdvTreasureBonus, balance float64) string { var sb strings.Builder - sb.WriteString(fmt.Sprintf("⚔️ **%s's Adventurer**\n\n", char.DisplayName)) + displayName, _ := loadDisplayName(char.UserID) + sb.WriteString(fmt.Sprintf("⚔️ **%s's Adventurer**\n\n", displayName)) // Stats sb.WriteString("📊 Stats:\n") @@ -275,8 +276,8 @@ func renderRivalNudge(char *AdventureCharacter) string { hours = 1 // floor — "<1h" feels worse than "1h" } rivalName := string(c.ChallengerID) - if rc, err := loadAdvCharacter(c.ChallengerID); err == nil && rc != nil && rc.DisplayName != "" { - rivalName = rc.DisplayName + if name, err := loadDisplayName(c.ChallengerID); err == nil && name != "" { + rivalName = name } return fmt.Sprintf("⚔️ **Rival challenge open** — %s, round %d/3, €%d on the line · expires in %dh · reply **rock**, **paper**, or **scissors**", rivalName, c.Round, c.Stake, hours) @@ -294,8 +295,9 @@ func renderAdvMorningDM(char *AdventureCharacter, equip map[EquipmentSlot]*AdvEq // Pick a morning greeting greeting, _ := advPickFlavor(MorningDM, char.UserID, "morning_dm") + displayName, _ := loadDisplayName(char.UserID) vars := map[string]string{ - "{name}": char.DisplayName, + "{name}": displayName, "{character_sheet}": fmt.Sprintf( " ⚔️ Combat Lv.%d ⛏️ Mining Lv.%d 🌿 Foraging Lv.%d 🎣 Fishing Lv.%d\n 💰 €%.0f", char.CombatLevel, char.MiningSkill, char.ForagingSkill, char.FishingSkill, balance), @@ -503,8 +505,9 @@ func renderAdvDeathStatusDM(char *AdventureCharacter) string { func renderAdvRespawnDM(char *AdventureCharacter) string { text, _ := advPickFlavor(RespawnDM, char.UserID, "respawn_dm") + displayName, _ := loadDisplayName(char.UserID) return advSubstituteFlavor(text, map[string]string{ - "{name}": char.DisplayName, + "{name}": displayName, }) } @@ -664,8 +667,9 @@ func masteryBar(value, total int) string { func renderAdvIdleShameDM(char *AdventureCharacter) string { text, _ := advPickFlavor(IdleShameDM, char.UserID, "idle_shame") + displayName, _ := loadDisplayName(char.UserID) return advSubstituteFlavor(text, map[string]string{ - "{name}": char.DisplayName, + "{name}": displayName, }) } @@ -673,8 +677,9 @@ func renderAdvIdleShameDM(char *AdventureCharacter) string { func renderAdvOnboardingDM(char *AdventureCharacter) string { text, _ := advPickFlavor(OnboardingDM, char.UserID, "onboarding") + displayName, _ := loadDisplayName(char.UserID) return advSubstituteFlavor(text, map[string]string{ - "{name}": char.DisplayName, + "{name}": displayName, }) } @@ -890,8 +895,9 @@ func renderAdvLeaderboard(chars []AdventureCharacter) string { var entries []entry for _, c := range chars { score := (c.CombatLevel + c.MiningSkill + c.ForagingSkill + c.FishingSkill) * 10 + name, _ := loadDisplayName(c.UserID) entries = append(entries, entry{ - Name: c.DisplayName, + Name: name, Score: score, Levels: fmt.Sprintf("⚔️%d ⛏️%d 🌿%d 🎣%d", c.CombatLevel, c.MiningSkill, c.ForagingSkill, c.FishingSkill), Streak: c.CurrentStreak, diff --git a/internal/plugin/adventure_robbie.go b/internal/plugin/adventure_robbie.go index 288a700..b8248ad 100644 --- a/internal/plugin/adventure_robbie.go +++ b/internal/plugin/adventure_robbie.go @@ -83,7 +83,8 @@ func (p *AdventurePlugin) robbieVisitAll() { time.Sleep(time.Duration(1000+rand.IntN(2000)) * time.Millisecond) } - p.robbieVisitPlayer(char.UserID, char.DisplayName) + name, _ := loadDisplayName(char.UserID) + p.robbieVisitPlayer(char.UserID, name) } } diff --git a/internal/plugin/adventure_scheduler.go b/internal/plugin/adventure_scheduler.go index ccf1e71..e838377 100644 --- a/internal/plugin/adventure_scheduler.go +++ b/internal/plugin/adventure_scheduler.go @@ -232,8 +232,9 @@ func (p *AdventurePlugin) postDailySummary() { // Build player summaries var players []AdvPlayerDaySummary for _, c := range chars { + dispName, _ := loadDisplayName(c.UserID) ps := AdvPlayerDaySummary{ - DisplayName: c.DisplayName, + DisplayName: dispName, CombatLevel: c.CombatLevel, MiningSkill: c.MiningSkill, ForagingSkill: c.ForagingSkill, diff --git a/internal/plugin/combat_bridge.go b/internal/plugin/combat_bridge.go index 9b88707..553e643 100644 --- a/internal/plugin/combat_bridge.go +++ b/internal/plugin/combat_bridge.go @@ -101,8 +101,9 @@ func (p *AdventurePlugin) runDungeonCombat( ability = &MonsterAbility{Name: "Stone Skin", Phase: "opening", ProcChance: 0.20, Effect: "armor_break"} } + displayName, _ := loadDisplayName(userID) player := Combatant{ - Name: char.DisplayName, + Name: displayName, Stats: playerStats, Mods: playerMods, IsPlayer: true, diff --git a/internal/plugin/dnd_expedition_combat.go b/internal/plugin/dnd_expedition_combat.go index b8090b0..8c94349 100644 --- a/internal/plugin/dnd_expedition_combat.go +++ b/internal/plugin/dnd_expedition_combat.go @@ -378,8 +378,8 @@ func (p *AdventurePlugin) tryPatrolEncounter( // Phases: forward-simulating engine play-by-play. playerName := "You" - if char, _ := loadAdvCharacter(userID); char != nil && char.DisplayName != "" { - playerName = char.DisplayName + if name, _ := loadDisplayName(userID); name != "" { + playerName = name } phases = RenderCombatLog(result, playerName, monster.Name) diff --git a/internal/plugin/dnd_zone_cmd.go b/internal/plugin/dnd_zone_cmd.go index f87e6ba..0418d9e 100644 --- a/internal/plugin/dnd_zone_cmd.go +++ b/internal/plugin/dnd_zone_cmd.go @@ -568,8 +568,8 @@ func (p *AdventurePlugin) resolveCombatRoom(userID id.UserID, run *DungeonRun, z // Phases: forward-simulating engine play-by-play. Use the player's // display name when available so narrative lines read naturally. playerName := "You" - if char, _ := loadAdvCharacter(userID); char != nil && char.DisplayName != "" { - playerName = char.DisplayName + if name, _ := loadDisplayName(userID); name != "" { + playerName = name } phases = RenderCombatLog(result, playerName, monster.Name) @@ -645,8 +645,8 @@ func (p *AdventurePlugin) resolveBossRoom(userID id.UserID, run *DungeonRun, zon intro = fmt.Sprintf("👑 **Boss — %s** (HP %d, AC %d)", monster.Name, monster.HP, monster.AC) playerName := "You" - if char, _ := loadAdvCharacter(userID); char != nil && char.DisplayName != "" { - playerName = char.DisplayName + if name, _ := loadDisplayName(userID); name != "" { + playerName = name } phases = RenderCombatLog(result, playerName, monster.Name) diff --git a/internal/plugin/dnd_zone_combat.go b/internal/plugin/dnd_zone_combat.go index db187b4..7e206dc 100644 --- a/internal/plugin/dnd_zone_combat.go +++ b/internal/plugin/dnd_zone_combat.go @@ -164,8 +164,9 @@ func (p *AdventurePlugin) runZoneCombat( } applyPendingCast(userID, dndChar, &playerStats, &playerMods, &enemyStats) + displayName, _ := loadDisplayName(userID) player := Combatant{ - Name: char.DisplayName, + Name: displayName, Stats: playerStats, Mods: playerMods, IsPlayer: true,