From 8891426abe633c724b35f3b5399f5f225427e32d Mon Sep 17 00:00:00 2001 From: Flechazo <2558755403@qq.com> Date: Sun, 4 Oct 2026 03:10:19 +0800 Subject: [PATCH] fix(server): restore earned quest costume before validating upgrades --- go/cmd/bd2server/main.go | 24 ++++++----- .../player/quest_costume_restart_test.go | 43 +++++++++++++++++++ 2 files changed, 56 insertions(+), 11 deletions(-) create mode 100644 go/internal/server/player/quest_costume_restart_test.go diff --git a/go/cmd/bd2server/main.go b/go/cmd/bd2server/main.go index c7cfe75..880d0cf 100644 --- a/go/cmd/bd2server/main.go +++ b/go/cmd/bd2server/main.go @@ -440,7 +440,19 @@ func serve(args []string) (serveErr error) { if err := ownedEquipment.AttachOptionReroll(equipmentOptionReroll, wallet, ownedItems); err != nil { return fmt.Errorf("attach equipment option reroll GameData: %w", err) } - collection, err := player.OpenCollectionStore(stateRepository, starter.Costumes) + worldService, err := world.Load(filepath.Clean(*worldSeed), gameData, *gameDataVersion, + stateRepository, progressState, starter, ownedEquipment, ownedItems, wallet) + if err != nil { + return fmt.Errorf("load world state: %w", err) + } + // Restore all earned seed ownership before validating persisted upgrades. + // A quest costume is not a collection entry; attaching it after opening + // collection would reject its otherwise valid burst ledger on restart. + baseCostumes := append([]player.Costume(nil), starter.Costumes...) + if reward, earned := worldService.EarnedQuestCostume(); earned { + baseCostumes = append(baseCostumes, reward) + } + collection, err := player.OpenCollectionStore(stateRepository, baseCostumes) if err != nil { return fmt.Errorf("load owned collection: %w", err) } @@ -478,22 +490,12 @@ func serve(args []string) (serveErr error) { gachaService.AttachPreviewMission(func() error { return missionService.CompleteMission(gamedata.MissionKey{GroupType: 0, GroupID: 1, ID: 111}) }) - worldService, err := world.Load(filepath.Clean(*worldSeed), gameData, *gameDataVersion, - stateRepository, progressState, starter, ownedEquipment, ownedItems, wallet) - if err != nil { - return fmt.Errorf("load world state: %w", err) - } if err := collection.BindBaseCharacters(worldService.CharacterService().RawAll()); err != nil { return fmt.Errorf("bind base collection characters: %w", err) } if err := mailService.AttachCostumeRewards(collection, limitedCostumes); err != nil { return fmt.Errorf("attach limited costume mail rewards: %w", err) } - if reward, earned := worldService.EarnedQuestCostume(); earned { - if err := collection.AttachRewardCostume(reward); err != nil { - return fmt.Errorf("attach earned quest costume: %w", err) - } - } if err := worldService.AttachCollection(collection); err != nil { return fmt.Errorf("attach gacha collection state: %w", err) } diff --git a/go/internal/server/player/quest_costume_restart_test.go b/go/internal/server/player/quest_costume_restart_test.go new file mode 100644 index 0000000..6bf2ce5 --- /dev/null +++ b/go/internal/server/player/quest_costume_restart_test.go @@ -0,0 +1,43 @@ +package player + +import ( + "strings" + "testing" + + "bd2server/internal/server/stateio" + "bd2server/internal/server/wire" +) + +func TestEarnedQuestCostumeMustBeRestoredBeforeBurstValidation(t *testing.T) { + store := stateio.NewMemory() + base := []Costume{{InvenIndex: 88, ID: 900}} + reward := Costume{InvenIndex: 99, ID: 4202, UseChar: 77} + collection, err := OpenCollectionStore(store, base) + if err != nil { + t.Fatal(err) + } + if err := collection.EnsurePersisted(); err != nil { + t.Fatal(err) + } + if err := collection.AttachRewardCostume(reward); err != nil { + t.Fatal(err) + } + record := CostumeBurstUpgradeRecord{CostumeID: reward.ID, Level: 1, Digest: strings.Repeat("a", 64), Code: 578, Body: wire.AppendVarint(nil, 1, 1)} + if err := collection.ApplyCostumeBurst(reward.InvenIndex, 0, 1, record); err != nil { + t.Fatal(err) + } + if _, err := OpenCollectionStore(store, base); err == nil { + t.Fatal("accepted burst ledger with incomplete restored ownership") + } + reloaded, err := OpenCollectionStore(store, append(base, reward)) + if err != nil { + t.Fatal(err) + } + got, owned := reloaded.CostumeByID(reward.ID) + if !owned || got.InvenIndex != reward.InvenIndex || got.BurstLevel != 1 { + t.Fatalf("restored reward costume=%+v owned=%v", got, owned) + } + if got, found := reloaded.CostumeBurstReplay(reward.InvenIndex, 1); !found || got.Digest != record.Digest { + t.Fatal("restart lost burst upgrade replay") + } +}