Restore event and mini hubs, current shop and hunt calendars, and regular seasons; preserve all published windows. Implement story/quiz routes, native shop purchases, avatar sets, currencies, rank-change items and permanent buffs with rollback and reward coverage tests. Refresh client buff stats and update Cash Shop to 0.1.1.
333 lines
9.2 KiB
Go
333 lines
9.2 KiB
Go
package events_test
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"bd2server/internal/server/calendar"
|
|
"bd2server/internal/server/events"
|
|
"bd2server/internal/server/gamedata"
|
|
"bd2server/internal/server/hunting"
|
|
"bd2server/internal/server/player"
|
|
"bd2server/internal/server/stateio"
|
|
"bd2server/internal/server/wire"
|
|
)
|
|
|
|
type auditedLeaves struct{}
|
|
|
|
func (auditedLeaves) Resolve(rewards []gamedata.BattleReward) ([]gamedata.BattleReward, error) {
|
|
return rewards, nil
|
|
}
|
|
func auditArray(raw []byte, number int) []uint64 {
|
|
var result []uint64
|
|
_ = wire.Walk(raw, func(f wire.Field) error {
|
|
if f.Number != number {
|
|
return nil
|
|
}
|
|
if f.Type == 0 {
|
|
v, _ := binary.Uvarint(f.Value)
|
|
result = append(result, v)
|
|
}
|
|
if f.Type == 2 {
|
|
for p := f.Value; len(p) > 0; {
|
|
v, n := binary.Uvarint(p)
|
|
if n <= 0 {
|
|
break
|
|
}
|
|
result = append(result, v)
|
|
p = p[n:]
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
return result
|
|
}
|
|
func TestInstalledScheduledTaskAttendanceAndPassRewardCoverage(t *testing.T) {
|
|
root := os.Getenv("BD2_REAL_GAMEDATA")
|
|
if root == "" {
|
|
t.Skip("BD2_REAL_GAMEDATA not configured")
|
|
}
|
|
const version = "20260923193640"
|
|
design, err := gamedata.LoadEventTasksDesign(root, version)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
schedules, err := calendar.LoadDirectory(filepath.Join("..", "..", "..", "..", "schedules"), "2.35.10", version)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
play, err := gamedata.LoadEventPlayCatalog(root, version)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
exchanges, err := gamedata.LoadEventExchangeCatalog(root, version)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var roots []gamedata.BattleReward
|
|
appendReward := func(r gamedata.Reward) {
|
|
if r.Type != 0 && r.Count != 0 {
|
|
roots = append(roots, gamedata.BattleReward(r))
|
|
}
|
|
}
|
|
missionGroups := map[uint64]bool{}
|
|
appendPass := func(id uint64) {
|
|
pass := design.Passes[id]
|
|
missionGroups[pass.MissionGroup] = true
|
|
appendReward(pass.Core)
|
|
for _, level := range design.PassLevels[pass.LevelGroup] {
|
|
appendReward(level.Basic)
|
|
appendReward(level.Premium)
|
|
}
|
|
for _, buy := range design.PassBuys[id] {
|
|
for _, reward := range buy.Rewards {
|
|
appendReward(reward)
|
|
}
|
|
}
|
|
}
|
|
for _, schedule := range schedules.Events {
|
|
switch schedule.Type {
|
|
case 0:
|
|
group := design.Attendance[schedule.ID].Group
|
|
groups := map[uint64]bool{group: true}
|
|
for _, row := range design.AttendanceGroups {
|
|
if row.Group == group {
|
|
groups[row.ID] = true
|
|
}
|
|
}
|
|
for group := range groups {
|
|
for _, reward := range design.AttendanceRewards[group] {
|
|
appendReward(reward.Basic)
|
|
appendReward(reward.Premium)
|
|
}
|
|
}
|
|
case 1:
|
|
for key, box := range design.LimitRewards {
|
|
if key[0] == schedule.ID {
|
|
roots = append(roots, gamedata.BattleReward{Type: 9, ID: box, Count: 1})
|
|
}
|
|
}
|
|
case 4:
|
|
missionGroups[schedule.ID] = true
|
|
case 7:
|
|
for _, entry := range exchanges.Groups[schedule.ID].Entries {
|
|
appendReward(entry.Reward)
|
|
}
|
|
case 9:
|
|
for _, row := range play.Rows("PackEventBattleTable", 4, schedule.ID) {
|
|
rewards, err := gamedata.EventPlayRewards(row, 9, 10, 8)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
roots = append(roots, rewards...)
|
|
}
|
|
case 10:
|
|
for _, row := range play.Rows("PackEventStoryTable", 1, schedule.ID) {
|
|
rewards, err := gamedata.EventPlayRewards(row, 11, 10, 9)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
roots = append(roots, rewards...)
|
|
}
|
|
case 12, 13, 17, 19:
|
|
game, err := gamedata.LoadEventGame(root, version, schedule.Type, schedule.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, rows := range [][]gamedata.EventGameReward{game.Cells, game.Lines, game.Complete} {
|
|
for _, row := range rows {
|
|
roots = append(roots, row.Rewards...)
|
|
}
|
|
}
|
|
case 5:
|
|
appendPass(schedule.ID)
|
|
}
|
|
}
|
|
for id := range missionGroups {
|
|
for _, group := range design.MissionGroups[id].Groups {
|
|
for _, mission := range design.Missions {
|
|
if mission.Group == group {
|
|
for _, reward := range mission.Rewards {
|
|
appendReward(reward)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// The installed mini-game families have verified direct reward fields;
|
|
// validate each design row, including the next published game variants.
|
|
for _, spec := range []struct {
|
|
table string
|
|
typ, id, count int
|
|
}{{"FieldMiniGameRewardTable", 6, 5, 4}, {"SichuanRewardTable", 6, 5, 4}, {"HopscotchRewardTable", 7, 6, 4}, {"ActionGameMissionTable", 10, 9, 8}, {"MGDRewardTable", 5, 3, 2}} {
|
|
for _, row := range play.Tables[spec.table] {
|
|
rewards, err := gamedata.EventPlayRewards(row, spec.typ, spec.id, spec.count)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
roots = append(roots, rewards...)
|
|
}
|
|
}
|
|
if schedules.MonsterHunt != nil {
|
|
seen := map[uint64]bool{}
|
|
for _, season := range schedules.MonsterHunt.Seasons {
|
|
if seen[season.HuntID] {
|
|
continue
|
|
}
|
|
seen[season.HuntID] = true
|
|
hunt, err := gamedata.LoadMonsterHunt(root, version, season.HuntID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, reward := range hunt.Rewards {
|
|
roots = append(roots, reward.Clear...)
|
|
roots = append(roots, reward.Daily...)
|
|
}
|
|
for _, rows := range hunt.Ranks {
|
|
for _, reward := range rows {
|
|
roots = append(roots, reward.Rewards...)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if len(roots) == 0 {
|
|
t.Fatal("published calendar has no audited reward definitions")
|
|
}
|
|
// Walk every possible OPEN-box branch, not one RNG sample. DIRECT boxes are
|
|
// actual inventory rewards and remain intact. This is a regression oracle
|
|
// for the verified protobuf reward graph fields.
|
|
db, closeDB, err := gamedata.OpenDatabase(root, version, "common")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer closeDB()
|
|
tables := map[string]map[uint64][]byte{}
|
|
for _, name := range []string{"RandomBoxTable", "RewardGroupTable"} {
|
|
rows, err := db.Query("SELECT id,ProtoBuf FROM " + name)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tables[name] = map[uint64][]byte{}
|
|
for rows.Next() {
|
|
var id uint64
|
|
var raw []byte
|
|
if err = rows.Scan(&id, &raw); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tables[name][id] = raw
|
|
}
|
|
if err = rows.Err(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rows.Close()
|
|
}
|
|
leaves := map[[2]uint64]gamedata.BattleReward{}
|
|
var walk func(gamedata.BattleReward, map[uint64]bool)
|
|
walk = func(reward gamedata.BattleReward, seen map[uint64]bool) {
|
|
if reward.Type != 9 {
|
|
reward.Count = 1
|
|
leaves[[2]uint64{reward.Type, reward.ID}] = reward
|
|
return
|
|
}
|
|
raw, ok := tables["RandomBoxTable"][reward.ID]
|
|
if !ok {
|
|
t.Fatalf("unknown scheduled box%d", reward.ID)
|
|
}
|
|
drop, _, _ := wire.Varint(raw, 1)
|
|
if drop == 1 {
|
|
reward.Count = 1
|
|
leaves[[2]uint64{9, reward.ID}] = reward
|
|
return
|
|
}
|
|
if seen[reward.ID] {
|
|
t.Fatalf("scheduled box cycle%d", reward.ID)
|
|
}
|
|
seen[reward.ID] = true
|
|
defer delete(seen, reward.ID)
|
|
gid, _, _ := wire.Varint(raw, 9)
|
|
group, ok := tables["RewardGroupTable"][gid]
|
|
if !ok {
|
|
t.Fatalf("unknown scheduled reward group%d", gid)
|
|
}
|
|
types, ids, counts := auditArray(group, 6), auditArray(group, 5), auditArray(group, 4)
|
|
if len(types) == 0 || len(types) != len(ids) || len(types) != len(counts) {
|
|
t.Fatalf("malformed scheduled reward group%d", gid)
|
|
}
|
|
for i, typ := range types {
|
|
if counts[i] != 0 {
|
|
walk(gamedata.BattleReward{Type: typ, ID: ids[i], Count: 1}, seen)
|
|
}
|
|
}
|
|
}
|
|
for _, reward := range roots {
|
|
walk(reward, map[uint64]bool{})
|
|
}
|
|
costumes, err := gamedata.LoadRewardCostumeCatalog(root, version)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
equipmentDesign, err := gamedata.LoadRewardEquipmentCatalog(root, version)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
owned, err := gamedata.LoadOwnedEventItemDesign(root, version)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
prestige, err := gamedata.LoadPrestigeSkins(root, version)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
avatars, err := gamedata.LoadAvatarRewardDesign(root, version)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
buffDesign, err := gamedata.LoadBuffRewardDesign(root, version)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
store := stateio.NewMemory()
|
|
items, err := player.OpenInventory(store, &player.Starter{Version: "2.35.10"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wallet, err := player.OpenWallet(store, player.Currency{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
collection, err := player.OpenCollectionStore(store, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
equipment, err := player.OpenEquipmentInventory(store)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
economy, err := events.NewEconomy(store, items, wallet, collection, equipment, costumes, equipmentDesign, auditedLeaves{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
economy.AttachOwnedItemDesign(owned)
|
|
economy.AttachPrestigeSkins(prestige)
|
|
economy.AttachAvatarRewards(avatars)
|
|
buffs, err := events.OpenBuffRewards(store, buffDesign)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
economy.AttachBuffRewards(buffs)
|
|
ap, err := hunting.Open(store, root, version, items, wallet, func() (int, error) { return 21, nil }, 0, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
economy.AttachHuntingAP(ap)
|
|
for key, reward := range leaves {
|
|
if _, err = economy.Apply(fmt.Sprintf("coverage:%d:%d", key[0], key[1]), nil, []gamedata.Reward{gamedata.Reward(reward)}); err != nil {
|
|
t.Errorf("scheduled reward leaf %d:%d unsupported: %v", key[0], key[1], err)
|
|
}
|
|
}
|
|
t.Logf("validated %d published reward roots and %d distinct possible leaves against real grant domains", len(roots), len(leaves))
|
|
}
|