feat: split client tooling and add OAuth server login
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
package gamedata
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// GrowthMaterial is a stack of ResourceTable material consumed by CharGrowth.
|
||||
type GrowthMaterial struct{ ID, Count uint64 }
|
||||
|
||||
// CharacterGrowth applies ResourceTable.MagicValue to the character's exact
|
||||
// CharGrowthTable/CharLevelTable curve from the installed GameData.
|
||||
func CharacterGrowth(root, version string, charID int, level, exp uint64, materials []GrowthMaterial) (uint64, uint64, []GrowthMaterial, error) {
|
||||
if charID <= 0 || level == 0 || len(materials) == 0 {
|
||||
return 0, 0, nil, fmt.Errorf("gamedata: invalid character growth input")
|
||||
}
|
||||
plain, err := ReadQuestDatabase(root, version)
|
||||
if err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
dir, err := os.MkdirTemp("", "bd2-growth-")
|
||||
if err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
path := filepath.Join(dir, "common.db")
|
||||
if err := os.WriteFile(path, plain, 0o600); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
db, err := sql.Open("sqlite", "file:"+filepath.ToSlash(path)+"?mode=ro")
|
||||
if err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var charProto []byte
|
||||
if err := db.QueryRow("SELECT ProtoBuf FROM CharTable WHERE id=?", charID).Scan(&charProto); err != nil {
|
||||
return 0, 0, nil, fmt.Errorf("gamedata: character %d: %w", charID, err)
|
||||
}
|
||||
growthIDs, err := packedInts(charProto, 1)
|
||||
if err != nil || len(growthIDs) != 1 {
|
||||
return 0, 0, nil, fmt.Errorf("gamedata: character %d growth ID %v: %w", charID, growthIDs, err)
|
||||
}
|
||||
var growthProto []byte
|
||||
if err := db.QueryRow("SELECT ProtoBuf FROM CharGrowthTable WHERE id=?", growthIDs[0]).Scan(&growthProto); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
groups, err := packedInts(growthProto, 1)
|
||||
if err != nil || len(groups) != 1 {
|
||||
return 0, 0, nil, fmt.Errorf("gamedata: growth %d group %v: %w", growthIDs[0], groups, err)
|
||||
}
|
||||
maximum, err := packedInts(growthProto, 9)
|
||||
if err != nil || len(maximum) != 1 || maximum[0] < level {
|
||||
return 0, 0, nil, fmt.Errorf("gamedata: growth %d maximum %v: %w", growthIDs[0], maximum, err)
|
||||
}
|
||||
|
||||
var gained uint64
|
||||
for _, material := range materials {
|
||||
if material.ID == 0 || material.Count == 0 {
|
||||
return 0, 0, nil, fmt.Errorf("gamedata: invalid growth material")
|
||||
}
|
||||
var proto []byte
|
||||
if err := db.QueryRow("SELECT ProtoBuf FROM ResourceTable WHERE id=?", material.ID).Scan(&proto); err != nil {
|
||||
return 0, 0, nil, fmt.Errorf("gamedata: resource %d: %w", material.ID, err)
|
||||
}
|
||||
values, err := packedInts(proto, 9)
|
||||
if err != nil || len(values) != 1 || values[0] == 0 {
|
||||
return 0, 0, nil, fmt.Errorf("gamedata: resource %d magic value %v: %w", material.ID, values, err)
|
||||
}
|
||||
if material.Count > (^uint64(0)-gained)/values[0] {
|
||||
return 0, 0, nil, fmt.Errorf("gamedata: growth experience overflow")
|
||||
}
|
||||
gained += material.Count * values[0]
|
||||
}
|
||||
|
||||
curve := make(map[uint64]uint64)
|
||||
rows, err := db.Query("SELECT ProtoBuf FROM CharLevelTable")
|
||||
if err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var proto []byte
|
||||
if err := rows.Scan(&proto); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
group, _ := packedInts(proto, 5)
|
||||
if len(group) != 1 || group[0] != groups[0] {
|
||||
continue
|
||||
}
|
||||
levels, _ := packedInts(proto, 7)
|
||||
required, _ := packedInts(proto, 8)
|
||||
if len(levels) == 1 && len(required) == 1 {
|
||||
curve[levels[0]] = required[0]
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
currentLevel, currentExp := level, exp
|
||||
for gained > 0 && currentLevel < maximum[0] {
|
||||
required := curve[currentLevel]
|
||||
if required == 0 || currentExp >= required {
|
||||
return 0, 0, nil, fmt.Errorf("gamedata: missing/invalid level curve group %d level %d", groups[0], currentLevel)
|
||||
}
|
||||
need := required - currentExp
|
||||
if gained < need {
|
||||
currentExp += gained
|
||||
gained = 0
|
||||
break
|
||||
}
|
||||
gained -= need
|
||||
currentLevel++
|
||||
currentExp = 0
|
||||
}
|
||||
if currentLevel == maximum[0] {
|
||||
currentExp = 0
|
||||
refunds, err := refundGrowthResources(db, gained)
|
||||
return currentLevel, currentExp, refunds, err
|
||||
}
|
||||
return currentLevel, currentExp, nil, nil
|
||||
}
|
||||
|
||||
// Convert max-level overflow back into the installed experience resources.
|
||||
// The official 2.34.13 server uses larger denominations greedily and rounds
|
||||
// the final remainder up to one smallest slime. This can refund slightly
|
||||
// more nominal EXP than the overflow: the verified tutorial sample has 551
|
||||
// overflow EXP and returns resource 7 x3 (600 EXP), not x2.
|
||||
func refundGrowthResources(db *sql.DB, overflow uint64) ([]GrowthMaterial, error) {
|
||||
if overflow == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
rows, err := db.Query("SELECT id,ProtoBuf FROM ResourceTable")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
type unit struct{ id, value uint64 }
|
||||
var units []unit
|
||||
for rows.Next() {
|
||||
var id uint64
|
||||
var proto []byte
|
||||
if err := rows.Scan(&id, &proto); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
kind, err := packedInts(proto, 13)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
value, err := packedInts(proto, 9)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(kind) == 1 && kind[0] == 1 && len(value) == 1 && value[0] > 0 {
|
||||
units = append(units, unit{id, value[0]})
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(units) == 0 {
|
||||
return nil, fmt.Errorf("gamedata: missing experience resources")
|
||||
}
|
||||
sort.Slice(units, func(i, j int) bool { return units[i].value > units[j].value })
|
||||
var refunds []GrowthMaterial
|
||||
remaining := overflow
|
||||
for i, unit := range units {
|
||||
count := remaining / unit.value
|
||||
if i == len(units)-1 && remaining%unit.value != 0 {
|
||||
count++
|
||||
}
|
||||
if count != 0 {
|
||||
refunds = append(refunds, GrowthMaterial{ID: unit.id, Count: count})
|
||||
if count > remaining/unit.value {
|
||||
remaining = 0
|
||||
} else {
|
||||
remaining -= count * unit.value
|
||||
}
|
||||
}
|
||||
}
|
||||
return refunds, nil
|
||||
}
|
||||
Reference in New Issue
Block a user