fix(server): initialize temporary story talents from valid GameData skill levels
This commit is contained in:
@@ -5,6 +5,7 @@ import "fmt"
|
||||
type StoryCharacterDesign struct {
|
||||
CharacterID, UniqueCharacterID, Level, CostumeID, HP, Order uint64
|
||||
TemporaryPack uint64
|
||||
InitialTalentLevel uint64
|
||||
}
|
||||
|
||||
// StoryCharacterCatalog contains only authored temporary character rows.
|
||||
@@ -32,6 +33,7 @@ func LoadStoryCharacterCatalog(root, version string, packs []int, placeholderCos
|
||||
}
|
||||
catalog := &StoryCharacterCatalog{formations: map[int]map[int]QuestFormation{}, characters: map[[2]int][]StoryCharacterDesign{}}
|
||||
instances := map[[3]uint64]uint64{}
|
||||
initialTalents := map[uint64]uint64{}
|
||||
for _, pack := range packs {
|
||||
formations, err := loadQuestFormationsDB(db, pack)
|
||||
if err != nil {
|
||||
@@ -62,6 +64,23 @@ func LoadStoryCharacterCatalog(root, version string, packs []int, placeholderCos
|
||||
if characterType != 1 || temporaryPack == 0 {
|
||||
continue
|
||||
}
|
||||
talentID, err := optionalScalar(data, 18)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var talentLevel uint64
|
||||
if talentID != 0 {
|
||||
// CharDBInfo.talent_level is the TalentSkillTable row ID,
|
||||
// independent of the authored battle character level.
|
||||
var known bool
|
||||
talentLevel, known = initialTalents[talentID]
|
||||
if !known {
|
||||
if err := db.QueryRow("SELECT MIN(skill.id) FROM TalentTable talent JOIN TalentSkillTable skill ON skill.groupId=talent.talentSkillGroupId WHERE talent.id=? AND skill.id>0", talentID).Scan(&talentLevel); err != nil {
|
||||
return nil, fmt.Errorf("gamedata: story character %d initial talent %d: %w", row.CharacterID, talentID, err)
|
||||
}
|
||||
initialTalents[talentID] = talentLevel
|
||||
}
|
||||
}
|
||||
costume, err := requiredScalar(data, 5)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -85,7 +104,11 @@ func LoadStoryCharacterCatalog(root, version string, packs []int, placeholderCos
|
||||
}
|
||||
instances[instance] = costume
|
||||
key := [2]int{pack, quest}
|
||||
catalog.characters[key] = append(catalog.characters[key], StoryCharacterDesign{row.CharacterID, unique, row.Level, costume, uint64(base.Health), row.Order, temporaryPack})
|
||||
catalog.characters[key] = append(catalog.characters[key], StoryCharacterDesign{
|
||||
CharacterID: row.CharacterID, UniqueCharacterID: unique, Level: row.Level,
|
||||
CostumeID: costume, HP: uint64(base.Health), Order: row.Order,
|
||||
TemporaryPack: temporaryPack, InitialTalentLevel: talentLevel,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func (s *Service) ResolveStoryParty(packID, questID int) ([]player.Character, er
|
||||
return nil, fmt.Errorf("world: story instance namespace overflow")
|
||||
}
|
||||
index := player.StoryCharacterIndexBase | uint64(packID)<<40 | d.CharacterID<<8 | d.Level
|
||||
temporary = append(temporary, player.Character{InvenIndex: index, ID: d.CharacterID, HP: d.HP, Level: d.Level, CostumeID: d.CostumeID})
|
||||
temporary = append(temporary, player.Character{InvenIndex: index, ID: d.CharacterID, HP: d.HP, Level: d.Level, CostumeID: d.CostumeID, TalentLevel: d.InitialTalentLevel})
|
||||
}
|
||||
if s.characters == nil {
|
||||
return nil, fmt.Errorf("world: story character store unavailable")
|
||||
|
||||
@@ -1,14 +1,98 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"bd2server/internal/server/accountstate"
|
||||
"bd2server/internal/server/gamedata"
|
||||
"bd2server/internal/server/player"
|
||||
"bd2server/internal/server/stateio"
|
||||
"bd2server/internal/server/wire"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// CharTable10140 is the temporary blacksmith joining pack1 quest11.
|
||||
// Its TalentSkillTable group901 has row1 only; omitted field8 defaults to zero
|
||||
// in the client and causes a lookup of the nonexistent group901/row0.
|
||||
func TestInstalledStoryPartyInitialTalentPersistsAndEncodes(t *testing.T) {
|
||||
root := os.Getenv("BD2_REAL_GAMEDATA")
|
||||
if root == "" {
|
||||
t.Skip("BD2_REAL_GAMEDATA not configured")
|
||||
}
|
||||
catalog, err := gamedata.LoadStoryCharacterCatalog(root, "20260923193640", []int{1}, 996000)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := filepath.Join(t.TempDir(), "state.db")
|
||||
open := func() (*accountstate.Repository, *Service) {
|
||||
t.Helper()
|
||||
storage, err := accountstate.Open(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = storage.Close() })
|
||||
inventory, err := player.OpenInventory(storage, &player.Starter{Version: "2.35.10"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := testService()
|
||||
s.storyRoster = catalog
|
||||
s.characters, err = player.OpenCharacterStore(storage, nil, inventory, "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.characters.EnsurePersisted(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return storage, s
|
||||
}
|
||||
storage, s := open()
|
||||
party, err := s.ResolveStoryParty(1, 11)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var joined player.Character
|
||||
for _, c := range party {
|
||||
if c.ID == 10140 {
|
||||
joined = c
|
||||
}
|
||||
}
|
||||
if joined.InvenIndex == 0 || joined.TalentLevel != 1 {
|
||||
t.Fatalf("temporary blacksmith has invalid initial talent: %+v", joined)
|
||||
}
|
||||
if err = s.characters.SetCurrentHealth(joined.InvenIndex, 7); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = storage.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, s = open()
|
||||
characters, _, err := s.resolveActivePartyWires(1, 11)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, data := range characters {
|
||||
id, _, err := wire.Varint(data, 2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if id != joined.ID {
|
||||
continue
|
||||
}
|
||||
level, present, err := wire.Varint(data, 8)
|
||||
if err != nil || !present || level != 1 {
|
||||
t.Fatalf("CharDBInfo talent_level=%d present=%v err=%v", level, present, err)
|
||||
}
|
||||
hp, _, err := wire.Varint(data, 3)
|
||||
if err != nil || hp != 7 {
|
||||
t.Fatalf("reconnect changed temporary HP=%d err=%v", hp, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Fatal("temporary blacksmith omitted from party CharDBInfo")
|
||||
}
|
||||
|
||||
func TestInstalledAdjacentStoryPartyRetainsHealth(t *testing.T) {
|
||||
root := os.Getenv("BD2_REAL_GAMEDATA")
|
||||
if root == "" {
|
||||
|
||||
Reference in New Issue
Block a user