264 lines
7.9 KiB
Go
264 lines
7.9 KiB
Go
package player
|
|
|
|
import (
|
|
"path/filepath"
|
|
"reflect"
|
|
"sort"
|
|
"testing"
|
|
|
|
"bd2server/internal/server/wire"
|
|
)
|
|
|
|
func TestEquipmentBatchUseOneClickClearPreservesUnfilteredSlots(t *testing.T) {
|
|
fixture := newEquipmentBatchUseFixture(t)
|
|
weapon := fixture.grant(t, "clear-weapon", 10, 100)
|
|
armor := fixture.grant(t, "clear-armor", 11, 100)
|
|
|
|
code, response, handled, err := fixture.equipment.Handle("/EquipBatchUse", equipmentBatchUseRequest(
|
|
equipmentBatchUseEntry(100, 0, armor.InvenIndex, 0, 0, 0),
|
|
))
|
|
if err != nil || !handled || code != 276 {
|
|
t.Fatalf("batch clear code=%d handled=%v err=%v", code, handled, err)
|
|
}
|
|
if got := responseCharacterIndices(t, response); !reflect.DeepEqual(got, []uint64{100}) {
|
|
t.Fatalf("response characters=%v", got)
|
|
}
|
|
assertEquipmentOwners(t, fixture.equipment, map[uint64]uint64{
|
|
weapon.InvenIndex: 0,
|
|
armor.InvenIndex: 100,
|
|
})
|
|
}
|
|
|
|
func TestEquipmentBatchUseAutoMountReplacesOnlyRequestedSlot(t *testing.T) {
|
|
fixture := newEquipmentBatchUseFixture(t)
|
|
oldWeapon := fixture.grant(t, "auto-old-weapon", 10, 100)
|
|
armor := fixture.grant(t, "auto-armor", 11, 100)
|
|
newWeapon := fixture.grant(t, "auto-new-weapon", 12, 0)
|
|
|
|
code, response, handled, err := fixture.equipment.Handle("/EquipBatchUse", equipmentBatchUseRequest(
|
|
equipmentBatchUseEntry(100, newWeapon.InvenIndex, armor.InvenIndex, 0, 0, 0),
|
|
))
|
|
if err != nil || !handled || code != 276 {
|
|
t.Fatalf("auto mount code=%d handled=%v err=%v", code, handled, err)
|
|
}
|
|
if got := responseCharacterIndices(t, response); !reflect.DeepEqual(got, []uint64{100}) {
|
|
t.Fatalf("response characters=%v", got)
|
|
}
|
|
assertEquipmentOwners(t, fixture.equipment, map[uint64]uint64{
|
|
oldWeapon.InvenIndex: 0,
|
|
armor.InvenIndex: 100,
|
|
newWeapon.InvenIndex: 100,
|
|
})
|
|
}
|
|
|
|
func TestEquipmentBatchUseTransfersAcrossCharactersReturnsBothAndPersists(t *testing.T) {
|
|
fixture := newEquipmentBatchUseFixture(t)
|
|
transferred := fixture.grant(t, "cross-character", 10, 200)
|
|
|
|
code, response, handled, err := fixture.equipment.Handle("/EquipBatchUse", equipmentBatchUseRequest(
|
|
equipmentBatchUseEntry(200, 0, 0, 0, 0, 0),
|
|
equipmentBatchUseEntry(100, transferred.InvenIndex, 0, 0, 0, 0),
|
|
))
|
|
if err != nil || !handled || code != 276 {
|
|
t.Fatalf("cross-character batch code=%d handled=%v err=%v", code, handled, err)
|
|
}
|
|
// The target changes and the previous owner loses equipment. Both
|
|
// characters must be returned so the client can refresh their derived HP.
|
|
if got := responseCharacterIndices(t, response); !reflect.DeepEqual(got, []uint64{100, 200}) {
|
|
t.Fatalf("response characters=%v", got)
|
|
}
|
|
assertEquipmentOwners(t, fixture.equipment, map[uint64]uint64{transferred.InvenIndex: 100})
|
|
|
|
restarted, err := OpenEquipmentInventory(testStore(fixture.equipmentPath))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := restarted.AttachSlots(fixture.slots); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := restarted.AttachCharacters(fixture.characters); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertEquipmentOwners(t, restarted, map[uint64]uint64{transferred.InvenIndex: 100})
|
|
}
|
|
|
|
func TestEquipmentBatchUseRejectsInvalidSnapshotsAtomically(t *testing.T) {
|
|
fixture := newEquipmentBatchUseFixture(t)
|
|
weapon := fixture.grant(t, "invalid-weapon", 10, 100)
|
|
armor := fixture.grant(t, "invalid-armor", 11, 100)
|
|
want := fixture.equipment.All()
|
|
|
|
tests := []struct {
|
|
name string
|
|
request []byte
|
|
}{
|
|
{
|
|
name: "duplicate equipment",
|
|
request: equipmentBatchUseRequest(
|
|
equipmentBatchUseEntry(100, weapon.InvenIndex, weapon.InvenIndex, 0, 0, 0),
|
|
),
|
|
},
|
|
{
|
|
name: "equipment in wrong slot",
|
|
request: equipmentBatchUseRequest(
|
|
equipmentBatchUseEntry(100, 0, weapon.InvenIndex, 0, 0, 0),
|
|
),
|
|
},
|
|
{
|
|
name: "unknown equipment",
|
|
request: equipmentBatchUseRequest(
|
|
equipmentBatchUseEntry(100, 999999999, armor.InvenIndex, 0, 0, 0),
|
|
),
|
|
},
|
|
{
|
|
name: "unknown character",
|
|
request: equipmentBatchUseRequest(
|
|
equipmentBatchUseEntry(999, weapon.InvenIndex, armor.InvenIndex, 0, 0, 0),
|
|
),
|
|
},
|
|
{
|
|
name: "transfer without previous owner snapshot",
|
|
request: equipmentBatchUseRequest(
|
|
equipmentBatchUseEntry(200, weapon.InvenIndex, 0, 0, 0, 0),
|
|
),
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if _, _, handled, err := fixture.equipment.Handle("/EquipBatchUse", test.request); err == nil || !handled {
|
|
t.Fatalf("invalid batch handled=%v err=%v", handled, err)
|
|
}
|
|
if got := fixture.equipment.All(); !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("invalid batch mutated state:\n got=%+v\nwant=%+v", got, want)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Reopening the store proves rejected requests also left no partial disk
|
|
// mutation behind.
|
|
restarted, err := OpenEquipmentInventory(testStore(fixture.equipmentPath))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := restarted.All(); !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("invalid batches mutated persisted state:\n got=%+v\nwant=%+v", got, want)
|
|
}
|
|
}
|
|
|
|
type equipmentBatchUseFixture struct {
|
|
equipmentPath string
|
|
equipment *EquipmentInventory
|
|
characters *CharacterStore
|
|
slots map[uint64]uint64
|
|
}
|
|
|
|
func newEquipmentBatchUseFixture(t *testing.T) *equipmentBatchUseFixture {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
inventory, err := OpenInventory(testStore(filepath.Join(dir, "items.json")), &Starter{Version: "2.35.10"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
characters, err := OpenCharacterStore(testStore(filepath.Join(dir, "characters.json")), []Character{
|
|
{InvenIndex: 100, ID: 350, Level: 20},
|
|
{InvenIndex: 200, ID: 351, Level: 20},
|
|
}, inventory, "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path := filepath.Join(dir, "equipment.json")
|
|
equipment, err := OpenEquipmentInventory(testStore(path))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
slots := map[uint64]uint64{10: 0, 11: 1, 12: 0, 13: 2, 14: 3, 15: 4}
|
|
if err := equipment.AttachSlots(slots); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := equipment.AttachCharacters(characters); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return &equipmentBatchUseFixture{
|
|
equipmentPath: path,
|
|
equipment: equipment,
|
|
characters: characters,
|
|
slots: slots,
|
|
}
|
|
}
|
|
|
|
func (f *equipmentBatchUseFixture) grant(t *testing.T, identity string, designID, character uint64) Equipment {
|
|
t.Helper()
|
|
entry, err := f.equipment.GrantOnce(identity, designID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if character == 0 {
|
|
return entry
|
|
}
|
|
f.equipment.mu.Lock()
|
|
next := cloneEquipmentSnapshot(f.equipment.owned)
|
|
for i := range next.Equipment {
|
|
if next.Equipment[i].InvenIndex == entry.InvenIndex {
|
|
next.Equipment[i].UseChar = character
|
|
entry = next.Equipment[i]
|
|
break
|
|
}
|
|
}
|
|
if err := f.equipment.commitLocked(next, "batch-use test setup"); err != nil {
|
|
f.equipment.mu.Unlock()
|
|
t.Fatal(err)
|
|
}
|
|
f.equipment.mu.Unlock()
|
|
return entry
|
|
}
|
|
|
|
func equipmentBatchUseRequest(entries ...[]byte) []byte {
|
|
request := wire.AppendVarint(nil, 1, 1)
|
|
for _, entry := range entries {
|
|
request = wire.AppendBytes(request, 2, entry)
|
|
}
|
|
return request
|
|
}
|
|
|
|
func equipmentBatchUseEntry(character uint64, equipment ...uint64) []byte {
|
|
entry := wire.AppendVarint(nil, 1, character)
|
|
for _, index := range equipment {
|
|
entry = wire.AppendVarint(entry, 2, index)
|
|
}
|
|
return entry
|
|
}
|
|
|
|
func responseCharacterIndices(t *testing.T, response []byte) []uint64 {
|
|
t.Helper()
|
|
var result []uint64
|
|
if err := wire.Walk(response, func(field wire.Field) error {
|
|
if field.Number != 1 {
|
|
return nil
|
|
}
|
|
index, found, err := wire.Varint(field.Value, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !found || index == 0 {
|
|
t.Fatalf("response contains invalid CharDBInfo")
|
|
}
|
|
result = append(result, index)
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sort.Slice(result, func(i, j int) bool { return result[i] < result[j] })
|
|
return result
|
|
}
|
|
|
|
func assertEquipmentOwners(t *testing.T, inventory *EquipmentInventory, want map[uint64]uint64) {
|
|
t.Helper()
|
|
got := make(map[uint64]uint64)
|
|
for _, equipment := range inventory.All() {
|
|
got[equipment.InvenIndex] = equipment.UseChar
|
|
}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("equipment owners=%v want=%v", got, want)
|
|
}
|
|
}
|