feat(server): implement persistent costume potential connections and stat updates
This commit is contained in:
@@ -692,6 +692,9 @@ func serve(args []string) (serveErr error) {
|
||||
return fmt.Errorf("read initial hunting AP: %w", err)
|
||||
}
|
||||
gameplayStore := stateio.EntrySnapshotStore{Entries: stateRepository, Domain: "missions", Bucket: "gameplay"}
|
||||
if err := costumePotentialService.AttachConnectStore(gameplayStore); err != nil {
|
||||
return fmt.Errorf("attach costume potential connection state: %w", err)
|
||||
}
|
||||
if err := worldService.AttachFieldMonsterState(gameplayStore); err != nil {
|
||||
return fmt.Errorf("attach field monster state: %w", err)
|
||||
}
|
||||
|
||||
@@ -147,6 +147,9 @@ func preflight(args []string) error {
|
||||
if _, err := gamedata.LoadCostumeBurstDesign(gameData, versions.GameDataVersion); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := gamedata.LoadCostumePotentialDesign(gameData, versions.GameDataVersion); err != nil {
|
||||
return fmt.Errorf("preflight costume potential design: %w", err)
|
||||
}
|
||||
if _, err := gamedata.LoadTalentUseDesign(gameData, versions.GameDataVersion); err != nil {
|
||||
return fmt.Errorf("preflight talent skill design: %w", err)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ type CostumePotentialDesign struct {
|
||||
CostumeUnique map[uint64]uint64
|
||||
CharacterGrade map[uint64]uint64
|
||||
CharacterUnique map[uint64]uint64
|
||||
CharacterTypes map[uint64]uint64
|
||||
CostumeActive map[uint64]bool
|
||||
}
|
||||
|
||||
func LoadCostumePotentialDesign(root, version string) (*CostumePotentialDesign, error) {
|
||||
@@ -36,6 +38,31 @@ func LoadCostumePotentialDesign(root, version string) (*CostumePotentialDesign,
|
||||
|
||||
func loadCostumePotentialDesign(db *sql.DB) (*CostumePotentialDesign, error) {
|
||||
d := &CostumePotentialDesign{Nodes: map[uint64]map[uint64]CostumePotentialNode{}, CostumeUnique: map[uint64]uint64{}, CharacterGrade: map[uint64]uint64{}, CharacterUnique: map[uint64]uint64{}}
|
||||
d.CharacterTypes = map[uint64]uint64{}
|
||||
d.CostumeActive = map[uint64]bool{}
|
||||
groups, err := db.Query("SELECT id,ProtoBuf FROM CostumeNodeGroupTable")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for groups.Next() {
|
||||
var id uint64
|
||||
var raw []byte
|
||||
if err = groups.Scan(&id, &raw); err != nil {
|
||||
groups.Close()
|
||||
return nil, err
|
||||
}
|
||||
active, e := optionalScalar(raw, 3)
|
||||
if e != nil || active > 1 {
|
||||
groups.Close()
|
||||
return nil, fmt.Errorf("gamedata: invalid potential active group")
|
||||
}
|
||||
d.CostumeActive[id] = active == 1
|
||||
}
|
||||
if err = groups.Err(); err != nil {
|
||||
groups.Close()
|
||||
return nil, err
|
||||
}
|
||||
groups.Close()
|
||||
rows, err := db.Query("SELECT groupId,id,ProtoBuf FROM CostumeNodeTable ORDER BY groupId,id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -125,6 +152,12 @@ func loadCostumePotentialDesign(db *sql.DB) (*CostumePotentialDesign, error) {
|
||||
return nil, err
|
||||
}
|
||||
grade, _ := packedInts(proto, 10)
|
||||
typeID, e := optionalScalar(proto, 19)
|
||||
if e != nil {
|
||||
rows.Close()
|
||||
return nil, e
|
||||
}
|
||||
d.CharacterTypes[id] = typeID
|
||||
unique, _ := packedInts(proto, 20)
|
||||
if len(grade) == 1 && len(unique) == 1 {
|
||||
d.CharacterGrade[id] = grade[0]
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package pictorial
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"bd2server/internal/server/gamedata"
|
||||
"bd2server/internal/server/player"
|
||||
"bd2server/internal/server/stateio"
|
||||
"bd2server/internal/server/wire"
|
||||
)
|
||||
|
||||
func TestMaximumHealthIncludesOwnedEquipmentPotentialAndAwakening(t *testing.T) {
|
||||
@@ -35,3 +38,90 @@ func TestMaximumHealthIncludesOwnedEquipmentPotentialAndAwakening(t *testing.T)
|
||||
t.Fatal("ignored equipment stat failure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPotentialConnectionSwitchRecomputesMaximumWithoutHealingOrChangingCostume(t *testing.T) {
|
||||
store := stateio.NewMemory()
|
||||
items, err := player.OpenInventory(store, &player.Starter{Version: "2.35.10"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
characters, err := player.OpenCharacterStore(store, []player.Character{{InvenIndex: 77, ID: 350, HP: 80, Level: 1, UseCostume: 1, CostumeID: 100}}, items, "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
collection, err := player.OpenCollectionStore(store, []player.Costume{{InvenIndex: 1, ID: 100, UseChar: 77}, {InvenIndex: 2, ID: 200, UseChar: 77}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, index := range []uint64{1, 2} {
|
||||
if err = collection.ActivateCostumePotential(index, []uint64{1, 2}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
wallet, err := player.OpenWallet(store, player.Currency{Gold: 100})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d := &gamedata.CostumePotentialDesign{CharacterTypes: map[uint64]uint64{350: 0}, CostumeActive: map[uint64]bool{100: true, 200: true}, CharacterUnique: map[uint64]uint64{350: 9}, CostumeUnique: map[uint64]uint64{100: 9, 200: 9}, Nodes: map[uint64]map[uint64]gamedata.CostumePotentialNode{
|
||||
100: {1: {ID: 1, NodeType: 2, StatType: 1, StatValue: 10}, 2: {ID: 2, NodeType: 1, StatType: 1, StatValue: 100}},
|
||||
200: {1: {ID: 1, NodeType: 2, StatType: 1, StatValue: 5}, 2: {ID: 2, NodeType: 1, StatType: 1, StatValue: 20}},
|
||||
}}
|
||||
potential, err := player.NewCostumePotentialService(d, collection, characters, items, wallet)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = potential.AttachConnectStore(store); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
potential.BeginSession("health")
|
||||
stats := &Service{Design: &gamedata.PictorialDesign{}, Owned: &ownedState{}, PotentialContributions: potential.Contributions}
|
||||
stats.baseHealth.Store([2]uint64{350, 1}, float64(100))
|
||||
if err = characters.AttachMaxHealth(stats.MaxHealth); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request := func(seq, costume uint64) []byte {
|
||||
row := wire.AppendVarint(wire.AppendVarint(nil, 1, 77), 2, costume)
|
||||
return wire.AppendBytes(wire.AppendVarint(nil, 1, seq), 2, row)
|
||||
}
|
||||
first := request(1, 100)
|
||||
if _, _, _, err = potential.Handle("/CostumePotentialConnect", first); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if maximum, err := characters.MaxHealth(77); err != nil || maximum != 215 {
|
||||
t.Fatalf("first maximum=%d err=%v", maximum, err)
|
||||
}
|
||||
if hp, err := characters.CurrentHealth(77); err != nil || hp != 80 {
|
||||
t.Fatal("connecting potential healed damage")
|
||||
}
|
||||
if err = characters.SetCurrentHealth(77, 200); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second := request(2, 200)
|
||||
code, out, handled, err := potential.Handle("/CostumePotentialConnect", second)
|
||||
if err != nil || code != 267 || !handled {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c, ok := characters.Find(77)
|
||||
if !ok || c.ConnectPotentialCostume != 200 || c.UseCostume != 1 || c.CostumeID != 100 || c.HP != 135 {
|
||||
t.Fatalf("connection changed worn costume or failed HP clamp: %+v", c)
|
||||
}
|
||||
if maximum, err := characters.MaxHealth(77); err != nil || maximum != 135 {
|
||||
t.Fatalf("switched maximum=%d err=%v", maximum, err)
|
||||
}
|
||||
_, again, _, err := potential.Handle("/CostumePotentialConnect", second)
|
||||
if err != nil || !bytes.Equal(out, again) {
|
||||
t.Fatal("connection replay changed response")
|
||||
}
|
||||
if err = characters.SetCurrentHealth(77, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, _, err = potential.Handle("/CostumePotentialConnect", request(3, 100)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if hp, err := characters.CurrentHealth(77); err != nil || hp != 0 {
|
||||
t.Fatal("potential connection revived dead character")
|
||||
}
|
||||
if wallet.Snapshot().Gold != 100 || len(items.All()) != 0 {
|
||||
t.Fatal("free connection charged materials or currency")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,17 +8,22 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"bd2server/internal/server/gamedata"
|
||||
"bd2server/internal/server/stateio"
|
||||
"bd2server/internal/server/wire"
|
||||
)
|
||||
|
||||
type CostumePotentialService struct {
|
||||
design *gamedata.CostumePotentialDesign
|
||||
collection *CollectionStore
|
||||
characters *CharacterStore
|
||||
inventory *Inventory
|
||||
wallet *Wallet
|
||||
design *gamedata.CostumePotentialDesign
|
||||
collection *CollectionStore
|
||||
characters *CharacterStore
|
||||
inventory *Inventory
|
||||
wallet *Wallet
|
||||
connectStore stateio.Store
|
||||
connectSession string
|
||||
connectMu sync.Mutex
|
||||
}
|
||||
|
||||
func NewCostumePotentialService(design *gamedata.CostumePotentialDesign, collection *CollectionStore, characters *CharacterStore, inventory *Inventory, wallet *Wallet) (*CostumePotentialService, error) {
|
||||
@@ -104,6 +109,9 @@ func (s *CostumePotentialService) Contributions(character Character) ([]gamedata
|
||||
}
|
||||
|
||||
func (s *CostumePotentialService) Handle(path string, request []byte) (int, []byte, bool, error) {
|
||||
if path == "/CostumePotentialConnect" {
|
||||
return s.connect(request)
|
||||
}
|
||||
if path != "/CostumeNodeActivation" {
|
||||
return 0, nil, false, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"bd2server/internal/server/stateio"
|
||||
"bd2server/internal/server/wire"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
type potentialConnectReceipt struct {
|
||||
Digest string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
func (s *CostumePotentialService) AttachConnectStore(store stateio.Store) error {
|
||||
if store == nil {
|
||||
return fmt.Errorf("player: missing potential connect store")
|
||||
}
|
||||
s.connectStore = store
|
||||
return nil
|
||||
}
|
||||
func (s *CostumePotentialService) BeginSession(id string) {
|
||||
s.connectMu.Lock()
|
||||
defer s.connectMu.Unlock()
|
||||
s.connectSession = id
|
||||
}
|
||||
func (s *CostumePotentialService) connect(request []byte) (int, []byte, bool, error) {
|
||||
s.connectMu.Lock()
|
||||
defer s.connectMu.Unlock()
|
||||
fail := func(e error) (int, []byte, bool, error) { return 267, nil, true, e }
|
||||
seq, ok, err := wire.Varint(request, 1)
|
||||
if err != nil || !ok || seq == 0 || seq > math.MaxInt32 || s.connectStore == nil || s.connectSession == "" {
|
||||
return fail(fmt.Errorf("player: invalid potential connection session/sequence"))
|
||||
}
|
||||
var receipts map[string]potentialConnectReceipt
|
||||
b, err := s.connectStore.Load("potentialconnect")
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if b != nil {
|
||||
if err = json.Unmarshal(b, &receipts); err != nil || receipts == nil {
|
||||
return fail(fmt.Errorf("player: invalid potential connection receipts"))
|
||||
}
|
||||
} else {
|
||||
receipts = map[string]potentialConnectReceipt{}
|
||||
}
|
||||
for _, r := range receipts {
|
||||
hash, e := hex.DecodeString(r.Digest)
|
||||
if e != nil || len(hash) != sha256.Size {
|
||||
return fail(fmt.Errorf("player: malformed potential connection receipt digest"))
|
||||
}
|
||||
if e = wire.Walk(r.Body, func(wire.Field) error { return nil }); e != nil {
|
||||
return fail(e)
|
||||
}
|
||||
}
|
||||
if err = wire.Walk(request, func(f wire.Field) error {
|
||||
if f.Number == 1 {
|
||||
if f.Type != 0 {
|
||||
return fmt.Errorf("player: invalid potential sequence wire")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
seqFields := 0
|
||||
wire.Walk(request, func(f wire.Field) error {
|
||||
if f.Number == 1 {
|
||||
seqFields++
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if seqFields != 1 {
|
||||
return fail(fmt.Errorf("player: duplicate potential sequence"))
|
||||
}
|
||||
key := fmt.Sprintf("%s:%d", s.connectSession, seq)
|
||||
digest := fmt.Sprintf("%x", sha256.Sum256(request))
|
||||
if prior, ok := receipts[key]; ok {
|
||||
if prior.Digest != digest {
|
||||
return fail(fmt.Errorf("player: changed potential connection replay"))
|
||||
}
|
||||
return 267, prior.Body, true, nil
|
||||
}
|
||||
var characters []Character
|
||||
seen := map[uint64]bool{}
|
||||
oldHP := map[uint64]uint64{}
|
||||
err = wire.Walk(request, func(f wire.Field) error {
|
||||
if f.Number != 2 {
|
||||
return nil
|
||||
}
|
||||
if f.Type != 2 || len(characters) >= 4096 {
|
||||
return fmt.Errorf("player: invalid potential connection list")
|
||||
}
|
||||
fields := map[int]bool{}
|
||||
if e := wire.Walk(f.Value, func(field wire.Field) error {
|
||||
if field.Number == 1 || field.Number == 2 {
|
||||
if field.Type != 0 || fields[field.Number] {
|
||||
return fmt.Errorf("player: duplicate or invalid connection scalar")
|
||||
}
|
||||
fields[field.Number] = true
|
||||
}
|
||||
return nil
|
||||
}); e != nil {
|
||||
return e
|
||||
}
|
||||
index, ok, e := wire.Varint(f.Value, 1)
|
||||
if e != nil || !ok || index == 0 || index > math.MaxInt64 || seen[index] {
|
||||
return fmt.Errorf("player: duplicate or invalid potential character")
|
||||
}
|
||||
costume, _, e := wire.Varint(f.Value, 2)
|
||||
if e != nil || costume == 0 || costume > math.MaxInt32 {
|
||||
return fmt.Errorf("player: potential connection requires a costume design ID")
|
||||
}
|
||||
c, owned := s.characters.Find(index)
|
||||
if !owned || IsStoryCharacter(c) || IsCharmCharacter(c) {
|
||||
return fmt.Errorf("player: potential character is not permanent owned")
|
||||
}
|
||||
unique := s.design.CharacterUnique[c.ID]
|
||||
characterType, knownType := s.design.CharacterTypes[c.ID]
|
||||
if unique == 0 || !knownType || characterType != 0 || !s.design.CostumeActive[costume] || s.design.CostumeUnique[costume] != unique || len(s.design.Nodes[costume]) == 0 {
|
||||
return fmt.Errorf("player: incompatible or unavailable potential costume")
|
||||
}
|
||||
ownedCostume := false
|
||||
for _, v := range s.collection.Costumes() {
|
||||
if v.ID == costume && v.UseChar == index {
|
||||
ownedCostume = true
|
||||
for _, id := range v.PotentialIDs {
|
||||
if _, ok := s.design.Nodes[costume][id]; !ok {
|
||||
return fmt.Errorf("player: invalid saved potential node")
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ownedCostume {
|
||||
return fmt.Errorf("player: potential costume not owned by requested character")
|
||||
}
|
||||
hp, e := s.characters.CurrentHealth(index)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
oldHP[index] = hp
|
||||
c.ConnectPotentialCostume = costume
|
||||
characters = append(characters, c)
|
||||
seen[index] = true
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if len(characters) == 0 {
|
||||
return fail(fmt.Errorf("player: empty potential connection list"))
|
||||
}
|
||||
// All links are validated before writes. The parent account transaction
|
||||
// includes both character ownership domains, HP and the response receipt.
|
||||
for _, c := range characters {
|
||||
if err = s.characters.setPotentialConnection(c.InvenIndex, c.ConnectPotentialCostume); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
}
|
||||
var out []byte
|
||||
for _, c := range characters {
|
||||
maximum, e := s.characters.MaxHealth(c.InvenIndex)
|
||||
if e != nil {
|
||||
return fail(e)
|
||||
}
|
||||
hp := oldHP[c.InvenIndex]
|
||||
if hp > maximum {
|
||||
hp = maximum
|
||||
}
|
||||
if e = s.characters.SetCurrentHealth(c.InvenIndex, hp); e != nil {
|
||||
return fail(e)
|
||||
}
|
||||
c.HP = hp
|
||||
out = wire.AppendBytes(out, 1, CharacterWire(c))
|
||||
}
|
||||
receipts[key] = potentialConnectReceipt{Digest: digest, Body: out}
|
||||
b, err = json.Marshal(receipts)
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if err = s.connectStore.Save("potentialconnect", b); err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
return 267, out, true, nil
|
||||
}
|
||||
func (s *CharacterStore) setPotentialConnection(index, costume uint64) error {
|
||||
s.mu.Lock()
|
||||
for i, c := range s.characters {
|
||||
if c.InvenIndex == index {
|
||||
next := append([]Character(nil), s.characters...)
|
||||
next[i].ConnectPotentialCostume = costume
|
||||
if err := s.persist(next); err != nil {
|
||||
s.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
s.characters = next
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
collection := s.collection
|
||||
s.mu.Unlock()
|
||||
if collection != nil {
|
||||
if c, ok := collection.FindCharacter(index); ok {
|
||||
c.ConnectPotentialCostume = costume
|
||||
return collection.UpdateCharacter(c.ID, c)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("player: unknown potential connection character")
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"bd2server/internal/server/accountstate"
|
||||
"bd2server/internal/server/gamedata"
|
||||
"bd2server/internal/server/stateio"
|
||||
"bd2server/internal/server/wire"
|
||||
"bytes"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func connectTestService(t *testing.T, store stateio.Store) (*CostumePotentialService, *CharacterStore) {
|
||||
t.Helper()
|
||||
inv, e := OpenInventory(store, &Starter{Version: "2.35.10"})
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
chars, e := OpenCharacterStore(store, []Character{{InvenIndex: 77, ID: 91, Level: 1, HP: 80}}, inv, "", "")
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
if e = chars.EnsurePersisted(); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
collection, e := OpenCollectionStore(store, nil)
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
if len(collection.Costumes()) == 0 {
|
||||
v := cloneCollection(collection.data)
|
||||
v.Costumes = []Costume{{InvenIndex: 88, ID: 100, UseChar: 77}, {InvenIndex: 89, ID: 200, UseChar: 77}, {InvenIndex: 90, ID: 300, UseChar: 77}}
|
||||
if e = collection.commit(v); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
}
|
||||
wallet, e := OpenWallet(store, Currency{Gold: 100})
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
d := &gamedata.CostumePotentialDesign{CharacterUnique: map[uint64]uint64{91: 9}, CostumeUnique: map[uint64]uint64{100: 9, 200: 9, 300: 10}, Nodes: map[uint64]map[uint64]gamedata.CostumePotentialNode{100: {1: {ID: 1}}, 200: {1: {ID: 1}}, 300: {1: {ID: 1}}}}
|
||||
s, e := NewCostumePotentialService(d, collection, chars, inv, wallet)
|
||||
d.CharacterTypes = map[uint64]uint64{91: 0}
|
||||
d.CostumeActive = map[uint64]bool{100: true, 200: true, 300: true}
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
s.AttachConnectStore(store)
|
||||
s.BeginSession("test")
|
||||
chars.AttachMaxHealth(func(c Character) (uint64, error) {
|
||||
if c.ConnectPotentialCostume == 200 {
|
||||
return 50, nil
|
||||
}
|
||||
return 100, nil
|
||||
})
|
||||
return s, chars
|
||||
}
|
||||
func connectRequest(seq uint64, rows ...[2]uint64) []byte {
|
||||
b := wire.AppendVarint(nil, 1, seq)
|
||||
for _, r := range rows {
|
||||
v := wire.AppendVarint(wire.AppendVarint(nil, 1, r[0]), 2, r[1])
|
||||
b = wire.AppendBytes(b, 2, v)
|
||||
}
|
||||
return b
|
||||
}
|
||||
func TestPotentialConnectFreeZeroNodesSwitchClampsAndReplaysAcrossRestart(t *testing.T) {
|
||||
store := stateio.NewMemory()
|
||||
s, chars := connectTestService(t, store)
|
||||
if _, _, _, e := s.Handle("/CostumePotentialConnect", connectRequest(1, [2]uint64{77, 100})); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
c, _ := chars.Find(77)
|
||||
if c.ConnectPotentialCostume != 100 || c.HP != 80 || s.wallet.Snapshot().Gold != 100 {
|
||||
t.Fatal("auto connection charged or healed")
|
||||
}
|
||||
b := connectRequest(2, [2]uint64{77, 200})
|
||||
_, reply, _, e := s.Handle("/CostumePotentialConnect", b)
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
c, _ = chars.Find(77)
|
||||
if c.HP != 50 {
|
||||
t.Fatal("maxHP decrease not clamped")
|
||||
}
|
||||
next, _ := connectTestService(t, store)
|
||||
_, again, _, e := next.Handle("/CostumePotentialConnect", b)
|
||||
if e != nil || !bytes.Equal(reply, again) {
|
||||
t.Fatal("restart lost exact connection replay")
|
||||
}
|
||||
if _, _, _, e = next.Handle("/CostumePotentialConnect", connectRequest(3, [2]uint64{77, 100})); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
c, _ = next.characters.Find(77)
|
||||
if c.HP != 50 {
|
||||
t.Fatal("maxHP increase healed player")
|
||||
}
|
||||
if e = next.characters.SetCurrentHealth(77, 0); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
if _, _, _, e = next.Handle("/CostumePotentialConnect", connectRequest(4, [2]uint64{77, 200})); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
c, _ = next.characters.Find(77)
|
||||
if c.HP != 0 {
|
||||
t.Fatal("connection revived dead character")
|
||||
}
|
||||
}
|
||||
func TestPotentialConnectRejectsUnownedOtherCharacterAndDuplicateBatch(t *testing.T) {
|
||||
s, chars := connectTestService(t, stateio.NewMemory())
|
||||
for _, b := range [][]byte{connectRequest(1, [2]uint64{77, 300}), connectRequest(2, [2]uint64{77, 88}), connectRequest(3, [2]uint64{77, 0}), connectRequest(4, [2]uint64{77, 100}, [2]uint64{77, 200}), connectRequest(5, [2]uint64{77, 100}, [2]uint64{999, 200})} {
|
||||
if _, _, _, e := s.Handle("/CostumePotentialConnect", b); e == nil {
|
||||
t.Fatal("invalid potential connection accepted")
|
||||
}
|
||||
}
|
||||
c, _ := chars.Find(77)
|
||||
if c.ConnectPotentialCostume != 0 {
|
||||
t.Fatal("invalid batch partially updated character")
|
||||
}
|
||||
s.design.CostumeActive[100] = false
|
||||
if _, _, _, e := s.Handle("/CostumePotentialConnect", connectRequest(6, [2]uint64{77, 100})); e == nil {
|
||||
t.Fatal("inactive potential costume connected")
|
||||
}
|
||||
s.design.CostumeActive[100] = true
|
||||
s.design.CharacterTypes[91] = 1
|
||||
if _, _, _, e := s.Handle("/CostumePotentialConnect", connectRequest(7, [2]uint64{77, 100})); e == nil {
|
||||
t.Fatal("temporary design character connected")
|
||||
}
|
||||
s.design.CharacterTypes[91] = 0
|
||||
duplicate := wire.AppendVarint(connectRequest(8, [2]uint64{77, 100}), 1, 9)
|
||||
if _, _, _, e := s.Handle("/CostumePotentialConnect", duplicate); e == nil {
|
||||
t.Fatal("duplicate sequence accepted")
|
||||
}
|
||||
row := wire.AppendVarint(wire.AppendVarint(wire.AppendVarint(nil, 1, 77), 2, 100), 2, 200)
|
||||
duplicate = wire.AppendBytes(wire.AppendVarint(nil, 1, 10), 2, row)
|
||||
if _, _, _, e := s.Handle("/CostumePotentialConnect", duplicate); e == nil {
|
||||
t.Fatal("duplicate nested costume scalar accepted")
|
||||
}
|
||||
}
|
||||
|
||||
type connectFailStore struct{ stateio.Store }
|
||||
|
||||
func (s connectFailStore) Save(name string, b []byte) error {
|
||||
if name == "potentialconnect" {
|
||||
return errors.New("receipt failed")
|
||||
}
|
||||
return s.Store.Save(name, b)
|
||||
}
|
||||
func TestPotentialConnectSQLiteFailureRollsBackLinkAndHealth(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "state.db")
|
||||
repo, e := accountstate.Open(path)
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
s, _ := connectTestService(t, repo)
|
||||
s.AttachConnectStore(connectFailStore{repo})
|
||||
op, e := repo.BeginOperation()
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
b := connectRequest(1, [2]uint64{77, 200})
|
||||
if _, _, _, e = s.Handle("/CostumePotentialConnect", b); e == nil {
|
||||
t.Fatal("receipt failure ignored")
|
||||
}
|
||||
if e = op.Rollback(); e != nil && !errors.Is(e, stateio.ErrStateRecoveryRequired) {
|
||||
t.Fatal(e)
|
||||
}
|
||||
repo.Close()
|
||||
repo, e = accountstate.Open(path)
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
defer repo.Close()
|
||||
next, chars := connectTestService(t, repo)
|
||||
c, _ := chars.Find(77)
|
||||
if c.ConnectPotentialCostume != 0 || c.HP != 80 {
|
||||
t.Fatal("failed connection persisted link/HP")
|
||||
}
|
||||
op, e = repo.BeginOperation()
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
if _, _, _, e = next.Handle("/CostumePotentialConnect", b); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
if e = op.Commit(); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPotentialConnectionCollectionCharacterFallbackPersistsAcrossRestart(t *testing.T) {
|
||||
store := stateio.NewMemory()
|
||||
s, chars := connectTestService(t, store)
|
||||
v := cloneCollection(s.collection.data)
|
||||
v.Characters = []Character{{InvenIndex: 78, ID: 92, Level: 1, HP: 40, CostumeID: 201, UseCostume: 91}}
|
||||
v.Costumes = append(v.Costumes, Costume{InvenIndex: 91, ID: 201, UseChar: 78})
|
||||
if e := s.collection.commit(v); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
if e := chars.AttachCollection(s.collection); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
s.design.CharacterUnique[92] = 9
|
||||
s.design.CharacterTypes[92] = 0
|
||||
s.design.CostumeUnique[201] = 9
|
||||
s.design.CostumeActive[201] = true
|
||||
s.design.Nodes[201] = map[uint64]gamedata.CostumePotentialNode{1: {ID: 1}}
|
||||
b := connectRequest(1, [2]uint64{78, 201})
|
||||
_, response, _, e := s.Handle("/CostumePotentialConnect", b)
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
c, ok := chars.Find(78)
|
||||
if !ok || c.ConnectPotentialCostume != 201 || c.UseCostume != 91 || c.CostumeID != 201 || c.HP != 40 {
|
||||
t.Fatal("collection connection changed appearance or HP")
|
||||
}
|
||||
collection, e := OpenCollectionStore(store, nil)
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
c, ok = collection.FindCharacter(78)
|
||||
if !ok || c.ConnectPotentialCostume != 201 {
|
||||
t.Fatal("collection connection lost after restart")
|
||||
}
|
||||
s.collection = collection
|
||||
chars.AttachCollection(collection)
|
||||
s.AttachConnectStore(store)
|
||||
s.BeginSession("test")
|
||||
_, again, _, e := s.Handle("/CostumePotentialConnect", b)
|
||||
if e != nil || !bytes.Equal(response, again) {
|
||||
t.Fatal("collection reconnect replay lost")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user