fix(server): preserve current health when loading character stats
This commit is contained in:
@@ -1,11 +1,65 @@
|
||||
package battle
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"bd2server/internal/server/wire"
|
||||
)
|
||||
|
||||
func TestBattleDefeatAndRunCommitZeroAndInjuredHealth(t *testing.T) {
|
||||
// EBattleResultType: RED_WIN=2, CONDITION_FAIL=3, RUN=4. A dead
|
||||
// CharDBInfo omits hp because proto3's default is zero; it must be committed
|
||||
// rather than ignored and replaced with an earlier full-health value.
|
||||
for _, result := range []uint64{2, 3, 4} {
|
||||
t.Run(strconv.FormatUint(result, 10), func(t *testing.T) {
|
||||
s := &Service{}
|
||||
calls := 0
|
||||
var saved map[uint64]uint64
|
||||
s.AttachCommittedHealth(func(health map[uint64]uint64) error { calls++; saved = health; return nil })
|
||||
s.BeginSession("settlement")
|
||||
enter := wire.AppendVarint(wire.AppendVarint(request(1), 4, 1), 5, 1)
|
||||
if _, _, _, err := s.Handle("/BattleEnter", enter); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
start := wire.AppendVarint(request(2), 2, 11)
|
||||
start = wire.AppendBytes(start, 5, wire.AppendVarint(wire.AppendVarint(nil, 2, 100), 4, 100))
|
||||
start = wire.AppendBytes(start, 5, wire.AppendVarint(wire.AppendVarint(nil, 2, 200), 4, 100))
|
||||
if _, _, _, err := s.Handle("/BattleStart", start); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
end := wire.AppendVarint(request(3), 2, result)
|
||||
end = wire.AppendBytes(end, 3, wire.AppendVarint(nil, 1, 100))
|
||||
end = wire.AppendBytes(end, 3, wire.AppendVarint(wire.AppendVarint(nil, 1, 200), 3, 21))
|
||||
_, response, _, err := s.Handle("/BattleEnd", end)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if calls != 1 || len(saved) != 2 || saved[100] != 0 || saved[200] != 21 {
|
||||
t.Fatalf("result=%d saved health=%v calls=%d", result, saved, calls)
|
||||
}
|
||||
if err := wire.Walk(response, func(field wire.Field) error {
|
||||
if field.Number == 3 {
|
||||
index, _, _ := wire.Varint(field.Value, 1)
|
||||
hp, _, _ := wire.Varint(field.Value, 3)
|
||||
if hp != saved[index] {
|
||||
t.Fatal("response differs from settled HP")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, _, err := s.Handle("/BattleExit", request(4)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if calls != 1 {
|
||||
t.Fatal("exit wrote health after settlement")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBattleHealthCommitsOnlyAtEndAndChecksParticipants(t *testing.T) {
|
||||
s := &Service{}
|
||||
calls := 0
|
||||
|
||||
@@ -188,29 +188,38 @@ func (s *CharacterStore) MaxHealth(index uint64) (uint64, error) {
|
||||
return character.HP, nil
|
||||
}
|
||||
|
||||
// CurrentHealth resolves persisted current HP independently from maximum HP.
|
||||
// A missing current_hp entry retains the owned character record's HP, including
|
||||
// zero. Recomputing equipment or other maximum-health stats must never heal it.
|
||||
func (s *CharacterStore) CurrentHealth(index uint64) (uint64, error) {
|
||||
maximum, err := s.MaxHealth(index)
|
||||
var saved Character
|
||||
for _, character := range s.RawAll() {
|
||||
if character.InvenIndex == index {
|
||||
saved = character
|
||||
break
|
||||
}
|
||||
}
|
||||
if saved.InvenIndex == 0 {
|
||||
return 0, fmt.Errorf("player: unknown health character %d", index)
|
||||
}
|
||||
return s.savedCurrentHealth(saved)
|
||||
}
|
||||
|
||||
func (s *CharacterStore) savedCurrentHealth(saved Character) (uint64, error) {
|
||||
if s.store == nil {
|
||||
return saved.HP, nil
|
||||
}
|
||||
data, found, err := s.store.LoadEntry("characters", "current_hp", strconv.FormatUint(saved.InvenIndex, 10))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return s.currentHealthAtMaximum(index, maximum)
|
||||
}
|
||||
|
||||
func (s *CharacterStore) currentHealthAtMaximum(index, maximum uint64) (uint64, error) {
|
||||
if s.store == nil {
|
||||
return maximum, nil
|
||||
}
|
||||
data, found, err := s.store.LoadEntry("characters", "current_hp", strconv.FormatUint(index, 10))
|
||||
if err != nil || !found {
|
||||
return maximum, err
|
||||
if !found {
|
||||
return saved.HP, nil
|
||||
}
|
||||
var hp uint64
|
||||
if json.Unmarshal(data, &hp) != nil {
|
||||
return 0, errors.New("player: invalid saved current health")
|
||||
}
|
||||
if hp > maximum {
|
||||
hp = maximum
|
||||
}
|
||||
return hp, nil
|
||||
}
|
||||
|
||||
@@ -381,6 +390,9 @@ func (s *FoodService) recoverCharacter(index uint64, items []Item) (Character, e
|
||||
}
|
||||
total += value
|
||||
}
|
||||
if current > maximum {
|
||||
current = maximum
|
||||
}
|
||||
if total >= maximum-current {
|
||||
character.HP = maximum
|
||||
} else {
|
||||
|
||||
@@ -315,6 +315,104 @@ func TestCurrentHealthGrowthAndImmortalClearPersistedInjury(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentHealthRetainsSavedCharacterHPWithoutSeparateEntryAcrossReopen(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "state.db")
|
||||
repo, err := accountstate.Open(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seed := []Character{{InvenIndex: 77, ID: 350, Level: 1, HP: 17}, {InvenIndex: 78, ID: 360, Level: 1, HP: 0}}
|
||||
open := func(store stateio.Store) *CharacterStore {
|
||||
inventory, err := OpenInventory(store, &Starter{Version: "2.35.10"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
characters, err := OpenCharacterStore(store, seed, inventory, "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = characters.AttachMaxHealth(func(Character) (uint64, error) { return 500, nil }); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = characters.EnsurePersisted(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return characters
|
||||
}
|
||||
check := func(characters *CharacterStore) {
|
||||
for _, saved := range seed {
|
||||
hp, err := characters.CurrentHealth(saved.InvenIndex)
|
||||
if err != nil || hp != saved.HP {
|
||||
t.Fatalf("current %d=%d want=%d err=%v", saved.InvenIndex, hp, saved.HP, err)
|
||||
}
|
||||
character, found := characters.Find(saved.InvenIndex)
|
||||
if !found || character.HP != saved.HP {
|
||||
t.Fatalf("Find=%+v found=%v", character, found)
|
||||
}
|
||||
}
|
||||
all := characters.All()
|
||||
if len(all) != 2 || all[0].HP != 17 || all[1].HP != 0 {
|
||||
t.Fatalf("All=%+v", all)
|
||||
}
|
||||
maximum, err := characters.MaxHealth(77)
|
||||
if err != nil || maximum != 500 {
|
||||
t.Fatalf("maximum=%d err=%v", maximum, err)
|
||||
}
|
||||
}
|
||||
characters := open(repo)
|
||||
check(characters)
|
||||
rows, err := repo.ListEntries("characters", "current_hp")
|
||||
if err != nil || len(rows) != 0 {
|
||||
t.Fatalf("reads created entries=%v err=%v", rows, err)
|
||||
}
|
||||
if err = repo.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
repo, err = accountstate.Open(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer repo.Close()
|
||||
characters = open(repo)
|
||||
check(characters)
|
||||
if err = characters.SetCurrentHealth(77, 300); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = characters.SetCurrentHealth(78, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if hp, _ := characters.CurrentHealth(77); hp != 300 {
|
||||
t.Fatalf("explicit health entry lost=%d", hp)
|
||||
}
|
||||
// Revival must write the restored value, since deleting the entry alone
|
||||
// would expose the zero HP in the owned character record again.
|
||||
request := wire.AppendVarint(wire.AppendVarint(nil, 1, 1), 2, 78)
|
||||
if _, _, _, err = characters.Handle("/CharImmortal", request); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if hp, _ := characters.CurrentHealth(78); hp != 500 {
|
||||
t.Fatalf("revived health=%d", hp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEatFoodRecoversSavedHPWithoutSeparateCurrentHealthEntry(t *testing.T) {
|
||||
food, inventory, characters := foodTestService(t, stateio.NewMemory())
|
||||
// The seed has current HP 7 and maximum 100, with no current_hp entry.
|
||||
stacks, err := inventory.GrantOnce("saved-health-food", []gamedata.BattleReward{{Type: 5, ID: 101, Count: 1}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, _, err = food.Handle("/EatFood", foodRequest(1, 77, 21, stacks[0])); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if hp, _ := characters.CurrentHealth(77); hp != 22 {
|
||||
t.Fatalf("recovered saved health=%d want22", hp)
|
||||
}
|
||||
if len(inventory.All()) != 0 {
|
||||
t.Fatal("food was not consumed")
|
||||
}
|
||||
}
|
||||
|
||||
type failFoodStore struct {
|
||||
stateio.AtomicEntryStore
|
||||
fail bool
|
||||
|
||||
@@ -269,9 +269,13 @@ func TestEquipmentMainOptionChangeEquippedCharacterHealthReadsEquipment(t *testi
|
||||
t.Fatalf("equipped character response missing: found=%v err=%v", found, err)
|
||||
}
|
||||
hp, found, err := wire.Varint(character, 3)
|
||||
if err != nil || !found || hp != 123 {
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@ func TestEquipmentStatContributionsUseOnlyEquippedOwnerAndAllHealthOptions(t *te
|
||||
if hp := gamedata.AggregateStats(gamedata.BaseStats{Health: 100}, got).Health; hp != 121 {
|
||||
t.Fatalf("equipped health=%v", hp)
|
||||
}
|
||||
// A normal Find callback must safely enter equipment snapshot resolution.
|
||||
// Maximum health must safely enter equipment snapshot resolution without
|
||||
// changing the persisted current HP returned by Find.
|
||||
_, _, characters := foodTestService(t, stateio.NewMemory())
|
||||
if err := characters.AttachMaxHealth(func(c Character) (uint64, error) {
|
||||
contributions, err := s.StatContributions(c)
|
||||
@@ -36,10 +37,17 @@ func TestEquipmentStatContributionsUseOnlyEquippedOwnerAndAllHealthOptions(t *te
|
||||
t.Fatal(err)
|
||||
}
|
||||
done := make(chan Character, 1)
|
||||
go func() { c, _ := characters.Find(77); done <- c }()
|
||||
go func() {
|
||||
maximum, err := characters.MaxHealth(77)
|
||||
if err != nil || maximum != 121 {
|
||||
t.Errorf("maximum=%d err=%v", maximum, err)
|
||||
}
|
||||
c, _ := characters.Find(77)
|
||||
done <- c
|
||||
}()
|
||||
select {
|
||||
case c := <-done:
|
||||
if c.HP != 121 {
|
||||
if c.HP != 7 {
|
||||
t.Fatalf("Find health=%d", c.HP)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
|
||||
@@ -223,18 +223,8 @@ func validateCharacters(characters []Character) error {
|
||||
|
||||
func (s *CharacterStore) All() []Character {
|
||||
characters := s.RawAll()
|
||||
s.mu.Lock()
|
||||
maxHealth := s.maxHealth
|
||||
s.mu.Unlock()
|
||||
if maxHealth != nil {
|
||||
for i := range characters {
|
||||
if hp, err := maxHealth(characters[i]); err == nil {
|
||||
characters[i].HP = hp
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := range characters {
|
||||
if hp, err := s.currentHealthAtMaximum(characters[i].InvenIndex, characters[i].HP); err == nil {
|
||||
if hp, err := s.savedCurrentHealth(characters[i]); err == nil {
|
||||
characters[i].HP = hp
|
||||
}
|
||||
}
|
||||
@@ -255,38 +245,14 @@ func (s *CharacterStore) RawAll() []Character {
|
||||
}
|
||||
|
||||
func (s *CharacterStore) Find(inventoryIndex uint64) (Character, bool) {
|
||||
s.mu.Lock()
|
||||
for _, character := range s.characters {
|
||||
for _, character := range s.RawAll() {
|
||||
if character.InvenIndex == inventoryIndex {
|
||||
maxHealth := s.maxHealth
|
||||
s.mu.Unlock()
|
||||
if maxHealth != nil {
|
||||
if hp, err := maxHealth(character); err == nil {
|
||||
character.HP = hp
|
||||
}
|
||||
}
|
||||
if hp, err := s.currentHealthAtMaximum(character.InvenIndex, character.HP); err == nil {
|
||||
if hp, err := s.savedCurrentHealth(character); err == nil {
|
||||
character.HP = hp
|
||||
}
|
||||
return character, true
|
||||
}
|
||||
}
|
||||
collection := s.collection
|
||||
s.mu.Unlock()
|
||||
if collection != nil {
|
||||
character, found := collection.FindCharacter(inventoryIndex)
|
||||
if found && s.maxHealth != nil {
|
||||
if hp, err := s.maxHealth(character); err == nil {
|
||||
character.HP = hp
|
||||
}
|
||||
}
|
||||
if found {
|
||||
if hp, err := s.currentHealthAtMaximum(character.InvenIndex, character.HP); err == nil {
|
||||
character.HP = hp
|
||||
}
|
||||
}
|
||||
return character, found
|
||||
}
|
||||
return Character{}, false
|
||||
}
|
||||
|
||||
@@ -508,7 +474,8 @@ func (s *CharacterStore) promoteCharacter(current Character, position int, fromC
|
||||
// charImmortal completes the automatic post-battle revival for characters
|
||||
// whose TalentSkillTable.ClassType is 14. The story character 6010 has
|
||||
// ValueList[0]=10000 at every talent level (100%). The authoritative maximum
|
||||
// HP is recomputed by Find from the same calculator as character growth.
|
||||
// Maximum HP is calculated separately from the persisted current HP and the
|
||||
// restored value is saved explicitly for subsequent character snapshots.
|
||||
func (s *CharacterStore) charImmortal(request []byte) (int, []byte, bool, error) {
|
||||
seq, present, err := wire.Varint(request, 1)
|
||||
if err != nil || !present || seq == 0 {
|
||||
@@ -557,7 +524,11 @@ func (s *CharacterStore) charImmortal(request []byte) (int, []byte, bool, error)
|
||||
response = wire.AppendBytes(response, 1, CharacterWire(character))
|
||||
}
|
||||
for _, index := range indices {
|
||||
if err := s.resetCurrentHealth(index); err != nil {
|
||||
maximum, err := s.MaxHealth(index)
|
||||
if err != nil {
|
||||
return 0, nil, true, err
|
||||
}
|
||||
if err := s.SetCurrentHealth(index, maximum); err != nil {
|
||||
return 0, nil, true, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"bd2server/internal/server/player"
|
||||
"bd2server/internal/server/progress"
|
||||
"bd2server/internal/server/stateio"
|
||||
"bd2server/internal/server/wire"
|
||||
)
|
||||
|
||||
func TestTutorialCharInfoRestoresFieldHealthWithoutExposingStoryRoster(t *testing.T) {
|
||||
for _, hp := range []uint64{0, 17} {
|
||||
t.Run(strconv.FormatUint(hp, 10), func(t *testing.T) {
|
||||
storage := stateio.NewMemory()
|
||||
starter := &player.Starter{Version: "2.35.10", Characters: []player.Character{{InvenIndex: 77, ID: 350, Level: 1, HP: 100}}}
|
||||
inventory, err := player.OpenInventory(storage, starter)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
all := append(append([]player.Character(nil), starter.Characters...), player.Character{InvenIndex: 88, ID: 650, HP: 100, Level: 1})
|
||||
characters, err := player.OpenCharacterStore(storage, all, inventory, "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := characters.AttachMaxHealth(func(player.Character) (uint64, error) { return 100, nil }); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := characters.EnsurePersisted(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := characters.SetCurrentHealth(77, hp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
characters, err = player.OpenCharacterStore(storage, all, inventory, "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := characters.AttachMaxHealth(func(player.Character) (uint64, error) { return 100, nil }); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := &Service{seed: Seed{PackID: 21, BattleUnlockQuestID: 26}, state: progress.NewStore(), starter: starter, characters: characters}
|
||||
code, response, handled, err := s.Handle("/CharInfo", wire.AppendVarint(nil, 1, 1))
|
||||
if err != nil || code != 9 || !handled {
|
||||
t.Fatalf("code=%d handled=%v err=%v", code, handled, err)
|
||||
}
|
||||
count := 0
|
||||
if err := wire.Walk(response, func(field wire.Field) error {
|
||||
if field.Number == 1 {
|
||||
count++
|
||||
index, _, _ := wire.Varint(field.Value, 1)
|
||||
got, _, _ := wire.Varint(field.Value, 3)
|
||||
if index != 77 || got != hp {
|
||||
t.Fatalf("tutorial character=%d hp=%d want=%d", index, got, hp)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count != 1 {
|
||||
t.Fatalf("tutorial exposed %d characters", count)
|
||||
}
|
||||
if _, _, handled, err := s.Handle("/CharInfo", nil); !handled || err == nil {
|
||||
t.Fatal("invalid character request accepted")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackInfoStoryCharactersUsePersistedDeadAndInjuredHP(t *testing.T) {
|
||||
storage := stateio.NewMemory()
|
||||
starter := &player.Starter{Version: "2.35.10"}
|
||||
inventory, err := player.OpenInventory(storage, starter)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
all := []player.Character{{InvenIndex: 77, ID: 350, HP: 100, Level: 1}, {InvenIndex: 88, ID: 650, HP: 100, Level: 1}}
|
||||
characters, err := player.OpenCharacterStore(storage, all, inventory, "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := characters.AttachMaxHealth(func(player.Character) (uint64, error) { return 100, nil }); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := characters.EnsurePersisted(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := characters.SetCurrentHealth(77, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := characters.SetCurrentHealth(88, 23); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
characters, err = player.OpenCharacterStore(storage, all, inventory, "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := characters.AttachMaxHealth(func(player.Character) (uint64, error) { return 100, nil }); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
state := progress.NewStore()
|
||||
if err := state.ClearQuest(26, 21); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := &Service{seed: Seed{PackID: 21, BattleUnlockQuestID: 26, RewardCharacter: all[0], StoryCharacters: all[1:]}, state: state, starter: starter, characters: characters}
|
||||
response, err := s.packInfoFor(21)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
count := 0
|
||||
if err := wire.Walk(response, func(field wire.Field) error {
|
||||
if field.Number == 1 {
|
||||
count++
|
||||
index, _, _ := wire.Varint(field.Value, 1)
|
||||
hp, _, _ := wire.Varint(field.Value, 3)
|
||||
want := map[uint64]uint64{77: 0, 88: 23}
|
||||
if expected, exists := want[index]; !exists || hp != expected {
|
||||
t.Fatalf("pack character %d hp=%d", index, hp)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count != 2 {
|
||||
t.Fatalf("pack story roster count=%d", count)
|
||||
}
|
||||
}
|
||||
@@ -157,11 +157,25 @@ func (s *Service) Handle(path string, request []byte) (int, []byte, bool, error)
|
||||
}
|
||||
return 4, s.accountPackInfo(), true, nil
|
||||
case "/CharInfo":
|
||||
if !s.state.QuestCleared(s.seed.BattleUnlockQuestID, s.seed.PackID) {
|
||||
return 0, nil, false, nil
|
||||
seq, found, err := wire.Varint(request, 1)
|
||||
if err != nil || !found || seq == 0 {
|
||||
return 0, nil, true, errors.New("world: CharInfo missing sequence")
|
||||
}
|
||||
var response []byte
|
||||
characters := s.characters.All()
|
||||
if !s.state.QuestCleared(s.seed.BattleUnlockQuestID, s.seed.PackID) {
|
||||
// The tutorial roster still contains only starter identities, but
|
||||
// their field HP must come from persisted state rather than falling
|
||||
// through to Starter.Handle's immutable new-account HP.
|
||||
characters = make([]player.Character, 0, len(s.starter.Characters))
|
||||
for _, seeded := range s.starter.Characters {
|
||||
character, exists := s.characters.Find(seeded.InvenIndex)
|
||||
if !exists {
|
||||
return 0, nil, true, fmt.Errorf("world: missing starter character %d", seeded.InvenIndex)
|
||||
}
|
||||
characters = append(characters, character)
|
||||
}
|
||||
}
|
||||
slog.Info("team trace: deliver owned characters", "characters", characters)
|
||||
for _, character := range characters {
|
||||
response = wire.AppendBytes(response, 1, encodeCharacter(character))
|
||||
|
||||
Reference in New Issue
Block a user