fix(server): restore saved login destination and arena field position
This commit is contained in:
@@ -445,6 +445,9 @@ func serve(args []string) (serveErr error) {
|
||||
if err != nil {
|
||||
return fmt.Errorf("load world state: %w", err)
|
||||
}
|
||||
if err := login.AttachLastPlayedPack(worldService); err != nil {
|
||||
return fmt.Errorf("attach persisted login destination: %w", err)
|
||||
}
|
||||
// Restore all earned seed ownership before validating persisted upgrades.
|
||||
// A quest costume is not a collection entry; attaching it after opening
|
||||
// collection would reject its otherwise valid burst ledger on restart.
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package account
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"bd2server/internal/server/wire"
|
||||
)
|
||||
|
||||
type lastPackFixture struct {
|
||||
pack uint64
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *lastPackFixture) LastPlayedPackID() (uint64, error) { return f.pack, f.err }
|
||||
|
||||
func TestLoginUsesSavedPackAndPreservesSeedOnlyForNewAccount(t *testing.T) {
|
||||
seed := &LoginSeed{Version: StateVersion(), PacketCode: 3, UserInfo: wire.AppendVarint(wire.AppendVarint(nil, 1, 1), 4, 21)}
|
||||
provider := &lastPackFixture{}
|
||||
if err := seed.AttachLastPlayedPack(provider); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, tc := range []struct{ saved, want uint64 }{{0, 21}, {3001, 3001}, {22, 22}} {
|
||||
provider.pack = tc.saved
|
||||
body, err := seed.Login(wire.AppendVarint(nil, 1, 1), []byte("0123456789abcdef0123456789abcdef"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
user, _, _ := wire.Bytes(body, 1)
|
||||
pack, _, _ := wire.Varint(user, 4)
|
||||
if pack != tc.want {
|
||||
t.Fatalf("saved=%d login pack=%d want=%d", tc.saved, pack, tc.want)
|
||||
}
|
||||
}
|
||||
provider.err = errors.New("position unavailable")
|
||||
if _, err := seed.Login(nil, []byte("0123456789abcdef0123456789abcdef")); err == nil {
|
||||
t.Fatal("ignored saved position error")
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,21 @@ type LoginSeed struct {
|
||||
inventorySlots InventorySlotProvider
|
||||
firstGacha FirstGachaProvider
|
||||
friendshipAP FriendshipAPProvider
|
||||
lastPlayedPack LastPlayedPackProvider
|
||||
}
|
||||
|
||||
// LastPlayedPackProvider reads the persisted return destination. A zero value
|
||||
// means there is no saved destination yet, so a new account uses its seed.
|
||||
type LastPlayedPackProvider interface {
|
||||
LastPlayedPackID() (uint64, error)
|
||||
}
|
||||
|
||||
func (s *LoginSeed) AttachLastPlayedPack(provider LastPlayedPackProvider) error {
|
||||
if provider == nil {
|
||||
return errors.New("account: missing last played pack provider")
|
||||
}
|
||||
s.lastPlayedPack = provider
|
||||
return nil
|
||||
}
|
||||
|
||||
// FirstGachaProvider reads the mutable account flag for every login, including
|
||||
@@ -339,6 +354,17 @@ func (s *LoginSeed) Login(request, sessionKey []byte) ([]byte, error) {
|
||||
return nil, fmt.Errorf("account: session key: %w", err)
|
||||
}
|
||||
user := append([]byte(nil), s.UserInfo...)
|
||||
if s.lastPlayedPack != nil {
|
||||
packID, err := s.lastPlayedPack.LastPlayedPackID()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("account: last played pack: %w", err)
|
||||
}
|
||||
if packID > 0 {
|
||||
if user, _, err = wire.ReplaceVarint(user, 4, packID); err != nil {
|
||||
return nil, fmt.Errorf("account: replace last played pack: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if s.currencies != nil {
|
||||
gold, freeJewelry, jewelry, mileage := s.currencies.Currencies()
|
||||
var err error
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
package gamedata
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// FieldPack describes non-story arena fields. PackType comes from the protocol
|
||||
// enum; IDs and entry restrictions come from the installed GameData tables.
|
||||
type FieldPack struct {
|
||||
ID, Type, BuyPrice, UseSchedule uint64
|
||||
TicketID, SquadLevel uint64
|
||||
HasOpenRule bool
|
||||
MapIDs map[int]bool
|
||||
}
|
||||
|
||||
func LoadFieldPacks(root, version string) (map[int]FieldPack, error) {
|
||||
db, closeDB, err := openStatDatabase(root, version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer closeDB()
|
||||
return loadFieldPacks(db)
|
||||
}
|
||||
|
||||
func loadFieldPacks(db *sql.DB) (map[int]FieldPack, error) {
|
||||
result := map[int]FieldPack{}
|
||||
rows, err := db.Query("SELECT id,ProtoBuf FROM PackTable ORDER BY id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for rows.Next() {
|
||||
var id int
|
||||
var raw []byte
|
||||
if err := rows.Scan(&id, &raw); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
values := map[int]uint64{}
|
||||
for _, field := range []int{7, 25, 55, 65} {
|
||||
v, e := packedInts(raw, field)
|
||||
if e != nil || len(v) > 1 {
|
||||
rows.Close()
|
||||
return nil, fmt.Errorf("gamedata: invalid arena pack %d field %d", id, field)
|
||||
}
|
||||
if len(v) == 1 {
|
||||
values[field] = v[0]
|
||||
}
|
||||
}
|
||||
if values[55] != 3 && values[55] != 10 {
|
||||
continue
|
||||
}
|
||||
if id <= 0 || values[25] != uint64(id) {
|
||||
rows.Close()
|
||||
return nil, fmt.Errorf("gamedata: invalid field pack identity %d", id)
|
||||
}
|
||||
result[id] = FieldPack{ID: uint64(id), Type: values[55], BuyPrice: values[7], UseSchedule: values[65], MapIDs: map[int]bool{}}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err = db.Query("SELECT id,ProtoBuf FROM ContentOpenTable WHERE groupId=1 ORDER BY id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for rows.Next() {
|
||||
var id int
|
||||
var raw []byte
|
||||
if err := rows.Scan(&id, &raw); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
pack, exists := result[id]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
pack.HasOpenRule = true
|
||||
for field, target := range map[int]*uint64{5: &pack.SquadLevel, 6: &pack.TicketID} {
|
||||
v, e := packedInts(raw, field)
|
||||
if e != nil || len(v) > 1 {
|
||||
rows.Close()
|
||||
return nil, fmt.Errorf("gamedata: invalid field pack opening %d", id)
|
||||
}
|
||||
if len(v) == 1 {
|
||||
*target = v[0]
|
||||
}
|
||||
}
|
||||
result[id] = pack
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err = db.Query("SELECT id,packId FROM MapTable ORDER BY id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for rows.Next() {
|
||||
var id, packID int
|
||||
if err := rows.Scan(&id, &packID); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
if pack, exists := result[packID]; exists {
|
||||
pack.MapIDs[id] = true
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for id, pack := range result {
|
||||
if len(pack.MapIDs) == 0 {
|
||||
return nil, fmt.Errorf("gamedata: arena pack %d has no map", id)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package gamedata
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"bd2server/internal/server/wire"
|
||||
)
|
||||
|
||||
func TestFieldPacksLoadMetadataMapsAndEntryRestrictions(t *testing.T) {
|
||||
db, err := sql.Open("sqlite", ":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
for _, query := range []string{"CREATE TABLE PackTable(id INTEGER,ProtoBuf BLOB)", "CREATE TABLE ContentOpenTable(groupId INTEGER,id INTEGER,ProtoBuf BLOB)", "CREATE TABLE MapTable(id INTEGER,packId INTEGER)"} {
|
||||
if _, err := db.Exec(query); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, row := range []struct{ id, typ uint64 }{{901, 3}, {902, 10}, {903, 0}} {
|
||||
proto := wire.AppendVarint(wire.AppendVarint(nil, 25, row.id), 55, row.typ)
|
||||
if row.id == 902 {
|
||||
proto = wire.AppendVarint(proto, 7, 20)
|
||||
proto = wire.AppendVarint(proto, 65, 1)
|
||||
}
|
||||
if _, err := db.Exec("INSERT INTO PackTable VALUES(?,?)", row.id, proto); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
opening := wire.AppendVarint(wire.AppendVarint(nil, 5, 6), 6, 555)
|
||||
if _, err := db.Exec("INSERT INTO ContentOpenTable VALUES(1,901,?)", opening); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := db.Exec("INSERT INTO MapTable VALUES(9011,901),(9012,901),(9021,902),(9031,903)"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
packs, err := loadFieldPacks(db)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first := packs[901]
|
||||
if first.ID != 901 || first.Type != 3 || first.TicketID != 555 || first.SquadLevel != 6 || !first.HasOpenRule || !first.MapIDs[9011] || first.MapIDs[9031] {
|
||||
t.Fatalf("arena metadata=%+v", first)
|
||||
}
|
||||
second := packs[902]
|
||||
if second.BuyPrice != 20 || second.UseSchedule != 1 || len(second.MapIDs) != 1 {
|
||||
t.Fatalf("other arena=%+v", second)
|
||||
}
|
||||
if _, found := packs[903]; found {
|
||||
t.Fatal("story loaded as arena")
|
||||
}
|
||||
if _, err := db.Exec("DELETE FROM MapTable WHERE packId=902"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := loadFieldPacks(db); err == nil {
|
||||
t.Fatal("arena without valid map accepted")
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,31 @@ func TestStorePersistsProgressAcrossRestart(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLastPlayedPackTracksPersistedPositionAcrossRestart(t *testing.T) {
|
||||
storage := stateio.NewMemory()
|
||||
store, err := OpenStore(storage)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if id, err := store.LastPlayedPackID(); err != nil || id != 0 {
|
||||
t.Fatalf("new account pack=%d err=%v", id, err)
|
||||
}
|
||||
for _, pack := range []uint64{21, 3001} {
|
||||
request := wire.AppendVarint(nil, 2, pack)
|
||||
request = wire.AppendString(request, 3, `{"MapId":30011,"PlayerPosition":{"x":1,"y":2,"z":3}}`)
|
||||
if err := store.SaveUserPosition(request); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
store, err = OpenStore(storage)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if id, err := store.LastPlayedPackID(); err != nil || id != pack {
|
||||
t.Fatalf("restored pack=%d want=%d err=%v", id, pack, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreKeepsPackOverlap(t *testing.T) {
|
||||
storage := stateio.NewMemory()
|
||||
initial := `{"version":2,"position":{"PackID":0,"Position":{"MapId":0,"PlayerPosition":{"x":0,"y":0,"z":0},"ColleaguePositions":null},"RawJSON":""},"tutorials":[],"quests":{"21:1":{"QuestID":1,"PackID":21,"Values":[7]}},"cleared_quests":{"21:1":true}}`
|
||||
|
||||
@@ -271,6 +271,22 @@ func (s *Store) Position() (SavedPosition, bool) {
|
||||
return s.position, s.position.PackID != 0
|
||||
}
|
||||
|
||||
// LastPlayedPackID is the pack of the latest committed field position. The
|
||||
// client suppresses SaveUserPosition in hidden packs, retaining the outside
|
||||
// field position rather than choosing a temporary hidden scene on relogin.
|
||||
// Zero means no position has been committed, so LoginUser can keep its seed.
|
||||
func (s *Store) LastPlayedPackID() (uint64, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
if s.position.PackID == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
if s.position.PackID < 0 || s.position.PackID > int(^uint32(0)>>1) || s.position.Position.MapID <= 0 || s.position.RawJSON == "" {
|
||||
return 0, ErrInvalidPosition
|
||||
}
|
||||
return uint64(s.position.PackID), nil
|
||||
}
|
||||
|
||||
func (s *Store) TutorialCleared(id int) bool {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
@@ -345,6 +345,9 @@ func (s *Server) dispatch(path string, request []byte) (int, []byte, error) {
|
||||
if err := s.progress.SaveUserPosition(request); err != nil {
|
||||
return 0, nil, fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
if saved, found := s.progress.Position(); found {
|
||||
slog.Info("field position saved", "pack", saved.PackID, "map", saved.Position.MapID)
|
||||
}
|
||||
return 7, nil, nil
|
||||
case "/TutorialClear":
|
||||
if err := s.progress.ClearTutorial(request); err != nil {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"bd2server/internal/server/gamedata"
|
||||
)
|
||||
|
||||
// LastPlayedPackID prevents LoginUser from selecting a field that this server
|
||||
// cannot restore. A fresh account keeps the versioned login seed's pack.
|
||||
func (s *Service) LastPlayedPackID() (uint64, error) {
|
||||
id, err := s.state.LastPlayedPackID()
|
||||
if err != nil || id == 0 {
|
||||
return id, err
|
||||
}
|
||||
if !s.packUnlocked(int(id)) {
|
||||
return 0, errors.New("world: saved login pack is unavailable")
|
||||
}
|
||||
if pack, field := s.fieldPacks[int(id)]; field {
|
||||
saved, _ := s.state.Position()
|
||||
if !pack.MapIDs[saved.Position.MapID] {
|
||||
return 0, errors.New("world: saved arena map does not belong to pack")
|
||||
}
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (s *Service) AttachSquadLevel(provider func() (uint64, error)) error {
|
||||
if provider == nil {
|
||||
return errors.New("world: nil squad level provider")
|
||||
}
|
||||
s.squadLevel = provider
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) fieldPackUnlocked(pack gamedata.FieldPack) bool {
|
||||
// A valid committed field position is the existing account's entry marker.
|
||||
// Restoration does not repeat a purchase or an unlock check.
|
||||
if saved, found := s.state.Position(); found && saved.PackID == int(pack.ID) && pack.MapIDs[saved.Position.MapID] {
|
||||
return true
|
||||
}
|
||||
// Scheduled arena availability needs its own persisted event domain.
|
||||
if pack.UseSchedule != 0 {
|
||||
return false
|
||||
}
|
||||
if pack.SquadLevel != 0 {
|
||||
if s.squadLevel == nil {
|
||||
return false
|
||||
}
|
||||
level, err := s.squadLevel()
|
||||
if err != nil || level < pack.SquadLevel {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if pack.HasOpenRule {
|
||||
if s.inventory == nil {
|
||||
return false
|
||||
}
|
||||
found := false
|
||||
for _, item := range s.inventory.All() {
|
||||
if item.Type == 19 && item.ID == pack.TicketID && item.Count != 0 {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// Paid packs require a durable purchase marker. An existing saved position
|
||||
// proves this account already entered it; this does not implement purchase.
|
||||
if pack.BuyPrice != 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"bd2server/internal/server/gamedata"
|
||||
"bd2server/internal/server/player"
|
||||
"bd2server/internal/server/progress"
|
||||
"bd2server/internal/server/stateio"
|
||||
"bd2server/internal/server/wire"
|
||||
)
|
||||
|
||||
func TestArenaPositionRestoresAcrossRestartWithoutStoryQuestOrRepurchase(t *testing.T) {
|
||||
storage := stateio.NewMemory()
|
||||
state, err := progress.OpenStore(storage)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const raw = `{"MapId":30011,"PlayerPosition":{"x":-2.1,"y":0,"z":6.4}}`
|
||||
request := wire.AppendVarint(nil, 2, 3001)
|
||||
request = wire.AppendString(request, 3, raw)
|
||||
if err := state.SaveUserPosition(request); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
state, err = progress.OpenStore(storage)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := testService()
|
||||
s.state = state
|
||||
s.fieldPacks = map[int]gamedata.FieldPack{3001: {ID: 3001, Type: 3, BuyPrice: 99, UseSchedule: 1, HasOpenRule: true, TicketID: 13001, SquadLevel: 50, MapIDs: map[int]bool{30011: true}}}
|
||||
if pack, err := s.LastPlayedPackID(); err != nil || pack != 3001 {
|
||||
t.Fatalf("restored login pack=%d err=%v", pack, err)
|
||||
}
|
||||
request = wire.AppendVarint(wire.AppendVarint(nil, 1, 1), 2, 3001)
|
||||
code, body, handled, err := s.Handle("/PackInGameInfo", request)
|
||||
if err != nil || !handled || code != 5 {
|
||||
t.Fatalf("arena info code=%d handled=%v err=%v", code, handled, err)
|
||||
}
|
||||
position, _, err := wire.Bytes(body, 4)
|
||||
if err != nil || string(position) != raw {
|
||||
t.Fatalf("restored arena position=%s err=%v", position, err)
|
||||
}
|
||||
if _, found, _ := wire.Bytes(body, 2); found {
|
||||
t.Fatal("arena inherited a story quest")
|
||||
}
|
||||
if pack, err := s.CurrentPackID(); err != nil || pack != 3001 {
|
||||
t.Fatalf("current arena pack=%d err=%v", pack, err)
|
||||
}
|
||||
request = wire.AppendVarint(wire.AppendVarint(nil, 1, 2), 2, 21)
|
||||
_, body, _, err = s.Handle("/PackInGameInfo", request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
position, _, _ = wire.Bytes(body, 4)
|
||||
if string(position) != "{}" {
|
||||
t.Fatal("arena position leaked into story pack")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArenaEntryPreservesTicketPurchaseAndMapRestrictions(t *testing.T) {
|
||||
s := testService()
|
||||
s.fieldPacks = map[int]gamedata.FieldPack{9001: {ID: 9001, Type: 3, HasOpenRule: true, TicketID: 555, MapIDs: map[int]bool{90011: true}}}
|
||||
inventory, err := player.OpenInventory(stateio.NewMemory(), &player.Starter{Version: "2.35.10"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.inventory = inventory
|
||||
if s.packUnlocked(9001) {
|
||||
t.Fatal("missing content ticket accepted")
|
||||
}
|
||||
if _, err := inventory.GrantOnce("arena-ticket", []gamedata.BattleReward{{Type: 19, ID: 555, Count: 1}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !s.packUnlocked(9001) {
|
||||
t.Fatal("free arena with owned ticket refused")
|
||||
}
|
||||
pack := s.fieldPacks[9001]
|
||||
pack.BuyPrice = 10
|
||||
s.fieldPacks[9001] = pack
|
||||
if s.packUnlocked(9001) {
|
||||
t.Fatal("paid arena was purchased implicitly")
|
||||
}
|
||||
request := wire.AppendVarint(nil, 2, 9001)
|
||||
request = wire.AppendString(request, 3, `{"MapId":211,"PlayerPosition":{"x":1}}`)
|
||||
if err := s.state.SaveUserPosition(request); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := s.LastPlayedPackID(); err == nil {
|
||||
t.Fatal("foreign map restored as arena")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLastPlayedStoryPackRestoresItsExactPosition(t *testing.T) {
|
||||
s := testService()
|
||||
s.packs = map[int]map[int]gamedata.QuestDesign{21: s.quests, 22: {1: {ID: 1}}}
|
||||
s.transitions = map[int]gamedata.PackTransition{21: {PackID: 21, NextPackID: 22}}
|
||||
for quest := range s.quests {
|
||||
if err := s.state.ClearQuest(quest, 21); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
const raw = `{"MapId":221,"PlayerPosition":{"x":7.5,"y":0,"z":-2.2},"ColleaguePositions":null}`
|
||||
request := wire.AppendVarint(nil, 2, 22)
|
||||
request = wire.AppendString(request, 3, raw)
|
||||
if err := s.state.SaveUserPosition(request); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pack, err := s.LastPlayedPackID(); err != nil || pack != 22 {
|
||||
t.Fatalf("last story pack=%d err=%v", pack, err)
|
||||
}
|
||||
request = wire.AppendVarint(wire.AppendVarint(nil, 1, 1), 2, 22)
|
||||
_, body, _, err := s.Handle("/PackInGameInfo", request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
position, _, _ := wire.Bytes(body, 4)
|
||||
if string(position) != raw {
|
||||
t.Fatalf("story position=%s", position)
|
||||
}
|
||||
}
|
||||
@@ -63,17 +63,23 @@ func Load(seedPath, gameDataRoot, gameDataVersion string, storage stateio.Store,
|
||||
return nil, err
|
||||
}
|
||||
quests := packs[seed.PackID]
|
||||
fieldPacks, err := gamedata.LoadFieldPacks(gameDataRoot, gameDataVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, ok := quests[seed.StartQuestID]; !ok {
|
||||
return nil, fmt.Errorf("world: start quest %d is absent from QuestTable%d", seed.StartQuestID, seed.PackID)
|
||||
}
|
||||
transition := transitions[seed.PackID]
|
||||
activePack := seed.PackID
|
||||
if saved, found := state.Position(); found {
|
||||
if _, known := packs[saved.PackID]; known {
|
||||
_, storyKnown := packs[saved.PackID]
|
||||
_, fieldKnown := fieldPacks[saved.PackID]
|
||||
if storyKnown || fieldKnown {
|
||||
activePack = saved.PackID
|
||||
}
|
||||
}
|
||||
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}
|
||||
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}
|
||||
return service, nil
|
||||
}
|
||||
|
||||
@@ -107,6 +113,8 @@ func (s *Service) setCurrentPack(packID int) {
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
fieldPacks map[int]gamedata.FieldPack
|
||||
squadLevel func() (uint64, error)
|
||||
seed Seed
|
||||
state *progress.Store
|
||||
starter *player.Starter
|
||||
@@ -249,6 +257,9 @@ func requestQuest(request []byte) (int, int, error) {
|
||||
}
|
||||
|
||||
func (s *Service) questsFor(packID int) (map[int]gamedata.QuestDesign, bool) {
|
||||
if _, exists := s.fieldPacks[packID]; exists {
|
||||
return map[int]gamedata.QuestDesign{}, true
|
||||
}
|
||||
if s.packs != nil {
|
||||
quests, found := s.packs[packID]
|
||||
return quests, found
|
||||
@@ -273,6 +284,9 @@ func (s *Service) transitionFor(packID int) gamedata.PackTransition {
|
||||
// pack to be complete. This accepts the configured next story pack only after
|
||||
// its predecessor is complete, without exposing arbitrary GameData tables.
|
||||
func (s *Service) packUnlocked(packID int) bool {
|
||||
if pack, exists := s.fieldPacks[packID]; exists {
|
||||
return s.fieldPackUnlocked(pack)
|
||||
}
|
||||
current := s.seed.PackID
|
||||
for steps := 0; steps < 64 && current != 0; steps++ {
|
||||
if current == packID {
|
||||
@@ -425,9 +439,17 @@ func (s *Service) packInfoFor(packID int) ([]byte, error) {
|
||||
out = wire.AppendBytes(out, 3, packed)
|
||||
}
|
||||
position := "{}"
|
||||
mapID := 0
|
||||
restored := false
|
||||
if saved, found := s.state.Position(); found && saved.PackID == packID && saved.RawJSON != "" {
|
||||
if pack, arena := s.fieldPacks[packID]; arena && !pack.MapIDs[saved.Position.MapID] {
|
||||
return nil, fmt.Errorf("world: saved map %d does not belong to arena pack %d", saved.Position.MapID, packID)
|
||||
}
|
||||
position = saved.RawJSON
|
||||
mapID = saved.Position.MapID
|
||||
restored = true
|
||||
}
|
||||
slog.Info("world: deliver field position", "pack", packID, "map", mapID, "restored", restored)
|
||||
out = wire.AppendString(out, 4, position)
|
||||
// The remaining starter-only records were observed in the official
|
||||
// starter-pack response. They represent reputation, hunting-ground, statue,
|
||||
|
||||
Reference in New Issue
Block a user