mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Adv 2.0 post-L5 audit cleanup: drop redundant dual-writes + refresh stale comments
Audit confirmed saveAdvCharacter's fan-out (upsertAllPlayerMetaFromAdvChar) covers all 13 migrated subsystems, so the per-call-site upsertPlayerMetaXxx calls retained "as defense" through the L4-L5 migration are redundant post-L5h. Removed 16 such call sites across arena (2), scheduler (4), hospital (1), consumables (2), rival (1), housing (11), and reordered masterwork to drop a now-pointless explicit upsert before the load+save. Refreshed nine stale doc comments referencing the legacy adventure_characters table or "soak window / fallback" language that no longer applies after the L5 close-out (commit 596b2b7). No behavior change; go vet + go test ./... pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -468,13 +468,6 @@ func (p *AdventurePlugin) resolveArenaDeath(ctx MessageContext, run *ArenaRun, c
|
||||
if err := saveAdvCharacter(char); err != nil {
|
||||
slog.Error("arena: failed to save character after death", "user", ctx.Sender, "err", err)
|
||||
}
|
||||
// Dual-write to player_meta (Adv 2.0 Phase L2 step 5).
|
||||
if err := upsertPlayerMetaArena(ctx.Sender, char.ArenaWins, char.ArenaLosses, char.InvasionScore); err != nil {
|
||||
slog.Error("player_meta: arena loss dual-write failed", "user", ctx.Sender, "err", err)
|
||||
}
|
||||
if err := upsertPlayerMetaSkillState(ctx.Sender, skillStateFromAdvChar(char)); err != nil {
|
||||
slog.Error("player_meta: arena loss skill dual-write failed", "user", ctx.Sender, "err", err)
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
run.Status = "dead"
|
||||
@@ -550,13 +543,6 @@ func (p *AdventurePlugin) arenaCompleteSession(userID id.UserID, run *ArenaRun,
|
||||
if err := saveAdvCharacter(char); err != nil {
|
||||
slog.Error("arena: failed to save character after session complete", "user", userID, "err", err)
|
||||
}
|
||||
// Dual-write to player_meta (Adv 2.0 Phase L2 step 5).
|
||||
if err := upsertPlayerMetaArena(userID, char.ArenaWins, char.ArenaLosses, char.InvasionScore); err != nil {
|
||||
slog.Error("player_meta: arena win dual-write failed", "user", userID, "err", err)
|
||||
}
|
||||
if err := upsertPlayerMetaSkillState(userID, skillStateFromAdvChar(char)); err != nil {
|
||||
slog.Error("player_meta: arena win skill dual-write failed", "user", userID, "err", err)
|
||||
}
|
||||
|
||||
// End run if not already ended
|
||||
if run.Status != "completed" && run.Status != "cashed_out" {
|
||||
|
||||
@@ -483,7 +483,6 @@ func autoCraftConsumables(userID id.UserID, items []AdvItem, foragingLevel int)
|
||||
char.CraftsSucceeded++
|
||||
checkAdvLevelUp(char, "foraging")
|
||||
_ = saveAdvCharacter(char)
|
||||
_ = upsertPlayerMetaSkillState(userID, skillStateFromAdvChar(char))
|
||||
}
|
||||
} else {
|
||||
// Failure: all ingredients destroyed
|
||||
@@ -497,7 +496,6 @@ func autoCraftConsumables(userID id.UserID, items []AdvItem, foragingLevel int)
|
||||
char.ForagingXP += craftXPFailure[bestRecipe.Tier]
|
||||
checkAdvLevelUp(char, "foraging")
|
||||
_ = saveAdvCharacter(char)
|
||||
_ = upsertPlayerMetaSkillState(userID, skillStateFromAdvChar(char))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,10 +199,9 @@ func (p *AdventurePlugin) resolveHospitalPay(ctx MessageContext, interaction *ad
|
||||
trackTaxPaid(ctx.Sender, potCut)
|
||||
}
|
||||
|
||||
// Revive. Phase L4a: restore DnDCharacter HP to full (canonical
|
||||
// post-L5 path) and keep flipping AdvCharacter.Alive during soak
|
||||
// since other systems still read it. HospitalVisits dual-writes
|
||||
// to player_meta + AdvCharacter (§11).
|
||||
// Revive. Restore DnDCharacter HP to full (canonical post-L5 path);
|
||||
// AdvCharacter.Alive flip + HospitalVisits++ propagate through the
|
||||
// saveAdvCharacter fan-out into player_meta.
|
||||
char.Alive = true
|
||||
char.DeadUntil = nil
|
||||
char.HospitalVisits++
|
||||
@@ -212,9 +211,6 @@ func (p *AdventurePlugin) resolveHospitalPay(ctx MessageContext, interaction *ad
|
||||
p.euro.Credit(ctx.Sender, float64(cost), "hospital_revival_refund")
|
||||
return p.SendDM(ctx.Sender, "Something went wrong during recovery. Your payment has been refunded.")
|
||||
}
|
||||
if err := upsertPlayerMetaHospitalVisits(char.UserID, char.HospitalVisits); err != nil {
|
||||
slog.Error("hospital: failed to dual-write hospital_visits to player_meta", "user", char.UserID, "err", err)
|
||||
}
|
||||
if dnd, err := LoadDnDCharacter(char.UserID); err == nil && dnd != nil {
|
||||
dnd.HPCurrent = dnd.HPMax
|
||||
if err := SaveDnDCharacter(dnd); err != nil {
|
||||
|
||||
@@ -253,7 +253,6 @@ func (p *AdventurePlugin) handleThomCmd(ctx MessageContext) error {
|
||||
userMu.Lock()
|
||||
char.ThomAnimalLineFired = true
|
||||
_ = saveAdvCharacter(char)
|
||||
_ = upsertPlayerMetaNPCState(char.UserID, npcStateFromAdvChar(char))
|
||||
userMu.Unlock()
|
||||
}
|
||||
return p.SendDM(ctx.Sender, text)
|
||||
@@ -331,8 +330,6 @@ func (p *AdventurePlugin) handleThomBuy(ctx MessageContext, char *AdventureChara
|
||||
p.euro.Credit(ctx.Sender, float64(totalCost), "house_purchase_refund")
|
||||
return p.SendDM(ctx.Sender, "Failed to save. You've been refunded.")
|
||||
}
|
||||
// L4e dual-write: mirror house state into player_meta.
|
||||
_ = upsertPlayerMetaHouseState(char.UserID, houseStateFromAdvChar(char))
|
||||
return p.SendDM(ctx.Sender, fmt.Sprintf("🏠 **%s** purchased outright for €%d.\n\nNo loan. No weekly payments. Thom is visibly disappointed.", def.Name, totalCost))
|
||||
}
|
||||
|
||||
@@ -364,8 +361,6 @@ func (p *AdventurePlugin) handleThomBuy(ctx MessageContext, char *AdventureChara
|
||||
}
|
||||
return p.SendDM(ctx.Sender, "Failed to save. You've been refunded.")
|
||||
}
|
||||
// L4e dual-write: mirror house state into player_meta.
|
||||
_ = upsertPlayerMetaHouseState(char.UserID, houseStateFromAdvChar(char))
|
||||
|
||||
weekly := houseWeeklyPayment(loanAmount, rate)
|
||||
var sb strings.Builder
|
||||
@@ -414,8 +409,6 @@ func (p *AdventurePlugin) handleThomPayoff(ctx MessageContext, char *AdventureCh
|
||||
p.euro.Credit(ctx.Sender, payoffAmount, "house_payoff_refund")
|
||||
return p.SendDM(ctx.Sender, "Failed to save. Your money has been refunded.")
|
||||
}
|
||||
// L4e dual-write: mirror house state into player_meta.
|
||||
_ = upsertPlayerMetaHouseState(char.UserID, houseStateFromAdvChar(char))
|
||||
|
||||
if char.HasPet() {
|
||||
return p.SendDM(ctx.Sender, fmt.Sprintf("🏠 %s", fmt.Sprintf(thomPaidOffPet, char.PetName)))
|
||||
@@ -469,8 +462,6 @@ func (p *AdventurePlugin) handleThomPay(ctx MessageContext, char *AdventureChara
|
||||
p.euro.Credit(ctx.Sender, float64(amount), "house_extra_payment_refund")
|
||||
return p.SendDM(ctx.Sender, "Failed to save. Your money has been refunded.")
|
||||
}
|
||||
// L4e dual-write: mirror house state into player_meta.
|
||||
_ = upsertPlayerMetaHouseState(char.UserID, houseStateFromAdvChar(char))
|
||||
|
||||
if char.HouseLoanBalance == 0 {
|
||||
if char.HasPet() {
|
||||
@@ -502,8 +493,6 @@ func (p *AdventurePlugin) handleThomAutopay(ctx MessageContext, char *AdventureC
|
||||
if err := saveAdvCharacter(char); err != nil {
|
||||
return p.SendDM(ctx.Sender, "Failed to save.")
|
||||
}
|
||||
// L4e dual-write: mirror house state into player_meta.
|
||||
_ = upsertPlayerMetaHouseState(char.UserID, houseStateFromAdvChar(char))
|
||||
|
||||
if char.HouseAutopay {
|
||||
return p.SendDM(ctx.Sender, "✅ Autopay enabled. 2% discount on weekly payments. Thom calls this a favour. It is not.")
|
||||
@@ -549,7 +538,6 @@ func (p *AdventurePlugin) processMortgagePayments() {
|
||||
freshChar.HouseLoanBalance = 0
|
||||
freshChar.HouseMissedPayments = 0
|
||||
_ = saveAdvCharacter(freshChar)
|
||||
_ = upsertPlayerMetaHouseState(freshChar.UserID, houseStateFromAdvChar(freshChar))
|
||||
userMu.Unlock()
|
||||
// Paid off!
|
||||
if freshChar.HasPet() {
|
||||
@@ -561,7 +549,6 @@ func (p *AdventurePlugin) processMortgagePayments() {
|
||||
}
|
||||
freshChar.HouseMissedPayments = 0
|
||||
_ = saveAdvCharacter(freshChar)
|
||||
_ = upsertPlayerMetaHouseState(freshChar.UserID, houseStateFromAdvChar(freshChar))
|
||||
userMu.Unlock()
|
||||
} else {
|
||||
// Missed payment
|
||||
@@ -572,7 +559,6 @@ func (p *AdventurePlugin) processMortgagePayments() {
|
||||
if freshChar.HouseMissedPayments >= 10 {
|
||||
freshChar.HouseLoanFrozen = true
|
||||
_ = saveAdvCharacter(freshChar)
|
||||
_ = upsertPlayerMetaHouseState(freshChar.UserID, houseStateFromAdvChar(freshChar))
|
||||
userMu.Unlock()
|
||||
if freshChar.HasPet() {
|
||||
_ = p.SendDM(freshChar.UserID, fmt.Sprintf("🏠 %s", fmt.Sprintf(thomFreezePet, freshChar.PetName)))
|
||||
@@ -581,7 +567,6 @@ func (p *AdventurePlugin) processMortgagePayments() {
|
||||
}
|
||||
} else {
|
||||
_ = saveAdvCharacter(freshChar)
|
||||
_ = upsertPlayerMetaHouseState(freshChar.UserID, houseStateFromAdvChar(freshChar))
|
||||
userMu.Unlock()
|
||||
if freshChar.HasPet() {
|
||||
_ = p.SendDM(freshChar.UserID, fmt.Sprintf("🏠 %s", fmt.Sprintf(thomMissedPaymentPet, freshChar.HouseLoanBalance, freshChar.PetName)))
|
||||
@@ -623,7 +608,6 @@ func (p *AdventurePlugin) sendMortgageRateChangeDMs(oldRate, newRate float64) {
|
||||
}
|
||||
freshChar.HouseCurrentRate = newRate
|
||||
_ = saveAdvCharacter(freshChar)
|
||||
_ = upsertPlayerMetaHouseState(freshChar.UserID, houseStateFromAdvChar(freshChar))
|
||||
userMu.Unlock()
|
||||
|
||||
newWeekly := houseWeeklyPayment(freshChar.HouseLoanBalance, newRate)
|
||||
|
||||
@@ -193,24 +193,17 @@ func (p *AdventurePlugin) checkMasterworkDrop(userID id.UserID, equip map[Equipm
|
||||
autoEquip = true // genuinely better than what they have
|
||||
}
|
||||
|
||||
// First-drop detection. Read via player_meta (L4c migration); during
|
||||
// soak it falls back to AdvCharacter for users whose row hasn't been
|
||||
// dual-written yet.
|
||||
// First-drop detection. Counter persists via saveAdvCharacter fan-out.
|
||||
priorDrops, err := loadMasterworkDrops(userID)
|
||||
if err != nil {
|
||||
slog.Error("adventure: failed to load masterwork counter", "user", userID, "err", err)
|
||||
}
|
||||
isFirstDrop := priorDrops == 0
|
||||
newDrops := priorDrops + 1
|
||||
if err := upsertPlayerMetaMasterworkDrops(userID, newDrops); err != nil {
|
||||
slog.Error("adventure: failed to save masterwork counter (player_meta)", "user", userID, "err", err)
|
||||
}
|
||||
// Dual-write to AdvCharacter during soak. Best-effort: if the row is
|
||||
// missing the player_meta write above is canonical.
|
||||
if char, lerr := loadAdvCharacter(userID); lerr == nil {
|
||||
char.MasterworkDropsReceived = newDrops
|
||||
if err := saveAdvCharacter(char); err != nil {
|
||||
slog.Error("adventure: failed to save masterwork counter (legacy)", "user", userID, "err", err)
|
||||
slog.Error("adventure: failed to save masterwork counter", "user", userID, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,16 +89,12 @@ func (p *AdventurePlugin) checkRivalPoolUnlock(char *AdventureCharacter) {
|
||||
return
|
||||
}
|
||||
// Promote to pool + send unlock DM if not yet announced.
|
||||
// Persistence rides the caller's saveAdvCharacter → fan-out.
|
||||
char.RivalPool = 1
|
||||
pool = 1
|
||||
if !notified {
|
||||
char.RivalUnlockedNotified = true
|
||||
notified = true
|
||||
p.SendDM(char.UserID, rivalUnlockDM)
|
||||
}
|
||||
if err := upsertPlayerMetaRivalState(char.UserID, pool, notified); err != nil {
|
||||
slog.Error("player_meta: rival unlock dual-write failed", "user", char.UserID, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ── DB CRUD ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -95,7 +95,6 @@ func (p *AdventurePlugin) sendMorningDMs() {
|
||||
if err := saveAdvCharacter(&char); err != nil {
|
||||
slog.Error("babysit: failed to save after daily trickle", "user", char.UserID, "err", err)
|
||||
}
|
||||
_ = upsertPlayerMetaPetState(char.UserID, petStateFromAdvChar(&char))
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -127,7 +126,6 @@ func (p *AdventurePlugin) sendMorningDMs() {
|
||||
if petEvent != "" {
|
||||
char.PetMorningDefense = true
|
||||
_ = saveAdvCharacter(&char)
|
||||
_ = upsertPlayerMetaPetState(char.UserID, petStateFromAdvChar(&char))
|
||||
}
|
||||
|
||||
// Send morning DM with choices
|
||||
@@ -388,7 +386,6 @@ func (p *AdventurePlugin) midnightReset() error {
|
||||
text += " — not all is lost."
|
||||
}
|
||||
_ = saveAdvCharacter(&char)
|
||||
_ = upsertPlayerMetaLifecycleState(char.UserID, lifecycleStateFromAdvChar(&char))
|
||||
}
|
||||
if err := p.SendDM(char.UserID, text); err != nil {
|
||||
slog.Error("adventure: failed to send idle shame DM", "user", char.UserID, "err", err)
|
||||
@@ -405,7 +402,6 @@ func (p *AdventurePlugin) midnightReset() error {
|
||||
char.BestStreak = char.CurrentStreak
|
||||
}
|
||||
_ = saveAdvCharacter(&char)
|
||||
_ = upsertPlayerMetaLifecycleState(char.UserID, lifecycleStateFromAdvChar(&char))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -497,11 +497,11 @@ func dndHPSnapshot(userID id.UserID) (cur, max int) {
|
||||
return c.HPCurrent, c.HPMax
|
||||
}
|
||||
|
||||
// markAdventureDead flips the legacy adventure_characters.alive flag and
|
||||
// starts the 6h respawn timer for a player who went down in a D&D-layer
|
||||
// combat. Without this, hp_current persists as 0 but the legacy alive
|
||||
// flag stays true — the "zombie" state where !hospital says "you're
|
||||
// alive!" while the sheet shows 0/33. Idempotent: bails if already dead.
|
||||
// markAdventureDead flips Alive=false in player_meta (via saveAdvCharacter
|
||||
// fan-out) and starts the 6h respawn timer for a player who went down in a
|
||||
// D&D-layer combat. Without this, hp_current persists as 0 but Alive stays
|
||||
// true — the "zombie" state where !hospital says "you're alive!" while the
|
||||
// sheet shows 0/33. Idempotent: bails if already dead.
|
||||
//
|
||||
// source is "zone" / "expedition" / "patrol"; location is human-readable
|
||||
// (e.g. "Forest of Shadows") and surfaces in the daily report and
|
||||
|
||||
@@ -126,9 +126,7 @@ func (p *AdventurePlugin) handleDnDLongRest(ctx MessageContext) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Eligibility: housing OR pay inn fee. Housing read flips to
|
||||
// player_meta via loadHouseState (L4e reader flip); falls back to
|
||||
// adventure_characters during the soak window.
|
||||
// Eligibility: housing OR pay inn fee.
|
||||
house, _ := loadHouseState(ctx.Sender)
|
||||
hasHousing := house.Tier > 0
|
||||
innPaid := false
|
||||
|
||||
@@ -42,10 +42,10 @@ func (p *AdventurePlugin) handleDnDAbilitiesCmd(ctx MessageContext) error {
|
||||
|
||||
// !sheet — read-only D&D character sheet renderer.
|
||||
//
|
||||
// Joins:
|
||||
// Sources:
|
||||
// dnd_character (D&D layer)
|
||||
// adventure_characters (legacy skills, pet, housing — for at-a-glance context)
|
||||
// adventure_equipment (current gear; renders with legacy fields until Phase 4)
|
||||
// player_meta (skills, pet, housing — for at-a-glance context)
|
||||
// adventure_equipment (current gear)
|
||||
// adventure_treasures (= attunement substrate per v1.1 §7.4)
|
||||
|
||||
func (p *AdventurePlugin) handleDnDSheetCmd(ctx MessageContext) error {
|
||||
|
||||
@@ -419,12 +419,7 @@ type SkillState struct {
|
||||
FishingXP int
|
||||
}
|
||||
|
||||
// HasSkills returns true when any skill or XP field is non-zero. Used as
|
||||
// the "row is migrated" marker in loadSkillState — an all-zero row is
|
||||
// indistinguishable from "no row yet" so it falls through to the legacy
|
||||
// table during the soak window. CombatLevel starts at 1 for any created
|
||||
// character (see createAdvCharacter), so the only fully-zero state is a
|
||||
// truly unmigrated row.
|
||||
// HasSkills returns true when any skill or XP field is non-zero.
|
||||
func (s SkillState) HasSkills() bool {
|
||||
return s.CombatLevel > 0 || s.MiningSkill > 0 || s.ForagingSkill > 0 || s.FishingSkill > 0 ||
|
||||
s.CombatXP > 0 || s.MiningXP > 0 || s.ForagingXP > 0 || s.FishingXP > 0
|
||||
@@ -458,9 +453,7 @@ func upsertPlayerMetaSkillState(userID id.UserID, s SkillState) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// loadSkillState returns the player's skill state from player_meta when
|
||||
// populated (HasSkills true), falling back to adventure_characters during
|
||||
// the L5a soak window.
|
||||
// loadSkillState returns the player's skill state from player_meta.
|
||||
func loadSkillState(userID id.UserID) (SkillState, error) {
|
||||
var s SkillState
|
||||
err := db.Get().QueryRow(
|
||||
@@ -483,10 +476,8 @@ func loadSkillState(userID id.UserID) (SkillState, error) {
|
||||
}
|
||||
|
||||
// skillStateFromAdvChar projects the skill-related fields off an
|
||||
// AdventureCharacter into a SkillState. Used by the dual-write path: every
|
||||
// site that mutates AdvCharacter skill or XP fields then calls
|
||||
// upsertPlayerMetaSkillState with this projection so the columns move
|
||||
// cleanly during the L5a soak window.
|
||||
// AdventureCharacter into a SkillState. Used by saveAdvCharacter's fan-out
|
||||
// to mirror skill columns into player_meta.
|
||||
func skillStateFromAdvChar(c *AdventureCharacter) SkillState {
|
||||
return SkillState{
|
||||
CombatLevel: c.CombatLevel,
|
||||
@@ -683,8 +674,7 @@ func upsertPlayerMetaNPCState(userID id.UserID, s NPCState) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// loadNPCState returns the NPC state from player_meta when populated,
|
||||
// otherwise falls back to adventure_characters during the L5c soak window.
|
||||
// loadNPCState returns the NPC state from player_meta.
|
||||
func loadNPCState(userID id.UserID) (NPCState, error) {
|
||||
var (
|
||||
s NPCState
|
||||
@@ -837,9 +827,7 @@ func upsertPlayerMetaLifecycleState(userID id.UserID, s LifecycleState) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// loadLifecycleState returns lifecycle state from player_meta when
|
||||
// populated, otherwise falls back to adventure_characters during the L5d
|
||||
// soak window.
|
||||
// loadLifecycleState returns lifecycle state from player_meta.
|
||||
func loadLifecycleState(userID id.UserID) (LifecycleState, error) {
|
||||
var (
|
||||
s LifecycleState
|
||||
@@ -1087,15 +1075,12 @@ func miscStateFromAdvChar(c *AdventureCharacter) MiscState {
|
||||
}
|
||||
}
|
||||
|
||||
// applyPlayerMetaOverlay overlays every player_meta-mirrored field onto an
|
||||
// AdventureCharacter struct that was just SELECTed from adventure_characters.
|
||||
// Phase L5h: saveAdvCharacter no longer writes the adventure_characters
|
||||
// columns, so the legacy row freezes at its pre-L5h state. The overlay
|
||||
// re-sources every migrated subsystem's value from player_meta (with
|
||||
// fallback-to-AdvCharacter inside each load helper for the fields where
|
||||
// player_meta is still empty), so callers continue to use char.X readers
|
||||
// transparently. Errors are logged and skipped — a partial overlay is safer
|
||||
// than a failed load.
|
||||
// applyPlayerMetaOverlay populates an AdventureCharacter from player_meta.
|
||||
// Post-L5 close-out: loadAdvCharacter no longer SELECTs from
|
||||
// adventure_characters — it default-inits the struct and overlays every
|
||||
// migrated subsystem's value from player_meta here. Errors are skipped — a
|
||||
// partial overlay is safer than a failed load (each field falls back to its
|
||||
// Go zero value, except Alive which is pre-set true).
|
||||
func applyPlayerMetaOverlay(c *AdventureCharacter) {
|
||||
if c == nil {
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user