fix(server): preserve quest parties, deduplicate commission restoration and remove team traces
This commit is contained in:
@@ -7,7 +7,6 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math"
|
||||
"sync"
|
||||
|
||||
@@ -223,7 +222,6 @@ func (s *Service) Handle(path string, request []byte) (int, []byte, bool, error)
|
||||
state.phase++
|
||||
state.index, state.deck, state.phaseStarted = next.DeckID, next.DeckID, false
|
||||
state.phaseSeq, state.phaseReply = seq, append([]byte(nil), response...)
|
||||
slog.Info("team trace: battle phase changed", "pack", state.pack, "monster", state.monster, "group", next.GroupID, "phase", next.ID, "enemyDeck", next.DeckID)
|
||||
return 632, response, true, nil
|
||||
case "/BattleVerifyState":
|
||||
if !state.entered {
|
||||
@@ -355,7 +353,6 @@ func (s *Service) Handle(path string, request []byte) (int, []byte, bool, error)
|
||||
state.enterReceipt = identity
|
||||
state.fieldInstance = fieldInstance
|
||||
state.phases, state.phase, state.phaseStarted, state.phaseSeq, state.phaseReply = phases, 0, false, 0, nil
|
||||
slog.Info("team trace: battle entered", "pack", packID, "monster", monster, "enemyDeck", deck, "mode", mode)
|
||||
return 52, response, true, nil
|
||||
case "/BattleRetry":
|
||||
if !state.entered {
|
||||
@@ -424,19 +421,6 @@ func (s *Service) Handle(path string, request []byte) (int, []byte, bool, error)
|
||||
state.index, state.round, state.phaseStarted = index, nextRound, true
|
||||
if nextRound == 1 {
|
||||
state.initialBlue = initialBlue
|
||||
// Log the actual submitted team after pre-battle DeckSave, rather
|
||||
// than inferring it from the deck that existed at BattleEnter.
|
||||
// BattleCharDBInfo fields 2/3/7/9 are inventory/character/costume/grid.
|
||||
blueTeam := make([]string, 0, len(initialBlue))
|
||||
for _, character := range initialBlue {
|
||||
inventory, _, _ := wire.Varint(character, 2)
|
||||
id, _, _ := wire.Varint(character, 3)
|
||||
costume, _, _ := wire.Varint(character, 7)
|
||||
grid, _, _ := wire.Varint(character, 9)
|
||||
blueTeam = append(blueTeam, fmt.Sprintf("inven=%d char=%d costume=%d grid=%d", inventory, id, costume, int32(grid)))
|
||||
}
|
||||
seq, _, _ := wire.Varint(request, 1)
|
||||
slog.Info("team trace: battle started", "seq", seq, "pack", state.pack, "monster", state.monster, "enemyDeck", state.deck, "mode", state.mode, "blueTeam", blueTeam)
|
||||
}
|
||||
// Stable per-battle/round seed; reproducible across retries.
|
||||
seed := index*7919 + state.round*104729
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
@@ -401,7 +400,6 @@ func (s *Store) Handle(path string, req []byte) (int, []byte, bool, error) {
|
||||
}
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
slog.Info("team trace: deliver saved battle deck", "deck", s.state.Deck)
|
||||
out := encodeDeck(s.state.Deck)
|
||||
if s.fieldSettingsDesign != nil {
|
||||
v, e := s.loadFieldSettings()
|
||||
@@ -437,14 +435,9 @@ func (s *Store) Handle(path string, req []byte) (int, []byte, bool, error) {
|
||||
if e = s.validateOwnedDeckLocked(x); e != nil {
|
||||
return 0, nil, true, e
|
||||
}
|
||||
seq, _, _ := wire.Varint(req, 1)
|
||||
slog.Info("team trace: client requested battle deck replacement", "seq", seq, "before", s.state.Deck, "after", x)
|
||||
n := clone(s.state)
|
||||
n.Deck = x
|
||||
e = s.commit(n)
|
||||
if e != nil {
|
||||
slog.Error("team trace: deck replacement failed", "seq", seq, "error", e)
|
||||
}
|
||||
return 10, nil, true, e
|
||||
case "/FieldDeckSave":
|
||||
if e := checkSeq(req); e != nil {
|
||||
|
||||
@@ -130,7 +130,7 @@ func (s *Service) handleQuestSelection(path string, request []byte) (int, []byte
|
||||
if err := s.state.RemoveQuest(quest, pack, 0); err != nil {
|
||||
return 0, nil, true, err
|
||||
}
|
||||
return 20, wire.AppendVarint(nil, 1, uint64(quest)), true, nil
|
||||
return 20, s.appendCurrentBattleDeck(wire.AppendVarint(nil, 1, uint64(quest)), 2), true, nil
|
||||
}
|
||||
selection, ok := s.state.Selection(pack)
|
||||
if !ok || selection.QuestID != quest || selection.Difficulty == 0 {
|
||||
@@ -140,7 +140,7 @@ func (s *Service) handleQuestSelection(path string, request []byte) (int, []byte
|
||||
if err := s.state.SelectQuest(pack, progress.QuestSelection{}); err != nil {
|
||||
return 0, nil, true, err
|
||||
}
|
||||
return 20, wire.AppendVarint(nil, 1, uint64(quest)), true, nil
|
||||
return 20, s.appendCurrentBattleDeck(wire.AppendVarint(nil, 1, uint64(quest)), 2), true, nil
|
||||
}
|
||||
level, _, err := wire.Varint(request, 4)
|
||||
if err != nil || level > 4 {
|
||||
@@ -164,6 +164,7 @@ func (s *Service) handleQuestSelection(path string, request []byte) (int, []byte
|
||||
return 0, nil, true, err
|
||||
}
|
||||
out := wire.AppendBytes(nil, 1, s.questInfoWire(pack, quest))
|
||||
out = s.appendCurrentBattleDeck(out, 3)
|
||||
items, err := s.ensureQuestItems(pack, quest)
|
||||
if err != nil {
|
||||
return 0, nil, true, err
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"bd2server/internal/server/deck"
|
||||
"bd2server/internal/server/gamedata"
|
||||
"bd2server/internal/server/player"
|
||||
"bd2server/internal/server/progress"
|
||||
"bd2server/internal/server/stateio"
|
||||
"bd2server/internal/server/wire"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -92,18 +94,48 @@ func TestDifficultySelectionPersistsAndRewardsRemainIndependent(t *testing.T) {
|
||||
}
|
||||
func TestSideQuestRemainsIndependentFromMainDifficulty(t *testing.T) {
|
||||
s := testService()
|
||||
var err error
|
||||
s.decks, err = deck.NewStore(deck.Seed{Version: "2.35.10", FieldDeck: []deck.FieldEntry{{Slot: 1, CharacterInvenIndex: 905}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
saved := []deck.DeckEntry{
|
||||
{CharacterInvenIndex: 905, CostumeInvenIndex: 0, Slot: 1},
|
||||
{CharacterInvenIndex: 904, CostumeInvenIndex: 8, Slot: 2},
|
||||
{CharacterInvenIndex: 903, CostumeInvenIndex: 11, Slot: 3},
|
||||
{CharacterInvenIndex: 902, CostumeInvenIndex: 3, Slot: 4},
|
||||
{CharacterInvenIndex: 901, CostumeInvenIndex: ^uint64(0), Slot: 5},
|
||||
}
|
||||
saveStoryTestDeck(t, s.decks, saved)
|
||||
call := func(path string, deckField int) {
|
||||
t.Helper()
|
||||
_, response, handled, err := s.Handle(path, selectionRequest(50, 21, 0))
|
||||
if err != nil || !handled {
|
||||
t.Fatalf("%s handled=%v err=%v", path, handled, err)
|
||||
}
|
||||
var entries [][]byte
|
||||
if err := wire.Walk(response, func(f wire.Field) error {
|
||||
if f.Number == deckField {
|
||||
entries = append(entries, f.Value)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := decodeStoryTestDeck(t, entries); !reflect.DeepEqual(got, saved) || !reflect.DeepEqual(s.decks.CurrentDeck(), saved) {
|
||||
t.Fatalf("%s changed saved battle deck: response=%+v saved=%+v", path, got, s.decks.CurrentDeck())
|
||||
}
|
||||
}
|
||||
pack := s.storyCatalog.Packs[21]
|
||||
pack.Quests[50] = gamedata.QuestDesign{ID: 50, Type: 1}
|
||||
s.storyCatalog.Packs[21] = pack
|
||||
if _, _, _, err := s.Handle("/QuestAccept", selectionRequest(50, 21, 0)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
call("/QuestAccept", 3)
|
||||
call("/QuestGiveUp", 2)
|
||||
call("/QuestAccept", 3)
|
||||
if first := s.firstUnclearedQuestFor(21); first != 1 {
|
||||
t.Fatalf("side replaced main%d", first)
|
||||
}
|
||||
if _, _, _, err := s.Handle("/QuestClear", selectionRequest(50, 21, 0)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
call("/QuestClear", 4)
|
||||
if !s.state.QuestCleared(50, 21) || s.state.QuestCleared(1, 21) {
|
||||
t.Fatal("side clear affected main")
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package world
|
||||
|
||||
import (
|
||||
"bd2server/internal/server/player"
|
||||
"bd2server/internal/server/wire"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
@@ -102,12 +101,5 @@ func (s *Service) resolveActivePartyWires(packID, questID int) ([][]byte, [][]by
|
||||
for _, c := range party {
|
||||
characters = append(characters, encodeCharacter(c))
|
||||
}
|
||||
var wires [][]byte
|
||||
if s.decks != nil {
|
||||
for _, entry := range s.decks.CurrentDeck() {
|
||||
data := wire.AppendVarint(wire.AppendVarint(wire.AppendVarint(nil, 1, entry.CharacterInvenIndex), 2, entry.CostumeInvenIndex), 3, entry.Slot)
|
||||
wires = append(wires, data)
|
||||
}
|
||||
}
|
||||
return characters, wires, nil
|
||||
return characters, s.currentBattleDeckWires(), nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package world
|
||||
|
||||
import (
|
||||
"bd2server/internal/server/todayquest"
|
||||
"bd2server/internal/server/wire"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
@@ -13,6 +14,48 @@ func (s *Service) AttachTodayQuests(service *todayquest.Service) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// QuestPacket always passes the repeated DeckInfo to CommonPacket.RefreshDeck,
|
||||
// which replaces the entire battle deck even when that list is empty. Keep
|
||||
// this live snapshot outside the commission reward receipt: retrying a clear
|
||||
// after the player changes formation must return the current saved deck.
|
||||
func (s *Service) commissionResponseDeck(code int, response []byte) []byte {
|
||||
field := 0
|
||||
switch code {
|
||||
case 17: // QuestAcceptResponse.deck_info
|
||||
field = 3
|
||||
case 18: // QuestClearResponse.deck_info
|
||||
field = 4
|
||||
case 20: // QuestGiveUpResponse.deck_info
|
||||
field = 2
|
||||
}
|
||||
if field != 0 {
|
||||
return s.appendCurrentBattleDeck(response, field)
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
func (s *Service) currentBattleDeckWires() [][]byte {
|
||||
if s.decks == nil {
|
||||
return nil
|
||||
}
|
||||
var entries [][]byte
|
||||
for _, current := range s.decks.CurrentDeck() {
|
||||
entry := wire.AppendVarint(nil, 1, current.CharacterInvenIndex)
|
||||
// DeckDBInfo field 2 is the battle-grid position, including zero and -1.
|
||||
entry = wire.AppendVarint(entry, 2, current.CostumeInvenIndex)
|
||||
entry = wire.AppendVarint(entry, 3, current.Slot)
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
func (s *Service) appendCurrentBattleDeck(response []byte, field int) []byte {
|
||||
for _, entry := range s.currentBattleDeckWires() {
|
||||
response = wire.AppendBytes(response, field, entry)
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
// NPCController checks whether any owned pack has completed its main story,
|
||||
// rather than requiring completion of the board's own pack.
|
||||
func (s *Service) CommissionPackUnlocked(pack int) bool {
|
||||
|
||||
@@ -2,6 +2,7 @@ package world
|
||||
|
||||
import (
|
||||
"bd2server/internal/server/accountstate"
|
||||
"bd2server/internal/server/deck"
|
||||
"bd2server/internal/server/gamedata"
|
||||
"bd2server/internal/server/player"
|
||||
"bd2server/internal/server/progress"
|
||||
@@ -24,8 +25,9 @@ func (e commissionEconomy) Apply(id string, costs, rewards []gamedata.Reward) ([
|
||||
}
|
||||
|
||||
// Synthetic IDs describe a two-node collection commission and an independent
|
||||
// pack. The source-backed contract is PackInGameInfo replacing both client
|
||||
// quest lists (CommonPacket.SendPackInGameInfoRequest), not an official capture.
|
||||
// pack. CommonPacket parses PackInGameInfo, requests TodayQuestInfo, then calls
|
||||
// PackManager.Enter from the waypoint callback. Those last two callbacks can
|
||||
// arrive in either order; only TodayQuestInfo restores commissions.
|
||||
func TestCommissionPackEntryRestoresSQLiteChainAndRollback(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "state.db")
|
||||
design := &gamedata.TodayQuestCatalog{Limit: 3, PostCount: 3, AchievementScore: 7,
|
||||
@@ -34,6 +36,7 @@ func TestCommissionPackEntryRestoresSQLiteChainAndRollback(t *testing.T) {
|
||||
101: {ID: 101, PackID: 21, NextID: 102, ConditionType: 19, ConditionCount: 1},
|
||||
102: {ID: 102, PackID: 21, PriorID: 101, ConditionType: 2, ConditionCount: 2, MagicValues: []uint64{71, 72}, GiveItemIDs: []uint64{100001}, Rewards: []gamedata.Reward{{Type: 4, Count: 10}}},
|
||||
201: {ID: 201, PackID: 22, ConditionType: 19, ConditionCount: 1},
|
||||
301: {ID: 301, PackID: 21, ConditionType: 19, ConditionCount: 1},
|
||||
}}
|
||||
open := func() (*accountstate.Repository, *Service, *player.Wallet, *player.Inventory) {
|
||||
t.Helper()
|
||||
@@ -48,6 +51,10 @@ func TestCommissionPackEntryRestoresSQLiteChainAndRollback(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.decks, err = deck.OpenStore(repo, deck.Seed{Version: "2.35.10", FieldDeck: []deck.FieldEntry{{Slot: 1, CharacterInvenIndex: 901}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wallet, err := player.OpenWallet(repo, player.Currency{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -81,6 +88,24 @@ func TestCommissionPackEntryRestoresSQLiteChainAndRollback(t *testing.T) {
|
||||
if !handled || err != nil {
|
||||
t.Fatalf("%s: handled=%v %v", path, handled, err)
|
||||
}
|
||||
// QuestAccept, QuestClear and QuestGiveUp all call client RefreshDeck
|
||||
// with a non-null repeated list: the response must contain the complete
|
||||
// current battle deck, even for a replayed commission reward receipt.
|
||||
deckField := map[string]int{"/QuestAccept": 3, "/QuestClear": 4, "/QuestGiveUp": 2}[path]
|
||||
if deckField != 0 {
|
||||
var entries [][]byte
|
||||
if err := wire.Walk(b, func(f wire.Field) error {
|
||||
if f.Number == deckField {
|
||||
entries = append(entries, f.Value)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := decodeStoryTestDeck(t, entries); !reflect.DeepEqual(got, s.decks.CurrentDeck()) {
|
||||
t.Fatalf("%s replaced saved formation: got=%+v want=%+v", path, got, s.decks.CurrentDeck())
|
||||
}
|
||||
}
|
||||
return b
|
||||
}
|
||||
assertEntry := func(s *Service, pack int, active map[int][]uint64, cleared []int) {
|
||||
@@ -90,13 +115,52 @@ func TestCommissionPackEntryRestoresSQLiteChainAndRollback(t *testing.T) {
|
||||
if !handled || err != nil {
|
||||
t.Fatalf("pack entry: handled=%v %v", handled, err)
|
||||
}
|
||||
got := map[int][]uint64{}
|
||||
var clears []int
|
||||
var packQuests []int
|
||||
var packClears []int
|
||||
// The pack response is parsed before TodayQuestInfo is requested. Its
|
||||
// regular task list is applied later, when the waypoint request finishes.
|
||||
if err := wire.Walk(body, func(f wire.Field) error {
|
||||
if f.Number == 2 {
|
||||
id, _, _ := wire.Varint(f.Value, 1)
|
||||
if _, exists := design.Quests[int(id)]; exists {
|
||||
t.Fatalf("commission %d duplicated into PackInGameInfo", id)
|
||||
}
|
||||
packQuests = append(packQuests, int(id))
|
||||
}
|
||||
if f.Number == 3 {
|
||||
for b := f.Value; len(b) > 0; {
|
||||
id, n := binary.Uvarint(b)
|
||||
if n <= 0 {
|
||||
t.Fatal("invalid cleared IDs")
|
||||
}
|
||||
if _, exists := design.Quests[int(id)]; exists {
|
||||
t.Fatalf("commission %d restored through pack completion list", id)
|
||||
}
|
||||
packClears = append(packClears, int(id))
|
||||
b = b[n:]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pack == 21 && !reflect.DeepEqual(packQuests, []int{1}) {
|
||||
t.Fatalf("pack entry lost ordinary main quest: %v", packQuests)
|
||||
}
|
||||
today := call(s, "/TodayQuestInfo", 0, 0)
|
||||
got := map[int][]uint64{}
|
||||
var clears []int
|
||||
var currentPackToday []int
|
||||
if err := wire.Walk(today, func(f wire.Field) error {
|
||||
if f.Number == 1 {
|
||||
id, _, _ := wire.Varint(f.Value, 1)
|
||||
qpack, _, _ := wire.Varint(f.Value, 6)
|
||||
if int(qpack) == pack {
|
||||
if _, duplicate := got[int(id)]; duplicate {
|
||||
t.Fatalf("TodayQuestInfo repeated commission %d", id)
|
||||
}
|
||||
got[int(id)] = nil
|
||||
currentPackToday = append(currentPackToday, int(id))
|
||||
return wire.Walk(f.Value, func(q wire.Field) error {
|
||||
if q.Number == 3 {
|
||||
n, _ := binary.Uvarint(q.Value)
|
||||
@@ -106,16 +170,10 @@ func TestCommissionPackEntryRestoresSQLiteChainAndRollback(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
if f.Number == 3 {
|
||||
for b := f.Value; len(b) > 0; {
|
||||
id, n := binary.Uvarint(b)
|
||||
if n <= 0 {
|
||||
t.Fatal("invalid cleared IDs")
|
||||
}
|
||||
if _, exists := design.Quests[int(id)]; exists {
|
||||
clears = append(clears, int(id))
|
||||
}
|
||||
b = b[n:]
|
||||
if f.Number == 2 {
|
||||
id, _ := binary.Uvarint(f.Value)
|
||||
if design.Quests[int(id)].PackID == pack {
|
||||
clears = append(clears, int(id))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -123,12 +181,76 @@ func TestCommissionPackEntryRestoresSQLiteChainAndRollback(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, active) || !reflect.DeepEqual(clears, cleared) {
|
||||
t.Fatalf("pack %d active=%v cleared=%v", pack, got, clears)
|
||||
t.Fatalf("pack %d TodayQuestInfo active=%v cleared=%v", pack, got, clears)
|
||||
}
|
||||
for _, todayFirst := range []bool{true, false} {
|
||||
var clientQuests []int
|
||||
// The pack completion snapshot was assigned before either of
|
||||
// these callbacks; TodayQuestInfo subsequently merges its IDs.
|
||||
clientClears := append([]int(nil), packClears...)
|
||||
addToday := func() {
|
||||
var ordinary []int
|
||||
for _, id := range clientQuests {
|
||||
if _, today := design.Quests[id]; !today {
|
||||
ordinary = append(ordinary, id)
|
||||
}
|
||||
}
|
||||
clientQuests = append(ordinary, currentPackToday...)
|
||||
for _, id := range clears {
|
||||
present := false
|
||||
for _, existing := range clientClears {
|
||||
present = present || existing == id
|
||||
}
|
||||
if !present {
|
||||
clientClears = append(clientClears, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
if todayFirst {
|
||||
addToday()
|
||||
clientQuests = append(clientQuests, packQuests...)
|
||||
} else {
|
||||
clientQuests = append(clientQuests, packQuests...)
|
||||
addToday()
|
||||
}
|
||||
seen := map[int]bool{}
|
||||
for _, id := range clientQuests {
|
||||
if seen[id] {
|
||||
t.Fatalf("HUD duplicate quest %d when todayFirst=%v", id, todayFirst)
|
||||
}
|
||||
seen[id] = true
|
||||
}
|
||||
if len(seen) != len(packQuests)+len(active) {
|
||||
t.Fatalf("entry callbacks lost tasks when todayFirst=%v: %v", todayFirst, clientQuests)
|
||||
}
|
||||
var completedToday []int
|
||||
for _, id := range clientClears {
|
||||
if _, today := design.Quests[id]; today {
|
||||
completedToday = append(completedToday, id)
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(completedToday, cleared) {
|
||||
t.Fatalf("entry callbacks lost completed commissions when todayFirst=%v: %v", todayFirst, completedToday)
|
||||
}
|
||||
}
|
||||
}
|
||||
repo, s, wallet, inv := open()
|
||||
savedDeck := []deck.DeckEntry{
|
||||
{CharacterInvenIndex: 901, CostumeInvenIndex: 0, Slot: 1},
|
||||
{CharacterInvenIndex: 905, CostumeInvenIndex: 8, Slot: 2},
|
||||
{CharacterInvenIndex: 903, CostumeInvenIndex: 11, Slot: 3},
|
||||
{CharacterInvenIndex: 904, CostumeInvenIndex: 3, Slot: 4},
|
||||
{CharacterInvenIndex: 902, CostumeInvenIndex: ^uint64(0), Slot: 5},
|
||||
}
|
||||
saveStoryTestDeck(t, s.decks, savedDeck)
|
||||
call(s, "/QuestAccept", 101, 21)
|
||||
call(s, "/QuestAccept", 201, 22)
|
||||
call(s, "/QuestAccept", 301, 21)
|
||||
call(s, "/QuestAccept", 101, 21)
|
||||
call(s, "/QuestGiveUp", 301, 21)
|
||||
if !reflect.DeepEqual(s.decks.CurrentDeck(), savedDeck) {
|
||||
t.Fatal("commission accepts or abandonment changed saved formation")
|
||||
}
|
||||
call(s, "/QuestUpdate", 101, 21, 1)
|
||||
call(s, "/QuestClear", 101, 21)
|
||||
call(s, "/QuestUpdate", 102, 21, 71)
|
||||
@@ -139,6 +261,9 @@ func TestCommissionPackEntryRestoresSQLiteChainAndRollback(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
repo, s, wallet, inv = open()
|
||||
if !reflect.DeepEqual(s.decks.CurrentDeck(), savedDeck) {
|
||||
t.Fatal("SQLite reopen lost selected formation")
|
||||
}
|
||||
assertEntry(s, 21, map[int][]uint64{102: {71}}, []int{101})
|
||||
assertEntry(s, 22, map[int][]uint64{201: nil}, nil)
|
||||
op, err := repo.BeginOperation()
|
||||
@@ -163,11 +288,23 @@ func TestCommissionPackEntryRestoresSQLiteChainAndRollback(t *testing.T) {
|
||||
}
|
||||
call(s, "/QuestUpdate", 102, 21, 72)
|
||||
call(s, "/QuestClear", 102, 21)
|
||||
// A retry is an idempotent reward operation, but its deck projection is a
|
||||
// live snapshot. Persist a different formation and reopen before replay.
|
||||
savedDeck[0], savedDeck[4] = savedDeck[4], savedDeck[0]
|
||||
savedDeck[0].Slot, savedDeck[4].Slot = 1, 5
|
||||
saveStoryTestDeck(t, s.decks, savedDeck)
|
||||
if err := repo.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
repo, s, wallet, inv = open()
|
||||
call(s, "/QuestClear", 102, 21)
|
||||
assertEntry(s, 21, map[int][]uint64{}, []int{101, 102})
|
||||
if wallet.Snapshot().Gold != 10 {
|
||||
t.Fatal("reward replay changed balance")
|
||||
}
|
||||
if !reflect.DeepEqual(s.decks.CurrentDeck(), savedDeck) {
|
||||
t.Fatal("replayed commission clear restored stale formation")
|
||||
}
|
||||
call(s, "/QuestGiveUp", 201, 22)
|
||||
assertEntry(s, 22, map[int][]uint64{}, nil)
|
||||
}
|
||||
|
||||
@@ -261,6 +261,9 @@ func (s *Service) AttachDecks(decks *deck.Store) error {
|
||||
func (s *Service) Handle(path string, request []byte) (int, []byte, bool, error) {
|
||||
if s.todayQuests != nil {
|
||||
if code, body, handled, err := s.todayQuests.Handle(path, request); handled {
|
||||
if err == nil {
|
||||
body = s.commissionResponseDeck(code, body)
|
||||
}
|
||||
return code, body, handled, err
|
||||
}
|
||||
}
|
||||
@@ -356,7 +359,6 @@ func (s *Service) Handle(path string, request []byte) (int, []byte, bool, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
slog.Info("team trace: deliver owned characters", "characters", characters)
|
||||
for _, character := range characters {
|
||||
response = wire.AppendBytes(response, 1, encodeCharacter(character))
|
||||
}
|
||||
@@ -375,7 +377,6 @@ func (s *Service) Handle(path string, request []byte) (int, []byte, bool, error)
|
||||
if s.collection != nil {
|
||||
costumes = s.collection.Costumes()
|
||||
}
|
||||
slog.Info("team trace: deliver owned costumes", "costumes", costumes, "quest26", s.seed.RewardCostume)
|
||||
var selections map[uint64]uint64
|
||||
if s.prestigeSelections != nil {
|
||||
var err error
|
||||
@@ -422,7 +423,6 @@ func (s *Service) Handle(path string, request []byte) (int, []byte, bool, error)
|
||||
if !s.packUnlocked(pack) {
|
||||
return 0, nil, true, fmt.Errorf("%w: unsupported pack %d", ErrInvalidRequest, pack)
|
||||
}
|
||||
slog.Info("team trace: deliver pack progress", "pack", pack, "clearedQuests", s.state.ClearedQuests(pack), "storyCharacters", s.storyCharacters(pack))
|
||||
if active := s.firstUnclearedQuestFor(pack); active != 0 {
|
||||
if _, err := s.ensureQuestItems(pack, active); err != nil {
|
||||
return 0, nil, true, err
|
||||
@@ -502,7 +502,6 @@ func (s *Service) Handle(path string, request []byte) (int, []byte, bool, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
slog.Info("team trace: quest cleared", "pack", pack, "quest", quest, "changesBattleDeck", false)
|
||||
return 18, s.clearResponse(pack, quest, design.Rewards[s.questDifficultyFor(pack, quest)], items, questEquipment, nextItems, nextChars), true, nil
|
||||
default:
|
||||
return 0, nil, false, nil
|
||||
@@ -740,19 +739,10 @@ func (s *Service) basePackInfoFor(packID int) ([]byte, error) {
|
||||
out = wire.AppendBytes(out, 2, quest)
|
||||
}
|
||||
cleared := s.state.ClearedQuests(packID, s.questDifficulty(packID))
|
||||
// Entering a pack replaces the client's entire task and completion lists.
|
||||
// Include commissions here as well as in QuestInfo so cross-pack travel and
|
||||
// login restore the same active chain and already completed nodes.
|
||||
if s.todayQuests != nil {
|
||||
quests, ids, err := s.todayQuests.Info(packID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, quest := range quests {
|
||||
out = wire.AppendBytes(out, 2, quest)
|
||||
}
|
||||
cleared = append(cleared, ids...)
|
||||
}
|
||||
// CommonPacket requests TodayQuestInfo after parsing this response, before
|
||||
// the waypoint callback calls PackManager.Enter. That separate response owns
|
||||
// commission restoration; including commissions here lets Enter append them
|
||||
// a second time when TodayQuestInfo arrives first, crashing the quest HUD.
|
||||
if len(cleared) != 0 {
|
||||
var packed []byte
|
||||
for _, id := range cleared {
|
||||
@@ -922,17 +912,8 @@ func (s *Service) clearResponse(packID, quest int, designRewards []gamedata.Rewa
|
||||
out = wire.AppendBytes(out, 11, info)
|
||||
}
|
||||
}
|
||||
if s.decks != nil {
|
||||
// Echo the player's saved formation, including quests that unlock a
|
||||
// character. Receiving a character is not a request to change the deck.
|
||||
for _, current := range s.decks.CurrentDeck() {
|
||||
entry := wire.AppendVarint(nil, 1, current.CharacterInvenIndex)
|
||||
// Preserve the saved battle-grid position, including cell zero.
|
||||
entry = wire.AppendVarint(entry, 2, current.CostumeInvenIndex)
|
||||
entry = wire.AppendVarint(entry, 3, current.Slot)
|
||||
out = wire.AppendBytes(out, 4, entry)
|
||||
}
|
||||
}
|
||||
// Receiving a character is not a request to change the saved formation.
|
||||
out = s.appendCurrentBattleDeck(out, 4)
|
||||
for _, char := range nextChars {
|
||||
out = wire.AppendBytes(out, 5, char)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user