fix(server): grant non-stackable chapter rewards and expose all completed packs
This commit is contained in:
@@ -2,13 +2,26 @@ package gamedata
|
|||||||
|
|
||||||
// LoadQuestCostumes loads only costume rewards referenced by the installed
|
// LoadQuestCostumes loads only costume rewards referenced by the installed
|
||||||
// quest catalog, using the same character and duplicate-copy metadata as gacha.
|
// 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)
|
db, closeDB, err := openStatDatabase(root, version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer closeDB()
|
defer closeDB()
|
||||||
catalog := &RegularGachaCatalog{characters: map[uint64]CharacterDesign{}}
|
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 _, quests := range packs {
|
||||||
for _, quest := range quests {
|
for _, quest := range quests {
|
||||||
for _, slot := range quest.Rewards {
|
for _, slot := range quest.Rewards {
|
||||||
|
|||||||
@@ -111,9 +111,9 @@ func loadStoryCatalog(db *sql.DB) (*StoryCatalog, error) {
|
|||||||
return nil, fmt.Errorf("gamedata: invalid story pack%d buy rewards", id)
|
return nil, fmt.Errorf("gamedata: invalid story pack%d buy rewards", id)
|
||||||
}
|
}
|
||||||
for i, count := range counts {
|
for i, count := range counts {
|
||||||
// PackTable may retain a disabled reward slot with a real type/ID
|
// Costume/equipment are instance rewards: their count is zero,
|
||||||
// and zero count. It contributes no granted reward.
|
// just as in QuestTable. Only zero-count stackable slots are empty.
|
||||||
if count == 0 {
|
if count == 0 && types[i] != 10 && types[i] != 11 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if types[i] == 0 || count > math.MaxInt32 || types[i] > math.MaxInt32 || ids[i] > math.MaxInt32 {
|
if types[i] == 0 || count > math.MaxInt32 || types[i] > math.MaxInt32 || ids[i] > math.MaxInt32 {
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ func TestStoryCatalogEnumeratesSeparateChainsAndContentTicketRules(t *testing.T)
|
|||||||
raw = wire.AppendVarint(raw, 8, 1)
|
raw = wire.AppendVarint(raw, 8, 1)
|
||||||
raw = wire.AppendVarint(raw, 9, 100)
|
raw = wire.AppendVarint(raw, 9, 100)
|
||||||
raw = wire.AppendVarint(raw, 10, 19)
|
raw = wire.AppendVarint(raw, 10, 19)
|
||||||
// An official-shaped disabled costume slot must be equivalent
|
// Non-stackable costume instances retain their ID with count zero.
|
||||||
// to omitting that slot; type and ID remain populated.
|
|
||||||
raw = wire.AppendVarint(raw, 8, 0)
|
raw = wire.AppendVarint(raw, 8, 0)
|
||||||
raw = wire.AppendVarint(raw, 9, 200)
|
raw = wire.AppendVarint(raw, 9, 200)
|
||||||
raw = wire.AppendVarint(raw, 10, 11)
|
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 {
|
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")
|
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)
|
t.Fatalf("buy rewards=%+v", rewards)
|
||||||
}
|
}
|
||||||
if len(d.TicketSources[101]) != 2 {
|
if len(d.TicketSources[101]) != 2 {
|
||||||
|
|||||||
@@ -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) {
|
func (s *Service) grantPurchaseRewards(identity string, rewards []gamedata.Reward) ([]player.Item, error) {
|
||||||
var itemRewards []gamedata.BattleReward
|
var itemRewards []gamedata.BattleReward
|
||||||
for _, reward := range rewards {
|
for _, reward := range rewards {
|
||||||
if reward.Count == 0 {
|
if reward.Count == 0 && reward.Type != 11 {
|
||||||
return nil, fmt.Errorf("world: empty pack purchase reward")
|
return nil, fmt.Errorf("world: empty pack purchase reward")
|
||||||
}
|
}
|
||||||
if reward.Type == 19 {
|
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})
|
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 {
|
if _, err := s.wallet.GrantQuestOnce(identity+":currency", rewards); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -78,6 +108,7 @@ func (s *Service) purchaseStoryPack(id int, initial bool) ([]byte, error) {
|
|||||||
if !exists {
|
if !exists {
|
||||||
return nil, fmt.Errorf("%w: unknown purchase pack %d", ErrInvalidRequest, id)
|
return nil, fmt.Errorf("%w: unknown purchase pack %d", ErrInvalidRequest, id)
|
||||||
}
|
}
|
||||||
|
buyRewards := pack.BuyRewards
|
||||||
identity := packPurchaseIdentity(id)
|
identity := packPurchaseIdentity(id)
|
||||||
if _, owned := s.collection.Grant(identity); owned {
|
if _, owned := s.collection.Grant(identity); owned {
|
||||||
return []byte{}, nil
|
return []byte{}, nil
|
||||||
@@ -85,12 +116,12 @@ func (s *Service) purchaseStoryPack(id int, initial bool) ([]byte, error) {
|
|||||||
if !initial && pack.BuyPrice != 0 {
|
if !initial && pack.BuyPrice != 0 {
|
||||||
return nil, fmt.Errorf("%w: paid pack purchase type %d is unsupported", ErrInvalidRequest, pack.BuyType)
|
return nil, fmt.Errorf("%w: paid pack purchase type %d is unsupported", ErrInvalidRequest, pack.BuyType)
|
||||||
}
|
}
|
||||||
for _, reward := range pack.BuyRewards {
|
for _, reward := range buyRewards {
|
||||||
if reward.Type != 3 && reward.Type != 4 && reward.Type != 12 && reward.Type != 19 {
|
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)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -98,7 +129,10 @@ func (s *Service) purchaseStoryPack(id int, initial bool) ([]byte, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var bundle []byte
|
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 {
|
if reward.Type == 3 || reward.Type == 4 || reward.Type == 12 {
|
||||||
currency := wire.AppendVarint(wire.AppendVarint(nil, 3, reward.Type), 4, reward.Count)
|
currency := wire.AppendVarint(wire.AppendVarint(nil, 3, reward.Type), 4, reward.Count)
|
||||||
bundle = wire.AppendBytes(bundle, 1, currency)
|
bundle = wire.AppendBytes(bundle, 1, currency)
|
||||||
|
|||||||
@@ -8,6 +8,13 @@ import (
|
|||||||
"testing"
|
"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) {
|
func TestStoryCatalogUsesTicketsAndQuestLinksAcrossIndependentPacks(t *testing.T) {
|
||||||
s := testService()
|
s := testService()
|
||||||
s.seed.PackID = 701
|
s.seed.PackID = 701
|
||||||
@@ -67,6 +74,26 @@ func TestStoryCatalogUsesTicketsAndQuestLinksAcrossIndependentPacks(t *testing.T
|
|||||||
if len(ids) != 2 || ids[0] != 405 || ids[1] != 701 {
|
if len(ids) != 2 || ids[0] != 405 || ids[1] != 701 {
|
||||||
t.Fatalf("terminal updates %v", ids)
|
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) {
|
func TestInitialPackPurchaseUsesDesignAndDurableIdentity(t *testing.T) {
|
||||||
@@ -86,7 +113,8 @@ func TestInitialPackPurchaseUsesDesignAndDurableIdentity(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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++ {
|
for i := 0; i < 2; i++ {
|
||||||
if err := s.EnsureInitialPackPurchase(); err != nil {
|
if err := s.EnsureInitialPackPurchase(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -95,6 +123,9 @@ func TestInitialPackPurchaseUsesDesignAndDurableIdentity(t *testing.T) {
|
|||||||
if s.wallet.CatalystBalance() != 26 || len(s.inventory.All()) != 1 {
|
if s.wallet.CatalystBalance() != 26 || len(s.inventory.All()) != 1 {
|
||||||
t.Fatal("purchase reward repeated or hardcoded")
|
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 {
|
if _, owned := s.collection.Grant("pack-purchase:707"); !owned {
|
||||||
t.Fatal("missing purchase marker")
|
t.Fatal("missing purchase marker")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ func Load(seedPath, gameDataRoot, gameDataVersion string, storage stateio.Store,
|
|||||||
packs[id] = pack.Quests
|
packs[id] = pack.Quests
|
||||||
transitions[id] = gamedata.PackTransition{PackID: id, NextPackID: pack.NextPackID}
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -685,9 +685,13 @@ func (s *Service) accountPackInfo() []byte {
|
|||||||
for _, info := range s.packDBInfoRows() {
|
for _, info := range s.packDBInfoRows() {
|
||||||
out = wire.AppendBytes(out, 1, info)
|
out = wire.AppendBytes(out, 1, info)
|
||||||
}
|
}
|
||||||
if s.packComplete() {
|
for _, info := range s.packDBInfoRows() {
|
||||||
level := wire.AppendVarint(nil, 1, uint64(s.seed.PackID))
|
id, _, _ := wire.Varint(info, 1)
|
||||||
level = wire.AppendVarint(level, 3, uint64(len(s.state.ClearedQuests(s.seed.PackID))))
|
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)
|
level = wire.AppendVarint(level, 5, 1)
|
||||||
out = wire.AppendBytes(out, 2, level)
|
out = wire.AppendBytes(out, 2, level)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user