fix(server): 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.packSummaryTargets = packSummaryTargets
|
||||
service.attachPackDetailDesign(gameDataRoot, gameDataVersion)
|
||||
return service, nil
|
||||
}
|
||||
|
||||
@@ -118,6 +119,7 @@ func (s *Service) setCurrentPack(packID int) {
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
packDetailDesign func(int) (gamedata.PackDetailDesign, error)
|
||||
packSummaryTargets map[int]bool
|
||||
fieldPacks map[int]gamedata.FieldPack
|
||||
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) {
|
||||
switch path {
|
||||
case "/PackDetailInfo":
|
||||
return s.handlePackDetail(request)
|
||||
case "/PackSummaryInfoList":
|
||||
return s.handlePackSummary(request)
|
||||
case "/PackInfo":
|
||||
|
||||
Reference in New Issue
Block a user