From e8bb0cba145f79cbdc07391dce450f4b54e3a59f Mon Sep 17 00:00:00 2001 From: Flechazo <2558755403@qq.com> Date: Sun, 4 Oct 2026 13:24:16 +0800 Subject: [PATCH] fix(server): advance boss phases from GameData --- go/internal/server/battle/battle.go | 97 ++++++-- go/internal/server/battle/battle_test.go | 1 + go/internal/server/battle/phase_test.go | 208 ++++++++++++++++++ go/internal/server/gamedata/battle_phase.go | 118 ++++++++++ .../server/gamedata/battle_phase_test.go | 66 ++++++ 5 files changed, 477 insertions(+), 13 deletions(-) create mode 100644 go/internal/server/battle/phase_test.go create mode 100644 go/internal/server/gamedata/battle_phase.go create mode 100644 go/internal/server/gamedata/battle_phase_test.go diff --git a/go/internal/server/battle/battle.go b/go/internal/server/battle/battle.go index 05fc200..ccc7be5 100644 --- a/go/internal/server/battle/battle.go +++ b/go/internal/server/battle/battle.go @@ -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) diff --git a/go/internal/server/battle/battle_test.go b/go/internal/server/battle/battle_test.go index 348cf17..e6c6833 100644 --- a/go/internal/server/battle/battle_test.go +++ b/go/internal/server/battle/battle_test.go @@ -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 diff --git a/go/internal/server/battle/phase_test.go b/go/internal/server/battle/phase_test.go new file mode 100644 index 0000000..912253e --- /dev/null +++ b/go/internal/server/battle/phase_test.go @@ -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") + } +} diff --git a/go/internal/server/gamedata/battle_phase.go b/go/internal/server/gamedata/battle_phase.go new file mode 100644 index 0000000..5235c6d --- /dev/null +++ b/go/internal/server/gamedata/battle_phase.go @@ -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 +} diff --git a/go/internal/server/gamedata/battle_phase_test.go b/go/internal/server/gamedata/battle_phase_test.go new file mode 100644 index 0000000..22c1709 --- /dev/null +++ b/go/internal/server/gamedata/battle_phase_test.go @@ -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) + } + } +}