Files
bd2/go/internal/server/player/equipment_option_reroll_test.go
T

625 lines
26 KiB
Go

package player
import (
"fmt"
"path/filepath"
"reflect"
"testing"
"time"
"bd2server/internal/server/gamedata"
"bd2server/internal/server/wire"
)
func TestEquipmentOptionRerollLocksChargesReplaysAndRecoversPending(t *testing.T) {
fixture := newEquipmentOptionRerollFixture(t)
fixture.equipment.BeginSession("option-reroll-session")
request := fixture.rerollRequest(41, []bool{false, true}, []bool{false, true, false})
if rerollType, found, err := wire.Varint(request, 6); err != nil || !found || rerollType != 1 {
t.Fatalf("malformed test reroll type=%d found=%v err=%v request=%x", rerollType, found, err, request)
}
code, response, handled, err := fixture.equipment.Handle("/EquipOptionReRoll", request)
if err != nil || !handled || code != 192 {
t.Fatalf("reroll code=%d handled=%v err=%v", code, handled, err)
}
wantMain := []EquipmentOption{{GroupID: 100, ID: 1}, {GroupID: 101, ID: 2}}
wantSub := []EquipmentOption{{GroupID: 200, ID: 11}, {GroupID: 201, ID: 20}, {GroupID: 202, ID: 31}}
main, sub := optionRerollResponseOptions(t, response)
if !reflect.DeepEqual(main, wantMain) || !reflect.DeepEqual(sub, wantSub) {
t.Fatalf("candidate main=%+v sub=%+v", main, sub)
}
assertOptionRerollBalances(t, fixture, 800, 13)
// The candidate is not official equipment until the second-phase confirm.
current := fixture.equipment.All()[0]
if !reflect.DeepEqual(current.MainOption, fixture.original.MainOption) || !reflect.DeepEqual(current.SubOption, fixture.original.SubOption) {
t.Fatalf("reroll changed official equipment before confirm: %+v", current)
}
// Same session and sequence must replay exactly, without charging or rolling again.
replayCode, replay, replayHandled, replayErr := fixture.equipment.Handle("/EquipOptionReRoll", request)
if replayErr != nil || !replayHandled || replayCode != code || string(replay) != string(response) {
t.Fatalf("reroll replay code=%d handled=%v same=%v err=%v", replayCode, replayHandled, string(replay) == string(response), replayErr)
}
assertOptionRerollBalances(t, fixture, 800, 13)
_, info, infoHandled, infoErr := fixture.equipment.Handle("/EquipInfo", wire.AppendVarint(nil, 1, 42))
if infoErr != nil || !infoHandled {
t.Fatalf("EquipInfo handled=%v err=%v", infoHandled, infoErr)
}
pending, found, err := wire.Bytes(info, 2)
if err != nil || !found {
t.Fatalf("EquipInfo missing pending option reroll: found=%v err=%v", found, err)
}
index, pendingMain, pendingSub := equipmentWireOptions(t, pending)
if index != fixture.original.InvenIndex || !reflect.DeepEqual(pendingMain, wantMain) || !reflect.DeepEqual(pendingSub, wantSub) {
t.Fatalf("EquipInfo pending index=%d main=%+v sub=%+v", index, pendingMain, pendingSub)
}
// Pending candidates are server state, not a transient UI cache. Reopening the
// equipment domain must preserve field 2 until the player resolves it.
restarted, err := OpenEquipmentInventory(testStore(fixture.equipmentPath))
if err != nil {
t.Fatal(err)
}
if err := restarted.AttachOptionReroll(fixture.design, fixture.wallet, fixture.inventory); err != nil {
t.Fatal(err)
}
restarted.BeginSession("option-reroll-after-restart")
_, restartedInfo, _, err := restarted.Handle("/EquipInfo", wire.AppendVarint(nil, 1, 43))
if err != nil {
t.Fatal(err)
}
restoredPending, found, err := wire.Bytes(restartedInfo, 2)
if err != nil || !found || string(restoredPending) != string(pending) {
t.Fatalf("pending did not survive restart: found=%v same=%v err=%v", found, string(restoredPending) == string(pending), err)
}
confirm := optionRerollConfirmRequest(44, fixture.original.InvenIndex, true)
confirmCode, confirmResponse, confirmHandled, confirmErr := restarted.Handle("/EquipOptionReRollConfirm", confirm)
if confirmErr != nil || !confirmHandled || confirmCode != 193 {
t.Fatalf("confirm code=%d handled=%v err=%v", confirmCode, confirmHandled, confirmErr)
}
confirmedWire, found, err := wire.Bytes(confirmResponse, 1)
if err != nil || !found {
t.Fatalf("confirm response missing equipment: found=%v err=%v", found, err)
}
_, confirmedMain, confirmedSub := equipmentWireOptions(t, confirmedWire)
if !reflect.DeepEqual(confirmedMain, wantMain) || !reflect.DeepEqual(confirmedSub, wantSub) {
t.Fatalf("confirmed main=%+v sub=%+v", confirmedMain, confirmedSub)
}
confirmed := restarted.All()[0]
if !reflect.DeepEqual(confirmed.MainOption, wantMain) || !reflect.DeepEqual(confirmed.SubOption, wantSub) {
t.Fatalf("confirmed equipment=%+v", confirmed)
}
_, resolvedInfo, _, err := restarted.Handle("/EquipInfo", wire.AppendVarint(nil, 1, 45))
if err != nil {
t.Fatal(err)
}
if _, found, _ := wire.Bytes(resolvedInfo, 2); found {
t.Fatal("confirmed candidate remained in EquipInfo field 2")
}
}
func TestEquipmentOptionRerollKeepPreservesOfficialOptions(t *testing.T) {
fixture := newEquipmentOptionRerollFixture(t)
fixture.equipment.BeginSession("option-reroll-keep")
request := fixture.rerollRequest(51, []bool{false, true}, []bool{false, true, false})
if code, _, handled, err := fixture.equipment.Handle("/EquipOptionReRoll", request); err != nil || !handled || code != 192 {
t.Fatalf("reroll code=%d handled=%v err=%v", code, handled, err)
}
assertOptionRerollBalances(t, fixture, 800, 13)
keep := optionRerollConfirmRequest(52, fixture.original.InvenIndex, false)
code, response, handled, err := fixture.equipment.Handle("/EquipOptionReRollConfirm", keep)
if err != nil || !handled || code != 193 {
t.Fatalf("keep code=%d handled=%v err=%v", code, handled, err)
}
keptWire, found, err := wire.Bytes(response, 1)
if err != nil || !found {
t.Fatalf("keep response missing equipment: found=%v err=%v", found, err)
}
_, keptMain, keptSub := equipmentWireOptions(t, keptWire)
if !reflect.DeepEqual(keptMain, fixture.original.MainOption) || !reflect.DeepEqual(keptSub, fixture.original.SubOption) {
t.Fatalf("keep response main=%+v sub=%+v", keptMain, keptSub)
}
current := fixture.equipment.All()[0]
if !reflect.DeepEqual(current.MainOption, fixture.original.MainOption) || !reflect.DeepEqual(current.SubOption, fixture.original.SubOption) {
t.Fatalf("keep changed official equipment: %+v", current)
}
_, info, _, err := fixture.equipment.Handle("/EquipInfo", wire.AppendVarint(nil, 1, 53))
if err != nil {
t.Fatal(err)
}
if _, found, _ := wire.Bytes(info, 2); found {
t.Fatal("discarded candidate remained in EquipInfo field 2")
}
}
func TestEquipmentOptionRerollRetryLocksPreviousCandidate(t *testing.T) {
fixture := newEquipmentOptionRerollFixture(t)
fixture.equipment.BeginSession("option-reroll-retry")
first := fixture.rerollRequest(54, []bool{false, false}, []bool{false, false, false})
if code, _, handled, err := fixture.equipment.Handle("/EquipOptionReRoll", first); err != nil || !handled || code != 192 {
t.Fatalf("first reroll code=%d handled=%v err=%v", code, handled, err)
}
firstCandidate := fixture.equipment.pendingReroll.Equipment
if firstCandidate.MainOption[1].ID == fixture.original.MainOption[1].ID || firstCandidate.SubOption[1].ID == fixture.original.SubOption[1].ID {
t.Fatalf("fixture did not change lock targets: first=%+v original=%+v", firstCandidate, fixture.original)
}
// The result screen sends only masks on a retry. Locked values must come
// from the first candidate even though it has not been confirmed yet.
second := fixture.rerollRequest(55, []bool{false, true}, []bool{false, true, false})
if code, _, handled, err := fixture.equipment.Handle("/EquipOptionReRoll", second); err != nil || !handled || code != 192 {
t.Fatalf("second reroll code=%d handled=%v err=%v", code, handled, err)
}
secondCandidate := fixture.equipment.pendingReroll.Equipment
if secondCandidate.MainOption[1] != firstCandidate.MainOption[1] || secondCandidate.SubOption[1] != firstCandidate.SubOption[1] {
t.Fatalf("retry lost locked candidate values: first=%+v second=%+v", firstCandidate, secondCandidate)
}
if secondCandidate.MainOption[1] == fixture.original.MainOption[1] || secondCandidate.SubOption[1] == fixture.original.SubOption[1] {
t.Fatalf("retry restored official values instead of candidate: original=%+v second=%+v", fixture.original, secondCandidate)
}
}
func TestEquipmentMainOptionChangeUpdatesPendingCandidate(t *testing.T) {
fixture := newEquipmentOptionRerollFixture(t)
fixture.equipment.BeginSession("main-option-change")
first := fixture.rerollRequest(56, []bool{false, false}, []bool{false, false, false})
if code, _, handled, err := fixture.equipment.Handle("/EquipOptionReRoll", first); err != nil || !handled || code != 192 {
t.Fatalf("reroll code=%d handled=%v err=%v", code, handled, err)
}
if got := fixture.equipment.pendingReroll.Equipment.MainOption[0].ID; got != 1 {
t.Fatalf("pending first main option=%d want=1 before change", got)
}
change := wire.AppendVarint(nil, 1, 57)
change = wire.AppendVarint(change, 2, fixture.original.InvenIndex)
change = wire.AppendVarint(change, 3, 100)
change = wire.AppendVarint(change, 4, 9)
if code, response, handled, err := fixture.equipment.Handle("/EquipMainOptChange", change); err != nil || !handled || code != 537 || len(response) != 0 {
t.Fatalf("main option change code=%d response=%x handled=%v err=%v", code, response, handled, err)
}
if got := fixture.equipment.All()[0].MainOption[0]; got != (EquipmentOption{GroupID: 100, ID: 9}) {
t.Fatalf("official first main option=%+v", got)
}
if got := fixture.equipment.pendingReroll.Equipment.MainOption[0]; got != (EquipmentOption{GroupID: 100, ID: 9}) {
t.Fatalf("pending first main option=%+v", got)
}
restarted, err := OpenEquipmentInventory(testStore(fixture.equipmentPath))
if err != nil {
t.Fatal(err)
}
if err := restarted.AttachOptionReroll(fixture.design, fixture.wallet, fixture.inventory); err != nil {
t.Fatal(err)
}
if got := restarted.All()[0].MainOption[0]; got != (EquipmentOption{GroupID: 100, ID: 9}) {
t.Fatalf("restarted official first main option=%+v", got)
}
if restarted.pendingReroll == nil || restarted.pendingReroll.Equipment.MainOption[0] != (EquipmentOption{GroupID: 100, ID: 9}) {
t.Fatalf("restarted pending candidate=%+v", restarted.pendingReroll)
}
confirm := optionRerollConfirmRequest(58, fixture.original.InvenIndex, true)
if code, _, handled, err := restarted.Handle("/EquipOptionReRollConfirm", confirm); err != nil || !handled || code != 193 {
t.Fatalf("confirm code=%d handled=%v err=%v", code, handled, err)
}
if got := restarted.All()[0].MainOption[0]; got != (EquipmentOption{GroupID: 100, ID: 9}) {
t.Fatalf("confirm reverted changed first main option: %+v", got)
}
}
func TestEquipmentMainOptionChangeRejectsForgedChoice(t *testing.T) {
fixture := newEquipmentOptionRerollFixture(t)
before := fixture.equipment.All()[0]
request := wire.AppendVarint(nil, 1, 59)
request = wire.AppendVarint(request, 2, fixture.original.InvenIndex)
request = wire.AppendVarint(request, 3, 100)
request = wire.AppendVarint(request, 4, 999)
if code, _, handled, err := fixture.equipment.Handle("/EquipMainOptChange", request); err == nil || !handled || code != 0 {
t.Fatalf("forged main option code=%d handled=%v err=%v", code, handled, err)
}
if after := fixture.equipment.All()[0]; !reflect.DeepEqual(after, before) {
t.Fatalf("forged main option changed equipment: before=%+v after=%+v", before, after)
}
}
func TestEquipmentMainOptionChangeEquippedCharacterHealthReadsEquipment(t *testing.T) {
fixture := newEquipmentOptionRerollFixture(t)
const characterIndex = 920000001
characters := &CharacterStore{characters: []Character{{InvenIndex: characterIndex, ID: 50, Level: 1}}}
if err := characters.AttachMaxHealth(func(Character) (uint64, error) {
// The production max-health provider obtains pictorial equipment via All.
if got := fixture.equipment.All()[0].MainOption[0].ID; got != 9 {
return 0, fmt.Errorf("health provider observed stale main option %d", got)
}
return 123, nil
}); err != nil {
t.Fatal(err)
}
fixture.equipment.characters = characters
fixture.equipment.owned.Equipment[0].UseChar = characterIndex
request := wire.AppendVarint(nil, 1, 60)
request = wire.AppendVarint(request, 2, fixture.original.InvenIndex)
request = wire.AppendVarint(request, 3, 100)
request = wire.AppendVarint(request, 4, 9)
type result struct {
code int
body []byte
handled bool
err error
}
done := make(chan result, 1)
go func() {
code, body, handled, err := fixture.equipment.Handle("/EquipMainOptChange", request)
done <- result{code, body, handled, err}
}()
select {
case got := <-done:
if got.err != nil || !got.handled || got.code != 537 {
t.Fatalf("change result=%+v", got)
}
character, found, err := wire.Bytes(got.body, 1)
if err != nil || !found || len(character) == 0 {
t.Fatalf("equipped character response missing: found=%v err=%v", found, err)
}
hp, found, err := wire.Varint(character, 3)
if err != nil || found || hp != 0 {
t.Fatalf("updated health=%d found=%v err=%v", hp, found, err)
}
maximum, err := characters.MaxHealth(characterIndex)
if err != nil || maximum != 123 {
t.Fatalf("updated maximum=%d err=%v", maximum, err)
}
case <-time.After(2 * time.Second):
t.Fatal("equipped main option change deadlocked while resolving character health")
}
}
func TestEquipmentOptionRerollRejectsForgedMaterialsWithoutCharge(t *testing.T) {
fixture := newEquipmentOptionRerollFixture(t)
fixture.equipment.BeginSession("option-reroll-forged-material")
request := optionRerollRequest(61, fixture.original.InvenIndex,
[]bool{false, true}, []bool{false, true, false}, 1,
[]Item{{Type: 4, Count: 1}, {InvenIndex: fixture.material.InvenIndex, Type: 8, ID: 17, Count: 1}})
if code, _, handled, err := fixture.equipment.Handle("/EquipOptionReRoll", request); err == nil || !handled || code != 0 {
t.Fatalf("forged materials code=%d handled=%v err=%v", code, handled, err)
}
assertOptionRerollBalances(t, fixture, 1000, 20)
_, info, _, err := fixture.equipment.Handle("/EquipInfo", wire.AppendVarint(nil, 1, 62))
if err != nil {
t.Fatal(err)
}
if _, found, _ := wire.Bytes(info, 2); found {
t.Fatal("rejected forged request created a pending candidate")
}
}
func TestEquipmentOptionRerollRejectsInvalidLockMasksWithoutCharge(t *testing.T) {
tests := []struct {
name string
main []bool
sub []bool
}{
{name: "wrong array length", main: []bool{false}, sub: []bool{false, false, false}},
{name: "first main slot locked", main: []bool{true, false}, sub: []bool{false, false, false}},
{name: "all rerollable slots locked", main: []bool{false, true}, sub: []bool{true, true, true}},
}
for i, test := range tests {
t.Run(test.name, func(t *testing.T) {
fixture := newEquipmentOptionRerollFixture(t)
fixture.equipment.BeginSession("option-reroll-invalid-lock")
locked := countLockedOptions(test.main) + countLockedOptions(test.sub)
request := optionRerollRequest(uint64(70+i), fixture.original.InvenIndex, test.main, test.sub, 1,
fixture.consumeItems(uint64(locked)))
if code, _, handled, err := fixture.equipment.Handle("/EquipOptionReRoll", request); err == nil || !handled || code != 0 {
t.Fatalf("invalid locks code=%d handled=%v err=%v", code, handled, err)
}
assertOptionRerollBalances(t, fixture, 1000, 20)
})
}
}
func TestEquipmentOptionRerollConversionConsumesMixedConcreteStacks(t *testing.T) {
fixture := newEquipmentOptionRerollFixture(t)
fixture.equipment.BeginSession("option-reroll-conversion")
fixture.design.Conversion = &gamedata.EquipmentOptionRerollConversion{
Ratio: 10, SourceType: 8, SourceID: 16, TargetType: 8, TargetID: 17,
}
sources, err := fixture.inventory.GrantOnce("option-reroll-source-material", []gamedata.BattleReward{{Type: 8, ID: 16, Count: 100}})
if err != nil || len(sources) != 1 {
t.Fatalf("grant conversion source=%+v err=%v", sources, err)
}
// Two locked slots require 7 target units. The client consumes two actual
// target units plus 50 source units (10 source == one target).
target := fixture.material
target.Count = 2
source := sources[0]
source.Count = 50
request := optionRerollRequest(81, fixture.original.InvenIndex,
[]bool{false, true}, []bool{false, true, false}, 1,
[]Item{{Type: 4, Count: 200}, target, source})
if code, _, handled, err := fixture.equipment.Handle("/EquipOptionReRoll", request); err != nil || !handled || code != 192 {
t.Fatalf("conversion reroll code=%d handled=%v err=%v", code, handled, err)
}
if got := fixture.wallet.Snapshot().Gold; got != 800 {
t.Fatalf("conversion gold=%d want=800", got)
}
if got := optionRerollItemCount(fixture.inventory, 8, 17); got != 18 {
t.Fatalf("target material=%d want=18", got)
}
if got := optionRerollItemCount(fixture.inventory, 8, 16); got != 50 {
t.Fatalf("source material=%d want=50", got)
}
}
func TestEquipmentOptionRerollConfirmEquippedReturnsCharacter(t *testing.T) {
fixture := newEquipmentOptionRerollFixture(t)
character := Character{InvenIndex: 535607162, ID: 350, Level: 20}
characters, err := OpenCharacterStore(testStore(filepath.Join(filepath.Dir(fixture.equipmentPath), "characters.json")),
[]Character{character}, fixture.inventory, "", "")
if err != nil {
t.Fatal(err)
}
if err := fixture.equipment.AttachCharacters(characters); err != nil {
t.Fatal(err)
}
fixture.equipment.mu.Lock()
next := cloneEquipmentSnapshot(fixture.equipment.owned)
next.Equipment[0].UseChar = character.InvenIndex
if err := fixture.equipment.commitLocked(next, "option reroll equipped test setup"); err != nil {
fixture.equipment.mu.Unlock()
t.Fatal(err)
}
fixture.equipment.mu.Unlock()
fixture.original = fixture.equipment.All()[0]
fixture.equipment.BeginSession("option-reroll-equipped")
request := fixture.rerollRequest(91, []bool{false, true}, []bool{false, true, false})
if code, _, handled, err := fixture.equipment.Handle("/EquipOptionReRoll", request); err != nil || !handled || code != 192 {
t.Fatalf("equipped reroll code=%d handled=%v err=%v", code, handled, err)
}
confirm := optionRerollConfirmRequest(92, fixture.original.InvenIndex, true)
code, response, handled, err := fixture.equipment.Handle("/EquipOptionReRollConfirm", confirm)
if err != nil || !handled || code != 193 {
t.Fatalf("equipped confirm code=%d handled=%v err=%v", code, handled, err)
}
charWire, found, err := wire.Bytes(response, 2)
if err != nil || !found {
t.Fatalf("confirm response missing CharDBInfo field 2: found=%v err=%v", found, err)
}
charIndex, found, err := wire.Varint(charWire, 1)
if err != nil || !found || charIndex != character.InvenIndex {
t.Fatalf("CharDBInfo index=%d want=%d found=%v err=%v", charIndex, character.InvenIndex, found, err)
}
}
type equipmentOptionRerollFixture struct {
design *gamedata.EquipmentOptionRerollDesign
equipment *EquipmentInventory
wallet *Wallet
inventory *Inventory
original Equipment
material Item
equipmentPath string
}
func newEquipmentOptionRerollFixture(t *testing.T) *equipmentOptionRerollFixture {
t.Helper()
dir := t.TempDir()
wallet, err := OpenWallet(testStore(filepath.Join(dir, "wallet.json")), Currency{Gold: 1000})
if err != nil {
t.Fatal(err)
}
inventory, err := OpenInventory(testStore(filepath.Join(dir, "items.json")), &Starter{Version: "2.35.10"})
if err != nil {
t.Fatal(err)
}
materials, err := inventory.GrantOnce("option-reroll-material", []gamedata.BattleReward{{Type: 8, ID: 17, Count: 20}})
if err != nil || len(materials) != 1 {
t.Fatalf("grant option material=%+v err=%v", materials, err)
}
equipmentPath := filepath.Join(dir, "equipment.json")
equipment, err := OpenEquipmentInventory(testStore(equipmentPath))
if err != nil {
t.Fatal(err)
}
design := optionRerollDomainTestDesign()
if err := equipment.AttachOptionReroll(design, wallet, inventory); err != nil {
t.Fatal(err)
}
entry, err := equipment.GrantOnce("option-reroll-equipment", 77)
if err != nil {
t.Fatal(err)
}
equipment.mu.Lock()
next := cloneEquipmentSnapshot(equipment.owned)
next.Equipment[0].Level = 9
next.Equipment[0].MainOption = []EquipmentOption{{GroupID: 100, ID: 1}, {GroupID: 101, ID: 2}}
next.Equipment[0].SubOption = []EquipmentOption{{GroupID: 200, ID: 10}, {GroupID: 201, ID: 20}, {GroupID: 202, ID: 30}}
next.Equipment[0].PrivateOption = &EquipmentOption{GroupID: 300, ID: 40}
next.Equipment[0].Rank = []uint64{4, 4, 4}
if err := equipment.commitLocked(next, "option reroll test setup"); err != nil {
equipment.mu.Unlock()
t.Fatal(err)
}
equipment.mu.Unlock()
original := equipment.All()[0]
if original.InvenIndex != entry.InvenIndex {
t.Fatalf("equipment index=%d want=%d", original.InvenIndex, entry.InvenIndex)
}
return &equipmentOptionRerollFixture{
design: design, equipment: equipment, wallet: wallet, inventory: inventory,
original: original, material: materials[0], equipmentPath: equipmentPath,
}
}
func optionRerollDomainTestDesign() *gamedata.EquipmentOptionRerollDesign {
choices := func(oldID, nextID uint64) []gamedata.WeightedOption {
// A zero-weight old value and one positive new value make the domain test
// deterministic while retaining two legal choices in every rerollable group.
return []gamedata.WeightedOption{{ID: oldID, Weight: 0}, {ID: nextID, Weight: 1}}
}
return &gamedata.EquipmentOptionRerollDesign{
Equipment: map[uint64]gamedata.EquipmentOptionRerollItem{77: {
ID: 77, OptionRerollID: 9, PrivateUniqueCharID: 350,
MainGroups: []uint64{100, 101}, SubGroups: []uint64{200, 201, 202}, PrivateGroups: []uint64{300},
}},
Costs: map[uint64]gamedata.EquipmentOptionRerollCost{9: {Resources: []gamedata.EquipmentOptionRerollResource{
{Type: 4, BaseCount: 100, LockCount: 50},
{Type: 8, ID: 17, BaseCount: 3, LockCount: 2},
}}},
Groups: map[uint64]gamedata.OptionGroup{
100: {ID: 100, Choices: choices(1, 9)},
101: {ID: 101, Choices: choices(2, 3)},
200: {ID: 200, Choices: choices(10, 11)},
201: {ID: 201, Choices: choices(20, 21)},
202: {ID: 202, Choices: choices(30, 31)},
300: {ID: 300, Choices: choices(40, 41)},
},
}
}
func (f *equipmentOptionRerollFixture) consumeItems(locked uint64) []Item {
material := f.material
material.Count = 3 + 2*locked
return []Item{{Type: 4, Count: 100 + 50*locked}, material}
}
func (f *equipmentOptionRerollFixture) rerollRequest(seq uint64, main, sub []bool) []byte {
locked := countLockedOptions(main) + countLockedOptions(sub)
return optionRerollRequest(seq, f.original.InvenIndex, main, sub, 1, f.consumeItems(uint64(locked)))
}
func optionRerollRequest(seq, equipmentIndex uint64, main, sub []bool, rerollType uint64, consume []Item) []byte {
request := wire.AppendVarint(nil, 1, seq)
request = wire.AppendVarint(request, 2, equipmentIndex)
request = appendPackedOptionLocks(request, 3, main)
request = appendPackedOptionLocks(request, 4, sub)
for _, item := range consume {
request = wire.AppendBytes(request, 5, ItemWire(item))
}
if rerollType != 0 {
request = wire.AppendVarint(request, 6, rerollType)
}
return request
}
func optionRerollConfirmRequest(seq, equipmentIndex uint64, confirm bool) []byte {
request := wire.AppendVarint(nil, 1, seq)
request = wire.AppendVarint(request, 2, equipmentIndex)
if confirm {
request = wire.AppendVarint(request, 3, 1)
}
return request
}
func appendPackedOptionLocks(dst []byte, field int, locks []bool) []byte {
packed := make([]byte, len(locks))
for i, locked := range locks {
if locked {
packed[i] = 1
}
}
return wire.AppendBytes(dst, field, packed)
}
func countLockedOptions(locks []bool) int {
count := 0
for _, locked := range locks {
if locked {
count++
}
}
return count
}
func optionRerollResponseOptions(t *testing.T, response []byte) ([]EquipmentOption, []EquipmentOption) {
t.Helper()
var main, sub []EquipmentOption
if err := wire.Walk(response, func(field wire.Field) error {
if field.Type != 2 || (field.Number != 1 && field.Number != 2) {
return nil
}
option := decodeOptionRerollOption(t, field.Value)
if field.Number == 1 {
main = append(main, option)
} else {
sub = append(sub, option)
}
return nil
}); err != nil {
t.Fatal(err)
}
return main, sub
}
func equipmentWireOptions(t *testing.T, equipment []byte) (uint64, []EquipmentOption, []EquipmentOption) {
t.Helper()
index, found, err := wire.Varint(equipment, 1)
if err != nil || !found {
t.Fatalf("equipment wire missing index: found=%v err=%v", found, err)
}
base, found, err := wire.Bytes(equipment, 5)
if err != nil || !found {
t.Fatalf("equipment wire missing base info: found=%v err=%v", found, err)
}
var main, sub []EquipmentOption
if err := wire.Walk(base, func(field wire.Field) error {
if field.Type != 2 || (field.Number != 3 && field.Number != 4) {
return nil
}
option := decodeOptionRerollOption(t, field.Value)
if field.Number == 3 {
main = append(main, option)
} else {
sub = append(sub, option)
}
return nil
}); err != nil {
t.Fatal(err)
}
return index, main, sub
}
func decodeOptionRerollOption(t *testing.T, data []byte) EquipmentOption {
t.Helper()
group, groupFound, err := wire.Varint(data, 1)
if err != nil || !groupFound {
t.Fatalf("option missing group: found=%v err=%v", groupFound, err)
}
id, idFound, err := wire.Varint(data, 2)
if err != nil || !idFound {
t.Fatalf("option missing id: found=%v err=%v", idFound, err)
}
return EquipmentOption{GroupID: group, ID: id}
}
func assertOptionRerollBalances(t *testing.T, fixture *equipmentOptionRerollFixture, gold, material uint64) {
t.Helper()
if got := fixture.wallet.Snapshot().Gold; got != gold {
t.Fatalf("gold=%d want=%d", got, gold)
}
got := optionRerollItemCount(fixture.inventory, 8, 17)
if got != material {
t.Fatalf("option material=%d want=%d", got, material)
}
}
func optionRerollItemCount(inventory *Inventory, itemType, id uint64) uint64 {
var count uint64
for _, item := range inventory.All() {
if item.Type == itemType && item.ID == id {
count += item.Count
}
}
return count
}