diff --git a/internal/plugin/adventure_arena.go b/internal/plugin/adventure_arena.go index aa6b3aa..71fda3f 100644 --- a/internal/plugin/adventure_arena.go +++ b/internal/plugin/adventure_arena.go @@ -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" { diff --git a/internal/plugin/adventure_consumables.go b/internal/plugin/adventure_consumables.go index 072a5d4..3785ad7 100644 --- a/internal/plugin/adventure_consumables.go +++ b/internal/plugin/adventure_consumables.go @@ -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)) } } } diff --git a/internal/plugin/adventure_hospital.go b/internal/plugin/adventure_hospital.go index c6f495e..c02204d 100644 --- a/internal/plugin/adventure_hospital.go +++ b/internal/plugin/adventure_hospital.go @@ -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 { diff --git a/internal/plugin/adventure_housing.go b/internal/plugin/adventure_housing.go index c58556b..f7db09d 100644 --- a/internal/plugin/adventure_housing.go +++ b/internal/plugin/adventure_housing.go @@ -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) diff --git a/internal/plugin/adventure_masterwork.go b/internal/plugin/adventure_masterwork.go index bdb43d4..0e43d86 100644 --- a/internal/plugin/adventure_masterwork.go +++ b/internal/plugin/adventure_masterwork.go @@ -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) } } diff --git a/internal/plugin/adventure_rival.go b/internal/plugin/adventure_rival.go index cd2573e..9c4df05 100644 --- a/internal/plugin/adventure_rival.go +++ b/internal/plugin/adventure_rival.go @@ -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 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/internal/plugin/adventure_scheduler.go b/internal/plugin/adventure_scheduler.go index 8332dc3..11fb01f 100644 --- a/internal/plugin/adventure_scheduler.go +++ b/internal/plugin/adventure_scheduler.go @@ -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)) } } diff --git a/internal/plugin/dnd_combat.go b/internal/plugin/dnd_combat.go index 6ecff88..b5c221f 100644 --- a/internal/plugin/dnd_combat.go +++ b/internal/plugin/dnd_combat.go @@ -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 diff --git a/internal/plugin/dnd_rest.go b/internal/plugin/dnd_rest.go index 0df303e..a04dfd9 100644 --- a/internal/plugin/dnd_rest.go +++ b/internal/plugin/dnd_rest.go @@ -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 diff --git a/internal/plugin/dnd_sheet.go b/internal/plugin/dnd_sheet.go index 85430a8..034897b 100644 --- a/internal/plugin/dnd_sheet.go +++ b/internal/plugin/dnd_sheet.go @@ -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 { diff --git a/internal/plugin/player_meta.go b/internal/plugin/player_meta.go index 83570c5..b92cfb7 100644 --- a/internal/plugin/player_meta.go +++ b/internal/plugin/player_meta.go @@ -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