diff --git a/go/internal/server/gamedata/quest_costume.go b/go/internal/server/gamedata/quest_costume.go index faed907..47fef66 100644 --- a/go/internal/server/gamedata/quest_costume.go +++ b/go/internal/server/gamedata/quest_costume.go @@ -2,13 +2,26 @@ package gamedata // LoadQuestCostumes loads only costume rewards referenced by the installed // quest catalog, using the same character and duplicate-copy metadata as gacha. -func LoadQuestCostumes(root, version string, packs map[int]map[int]QuestDesign) (*RegularGachaCatalog, error) { +func LoadQuestCostumes(root, version string, packs map[int]map[int]QuestDesign, story *StoryCatalog) (*RegularGachaCatalog, error) { db, closeDB, err := openStatDatabase(root, version) if err != nil { return nil, err } defer closeDB() catalog := &RegularGachaCatalog{characters: map[uint64]CharacterDesign{}} + if story != nil { + for _, pack := range story.Packs { + for _, reward := range pack.BuyRewards { + if reward.Type == 11 { + design, err := loadGachaCharacterDesign(db, reward.ID) + if err != nil { + return nil, err + } + catalog.characters[reward.ID] = design + } + } + } + } for _, quests := range packs { for _, quest := range quests { for _, slot := range quest.Rewards { diff --git a/go/internal/server/gamedata/story_catalog.go b/go/internal/server/gamedata/story_catalog.go index 1a7411a..cf99ab5 100644 --- a/go/internal/server/gamedata/story_catalog.go +++ b/go/internal/server/gamedata/story_catalog.go @@ -111,9 +111,9 @@ func loadStoryCatalog(db *sql.DB) (*StoryCatalog, error) { return nil, fmt.Errorf("gamedata: invalid story pack%d buy rewards", id) } for i, count := range counts { - // PackTable may retain a disabled reward slot with a real type/ID - // and zero count. It contributes no granted reward. - if count == 0 { + // Costume/equipment are instance rewards: their count is zero, + // just as in QuestTable. Only zero-count stackable slots are empty. + if count == 0 && types[i] != 10 && types[i] != 11 { continue } if types[i] == 0 || count > math.MaxInt32 || types[i] > math.MaxInt32 || ids[i] > math.MaxInt32 { diff --git a/go/internal/server/gamedata/story_catalog_test.go b/go/internal/server/gamedata/story_catalog_test.go index 0849fd1..6237b2e 100644 --- a/go/internal/server/gamedata/story_catalog_test.go +++ b/go/internal/server/gamedata/story_catalog_test.go @@ -26,8 +26,7 @@ func TestStoryCatalogEnumeratesSeparateChainsAndContentTicketRules(t *testing.T) raw = wire.AppendVarint(raw, 8, 1) raw = wire.AppendVarint(raw, 9, 100) raw = wire.AppendVarint(raw, 10, 19) - // An official-shaped disabled costume slot must be equivalent - // to omitting that slot; type and ID remain populated. + // Non-stackable costume instances retain their ID with count zero. raw = wire.AppendVarint(raw, 8, 0) raw = wire.AppendVarint(raw, 9, 200) raw = wire.AppendVarint(raw, 10, 11) @@ -82,7 +81,7 @@ func TestStoryCatalogEnumeratesSeparateChainsAndContentTicketRules(t *testing.T) if d.Packs[1].Quests[1].NextQuestID != 2 || d.Packs[1].Quests[2].PriorQuestID != 1 || d.Packs[1].Quests[3].Type != 1 { t.Fatal("quest edges/type were lost") } - if rewards := d.Packs[7].BuyRewards; len(rewards) != 1 || rewards[0].Type != 19 || rewards[0].ID != 100 { + if rewards := d.Packs[7].BuyRewards; len(rewards) != 2 || rewards[0].Type != 19 || rewards[0].ID != 100 || rewards[1].Type != 11 || rewards[1].ID != 200 || rewards[1].Count != 0 { t.Fatalf("buy rewards=%+v", rewards) } if len(d.TicketSources[101]) != 2 { diff --git a/go/internal/server/world/pack_purchase.go b/go/internal/server/world/pack_purchase.go index b4b69d4..3f2be07 100644 --- a/go/internal/server/world/pack_purchase.go +++ b/go/internal/server/world/pack_purchase.go @@ -47,7 +47,7 @@ func (s *Service) handlePackBuy(request []byte) (int, []byte, bool, error) { func (s *Service) grantPurchaseRewards(identity string, rewards []gamedata.Reward) ([]player.Item, error) { var itemRewards []gamedata.BattleReward for _, reward := range rewards { - if reward.Count == 0 { + if reward.Count == 0 && reward.Type != 11 { return nil, fmt.Errorf("world: empty pack purchase reward") } if reward.Type == 19 { @@ -57,6 +57,36 @@ func (s *Service) grantPurchaseRewards(identity string, rewards []gamedata.Rewar itemRewards = append(itemRewards, gamedata.BattleReward{Type: reward.Type, ID: reward.ID, Count: reward.Count}) } } + var costumeIDs []uint64 + for _, reward := range rewards { + if reward.Type == 11 { + if s.questCostumes == nil { + return nil, fmt.Errorf("world: missing purchase costume design") + } + if _, found := s.questCostumes.Character(reward.ID); !found { + return nil, fmt.Errorf("world: missing purchase costume %d", reward.ID) + } + costumeIDs = append(costumeIDs, reward.ID) + } + } + if len(costumeIDs) > 0 { + grant, err := s.collection.GrantCostumes(identity+":costumes", costumeIDs, s.questCostumes) + if err != nil { + return nil, err + } + var exchanges []gamedata.Reward + for _, x := range grant.Exchanges { + if x.ExchangeItemType != 20 { + return nil, fmt.Errorf("world: unsupported purchase costume exchange") + } + exchanges = append(exchanges, gamedata.Reward{Type: x.ExchangeItemType, ID: x.ExchangeItemID, Count: x.ExchangeCount}) + } + if len(exchanges) > 0 { + if _, err := s.wallet.GrantQuestOnce(identity+":costumes:exchange", exchanges); err != nil { + return nil, err + } + } + } if _, err := s.wallet.GrantQuestOnce(identity+":currency", rewards); err != nil { return nil, err } @@ -78,6 +108,7 @@ func (s *Service) purchaseStoryPack(id int, initial bool) ([]byte, error) { if !exists { return nil, fmt.Errorf("%w: unknown purchase pack %d", ErrInvalidRequest, id) } + buyRewards := pack.BuyRewards identity := packPurchaseIdentity(id) if _, owned := s.collection.Grant(identity); owned { return []byte{}, nil @@ -85,12 +116,12 @@ func (s *Service) purchaseStoryPack(id int, initial bool) ([]byte, error) { if !initial && pack.BuyPrice != 0 { return nil, fmt.Errorf("%w: paid pack purchase type %d is unsupported", ErrInvalidRequest, pack.BuyType) } - for _, reward := range pack.BuyRewards { - if reward.Type != 3 && reward.Type != 4 && reward.Type != 12 && reward.Type != 19 { + for _, reward := range buyRewards { + if reward.Type != 3 && reward.Type != 4 && reward.Type != 12 && reward.Type != 19 && reward.Type != 11 { return nil, fmt.Errorf("world: unsupported pack purchase reward type %d", reward.Type) } } - items, err := s.grantPurchaseRewards(identity, pack.BuyRewards) + items, err := s.grantPurchaseRewards(identity, buyRewards) if err != nil { return nil, err } @@ -98,7 +129,10 @@ func (s *Service) purchaseStoryPack(id int, initial bool) ([]byte, error) { return nil, err } var bundle []byte - for _, reward := range pack.BuyRewards { + if grant, found := s.collection.Grant(identity + ":costumes"); found { + bundle = append(bundle, player.CollectionRewardBundle(s.collection, grant)...) + } + for _, reward := range buyRewards { if reward.Type == 3 || reward.Type == 4 || reward.Type == 12 { currency := wire.AppendVarint(wire.AppendVarint(nil, 3, reward.Type), 4, reward.Count) bundle = wire.AppendBytes(bundle, 1, currency) diff --git a/go/internal/server/world/story_catalog_test.go b/go/internal/server/world/story_catalog_test.go index 29ee744..da0a391 100644 --- a/go/internal/server/world/story_catalog_test.go +++ b/go/internal/server/world/story_catalog_test.go @@ -8,6 +8,13 @@ import ( "testing" ) +type purchaseTestCostumes map[uint64]gamedata.CharacterDesign + +func (c purchaseTestCostumes) Character(id uint64) (gamedata.CharacterDesign, bool) { + d, ok := c[id] + return d, ok +} + func TestStoryCatalogUsesTicketsAndQuestLinksAcrossIndependentPacks(t *testing.T) { s := testService() s.seed.PackID = 701 @@ -67,6 +74,26 @@ func TestStoryCatalogUsesTicketsAndQuestLinksAcrossIndependentPacks(t *testing.T if len(ids) != 2 || ids[0] != 405 || ids[1] != 701 { t.Fatalf("terminal updates %v", ids) } + if err := s.state.ClearQuest(33, 405); err != nil { + t.Fatal(err) + } + if err := s.state.ClearQuest(50, 701); err != nil { + t.Fatal(err) + } + var counts = map[uint64]uint64{} + if err := wire.Walk(s.accountPackInfo(), func(f wire.Field) error { + if f.Number == 2 { + id, _, _ := wire.Varint(f.Value, 1) + count, _, _ := wire.Varint(f.Value, 3) + counts[id] = count + } + return nil + }); err != nil { + t.Fatal(err) + } + if len(counts) != 2 || counts[405] != 1 || counts[701] != 2 { + t.Fatalf("completed normal levels count sidequests or omit nonseed pack: %v", counts) + } } func TestInitialPackPurchaseUsesDesignAndDurableIdentity(t *testing.T) { @@ -86,7 +113,8 @@ func TestInitialPackPurchaseUsesDesignAndDurableIdentity(t *testing.T) { if err != nil { t.Fatal(err) } - s.storyCatalog = &gamedata.StoryCatalog{Packs: map[int]gamedata.StoryPack{707: {ID: 707, BuyRewards: []gamedata.Reward{{Type: 12, Count: 17}, {Type: 19, ID: 88, Count: 1}}}}} + s.storyCatalog = &gamedata.StoryCatalog{Packs: map[int]gamedata.StoryPack{707: {ID: 707, BuyRewards: []gamedata.Reward{{Type: 12, Count: 17}, {Type: 4, Count: 31}, {Type: 19, ID: 88, Count: 1}, {Type: 11, ID: 101, Count: 0}}}}} + s.questCostumes = purchaseTestCostumes{101: {ID: 10, HP: 100, CostumeMaxLevel: 5}} for i := 0; i < 2; i++ { if err := s.EnsureInitialPackPurchase(); err != nil { t.Fatal(err) @@ -95,6 +123,9 @@ func TestInitialPackPurchaseUsesDesignAndDurableIdentity(t *testing.T) { if s.wallet.CatalystBalance() != 26 || len(s.inventory.All()) != 1 { t.Fatal("purchase reward repeated or hardcoded") } + if s.wallet.Snapshot().Gold != 31 || len(s.collection.Costumes()) != 1 { + t.Fatal("purchase gold or non-stackable level-zero costume handled incorrectly") + } if _, owned := s.collection.Grant("pack-purchase:707"); !owned { t.Fatal("missing purchase marker") } diff --git a/go/internal/server/world/world.go b/go/internal/server/world/world.go index 999c6a2..4687367 100644 --- a/go/internal/server/world/world.go +++ b/go/internal/server/world/world.go @@ -67,7 +67,7 @@ func Load(seedPath, gameDataRoot, gameDataVersion string, storage stateio.Store, packs[id] = pack.Quests transitions[id] = gamedata.PackTransition{PackID: id, NextPackID: pack.NextPackID} } - questCostumes, err := gamedata.LoadQuestCostumes(gameDataRoot, gameDataVersion, packs) + questCostumes, err := gamedata.LoadQuestCostumes(gameDataRoot, gameDataVersion, packs, storyCatalog) if err != nil { return nil, err } @@ -685,9 +685,13 @@ func (s *Service) accountPackInfo() []byte { for _, info := range s.packDBInfoRows() { out = wire.AppendBytes(out, 1, info) } - if s.packComplete() { - level := wire.AppendVarint(nil, 1, uint64(s.seed.PackID)) - level = wire.AppendVarint(level, 3, uint64(len(s.state.ClearedQuests(s.seed.PackID)))) + for _, info := range s.packDBInfoRows() { + id, _, _ := wire.Varint(info, 1) + if !s.packCompleteFor(int(id)) { + continue + } + level := wire.AppendVarint(nil, 1, id) + level = wire.AppendVarint(level, 3, uint64(len(s.storyCatalog.Packs[int(id)].MainQuestIDs))) level = wire.AppendVarint(level, 5, 1) out = wire.AppendBytes(out, 2, level) }