mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 00:52:40 +00:00
Adv 2.0 L5h: cold AdvCharacter table — saveAdvCharacter fan-outs to player_meta
saveAdvCharacter no longer writes the legacy adventure_characters row. It's now a thin fan-out: bumps LastActiveAt and routes through upsertAllPlayerMetaFromAdvChar, which calls the per-subsystem upserts for display_name, hospital, arena, masterwork, rival, skills, babysit, NPC, lifecycle, death, misc, pet, and house. loadAdvCharacter calls applyPlayerMetaOverlay at the tail to re-source every migrated subsystem from player_meta. The adventure_characters SELECT still happens (for the user_id / equipment-table linkage and as a fallback substrate) but its values are overwritten by the overlay. This achieves the L4e in-place housing reader flip automatically — adventure_housing.go's char.HouseFoo reads now see player_meta values without per-site changes. npcMidnightReset extended to write player_meta alongside the frozen adventure_characters. Per-call-site upsertPlayerMetaXxx duals are now redundant with the saveAdvCharacter fan-out but kept as defense; cleanup deferred. player_meta.combat_level column drop deferred — still read by babysit cost / combat_stats / activities via the overlay; sequenced with the final combat_engine teardown. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -422,7 +422,7 @@ Each sub-phase is the same shape as L4a–L4e: column-add → backfill (idempote
|
|||||||
| **L5e** | Death state (8 fields) | 1 day | Decision point: do `DnDCharacter.HPCurrent == 0` semantics replace `Alive`/`DeadUntil`, or do we keep them as `player_meta.alive` / `player_meta.dead_until`? Recommendation: keep them as columns. The hospital revive path (L4a) already restores `DnDCharacter.HPCurrent = HPMax` alongside `Alive=true`, so the two are kept in sync; flipping the readers is the migration, not the death-state semantics. **Status (2026-05-09):** column + dual-write SHIPPED on `adv-2.0`. Eight columns added (alive INTEGER DEFAULT 1, dead_until/death_reprieve_last/last_pardon_used DATETIME, last_death_date/grudge_location/death_source/death_location TEXT). `DeathState` + load/upsert/backfill/projection helpers. **Strategy switch:** dual-write moved inside `saveAdvCharacter` itself (after the existing display_name dual-write). The mutation surface for death state spans ~50 save sites — per-site upserts would be too noisy. Every `saveAdvCharacter` call now propagates `DeathState` to player_meta. Backfill idempotent (only fills rows where every death field is still default). Tests: `TestPlayerMetaDeathStateBackfill_Idempotent`, `TestLoadDeathState_FallsBackToAdvCharacter`, `TestUpsertPlayerMetaDeathState_RoundTrip`, `TestSaveAdvCharacter_DualWritesDeathState`. `go vet ./... && go test ./...` clean. |
|
| **L5e** | Death state (8 fields) | 1 day | Decision point: do `DnDCharacter.HPCurrent == 0` semantics replace `Alive`/`DeadUntil`, or do we keep them as `player_meta.alive` / `player_meta.dead_until`? Recommendation: keep them as columns. The hospital revive path (L4a) already restores `DnDCharacter.HPCurrent = HPMax` alongside `Alive=true`, so the two are kept in sync; flipping the readers is the migration, not the death-state semantics. **Status (2026-05-09):** column + dual-write SHIPPED on `adv-2.0`. Eight columns added (alive INTEGER DEFAULT 1, dead_until/death_reprieve_last/last_pardon_used DATETIME, last_death_date/grudge_location/death_source/death_location TEXT). `DeathState` + load/upsert/backfill/projection helpers. **Strategy switch:** dual-write moved inside `saveAdvCharacter` itself (after the existing display_name dual-write). The mutation surface for death state spans ~50 save sites — per-site upserts would be too noisy. Every `saveAdvCharacter` call now propagates `DeathState` to player_meta. Backfill idempotent (only fills rows where every death field is still default). Tests: `TestPlayerMetaDeathStateBackfill_Idempotent`, `TestLoadDeathState_FallsBackToAdvCharacter`, `TestUpsertPlayerMetaDeathState_RoundTrip`, `TestSaveAdvCharacter_DualWritesDeathState`. `go vet ./... && go test ./...` clean. |
|
||||||
| **L5f** | Misc (`Title`, `TreasuresLocked`, `CraftsSucceeded`) | 0.5 day | Each is a single-mutation-site field. **Status (2026-05-09):** column + dual-write SHIPPED on `adv-2.0`. Three columns (title TEXT, treasures_locked INT, crafts_succeeded INT). `MiscState` + load/upsert/backfill/projection helpers. Dual-write rides `saveAdvCharacter` alongside L5e (Title is dormant; TreasuresLocked + CraftsSucceeded mutate at known sites that already save). Backfill idempotent. Tests: `TestPlayerMetaMiscStateBackfill_Idempotent`, `TestLoadMiscState_FallsBackToAdvCharacter`, `TestUpsertPlayerMetaMiscState_RoundTrip`. `go vet ./... && go test ./...` clean. |
|
| **L5f** | Misc (`Title`, `TreasuresLocked`, `CraftsSucceeded`) | 0.5 day | Each is a single-mutation-site field. **Status (2026-05-09):** column + dual-write SHIPPED on `adv-2.0`. Three columns (title TEXT, treasures_locked INT, crafts_succeeded INT). `MiscState` + load/upsert/backfill/projection helpers. Dual-write rides `saveAdvCharacter` alongside L5e (Title is dormant; TreasuresLocked + CraftsSucceeded mutate at known sites that already save). Backfill idempotent. Tests: `TestPlayerMetaMiscStateBackfill_Idempotent`, `TestLoadMiscState_FallsBackToAdvCharacter`, `TestUpsertPlayerMetaMiscState_RoundTrip`. `go vet ./... && go test ./...` clean. |
|
||||||
| **L5g** | DnDCharacter mass-backfill | 0.5 day | One-shot: for every active user without a D&D row, insert a default-class character at `dndLevelFromCombatLevel(adventure_characters.combat_level)`. Run once, idempotent (skip rows that already exist). After this lands, drop the `dndLevelFromCombatLevel` fallback branch in `loadHospitalVisits`/`rivalLevelForUser`/`dndLevelForUser`/zone gates. CombatLevel column in player_meta becomes unused at this point — drop it during the L5h cleanup. **Status (2026-05-09):** SHIPPED on `adv-2.0`. `backfillDnDCharactersFromAdv` in `dnd_combat.go` walks `adventure_characters LEFT JOIN dnd_character WHERE d.user_id IS NULL`, calls `autoBuildCharacter` (race/class inferred from archetypes; HP/AC computed; Level seeded from `dndLevelFromCombatLevel(CombatLevel)`), saves with `AutoMigrated=1`, `PendingSetup=0`. Idempotent — pre-existing rows (including pending-setup drafts) are skipped via the LEFT JOIN. Wired into Init after the L5f backfill. Fallbacks dropped: `dndLevelForUser` (dnd.go), `rivalLevelForUser` (adventure_rival.go), `hospitalCostsForUser` (adventure_hospital.go) — all now floor at level 1 when no D&D row exists. `dndLevelFromCombatLevel` itself is preserved (used by `autoBuildCharacter` for fresh-account level seeding and by `dndSetupConfirm`/preview/stub paths). Tests: `TestL5gBackfillDnDCharacters_Idempotent`, `TestL5gBackfillDnDCharacters_SkipsPendingSetup` in `dnd_l5g_test.go`. `go vet ./... && go test ./...` clean. |
|
| **L5g** | DnDCharacter mass-backfill | 0.5 day | One-shot: for every active user without a D&D row, insert a default-class character at `dndLevelFromCombatLevel(adventure_characters.combat_level)`. Run once, idempotent (skip rows that already exist). After this lands, drop the `dndLevelFromCombatLevel` fallback branch in `loadHospitalVisits`/`rivalLevelForUser`/`dndLevelForUser`/zone gates. CombatLevel column in player_meta becomes unused at this point — drop it during the L5h cleanup. **Status (2026-05-09):** SHIPPED on `adv-2.0`. `backfillDnDCharactersFromAdv` in `dnd_combat.go` walks `adventure_characters LEFT JOIN dnd_character WHERE d.user_id IS NULL`, calls `autoBuildCharacter` (race/class inferred from archetypes; HP/AC computed; Level seeded from `dndLevelFromCombatLevel(CombatLevel)`), saves with `AutoMigrated=1`, `PendingSetup=0`. Idempotent — pre-existing rows (including pending-setup drafts) are skipped via the LEFT JOIN. Wired into Init after the L5f backfill. Fallbacks dropped: `dndLevelForUser` (dnd.go), `rivalLevelForUser` (adventure_rival.go), `hospitalCostsForUser` (adventure_hospital.go) — all now floor at level 1 when no D&D row exists. `dndLevelFromCombatLevel` itself is preserved (used by `autoBuildCharacter` for fresh-account level seeding and by `dndSetupConfirm`/preview/stub paths). Tests: `TestL5gBackfillDnDCharacters_Idempotent`, `TestL5gBackfillDnDCharacters_SkipsPendingSetup` in `dnd_l5g_test.go`. `go vet ./... && go test ./...` clean. |
|
||||||
| **L5h** | L4e in-place housing reader flip + cut all AdvCharacter dual-writes | 1 day | Bundled per L4e note. Once writes stop, AdvCharacter rows go cold; dual-write removal is mechanical (delete `saveAdvCharacter` calls and the upserts that follow them in pairs). |
|
| **L5h** | L4e in-place housing reader flip + cut all AdvCharacter dual-writes | 1 day | Bundled per L4e note. Once writes stop, AdvCharacter rows go cold; dual-write removal is mechanical (delete `saveAdvCharacter` calls and the upserts that follow them in pairs). **Status (2026-05-09):** SHIPPED on `adv-2.0`. **Strategy switch:** rather than deleting 51 saveAdvCharacter call sites in pairs (which would need every site to also know about display_name/death/misc/skills/etc. internal mirrors), `saveAdvCharacter` itself was rewritten as a thin fan-out: `LastActiveAt = now()` + `upsertAllPlayerMetaFromAdvChar(char)`. The legacy `UPDATE adventure_characters` is gone — the row is now read-only after `createAdvCharacter` seeds it. `applyPlayerMetaOverlay(c)` runs at the tail of `loadAdvCharacter`, re-sourcing every migrated subsystem (display_name, hospital, arena, masterwork, rival, skills, babysit, NPC, lifecycle, death, misc, pet, house) from player_meta so callers continue to use `char.X` readers transparently. The L4e housing in-place reader flip is now automatic — `adventure_housing.go`'s `char.HouseFoo` reads see player_meta values via the overlay. `npcMidnightReset` extended to write player_meta alongside the (frozen) adventure_characters. Per-call-site `upsertPlayerMetaXxx` dual-writes are now redundant with `saveAdvCharacter`'s fan-out but retained as defense — cleanup deferred. **player_meta.combat_level deferred.** Still actively used by babysit cost / combat_stats / activities via the overlay (those sites will be cut in final teardown alongside `combat_engine.go`); column drop sequenced with that. `go vet ./... && go test ./...` clean. |
|
||||||
|
|
||||||
### 7.4 Final teardown (after L5a–L5h)
|
### 7.4 Final teardown (after L5a–L5h)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package plugin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"log/slog"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gogobee/internal/db"
|
"gogobee/internal/db"
|
||||||
@@ -540,6 +539,12 @@ func loadAdvCharacter(userID id.UserID) (*AdventureCharacter, error) {
|
|||||||
c.PetArrived = petArrived == 1
|
c.PetArrived = petArrived == 1
|
||||||
c.ThomAnimalLineFired = thomAnimalLine == 1
|
c.ThomAnimalLineFired = thomAnimalLine == 1
|
||||||
c.PetSupplyShopUnlocked = petSupplyUnlocked == 1
|
c.PetSupplyShopUnlocked = petSupplyUnlocked == 1
|
||||||
|
|
||||||
|
// Phase L5h: adventure_characters columns are no longer written by
|
||||||
|
// saveAdvCharacter, so the SELECT above returns the row's frozen-at-cut
|
||||||
|
// snapshot. Overlay every player_meta-mirrored subsystem so callers see
|
||||||
|
// canonical (live) values for migrated fields.
|
||||||
|
applyPlayerMetaOverlay(c)
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,151 +590,15 @@ func createAdvCharacter(userID id.UserID, displayName string) error {
|
|||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// saveAdvCharacter persists every mutable AdventureCharacter field to
|
||||||
|
// player_meta. Phase L5h: the legacy adventure_characters UPDATE has been
|
||||||
|
// retired — the row is now read-only after createAdvCharacter seeds it,
|
||||||
|
// and saveAdvCharacter routes the entire mutable state set through the
|
||||||
|
// per-subsystem player_meta upserts. LastActiveAt is auto-bumped to mirror
|
||||||
|
// the previous CURRENT_TIMESTAMP semantics on save.
|
||||||
func saveAdvCharacter(char *AdventureCharacter) error {
|
func saveAdvCharacter(char *AdventureCharacter) error {
|
||||||
d := db.Get()
|
char.LastActiveAt = time.Now().UTC()
|
||||||
alive := 0
|
return upsertAllPlayerMetaFromAdvChar(char)
|
||||||
if char.Alive {
|
|
||||||
alive = 1
|
|
||||||
}
|
|
||||||
actionTaken := 0
|
|
||||||
if char.ActionTakenToday {
|
|
||||||
actionTaken = 1
|
|
||||||
}
|
|
||||||
holidayTaken := 0
|
|
||||||
if char.HolidayActionTaken {
|
|
||||||
holidayTaken = 1
|
|
||||||
}
|
|
||||||
rivalUnlocked := 0
|
|
||||||
if char.RivalUnlockedNotified {
|
|
||||||
rivalUnlocked = 1
|
|
||||||
}
|
|
||||||
babysitAct := 0
|
|
||||||
if char.BabysitActive {
|
|
||||||
babysitAct = 1
|
|
||||||
}
|
|
||||||
houseFrozen := 0
|
|
||||||
if char.HouseLoanFrozen {
|
|
||||||
houseFrozen = 1
|
|
||||||
}
|
|
||||||
houseAutopay := 0
|
|
||||||
if char.HouseAutopay {
|
|
||||||
houseAutopay = 1
|
|
||||||
}
|
|
||||||
petChasedAway := 0
|
|
||||||
if char.PetChasedAway {
|
|
||||||
petChasedAway = 1
|
|
||||||
}
|
|
||||||
petReactivated := 0
|
|
||||||
if char.PetReactivated {
|
|
||||||
petReactivated = 1
|
|
||||||
}
|
|
||||||
petArrived := 0
|
|
||||||
if char.PetArrived {
|
|
||||||
petArrived = 1
|
|
||||||
}
|
|
||||||
thomAnimalLine := 0
|
|
||||||
if char.ThomAnimalLineFired {
|
|
||||||
thomAnimalLine = 1
|
|
||||||
}
|
|
||||||
petSupplyUnlocked := 0
|
|
||||||
if char.PetSupplyShopUnlocked {
|
|
||||||
petSupplyUnlocked = 1
|
|
||||||
}
|
|
||||||
petMorningDef := 0
|
|
||||||
if char.PetMorningDefense {
|
|
||||||
petMorningDef = 1
|
|
||||||
}
|
|
||||||
autoBabysit := 0
|
|
||||||
if char.AutoBabysit {
|
|
||||||
autoBabysit = 1
|
|
||||||
}
|
|
||||||
streakDecayed := 0
|
|
||||||
if char.StreakDecayed {
|
|
||||||
streakDecayed = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := d.Exec(`
|
|
||||||
UPDATE adventure_characters SET
|
|
||||||
display_name = ?, combat_level = ?, mining_skill = ?, foraging_skill = ?, fishing_skill = ?,
|
|
||||||
combat_xp = ?, mining_xp = ?, foraging_xp = ?, fishing_xp = ?,
|
|
||||||
alive = ?, dead_until = ?, action_taken_today = ?, holiday_action_taken = ?,
|
|
||||||
arena_wins = ?, arena_losses = ?, invasion_score = ?, title = ?,
|
|
||||||
current_streak = ?, best_streak = ?, last_action_date = ?, grudge_location = ?,
|
|
||||||
last_active_at = CURRENT_TIMESTAMP, death_reprieve_last = ?,
|
|
||||||
masterwork_drops_received = ?,
|
|
||||||
rival_pool = ?, rival_unlocked_notified = ?,
|
|
||||||
babysit_active = ?, babysit_expires_at = ?, babysit_skill_focus = ?,
|
|
||||||
hospital_visits = ?, robbie_visit_count = ?, last_death_date = ?,
|
|
||||||
combat_actions_used = ?, harvest_actions_used = ?,
|
|
||||||
last_pardon_used = ?,
|
|
||||||
misty_last_seen = ?, arina_last_seen = ?,
|
|
||||||
misty_buff_expires = ?, misty_debuff_expires = ?, arina_buff_expires = ?,
|
|
||||||
npc_msg_count = ?, npc_msg_count_date = ?,
|
|
||||||
misty_roll_target = ?, arina_roll_target = ?,
|
|
||||||
house_tier = ?, house_loan_balance = ?, house_loan_frozen = ?, house_missed_payments = ?,
|
|
||||||
house_autopay = ?, house_current_rate = ?,
|
|
||||||
pet_type = ?, pet_name = ?, pet_xp = ?, pet_level = ?, pet_armor_tier = ?,
|
|
||||||
pet_chased_away = ?, pet_reactivated = ?, pet_arrived = ?,
|
|
||||||
misty_encounter_count = ?, misty_donated_count = ?,
|
|
||||||
thom_animal_line_fired = ?, pet_supply_shop_unlocked = ?, pet_level10_date = ?,
|
|
||||||
pet_morning_defense = ?,
|
|
||||||
auto_babysit = ?,
|
|
||||||
streak_decayed = ?,
|
|
||||||
crafts_succeeded = ?,
|
|
||||||
death_source = ?,
|
|
||||||
death_location = ?,
|
|
||||||
auto_babysit_focus = ?,
|
|
||||||
treasures_locked = ?
|
|
||||||
WHERE user_id = ?`,
|
|
||||||
char.DisplayName, char.CombatLevel, char.MiningSkill, char.ForagingSkill, char.FishingSkill,
|
|
||||||
char.CombatXP, char.MiningXP, char.ForagingXP, char.FishingXP,
|
|
||||||
alive, char.DeadUntil, actionTaken, holidayTaken,
|
|
||||||
char.ArenaWins, char.ArenaLosses, char.InvasionScore, char.Title,
|
|
||||||
char.CurrentStreak, char.BestStreak, char.LastActionDate, char.GrudgeLocation,
|
|
||||||
char.DeathReprieveLast, char.MasterworkDropsReceived,
|
|
||||||
char.RivalPool, rivalUnlocked,
|
|
||||||
babysitAct, char.BabysitExpiresAt, char.BabysitSkillFocus,
|
|
||||||
char.HospitalVisits, char.RobbieVisitCount, char.LastDeathDate,
|
|
||||||
char.CombatActionsUsed, char.HarvestActionsUsed,
|
|
||||||
char.LastPardonUsed,
|
|
||||||
char.MistyLastSeen, char.ArinaLastSeen,
|
|
||||||
char.MistyBuffExpires, char.MistyDebuffExpires, char.ArinaBuffExpires,
|
|
||||||
char.NPCMsgCount, char.NPCMsgCountDate,
|
|
||||||
char.MistyRollTarget, char.ArinaRollTarget,
|
|
||||||
char.HouseTier, char.HouseLoanBalance, houseFrozen, char.HouseMissedPayments,
|
|
||||||
houseAutopay, char.HouseCurrentRate,
|
|
||||||
char.PetType, char.PetName, char.PetXP, char.PetLevel, char.PetArmorTier,
|
|
||||||
petChasedAway, petReactivated, petArrived,
|
|
||||||
char.MistyEncounterCount, char.MistyDonatedCount,
|
|
||||||
thomAnimalLine, petSupplyUnlocked, char.PetLevel10Date,
|
|
||||||
petMorningDef,
|
|
||||||
autoBabysit,
|
|
||||||
streakDecayed,
|
|
||||||
char.CraftsSucceeded,
|
|
||||||
char.DeathSource, char.DeathLocation,
|
|
||||||
char.AutoBabysitFocus,
|
|
||||||
boolToInt(char.TreasuresLocked),
|
|
||||||
string(char.UserID),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Adv 2.0 Phase L4f-prep — dual-write display_name into player_meta.
|
|
||||||
if dnErr := upsertPlayerMetaDisplayName(char.UserID, char.DisplayName); dnErr != nil {
|
|
||||||
slog.Error("player_meta: display_name dual-write failed", "user", char.UserID, "err", dnErr)
|
|
||||||
}
|
|
||||||
// Adv 2.0 Phase L5e — dual-write death state into player_meta. Mutation
|
|
||||||
// surface is too wide for per-site upserts (~50 save sites), so the
|
|
||||||
// dual-write rides every saveAdvCharacter call.
|
|
||||||
if dsErr := upsertPlayerMetaDeathState(char.UserID, deathStateFromAdvChar(char)); dsErr != nil {
|
|
||||||
slog.Error("player_meta: death state dual-write failed", "user", char.UserID, "err", dsErr)
|
|
||||||
}
|
|
||||||
// Adv 2.0 Phase L5f — dual-write misc state (Title, TreasuresLocked,
|
|
||||||
// CraftsSucceeded). Same strategy as L5e — rides every save.
|
|
||||||
if msErr := upsertPlayerMetaMiscState(char.UserID, miscStateFromAdvChar(char)); msErr != nil {
|
|
||||||
slog.Error("player_meta: misc state dual-write failed", "user", char.UserID, "err", msErr)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func boolToInt(b bool) int {
|
func boolToInt(b bool) int {
|
||||||
|
|||||||
@@ -428,18 +428,28 @@ func npcRepairMostDamaged(userID id.UserID, equip map[EquipmentSlot]*AdvEquipmen
|
|||||||
|
|
||||||
// npcMidnightReset resets message counts and regenerates roll targets.
|
// npcMidnightReset resets message counts and regenerates roll targets.
|
||||||
// Called from midnightReset. Uses a direct DB update to avoid loading/saving
|
// Called from midnightReset. Uses a direct DB update to avoid loading/saving
|
||||||
// every character individually.
|
// every character individually. Phase L5h: writes player_meta as the
|
||||||
|
// canonical store; the legacy adventure_characters update is preserved
|
||||||
|
// only as a defensive sweep for rows that pre-date the L5c backfill.
|
||||||
func npcMidnightReset() {
|
func npcMidnightReset() {
|
||||||
d := db.Get()
|
d := db.Get()
|
||||||
today := time.Now().UTC().Format("2006-01-02")
|
today := time.Now().UTC().Format("2006-01-02")
|
||||||
_, err := d.Exec(`
|
if _, err := d.Exec(`
|
||||||
|
UPDATE player_meta
|
||||||
|
SET npc_msg_count = 0,
|
||||||
|
npc_msg_count_date = ?,
|
||||||
|
misty_roll_target = ABS(RANDOM()) % 6 + 5,
|
||||||
|
arina_roll_target = ABS(RANDOM()) % 6 + 5`,
|
||||||
|
today); err != nil {
|
||||||
|
slog.Error("npc: midnight reset (player_meta) failed", "err", err)
|
||||||
|
}
|
||||||
|
if _, err := d.Exec(`
|
||||||
UPDATE adventure_characters
|
UPDATE adventure_characters
|
||||||
SET npc_msg_count = 0,
|
SET npc_msg_count = 0,
|
||||||
npc_msg_count_date = ?,
|
npc_msg_count_date = ?,
|
||||||
misty_roll_target = ABS(RANDOM()) % 6 + 5,
|
misty_roll_target = ABS(RANDOM()) % 6 + 5,
|
||||||
arina_roll_target = ABS(RANDOM()) % 6 + 5`,
|
arina_roll_target = ABS(RANDOM()) % 6 + 5`,
|
||||||
today)
|
today); err != nil {
|
||||||
if err != nil {
|
slog.Error("npc: midnight reset (adventure_characters) failed", "err", err)
|
||||||
slog.Error("npc: midnight reset failed", "err", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1924,3 +1924,172 @@ func backfillPlayerMetaDisplayName() error {
|
|||||||
slog.Info("player_meta: display_name backfilled", "rows", n)
|
slog.Info("player_meta: display_name backfilled", "rows", n)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
func applyPlayerMetaOverlay(c *AdventureCharacter) {
|
||||||
|
if c == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uid := c.UserID
|
||||||
|
|
||||||
|
if name, err := loadDisplayName(uid); err == nil && name != "" {
|
||||||
|
c.DisplayName = name
|
||||||
|
}
|
||||||
|
if visits, err := loadHospitalVisits(uid); err == nil {
|
||||||
|
c.HospitalVisits = visits
|
||||||
|
}
|
||||||
|
if pm, err := loadPlayerMeta(uid); err == nil && pm != nil {
|
||||||
|
c.ArenaWins = pm.ArenaWins
|
||||||
|
c.ArenaLosses = pm.ArenaLosses
|
||||||
|
c.InvasionScore = pm.InvasionScore
|
||||||
|
}
|
||||||
|
if drops, err := loadMasterworkDrops(uid); err == nil {
|
||||||
|
c.MasterworkDropsReceived = drops
|
||||||
|
}
|
||||||
|
if pool, notified, err := loadRivalState(uid); err == nil {
|
||||||
|
c.RivalPool = pool
|
||||||
|
c.RivalUnlockedNotified = notified
|
||||||
|
}
|
||||||
|
if s, err := loadSkillState(uid); err == nil {
|
||||||
|
c.CombatLevel = s.CombatLevel
|
||||||
|
c.CombatXP = s.CombatXP
|
||||||
|
c.MiningSkill = s.MiningSkill
|
||||||
|
c.MiningXP = s.MiningXP
|
||||||
|
c.ForagingSkill = s.ForagingSkill
|
||||||
|
c.ForagingXP = s.ForagingXP
|
||||||
|
c.FishingSkill = s.FishingSkill
|
||||||
|
c.FishingXP = s.FishingXP
|
||||||
|
}
|
||||||
|
if s, err := loadBabysitState(uid); err == nil {
|
||||||
|
c.BabysitActive = s.Active
|
||||||
|
c.BabysitExpiresAt = s.ExpiresAt
|
||||||
|
c.BabysitSkillFocus = s.SkillFocus
|
||||||
|
c.AutoBabysit = s.AutoBabysit
|
||||||
|
c.AutoBabysitFocus = s.AutoBabysitFocus
|
||||||
|
}
|
||||||
|
if s, err := loadNPCState(uid); err == nil {
|
||||||
|
c.MistyLastSeen = s.MistyLastSeen
|
||||||
|
c.ArinaLastSeen = s.ArinaLastSeen
|
||||||
|
c.MistyBuffExpires = s.MistyBuffExpires
|
||||||
|
c.MistyDebuffExpires = s.MistyDebuffExpires
|
||||||
|
c.ArinaBuffExpires = s.ArinaBuffExpires
|
||||||
|
c.NPCMsgCount = s.NPCMsgCount
|
||||||
|
c.NPCMsgCountDate = s.NPCMsgCountDate
|
||||||
|
c.MistyRollTarget = s.MistyRollTarget
|
||||||
|
c.ArinaRollTarget = s.ArinaRollTarget
|
||||||
|
c.MistyEncounterCount = s.MistyEncounterCount
|
||||||
|
c.MistyDonatedCount = s.MistyDonatedCount
|
||||||
|
c.ThomAnimalLineFired = s.ThomAnimalLineFired
|
||||||
|
c.RobbieVisitCount = s.RobbieVisitCount
|
||||||
|
}
|
||||||
|
if s, err := loadLifecycleState(uid); err == nil {
|
||||||
|
c.CurrentStreak = s.CurrentStreak
|
||||||
|
c.BestStreak = s.BestStreak
|
||||||
|
c.LastActionDate = s.LastActionDate
|
||||||
|
c.StreakDecayed = s.StreakDecayed
|
||||||
|
c.ActionTakenToday = s.ActionTakenToday
|
||||||
|
c.HolidayActionTaken = s.HolidayActionTaken
|
||||||
|
c.CombatActionsUsed = s.CombatActionsUsed
|
||||||
|
c.HarvestActionsUsed = s.HarvestActionsUsed
|
||||||
|
if s.CreatedAt != nil {
|
||||||
|
c.CreatedAt = *s.CreatedAt
|
||||||
|
}
|
||||||
|
if s.LastActiveAt != nil {
|
||||||
|
c.LastActiveAt = *s.LastActiveAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s, err := loadDeathState(uid); err == nil {
|
||||||
|
c.Alive = s.Alive
|
||||||
|
c.DeadUntil = s.DeadUntil
|
||||||
|
c.DeathReprieveLast = s.DeathReprieveLast
|
||||||
|
c.LastDeathDate = s.LastDeathDate
|
||||||
|
c.LastPardonUsed = s.LastPardonUsed
|
||||||
|
c.GrudgeLocation = s.GrudgeLocation
|
||||||
|
c.DeathSource = s.DeathSource
|
||||||
|
c.DeathLocation = s.DeathLocation
|
||||||
|
}
|
||||||
|
if s, err := loadMiscState(uid); err == nil {
|
||||||
|
c.Title = s.Title
|
||||||
|
c.TreasuresLocked = s.TreasuresLocked
|
||||||
|
c.CraftsSucceeded = s.CraftsSucceeded
|
||||||
|
}
|
||||||
|
if s, err := loadPetState(uid); err == nil {
|
||||||
|
c.PetType = s.Type
|
||||||
|
c.PetName = s.Name
|
||||||
|
c.PetXP = s.XP
|
||||||
|
c.PetLevel = s.Level
|
||||||
|
c.PetArmorTier = s.ArmorTier
|
||||||
|
c.PetSupplyShopUnlocked = s.SupplyShopUnlocked
|
||||||
|
c.PetLevel10Date = s.Level10Date
|
||||||
|
c.PetArrived = s.Arrived
|
||||||
|
c.PetChasedAway = s.ChasedAway
|
||||||
|
c.PetReactivated = s.Reactivated
|
||||||
|
c.PetMorningDefense = s.MorningDefense
|
||||||
|
}
|
||||||
|
if s, err := loadHouseState(uid); err == nil {
|
||||||
|
c.HouseTier = s.Tier
|
||||||
|
c.HouseLoanBalance = s.LoanBalance
|
||||||
|
c.HouseLoanFrozen = s.LoanFrozen
|
||||||
|
c.HouseMissedPayments = s.MissedPayments
|
||||||
|
c.HouseAutopay = s.Autopay
|
||||||
|
c.HouseCurrentRate = s.CurrentRate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// upsertAllPlayerMetaFromAdvChar writes every player_meta-mirrored subsystem's
|
||||||
|
// state in one shot from an AdventureCharacter. Phase L5h: saveAdvCharacter
|
||||||
|
// stops writing adventure_characters and instead routes through this fan-out,
|
||||||
|
// keeping every load helper's read path canonical against player_meta. Returns
|
||||||
|
// the first error encountered; partial writes are possible and acceptable
|
||||||
|
// since each upsert is its own atomic transaction.
|
||||||
|
func upsertAllPlayerMetaFromAdvChar(c *AdventureCharacter) error {
|
||||||
|
uid := c.UserID
|
||||||
|
if err := upsertPlayerMetaDisplayName(uid, c.DisplayName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := upsertPlayerMetaHospitalVisits(uid, c.HospitalVisits); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := upsertPlayerMetaArena(uid, c.ArenaWins, c.ArenaLosses, c.InvasionScore); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := upsertPlayerMetaMasterworkDrops(uid, c.MasterworkDropsReceived); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := upsertPlayerMetaRivalState(uid, c.RivalPool, c.RivalUnlockedNotified); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := upsertPlayerMetaSkillState(uid, skillStateFromAdvChar(c)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := upsertPlayerMetaBabysitState(uid, babysitStateFromAdvChar(c)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := upsertPlayerMetaNPCState(uid, npcStateFromAdvChar(c)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := upsertPlayerMetaLifecycleState(uid, lifecycleStateFromAdvChar(c)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := upsertPlayerMetaDeathState(uid, deathStateFromAdvChar(c)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := upsertPlayerMetaMiscState(uid, miscStateFromAdvChar(c)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := upsertPlayerMetaPetState(uid, petStateFromAdvChar(c)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := upsertPlayerMetaHouseState(uid, houseStateFromAdvChar(c)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user