188 lines
5.7 KiB
Go
188 lines
5.7 KiB
Go
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
|
|
}
|