fix(server): advance boss phases from GameData
This commit is contained in:
@@ -24,19 +24,25 @@ type Service struct {
|
||||
inventory *player.Inventory
|
||||
currentPack func() (int, error)
|
||||
loadRewards func(string, string, int, uint64) ([]gamedata.BattleReward, error)
|
||||
loadPhases func(string, string, int, uint64, uint64) ([]gamedata.BattlePhase, error)
|
||||
buffs func() ([]gamedata.PictorialBuffStat, error)
|
||||
onTutorialWin func() error
|
||||
commitHealth func(map[uint64]uint64) error
|
||||
}
|
||||
|
||||
type battleState struct {
|
||||
entered bool
|
||||
index uint64
|
||||
round uint64
|
||||
monster uint64
|
||||
deck uint64
|
||||
pack int
|
||||
initialBlue [][]byte
|
||||
entered bool
|
||||
index uint64
|
||||
round uint64
|
||||
monster uint64
|
||||
deck uint64
|
||||
pack int
|
||||
initialBlue [][]byte
|
||||
phases []gamedata.BattlePhase
|
||||
phase int
|
||||
phaseStarted bool
|
||||
phaseSeq uint64
|
||||
phaseReply []byte
|
||||
}
|
||||
|
||||
// BeginSession discards an unfinished battle when LoginUser creates a new
|
||||
@@ -102,7 +108,7 @@ func checkSeq(request []byte) error {
|
||||
}
|
||||
|
||||
func (s *Service) Handle(path string, request []byte) (int, []byte, bool, error) {
|
||||
if path != "/BattleEnter" && path != "/BattleStart" && path != "/BattleRetry" && path != "/BattleVerifyState" && path != "/BattleEnd" && path != "/BattleExit" {
|
||||
if path != "/BattleEnter" && path != "/BattleStart" && path != "/BattleRetry" && path != "/BattleVerifyState" && path != "/BattleEnd" && path != "/BattleExit" && path != "/BattlePhaseChange" {
|
||||
return 0, nil, false, nil
|
||||
}
|
||||
if err := checkSeq(request); err != nil {
|
||||
@@ -112,6 +118,36 @@ func (s *Service) Handle(path string, request []byte) (int, []byte, bool, error)
|
||||
defer s.mu.Unlock()
|
||||
state := s.stateLocked()
|
||||
switch path {
|
||||
case "/BattlePhaseChange":
|
||||
if !state.entered {
|
||||
return 0, nil, true, errors.New("battle: phase change before enter")
|
||||
}
|
||||
seq, _, _ := wire.Varint(request, 1)
|
||||
if seq == state.phaseSeq && state.phaseReply != nil {
|
||||
return 632, append([]byte(nil), state.phaseReply...), true, nil
|
||||
}
|
||||
if seq <= state.phaseSeq {
|
||||
return 0, nil, true, errors.New("battle: stale phase change sequence")
|
||||
}
|
||||
if len(state.phases) == 0 || state.phase+1 >= len(state.phases) {
|
||||
return 0, nil, true, errors.New("battle: no next phase")
|
||||
}
|
||||
if !state.phaseStarted {
|
||||
return 0, nil, true, errors.New("battle: phase change before current phase start")
|
||||
}
|
||||
next := state.phases[state.phase+1]
|
||||
response := wire.AppendVarint(nil, 1, next.GroupID)
|
||||
response = wire.AppendVarint(response, 2, next.ID)
|
||||
response = wire.AppendVarint(response, 8, next.DeckID)
|
||||
// The local engine does not simulate combat. With verification disabled
|
||||
// and no battle_result the client explicitly preserves its blue team;
|
||||
// fabricating a verified result would overwrite HP, SP and action state.
|
||||
// Dynamic official turn/SP values are not available in this request.
|
||||
state.phase++
|
||||
state.index, state.deck, state.phaseStarted = next.DeckID, next.DeckID, false
|
||||
state.phaseSeq, state.phaseReply = seq, append([]byte(nil), response...)
|
||||
slog.Info("team trace: battle phase changed", "pack", state.pack, "monster", state.monster, "group", next.GroupID, "phase", next.ID, "enemyDeck", next.DeckID)
|
||||
return 632, response, true, nil
|
||||
case "/BattleVerifyState":
|
||||
if !state.entered {
|
||||
return 0, nil, true, errors.New("battle: verify before enter")
|
||||
@@ -141,6 +177,20 @@ func (s *Service) Handle(path string, request []byte) (int, []byte, bool, error)
|
||||
return 0, nil, true, errors.New("battle: current pack resolver is unavailable")
|
||||
}
|
||||
monster, _, _ := wire.Varint(request, 3)
|
||||
var phases []gamedata.BattlePhase
|
||||
if s.loadPhases != nil || (s.gameDataRoot != "" && packID > 0 && monster != 0) {
|
||||
loader := s.loadPhases
|
||||
if loader == nil {
|
||||
loader = gamedata.BattleDeckPhases
|
||||
}
|
||||
phases, err = loader(s.gameDataRoot, s.gameDataVersion, packID, monster, deck)
|
||||
if err != nil {
|
||||
return 0, nil, true, fmt.Errorf("battle: load phases: %w", err)
|
||||
}
|
||||
if len(phases) != 0 && phases[0].DeckID != deck {
|
||||
return 0, nil, true, errors.New("battle: enter must select first phase deck")
|
||||
}
|
||||
}
|
||||
response := wire.AppendVarint(nil, 2, deck)
|
||||
if s.buffs != nil {
|
||||
buffs, err := s.buffs()
|
||||
@@ -160,6 +210,7 @@ func (s *Service) Handle(path string, request []byte) (int, []byte, bool, error)
|
||||
response = wire.AppendVarint(response, 6, 1)
|
||||
state.entered, state.index, state.round, state.initialBlue = true, 0, 0, nil
|
||||
state.monster, state.deck, state.pack = monster, deck, packID
|
||||
state.phases, state.phase, state.phaseStarted, state.phaseSeq, state.phaseReply = phases, 0, false, 0, nil
|
||||
slog.Info("team trace: battle entered", "pack", packID, "monster", monster, "enemyDeck", deck, "mode", mode)
|
||||
return 52, response, true, nil
|
||||
case "/BattleRetry":
|
||||
@@ -173,12 +224,24 @@ func (s *Service) Handle(path string, request []byte) (int, []byte, bool, error)
|
||||
if len(state.initialBlue) == 0 {
|
||||
return 0, nil, true, errors.New("battle: retry before initial battle state")
|
||||
}
|
||||
if len(state.phases) != 0 {
|
||||
// Retry requests carry the current deck; the response must restore
|
||||
// the first phase's deck, as PhaseBattleManager.ApplyRetryResponse does.
|
||||
if index != state.index {
|
||||
return 0, nil, true, errors.New("battle: retry index does not match current phase")
|
||||
}
|
||||
index = state.phases[0].DeckID
|
||||
}
|
||||
var response []byte
|
||||
for _, character := range state.initialBlue {
|
||||
response = wire.AppendBytes(response, 2, character)
|
||||
}
|
||||
response = wire.AppendVarint(response, 3, index)
|
||||
state.index, state.round = index, 0
|
||||
if len(state.phases) != 0 {
|
||||
state.deck = state.phases[0].DeckID
|
||||
}
|
||||
state.phase, state.phaseStarted, state.phaseSeq, state.phaseReply = 0, false, 0, nil
|
||||
return 58, response, true, nil
|
||||
case "/BattleStart":
|
||||
if !state.entered {
|
||||
@@ -191,10 +254,11 @@ func (s *Service) Handle(path string, request []byte) (int, []byte, bool, error)
|
||||
if state.index != 0 && index != state.index {
|
||||
return 0, nil, true, fmt.Errorf("battle: index changed from %d to %d", state.index, index)
|
||||
}
|
||||
state.index, state.round = index, state.round+1
|
||||
if state.round == 1 {
|
||||
state.initialBlue = nil
|
||||
if len(state.phases) != 0 && index != state.phases[state.phase].DeckID {
|
||||
return 0, nil, true, errors.New("battle: start index does not match current phase")
|
||||
}
|
||||
nextRound := state.round + 1
|
||||
var initialBlue [][]byte
|
||||
var response []byte
|
||||
err = wire.Walk(request, func(field wire.Field) error {
|
||||
if field.Type != 2 {
|
||||
@@ -204,8 +268,8 @@ func (s *Service) Handle(path string, request []byte) (int, []byte, bool, error)
|
||||
response = wire.AppendBytes(response, 1, field.Value)
|
||||
} else if field.Number == 5 {
|
||||
response = wire.AppendBytes(response, 2, field.Value)
|
||||
if state.round == 1 {
|
||||
state.initialBlue = append(state.initialBlue, append([]byte(nil), field.Value...))
|
||||
if nextRound == 1 {
|
||||
initialBlue = append(initialBlue, append([]byte(nil), field.Value...))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -213,6 +277,10 @@ func (s *Service) Handle(path string, request []byte) (int, []byte, bool, error)
|
||||
if err != nil {
|
||||
return 0, nil, true, err
|
||||
}
|
||||
state.index, state.round, state.phaseStarted = index, nextRound, true
|
||||
if nextRound == 1 {
|
||||
state.initialBlue = initialBlue
|
||||
}
|
||||
// Stable per-battle/round seed; reproducible across retries.
|
||||
seed := index*7919 + state.round*104729
|
||||
response = wire.AppendVarint(response, 3, seed)
|
||||
@@ -225,6 +293,9 @@ func (s *Service) Handle(path string, request []byte) (int, []byte, bool, error)
|
||||
if err != nil || !found || result == 0 {
|
||||
return 0, nil, true, errors.New("battle: invalid result")
|
||||
}
|
||||
if result == 1 && len(state.phases) != 0 && (state.phase != len(state.phases)-1 || !state.phaseStarted) {
|
||||
return 0, nil, true, errors.New("battle: victory before final phase start")
|
||||
}
|
||||
response := wire.AppendVarint(nil, 1, result)
|
||||
var resultCharacters [][]byte
|
||||
finishedHealth := make(map[uint64]uint64)
|
||||
|
||||
@@ -136,6 +136,7 @@ func TestBattleVictoryLocksPackAtEnterForRewardsAndIdentity(t *testing.T) {
|
||||
})
|
||||
var loadedPack int
|
||||
var loadedDeck uint64
|
||||
s.loadPhases = func(string, string, int, uint64, uint64) ([]gamedata.BattlePhase, error) { return nil, nil }
|
||||
s.loadRewards = func(_, _ string, packID int, deckID uint64) ([]gamedata.BattleReward, error) {
|
||||
loadedPack, loadedDeck = packID, deckID
|
||||
return []gamedata.BattleReward{{Type: 8, ID: 8, Count: 3}}, nil
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
package battle
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"bd2server/internal/server/gamedata"
|
||||
"bd2server/internal/server/player"
|
||||
"bd2server/internal/server/stateio"
|
||||
"bd2server/internal/server/wire"
|
||||
)
|
||||
|
||||
func phaseService(t *testing.T) *Service {
|
||||
t.Helper()
|
||||
s := &Service{loadPhases: func(string, string, int, uint64, uint64) ([]gamedata.BattlePhase, error) {
|
||||
return []gamedata.BattlePhase{{GroupID: 70, ID: 101, DeckID: 11}, {GroupID: 70, ID: 205, DeckID: 12}, {GroupID: 70, ID: 309, DeckID: 13}}, nil
|
||||
}}
|
||||
enter := wire.AppendVarint(wire.AppendVarint(request(1), 4, 11), 5, 1)
|
||||
if _, _, _, err := s.Handle("/BattleEnter", enter); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func phaseStart(t *testing.T, s *Service, seq, deck uint64, blue []byte) []byte {
|
||||
t.Helper()
|
||||
start := wire.AppendVarint(request(seq), 2, deck)
|
||||
start = wire.AppendBytes(start, 5, blue)
|
||||
code, reply, _, err := s.Handle("/BattleStart", start)
|
||||
if err != nil || code != 14 {
|
||||
t.Fatalf("start %d: code=%d err=%v", deck, code, err)
|
||||
}
|
||||
return reply
|
||||
}
|
||||
|
||||
func TestPhaseLifecyclePreservesClientStateAndRejectsSkipping(t *testing.T) {
|
||||
s := phaseService(t)
|
||||
blue := wire.AppendVarint(wire.AppendVarint(nil, 2, 400), 4, 1234)
|
||||
if _, _, _, err := s.Handle("/BattlePhaseChange", request(2)); err == nil {
|
||||
t.Fatal("phase advanced before start")
|
||||
}
|
||||
phaseStart(t, s, 3, 11, blue)
|
||||
commits := 0
|
||||
s.AttachCommittedHealth(func(map[uint64]uint64) error { commits++; return nil })
|
||||
win := wire.AppendVarint(request(4), 2, 1)
|
||||
if _, _, _, err := s.Handle("/BattleEnd", win); err == nil {
|
||||
t.Fatal("early victory accepted")
|
||||
}
|
||||
if commits != 0 {
|
||||
t.Fatal("early victory committed health")
|
||||
}
|
||||
code, reply, _, err := s.Handle("/BattlePhaseChange", request(5))
|
||||
if err != nil || code != 632 {
|
||||
t.Fatalf("phase response=%d %v", code, err)
|
||||
}
|
||||
for field, want := range map[int]uint64{1: 70, 2: 205, 8: 12} {
|
||||
got, found, err := wire.Varint(reply, field)
|
||||
if err != nil || !found || got != want {
|
||||
t.Fatalf("field %d = %d/%v: %v", field, got, found, err)
|
||||
}
|
||||
}
|
||||
if result, found, _ := wire.Bytes(reply, 3); found && len(result) != 0 {
|
||||
t.Fatal("fabricated verified battle result")
|
||||
}
|
||||
verify, _, _ := wire.Varint(reply, 7)
|
||||
if verify != 0 {
|
||||
t.Fatal("verification enabled without authoritative combat state")
|
||||
}
|
||||
_, replay, _, err := s.Handle("/BattlePhaseChange", request(5))
|
||||
if err != nil || !bytes.Equal(reply, replay) || s.stateLocked().phase != 1 {
|
||||
t.Fatal("replay advanced phase")
|
||||
}
|
||||
if _, _, _, err := s.Handle("/BattlePhaseChange", request(6)); err == nil {
|
||||
t.Fatal("skipped unstarted phase")
|
||||
}
|
||||
if _, _, _, err := s.Handle("/BattleStart", wire.AppendVarint(request(7), 2, 13)); err == nil {
|
||||
t.Fatal("skipped deck accepted")
|
||||
}
|
||||
changedBlue := wire.AppendVarint(wire.AppendVarint(nil, 2, 400), 4, 600)
|
||||
response := phaseStart(t, s, 8, 12, changedBlue)
|
||||
got, _, _ := wire.Bytes(response, 2)
|
||||
if !bytes.Equal(got, changedBlue) || !bytes.Equal(s.stateLocked().initialBlue[0], blue) {
|
||||
t.Fatal("phase start reset player state or retry baseline")
|
||||
}
|
||||
if _, _, _, err := s.Handle("/BattlePhaseChange", request(9)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, _, err := s.Handle("/BattleEnd", win); err == nil {
|
||||
t.Fatal("victory before final start")
|
||||
}
|
||||
phaseStart(t, s, 10, 13, changedBlue)
|
||||
if _, _, _, err := s.Handle("/BattlePhaseChange", request(11)); err == nil {
|
||||
t.Fatal("advanced past final phase")
|
||||
}
|
||||
if _, _, _, err := s.Handle("/BattleEnd", win); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, _, err := s.Handle("/BattlePhaseChange", request(12)); err == nil {
|
||||
t.Fatal("inactive phase request accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhaseRetryRestoresFirstDeckAndInitialBlue(t *testing.T) {
|
||||
s := phaseService(t)
|
||||
blue := wire.AppendVarint(wire.AppendVarint(nil, 2, 401), 4, 200)
|
||||
phaseStart(t, s, 2, 11, blue)
|
||||
if _, _, _, err := s.Handle("/BattlePhaseChange", request(3)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
phaseStart(t, s, 4, 12, wire.AppendVarint(nil, 4, 1))
|
||||
if _, _, _, err := s.Handle("/BattleRetry", wire.AppendVarint(request(5), 2, 11)); err == nil {
|
||||
t.Fatal("retry accepted wrong current deck")
|
||||
}
|
||||
code, response, _, err := s.Handle("/BattleRetry", wire.AppendVarint(request(6), 2, 12))
|
||||
index, _, _ := wire.Varint(response, 3)
|
||||
restored, _, _ := wire.Bytes(response, 2)
|
||||
if err != nil || code != 58 || index != 11 || !bytes.Equal(restored, blue) {
|
||||
t.Fatalf("retry: code=%d index=%d blue=%x err=%v", code, index, restored, err)
|
||||
}
|
||||
state := s.stateLocked()
|
||||
if state.phase != 0 || state.deck != 11 || state.phaseStarted || state.phaseReply != nil {
|
||||
t.Fatal("retry did not reset phase state")
|
||||
}
|
||||
if _, _, _, err := s.Handle("/BattlePhaseChange", request(7)); err == nil {
|
||||
t.Fatal("retry advanced before start")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMalformedPhaseStartDoesNotConsumeRound(t *testing.T) {
|
||||
s := phaseService(t)
|
||||
start := append(wire.AppendVarint(request(2), 2, 11), 0x2a, 0x80)
|
||||
if _, _, _, err := s.Handle("/BattleStart", start); err == nil {
|
||||
t.Fatal("malformed start accepted")
|
||||
}
|
||||
if state := s.stateLocked(); state.round != 0 || state.index != 0 || state.phaseStarted {
|
||||
t.Fatal("malformed request mutated battle")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhaseEntryRejectsLaterDeckWithoutReplacingActiveBattle(t *testing.T) {
|
||||
s := phaseService(t)
|
||||
phaseStart(t, s, 2, 11, wire.AppendVarint(nil, 2, 400))
|
||||
enter := wire.AppendVarint(wire.AppendVarint(request(3), 4, 12), 5, 1)
|
||||
if _, _, _, err := s.Handle("/BattleEnter", enter); err == nil {
|
||||
t.Fatal("entered a later phase directly")
|
||||
}
|
||||
state := s.stateLocked()
|
||||
if state.deck != 11 || state.index != 11 || !state.phaseStarted {
|
||||
t.Fatal("rejected enter replaced active battle")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrdinaryBattleRejectsPhaseChange(t *testing.T) {
|
||||
s := &Service{}
|
||||
enter := wire.AppendVarint(wire.AppendVarint(request(1), 4, 11), 5, 1)
|
||||
if _, _, _, err := s.Handle("/BattleEnter", enter); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
phaseStart(t, s, 2, 11, wire.AppendVarint(nil, 2, 400))
|
||||
if _, _, _, err := s.Handle("/BattlePhaseChange", request(3)); err == nil {
|
||||
t.Fatal("ordinary battle accepted phase change")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhaseVictoryRewardsOnlyFinalDeck(t *testing.T) {
|
||||
s := phaseService(t)
|
||||
var err error
|
||||
s.inventory, err = player.OpenInventory(stateio.NewMemory(), &player.Starter{Version: "2.35.10"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.gameDataRoot = "test-root"
|
||||
s.stateLocked().pack, s.stateLocked().monster = 77, 42
|
||||
loaded := uint64(0)
|
||||
s.loadRewards = func(_, _ string, pack int, deck uint64) ([]gamedata.BattleReward, error) {
|
||||
if pack != 77 {
|
||||
t.Fatalf("reward pack = %d", pack)
|
||||
}
|
||||
loaded = deck
|
||||
return []gamedata.BattleReward{{Type: 8, ID: 8, Count: 1}}, nil
|
||||
}
|
||||
blue := wire.AppendVarint(nil, 2, 400)
|
||||
phaseStart(t, s, 2, 11, blue)
|
||||
win := wire.AppendVarint(request(3), 2, 1)
|
||||
if _, _, _, err := s.Handle("/BattleEnd", win); err == nil {
|
||||
t.Fatal("early win accepted")
|
||||
}
|
||||
if loaded != 0 {
|
||||
t.Fatal("early win loaded rewards")
|
||||
}
|
||||
if _, _, _, err := s.Handle("/BattlePhaseChange", request(4)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
phaseStart(t, s, 5, 12, blue)
|
||||
if _, _, _, err := s.Handle("/BattlePhaseChange", request(6)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
phaseStart(t, s, 7, 13, blue)
|
||||
if _, _, _, err := s.Handle("/BattleEnd", win); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if loaded != 13 || len(s.inventory.GrantedItems("pack77:monster42:deck13")) != 1 {
|
||||
t.Fatalf("final reward deck=%d", loaded)
|
||||
}
|
||||
if len(s.inventory.GrantedItems("pack77:monster42:deck11")) != 0 {
|
||||
t.Fatal("granted first phase reward")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package gamedata
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"bd2server/internal/server/wire"
|
||||
)
|
||||
|
||||
// BattlePhase identifies a row in the pack's PhaseBattleTable. ID is the
|
||||
// design row ID sent as current_phase_id, not a zero-based list offset.
|
||||
type BattlePhase struct{ GroupID, ID, DeckID uint64 }
|
||||
|
||||
// BattleDeckPhases resolves a monster's phase group and verifies that the
|
||||
// selected deck belongs to it. Ordinary monsters have no phase group.
|
||||
func BattleDeckPhases(root, version string, packID int, monsterID, deckID uint64) ([]BattlePhase, error) {
|
||||
if packID <= 0 || monsterID == 0 || deckID == 0 || monsterID > 2147483647 || deckID > 2147483647 {
|
||||
return nil, fmt.Errorf("gamedata: invalid phase battle pack/monster/deck %d/%d/%d", packID, monsterID, deckID)
|
||||
}
|
||||
plain, err := ReadDatabase(root, version, fmt.Sprintf("pack%d", packID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dir, err := os.MkdirTemp("", "bd2-phase-")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
path := filepath.Join(dir, "pack.db")
|
||||
if err := os.WriteFile(path, plain, 0600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db, err := sql.Open("sqlite", "file:"+filepath.ToSlash(path)+"?mode=ro")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer db.Close()
|
||||
return battleDeckPhasesFromDB(db, monsterID, deckID)
|
||||
}
|
||||
|
||||
func battleDeckPhasesFromDB(db *sql.DB, monsterID, deckID uint64) ([]BattlePhase, error) {
|
||||
var monster []byte
|
||||
if err := db.QueryRow("SELECT ProtoBuf FROM FieldMonsterTable WHERE id=?", monsterID).Scan(&monster); err != nil {
|
||||
return nil, fmt.Errorf("gamedata: phase monster %d: %w", monsterID, err)
|
||||
}
|
||||
group, err := phaseScalar(monster, 21)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if group == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if group > 2147483647 {
|
||||
return nil, fmt.Errorf("gamedata: phase group exceeds int32")
|
||||
}
|
||||
rows, err := db.Query("SELECT id, ProtoBuf FROM PhaseBattleTable WHERE groupId=? ORDER BY id", group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var phases []BattlePhase
|
||||
selected := false
|
||||
for rows.Next() {
|
||||
var data []byte
|
||||
var sqlID uint64
|
||||
if err := rows.Scan(&sqlID, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rowGroup, err := phaseScalar(data, 3)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id, err := phaseScalar(data, 4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deck, err := phaseScalar(data, 2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rowGroup != group || id == 0 || deck == 0 || id > 2147483647 || deck > 2147483647 || id != sqlID || (len(phases) > 0 && phases[len(phases)-1].ID >= id) {
|
||||
return nil, fmt.Errorf("gamedata: invalid phase group %d row %d deck %d", rowGroup, id, deck)
|
||||
}
|
||||
phases = append(phases, BattlePhase{GroupID: group, ID: id, DeckID: deck})
|
||||
selected = selected || deck == deckID
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(phases) == 0 || !selected {
|
||||
return nil, fmt.Errorf("gamedata: phase group %d does not contain selected deck %d", group, deckID)
|
||||
}
|
||||
return phases, nil
|
||||
}
|
||||
|
||||
func phaseScalar(data []byte, number int) (uint64, error) {
|
||||
var value uint64
|
||||
err := wire.Walk(data, func(f wire.Field) error {
|
||||
if f.Number != number {
|
||||
return nil
|
||||
}
|
||||
if f.Type != 0 {
|
||||
return fmt.Errorf("gamedata: phase field %d has wire type %d", number, f.Type)
|
||||
}
|
||||
values, err := packedInts(data, number)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(values) != 1 {
|
||||
return fmt.Errorf("gamedata: phase field %d is not scalar", number)
|
||||
}
|
||||
value = values[0]
|
||||
return nil
|
||||
})
|
||||
return value, err
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package gamedata
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBattleDeckPhasesDesignRows(t *testing.T) {
|
||||
db, e := sql.Open("sqlite", ":memory:")
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
defer db.Close()
|
||||
db.SetMaxOpenConns(1)
|
||||
for _, q := range []string{"CREATE TABLE FieldMonsterTable(id INTEGER PRIMARY KEY, ProtoBuf BLOB)", "CREATE TABLE PhaseBattleTable(groupId INTEGER,id INTEGER,ProtoBuf BLOB)"} {
|
||||
if _, e := db.Exec(q); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
}
|
||||
for _, v := range []struct {
|
||||
id int
|
||||
b []byte
|
||||
}{{8, []byte{0xa8, 1, 1}}, {1, nil}, {2, []byte{0xa8, 1, 7}}} {
|
||||
if _, e := db.Exec("INSERT INTO FieldMonsterTable VALUES (?,?)", v.id, v.b); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
}
|
||||
// Rows reproduce Python-confirmed pack22 fields; group7 tests nonadjacent IDs.
|
||||
for _, v := range []struct{ g, id, d int }{{1, 2, 9}, {1, 1, 8}, {7, 50, 99}, {7, 10, 77}, {7, 30, 88}} {
|
||||
b := []byte{0x10, byte(v.d), 0x18, byte(v.g), 0x20, byte(v.id)}
|
||||
if _, e := db.Exec("INSERT INTO PhaseBattleTable VALUES (?,?,?)", v.g, v.id, b); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
}
|
||||
for _, c := range []struct {
|
||||
m, d uint64
|
||||
w []BattlePhase
|
||||
}{{8, 8, []BattlePhase{{1, 1, 8}, {1, 2, 9}}}, {8, 9, []BattlePhase{{1, 1, 8}, {1, 2, 9}}}, {1, 1, nil}, {2, 77, []BattlePhase{{7, 10, 77}, {7, 30, 88}, {7, 50, 99}}}} {
|
||||
g, e := battleDeckPhasesFromDB(db, c.m, c.d)
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
if !reflect.DeepEqual(g, c.w) {
|
||||
t.Fatalf("got %+v want %+v", g, c.w)
|
||||
}
|
||||
}
|
||||
if _, e := battleDeckPhasesFromDB(db, 8, 1); e == nil {
|
||||
t.Fatal("accepted unrelated deck")
|
||||
}
|
||||
for _, b := range [][]byte{{0x10, 8, 0x18, 2, 0x20, 1}, {0x10, 8, 0x18, 1, 0x20, 3}, {0x18, 1, 0x20, 1}} {
|
||||
if _, e := db.Exec("UPDATE PhaseBattleTable SET ProtoBuf=? WHERE groupId=1 AND id=1", b); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
if _, e := battleDeckPhasesFromDB(db, 8, 8); e == nil {
|
||||
t.Fatalf("accepted invalid row %x", b)
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestPhaseScalarRejectsInvalidDesign(t *testing.T) {
|
||||
for _, b := range [][]byte{{0x1a, 1, 1}, {0x18, 1, 0x18, 2}} {
|
||||
if _, e := phaseScalar(b, 3); e == nil {
|
||||
t.Fatalf("accepted %x", b)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user