feat(all): split client tooling and add OAuth server login
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
package gamedata
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
// CharacterTalent identifies the GameData rows which govern one character's
|
||||
// talent. MaxLevel is deliberately read per talent: not every talent has five
|
||||
// levels.
|
||||
type CharacterTalent struct {
|
||||
TalentID uint64
|
||||
GrowthGroup uint64
|
||||
MaxLevel uint64
|
||||
}
|
||||
|
||||
// TalentGrowthLevel describes the upgrade from Level to Level+1. NeedExp is
|
||||
// the experience earned within this level; player talent experience itself is
|
||||
// cumulative and is not reset by an upgrade.
|
||||
type TalentGrowthLevel struct {
|
||||
Level uint64
|
||||
NeedExp uint64
|
||||
Costs []PromotionCost
|
||||
}
|
||||
|
||||
type TalentUpgradeRule struct {
|
||||
CharacterID uint64
|
||||
TalentID uint64
|
||||
GrowthGroup uint64
|
||||
CurrentLevel uint64
|
||||
MaxLevel uint64
|
||||
RequiredTotalExp uint64
|
||||
Costs []PromotionCost
|
||||
}
|
||||
|
||||
// TalentGrowthDesign joins CharTable -> TalentTable -> TalentGrowthTable so
|
||||
// the runtime never guesses a growth group or hard-codes skill-book IDs.
|
||||
type TalentGrowthDesign struct {
|
||||
Characters map[uint64]CharacterTalent
|
||||
Levels map[[2]uint64]TalentGrowthLevel
|
||||
}
|
||||
|
||||
func LoadTalentGrowthDesign(root, version string) (*TalentGrowthDesign, error) {
|
||||
db, closeDB, err := openStatDatabase(root, version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer closeDB()
|
||||
return loadTalentGrowthDesign(db)
|
||||
}
|
||||
|
||||
func loadTalentGrowthDesign(db *sql.DB) (*TalentGrowthDesign, error) {
|
||||
if db == nil {
|
||||
return nil, errors.New("gamedata: nil talent growth database")
|
||||
}
|
||||
talents := make(map[uint64]CharacterTalent)
|
||||
rows, err := db.Query("SELECT id,ProtoBuf FROM TalentTable ORDER BY id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for rows.Next() {
|
||||
var id uint64
|
||||
var raw []byte
|
||||
if err := rows.Scan(&id, &raw); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
groups, err := packedInts(raw, 6)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
return nil, fmt.Errorf("gamedata: talent %d growth group: %w", id, err)
|
||||
}
|
||||
maximum, err := packedInts(raw, 11)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
return nil, fmt.Errorf("gamedata: talent %d maximum level: %w", id, err)
|
||||
}
|
||||
if len(groups) != 1 || groups[0] == 0 || len(maximum) != 1 || maximum[0] == 0 {
|
||||
rows.Close()
|
||||
return nil, fmt.Errorf("gamedata: talent %d has invalid growth metadata", id)
|
||||
}
|
||||
talents[id] = CharacterTalent{TalentID: id, GrowthGroup: groups[0], MaxLevel: maximum[0]}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
design := &TalentGrowthDesign{
|
||||
Characters: make(map[uint64]CharacterTalent),
|
||||
Levels: make(map[[2]uint64]TalentGrowthLevel),
|
||||
}
|
||||
rows, err = db.Query("SELECT id,ProtoBuf FROM CharTable ORDER BY id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for rows.Next() {
|
||||
var characterID uint64
|
||||
var raw []byte
|
||||
if err := rows.Scan(&characterID, &raw); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
talentIDs, err := packedInts(raw, 18)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
return nil, fmt.Errorf("gamedata: character %d talent: %w", characterID, err)
|
||||
}
|
||||
if len(talentIDs) == 0 {
|
||||
continue
|
||||
}
|
||||
if len(talentIDs) != 1 || talentIDs[0] == 0 {
|
||||
rows.Close()
|
||||
return nil, fmt.Errorf("gamedata: character %d has invalid talent reference", characterID)
|
||||
}
|
||||
talent, found := talents[talentIDs[0]]
|
||||
if !found {
|
||||
rows.Close()
|
||||
return nil, fmt.Errorf("gamedata: character %d references unknown talent %d", characterID, talentIDs[0])
|
||||
}
|
||||
design.Characters[characterID] = talent
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows, err = db.Query("SELECT groupId,id,ProtoBuf FROM TalentGrowthTable ORDER BY groupId,id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for rows.Next() {
|
||||
var group, level uint64
|
||||
var raw []byte
|
||||
if err := rows.Scan(&group, &level, &raw); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
counts, countErr := packedInts(raw, 2)
|
||||
ids, idErr := packedInts(raw, 3)
|
||||
types, typeErr := packedInts(raw, 4)
|
||||
needs, needErr := packedInts(raw, 6)
|
||||
if countErr != nil || idErr != nil || typeErr != nil || needErr != nil {
|
||||
rows.Close()
|
||||
return nil, fmt.Errorf("gamedata: talent growth %d/%d has invalid fields", group, level)
|
||||
}
|
||||
if group == 0 || level == 0 || len(counts) != len(ids) || len(ids) != len(types) || len(needs) > 1 {
|
||||
rows.Close()
|
||||
return nil, fmt.Errorf("gamedata: talent growth %d/%d has mismatched fields", group, level)
|
||||
}
|
||||
entry := TalentGrowthLevel{Level: level}
|
||||
if len(needs) == 1 {
|
||||
entry.NeedExp = needs[0]
|
||||
}
|
||||
for i := range counts {
|
||||
if types[i] == 0 || counts[i] == 0 || (types[i] == 4 && ids[i] != 0) || (types[i] != 4 && ids[i] == 0) {
|
||||
rows.Close()
|
||||
return nil, fmt.Errorf("gamedata: talent growth %d/%d has invalid cost", group, level)
|
||||
}
|
||||
entry.Costs = append(entry.Costs, PromotionCost{Type: types[i], ID: ids[i], Count: counts[i]})
|
||||
}
|
||||
key := [2]uint64{group, level}
|
||||
if _, exists := design.Levels[key]; exists {
|
||||
rows.Close()
|
||||
return nil, fmt.Errorf("gamedata: duplicate talent growth %d/%d", group, level)
|
||||
}
|
||||
design.Levels[key] = entry
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(design.Characters) == 0 || len(design.Levels) == 0 {
|
||||
return nil, errors.New("gamedata: empty talent growth design")
|
||||
}
|
||||
validated := make(map[uint64]bool)
|
||||
for _, talent := range design.Characters {
|
||||
if validated[talent.TalentID] {
|
||||
continue
|
||||
}
|
||||
validated[talent.TalentID] = true
|
||||
for level := uint64(1); level < talent.MaxLevel; level++ {
|
||||
entry, ok := design.Levels[[2]uint64{talent.GrowthGroup, level}]
|
||||
if !ok || entry.NeedExp == 0 || len(entry.Costs) == 0 {
|
||||
return nil, fmt.Errorf("gamedata: talent %d missing upgrade level %d", talent.TalentID, level)
|
||||
}
|
||||
}
|
||||
}
|
||||
return design, nil
|
||||
}
|
||||
|
||||
// UpgradeRule returns the exact current-level requirement and cumulative
|
||||
// experience threshold. It intentionally refuses max-level characters.
|
||||
func (d *TalentGrowthDesign) UpgradeRule(characterID, currentLevel uint64) (TalentUpgradeRule, error) {
|
||||
if d == nil || characterID == 0 || currentLevel == 0 {
|
||||
return TalentUpgradeRule{}, errors.New("gamedata: invalid talent upgrade lookup")
|
||||
}
|
||||
talent, found := d.Characters[characterID]
|
||||
if !found {
|
||||
return TalentUpgradeRule{}, fmt.Errorf("gamedata: character %d has no talent growth design", characterID)
|
||||
}
|
||||
if currentLevel >= talent.MaxLevel {
|
||||
return TalentUpgradeRule{}, fmt.Errorf("gamedata: character %d talent is already at maximum level %d", characterID, talent.MaxLevel)
|
||||
}
|
||||
var required uint64
|
||||
for level := uint64(1); level <= currentLevel; level++ {
|
||||
entry, ok := d.Levels[[2]uint64{talent.GrowthGroup, level}]
|
||||
if !ok || entry.NeedExp == 0 {
|
||||
return TalentUpgradeRule{}, fmt.Errorf("gamedata: talent %d missing growth level %d", talent.TalentID, level)
|
||||
}
|
||||
if required > math.MaxUint64-entry.NeedExp {
|
||||
return TalentUpgradeRule{}, fmt.Errorf("gamedata: talent %d experience threshold overflows", talent.TalentID)
|
||||
}
|
||||
required += entry.NeedExp
|
||||
}
|
||||
current := d.Levels[[2]uint64{talent.GrowthGroup, currentLevel}]
|
||||
if len(current.Costs) == 0 {
|
||||
return TalentUpgradeRule{}, fmt.Errorf("gamedata: talent %d level %d has no upgrade costs", talent.TalentID, currentLevel)
|
||||
}
|
||||
return TalentUpgradeRule{
|
||||
CharacterID: characterID, TalentID: talent.TalentID, GrowthGroup: talent.GrowthGroup,
|
||||
CurrentLevel: currentLevel, MaxLevel: talent.MaxLevel, RequiredTotalExp: required,
|
||||
Costs: append([]PromotionCost(nil), current.Costs...),
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user