fix: serve verified story pack exploration details
This commit is contained in:
@@ -0,0 +1,67 @@
|
|||||||
|
package gamedata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PackDetailDesign identifies the monsters whose state is consumed by the
|
||||||
|
// client's PackMapRewardInfo. Ordinary story battle rewards are excluded.
|
||||||
|
type PackDetailDesign struct{ RegenMonsterIDs []int }
|
||||||
|
|
||||||
|
func LoadPackDetailDesign(root, version string, packID int) (PackDetailDesign, error) {
|
||||||
|
if packID <= 0 {
|
||||||
|
return PackDetailDesign{}, fmt.Errorf("gamedata: invalid detail pack %d", packID)
|
||||||
|
}
|
||||||
|
plain, err := ReadDatabase(root, version, fmt.Sprintf("pack%d", packID))
|
||||||
|
if err != nil {
|
||||||
|
return PackDetailDesign{}, err
|
||||||
|
}
|
||||||
|
dir, err := os.MkdirTemp("", "bd2-pack-detail-")
|
||||||
|
if err != nil {
|
||||||
|
return PackDetailDesign{}, err
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(dir)
|
||||||
|
path := filepath.Join(dir, "pack.db")
|
||||||
|
if err := os.WriteFile(path, plain, 0o600); err != nil {
|
||||||
|
return PackDetailDesign{}, err
|
||||||
|
}
|
||||||
|
db, err := sql.Open("sqlite", "file:"+filepath.ToSlash(path)+"?mode=ro")
|
||||||
|
if err != nil {
|
||||||
|
return PackDetailDesign{}, err
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
return loadPackDetailDesign(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadPackDetailDesign(db *sql.DB) (PackDetailDesign, error) {
|
||||||
|
rows, err := db.Query("SELECT id,ProtoBuf FROM FieldMonsterTable ORDER BY id")
|
||||||
|
if err != nil {
|
||||||
|
return PackDetailDesign{}, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var design PackDetailDesign
|
||||||
|
for rows.Next() {
|
||||||
|
var id int
|
||||||
|
var raw []byte
|
||||||
|
if err := rows.Scan(&id, &raw); err != nil {
|
||||||
|
return PackDetailDesign{}, err
|
||||||
|
}
|
||||||
|
values := map[int]uint64{}
|
||||||
|
for _, field := range []int{24, 30, 31} {
|
||||||
|
v, err := packedInts(raw, field)
|
||||||
|
if err != nil || len(v) > 1 || id <= 0 {
|
||||||
|
return PackDetailDesign{}, fmt.Errorf("gamedata: invalid detail monster %d field %d", id, field)
|
||||||
|
}
|
||||||
|
if len(v) == 1 {
|
||||||
|
values[field] = v[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if values[31] == 1 && values[30] != 3 && values[24] > 0 {
|
||||||
|
design.RegenMonsterIDs = append(design.RegenMonsterIDs, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return design, rows.Err()
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package gamedata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bd2server/internal/server/wire"
|
||||||
|
"database/sql"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPackDetailDesignOnlyIncludesClientRegenRewardCategory(t *testing.T) {
|
||||||
|
db, err := sql.Open("sqlite", ":memory:")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
if _, err := db.Exec("CREATE TABLE FieldMonsterTable(id INTEGER,ProtoBuf BLOB)"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, r := range []struct{ id, regen, typ, skip int }{{1, 0, 0, 0}, {2, 1, 0, 0}, {3, 0, 0, 1}, {4, 1, 3, 1}, {5, 1, 0, 1}, {6, 1, 1, 1}, {7, 1, 0, 2}} {
|
||||||
|
raw := wire.AppendVarint(wire.AppendVarint(wire.AppendVarint(nil, 24, uint64(r.regen)), 30, uint64(r.typ)), 31, uint64(r.skip))
|
||||||
|
if _, err := db.Exec("INSERT INTO FieldMonsterTable VALUES(?,?)", r.id, raw); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
design, err := loadPackDetailDesign(db)
|
||||||
|
if err != nil || len(design.RegenMonsterIDs) != 2 || design.RegenMonsterIDs[0] != 5 || design.RegenMonsterIDs[1] != 6 {
|
||||||
|
t.Fatalf("design=%+v err=%v", design, err)
|
||||||
|
}
|
||||||
|
if _, err := db.Exec("INSERT INTO FieldMonsterTable VALUES(8,?)", wire.AppendVarint(wire.AppendVarint(nil, 24, 1), 24, 2)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := loadPackDetailDesign(db); err == nil {
|
||||||
|
t.Fatal("duplicate scalar accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package world
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bd2server/internal/server/gamedata"
|
||||||
|
"bd2server/internal/server/wire"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Service) handlePackDetail(request []byte) (int, []byte, bool, error) {
|
||||||
|
seq, found, err := wire.Varint(request, 1)
|
||||||
|
if err != nil || !found || seq == 0 || seq > 0x7fffffff {
|
||||||
|
return 0, nil, true, fmt.Errorf("%w: PackDetailInfo invalid sequence", ErrInvalidRequest)
|
||||||
|
}
|
||||||
|
packID, err := requestPack(request)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, true, err
|
||||||
|
}
|
||||||
|
if !s.packUnlocked(packID) || !s.packSummaryTargets[packID] {
|
||||||
|
return 0, nil, true, fmt.Errorf("%w: PackDetailInfo unavailable pack %d", ErrInvalidRequest, packID)
|
||||||
|
}
|
||||||
|
if s.packDetailDesign == nil {
|
||||||
|
return 0, nil, true, fmt.Errorf("world: missing pack detail design loader")
|
||||||
|
}
|
||||||
|
design, err := s.packDetailDesign(packID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, true, err
|
||||||
|
}
|
||||||
|
if len(design.RegenMonsterIDs) > 0 {
|
||||||
|
// Empty monster state means defeated to the client. Until regeneration
|
||||||
|
// has a persisted domain, do not invent active flags or claim history.
|
||||||
|
return 0, nil, true, fmt.Errorf("%w: PackDetailInfo pack %d requires regenerating monster state", ErrInvalidRequest, packID)
|
||||||
|
}
|
||||||
|
// These repeated fields contain obtained field objects and completed
|
||||||
|
// research objects, not static candidates. No such grants exist in the
|
||||||
|
// current domain. Ordinary story battle inventory grants are excluded by
|
||||||
|
// the GameData monster category verified above. The response's protobuf
|
||||||
|
// collections are non-null even when the payload has no encoded fields.
|
||||||
|
return 627, []byte{}, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) attachPackDetailDesign(root, version string) {
|
||||||
|
s.packDetailDesign = func(packID int) (gamedata.PackDetailDesign, error) {
|
||||||
|
return gamedata.LoadPackDetailDesign(root, version, packID)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package world
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bd2server/internal/server/gamedata"
|
||||||
|
"bd2server/internal/server/wire"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPackDetailUsesVerifiedDesignAndRejectsUnavailableState(t *testing.T) {
|
||||||
|
s := testService()
|
||||||
|
s.packSummaryTargets = map[int]bool{21: true, 22: true}
|
||||||
|
called := 0
|
||||||
|
s.packDetailDesign = func(id int) (gamedata.PackDetailDesign, error) {
|
||||||
|
called++
|
||||||
|
if id != 21 {
|
||||||
|
t.Fatalf("wrong pack %d", id)
|
||||||
|
}
|
||||||
|
return gamedata.PackDetailDesign{}, nil
|
||||||
|
}
|
||||||
|
request := wire.AppendVarint(wire.AppendVarint(nil, 1, 10), 2, 21)
|
||||||
|
if err := s.state.ClearQuest(1, 21); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
code, response, handled, err := s.Handle("/PackDetailInfo", request)
|
||||||
|
if err != nil || !handled || code != 627 || response == nil || len(response) != 0 || called != 1 {
|
||||||
|
t.Fatalf("detail code=%d body=%x handled=%v err=%v calls=%d", code, response, handled, err, called)
|
||||||
|
}
|
||||||
|
s.packDetailDesign = func(int) (gamedata.PackDetailDesign, error) {
|
||||||
|
return gamedata.PackDetailDesign{RegenMonsterIDs: []int{101}}, nil
|
||||||
|
}
|
||||||
|
_, _, handled, err = s.Handle("/PackDetailInfo", request)
|
||||||
|
if !handled || !errors.Is(err, ErrInvalidRequest) {
|
||||||
|
t.Fatalf("unsupported regeneration handled=%v err=%v", handled, err)
|
||||||
|
}
|
||||||
|
s.packDetailDesign = func(int) (gamedata.PackDetailDesign, error) {
|
||||||
|
return gamedata.PackDetailDesign{}, errors.New("missing design")
|
||||||
|
}
|
||||||
|
if _, _, _, err := s.Handle("/PackDetailInfo", request); err == nil {
|
||||||
|
t.Fatal("design failure swallowed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPackDetailRejectsInvalidAndLockedRequestsBeforeReadingDesign(t *testing.T) {
|
||||||
|
s := testService()
|
||||||
|
s.packSummaryTargets = map[int]bool{21: true, 22: true}
|
||||||
|
s.packDetailDesign = func(int) (gamedata.PackDetailDesign, error) {
|
||||||
|
t.Fatal("invalid request read GameData")
|
||||||
|
return gamedata.PackDetailDesign{}, nil
|
||||||
|
}
|
||||||
|
for _, request := range [][]byte{nil, {8}, wire.AppendVarint(nil, 1, 1), wire.AppendVarint(wire.AppendVarint(nil, 1, 1), 2, 22), wire.AppendVarint(wire.AppendVarint(nil, 1, 0x80000000), 2, 21)} {
|
||||||
|
if _, _, handled, err := s.Handle("/PackDetailInfo", request); !handled || err == nil {
|
||||||
|
t.Fatalf("request %x handled=%v err=%v", request, handled, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(s.packSummaryTargets, 21)
|
||||||
|
if _, _, _, err := s.Handle("/PackDetailInfo", wire.AppendVarint(wire.AppendVarint(nil, 1, 1), 2, 21)); err == nil {
|
||||||
|
t.Fatal("non-target pack accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,6 +85,7 @@ func Load(seedPath, gameDataRoot, gameDataVersion string, storage stateio.Store,
|
|||||||
}
|
}
|
||||||
service := &Service{seed: seed, state: state, starter: starter, equipment: equipment, inventory: inventory, wallet: wallet, characters: characters, quests: quests, transition: transition, packs: packs, transitions: transitions, activePack: activePack, fieldPacks: fieldPacks}
|
service := &Service{seed: seed, state: state, starter: starter, equipment: equipment, inventory: inventory, wallet: wallet, characters: characters, quests: quests, transition: transition, packs: packs, transitions: transitions, activePack: activePack, fieldPacks: fieldPacks}
|
||||||
service.packSummaryTargets = packSummaryTargets
|
service.packSummaryTargets = packSummaryTargets
|
||||||
|
service.attachPackDetailDesign(gameDataRoot, gameDataVersion)
|
||||||
return service, nil
|
return service, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +119,7 @@ func (s *Service) setCurrentPack(packID int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
|
packDetailDesign func(int) (gamedata.PackDetailDesign, error)
|
||||||
packSummaryTargets map[int]bool
|
packSummaryTargets map[int]bool
|
||||||
fieldPacks map[int]gamedata.FieldPack
|
fieldPacks map[int]gamedata.FieldPack
|
||||||
squadLevel func() (uint64, error)
|
squadLevel func() (uint64, error)
|
||||||
@@ -156,6 +158,8 @@ func (s *Service) AttachDecks(decks *deck.Store) error {
|
|||||||
|
|
||||||
func (s *Service) Handle(path string, request []byte) (int, []byte, bool, error) {
|
func (s *Service) Handle(path string, request []byte) (int, []byte, bool, error) {
|
||||||
switch path {
|
switch path {
|
||||||
|
case "/PackDetailInfo":
|
||||||
|
return s.handlePackDetail(request)
|
||||||
case "/PackSummaryInfoList":
|
case "/PackSummaryInfoList":
|
||||||
return s.handlePackSummary(request)
|
return s.handlePackSummary(request)
|
||||||
case "/PackInfo":
|
case "/PackInfo":
|
||||||
|
|||||||
Reference in New Issue
Block a user