fix: restore earned quest costume before validating upgrades

This commit is contained in:
2026-10-04 03:10:19 +08:00
parent 6b76ea9ba6
commit 1d3565db19
2 changed files with 56 additions and 11 deletions
+13 -11
View File
@@ -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)
}
@@ -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")
}
}