Implement hunting AP and progress, event tasks, exchanges, games, stories and actions; connect battle and reward handling. Add CashShop plugin and installation, server purchases and entitlements, recharge rules and plugin version metadata. Distribute binary calendars independently; cache GameData in memory and fix single-connection query deadlocks. Improve automatic login and recovery diagnostics; document configuration and release flows.
326 lines
12 KiB
Go
326 lines
12 KiB
Go
package gamedata
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
)
|
|
|
|
// PromotionCost is one exact CharGrowthTable.ClassupItem{Type,Id,Count} row.
|
|
// Currency (type 4) has no inventory index; the other rows are owned items.
|
|
type PromotionCost struct{ Type, ID, Count uint64 }
|
|
|
|
// PromotionGrowthResult is the authoritative result of one CharGrowth request
|
|
// that can cross one or more class-up boundaries. Costs contains the exact
|
|
// cumulative class-up costs selected by the request's aggregate gold amount.
|
|
type PromotionGrowthResult struct {
|
|
CharacterID uint64
|
|
Level uint64
|
|
Exp uint64
|
|
Costs []PromotionCost
|
|
Refunds []GrowthMaterial
|
|
}
|
|
|
|
// CharacterPromotion reads the installed character and growth tables. At a
|
|
// stage cap the client sends its class-up materials through CharGrowth, then
|
|
// detects a promotion by comparing the response character ID to the old ID.
|
|
func CharacterPromotion(root, version string, charID int, level, exp uint64) (uint64, []PromotionCost, error) {
|
|
db, release, err := OpenDatabase(root, version, "common")
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
defer release()
|
|
return characterPromotion(db, charID, level, exp)
|
|
}
|
|
|
|
// CharacterGrowthPromotions applies a client's combined class-up/growth
|
|
// request. The UI aggregates gold and class-up resources for every boundary
|
|
// crossed by the selected target level, while ordinary EXP resources remain
|
|
// in the same request. Gold therefore identifies an exact prefix of the
|
|
// CharTable.NextCharId chain; each prefix cost is then subtracted from the
|
|
// submitted resources and only the remainder contributes EXP.
|
|
func CharacterGrowthPromotions(root, version string, charID int, level, exp uint64, submitted []PromotionCost) (PromotionGrowthResult, error) {
|
|
db, release, err := OpenDatabase(root, version, "common")
|
|
if err != nil {
|
|
return PromotionGrowthResult{}, err
|
|
}
|
|
defer release()
|
|
return characterGrowthPromotions(db, charID, level, exp, submitted)
|
|
}
|
|
|
|
type promotionStage struct {
|
|
fromID, nextID uint64
|
|
maximum uint64
|
|
costs []PromotionCost
|
|
}
|
|
|
|
func characterGrowthPromotions(db *sql.DB, charID int, level, exp uint64, submitted []PromotionCost) (PromotionGrowthResult, error) {
|
|
if charID <= 0 || level == 0 || len(submitted) == 0 {
|
|
return PromotionGrowthResult{}, fmt.Errorf("gamedata: invalid combined promotion input")
|
|
}
|
|
requested := make(map[[2]uint64]uint64, len(submitted))
|
|
var requestedGold uint64
|
|
for _, cost := range submitted {
|
|
if cost.Count == 0 || (cost.Type != 4 && (cost.Type != 8 || cost.ID == 0)) {
|
|
return PromotionGrowthResult{}, fmt.Errorf("gamedata: invalid submitted promotion material")
|
|
}
|
|
key := [2]uint64{cost.Type, cost.ID}
|
|
if cost.Count > ^uint64(0)-requested[key] {
|
|
return PromotionGrowthResult{}, fmt.Errorf("gamedata: submitted promotion material overflow")
|
|
}
|
|
requested[key] += cost.Count
|
|
if cost.Type == 4 {
|
|
if cost.ID != 0 || requestedGold != 0 {
|
|
return PromotionGrowthResult{}, fmt.Errorf("gamedata: invalid submitted promotion currency")
|
|
}
|
|
requestedGold = cost.Count
|
|
}
|
|
}
|
|
if requestedGold == 0 {
|
|
return PromotionGrowthResult{}, fmt.Errorf("gamedata: combined promotion has no gold")
|
|
}
|
|
|
|
currentID := uint64(charID)
|
|
var stages []promotionStage
|
|
var cumulative []PromotionCost
|
|
var cumulativeGold uint64
|
|
for len(stages) < 32 {
|
|
nextID, costs, maximum, err := promotionDefinition(db, currentID)
|
|
if err != nil {
|
|
return PromotionGrowthResult{}, err
|
|
}
|
|
stages = append(stages, promotionStage{fromID: currentID, nextID: nextID, maximum: maximum, costs: costs})
|
|
cumulative = append(cumulative, costs...)
|
|
for _, cost := range costs {
|
|
if cost.Type == 4 {
|
|
if cost.Count > ^uint64(0)-cumulativeGold {
|
|
return PromotionGrowthResult{}, fmt.Errorf("gamedata: cumulative promotion gold overflow")
|
|
}
|
|
cumulativeGold += cost.Count
|
|
}
|
|
}
|
|
if cumulativeGold == requestedGold {
|
|
break
|
|
}
|
|
if cumulativeGold > requestedGold {
|
|
return PromotionGrowthResult{}, fmt.Errorf("gamedata: submitted gold %d does not match a promotion prefix", requestedGold)
|
|
}
|
|
currentID = nextID
|
|
}
|
|
if cumulativeGold != requestedGold {
|
|
return PromotionGrowthResult{}, fmt.Errorf("gamedata: submitted gold %d exceeds the promotion chain", requestedGold)
|
|
}
|
|
|
|
remaining := make(map[[2]uint64]uint64, len(requested))
|
|
for key, count := range requested {
|
|
remaining[key] = count
|
|
}
|
|
for _, cost := range cumulative {
|
|
key := [2]uint64{cost.Type, cost.ID}
|
|
if remaining[key] < cost.Count {
|
|
return PromotionGrowthResult{}, fmt.Errorf("gamedata: missing cumulative promotion cost %d/%d x%d", cost.Type, cost.ID, cost.Count-remaining[key])
|
|
}
|
|
remaining[key] -= cost.Count
|
|
}
|
|
if remaining[[2]uint64{4, 0}] != 0 {
|
|
return PromotionGrowthResult{}, fmt.Errorf("gamedata: unexpected promotion gold remainder")
|
|
}
|
|
|
|
var gained uint64
|
|
for key, count := range remaining {
|
|
if count == 0 {
|
|
continue
|
|
}
|
|
if key[0] != 8 || key[1] == 0 {
|
|
return PromotionGrowthResult{}, fmt.Errorf("gamedata: unexpected combined promotion material %d/%d x%d", key[0], key[1], count)
|
|
}
|
|
var proto []byte
|
|
if err := db.QueryRow("SELECT ProtoBuf FROM ResourceTable WHERE id=?", key[1]).Scan(&proto); err != nil {
|
|
return PromotionGrowthResult{}, fmt.Errorf("gamedata: promotion growth resource %d: %w", key[1], err)
|
|
}
|
|
values, err := packedInts(proto, 9)
|
|
if err != nil || len(values) != 1 || values[0] == 0 {
|
|
return PromotionGrowthResult{}, fmt.Errorf("gamedata: resource %d is not an experience material", key[1])
|
|
}
|
|
if count > (^uint64(0)-gained)/values[0] {
|
|
return PromotionGrowthResult{}, fmt.Errorf("gamedata: combined promotion experience overflow")
|
|
}
|
|
gained += count * values[0]
|
|
}
|
|
|
|
currentID = uint64(charID)
|
|
currentLevel := level
|
|
currentExp := exp
|
|
for _, stage := range stages {
|
|
if currentID != stage.fromID {
|
|
return PromotionGrowthResult{}, fmt.Errorf("gamedata: invalid promotion stage sequence")
|
|
}
|
|
definition, err := growthDefinition(db, currentID)
|
|
if err != nil {
|
|
return PromotionGrowthResult{}, err
|
|
}
|
|
currentLevel, currentExp, gained, err = applyGrowthExperience(db, definition, currentLevel, currentExp, gained)
|
|
if err != nil {
|
|
return PromotionGrowthResult{}, err
|
|
}
|
|
if currentLevel != stage.maximum {
|
|
return PromotionGrowthResult{}, fmt.Errorf("gamedata: insufficient experience to reach promotion cap %d", definition.maximum)
|
|
}
|
|
currentID = stage.nextID
|
|
}
|
|
finalDefinition, err := growthDefinition(db, currentID)
|
|
if err != nil {
|
|
return PromotionGrowthResult{}, err
|
|
}
|
|
currentLevel, currentExp, gained, err = applyGrowthExperience(db, finalDefinition, currentLevel, currentExp, gained)
|
|
if err != nil {
|
|
return PromotionGrowthResult{}, err
|
|
}
|
|
refunds, err := refundGrowthResources(db, gained)
|
|
if err != nil {
|
|
return PromotionGrowthResult{}, err
|
|
}
|
|
return PromotionGrowthResult{CharacterID: currentID, Level: currentLevel, Exp: currentExp, Costs: cumulative, Refunds: refunds}, nil
|
|
}
|
|
|
|
type characterGrowthDefinition struct {
|
|
group, maximum uint64
|
|
}
|
|
|
|
func growthDefinition(db *sql.DB, charID uint64) (characterGrowthDefinition, error) {
|
|
var charProto []byte
|
|
if err := db.QueryRow("SELECT ProtoBuf FROM CharTable WHERE id=?", charID).Scan(&charProto); err != nil {
|
|
return characterGrowthDefinition{}, fmt.Errorf("gamedata: character %d: %w", charID, err)
|
|
}
|
|
growthIDs, _ := packedInts(charProto, 1)
|
|
if len(growthIDs) != 1 {
|
|
return characterGrowthDefinition{}, fmt.Errorf("gamedata: character %d has no growth definition", charID)
|
|
}
|
|
var growthProto []byte
|
|
if err := db.QueryRow("SELECT ProtoBuf FROM CharGrowthTable WHERE id=?", growthIDs[0]).Scan(&growthProto); err != nil {
|
|
return characterGrowthDefinition{}, err
|
|
}
|
|
groups, _ := packedInts(growthProto, 1)
|
|
maximum, _ := packedInts(growthProto, 9)
|
|
if len(groups) != 1 || len(maximum) != 1 {
|
|
return characterGrowthDefinition{}, fmt.Errorf("gamedata: invalid growth definition %d", growthIDs[0])
|
|
}
|
|
return characterGrowthDefinition{group: groups[0], maximum: maximum[0]}, nil
|
|
}
|
|
|
|
func promotionDefinition(db *sql.DB, charID uint64) (uint64, []PromotionCost, uint64, error) {
|
|
definition, err := growthDefinition(db, charID)
|
|
if err != nil {
|
|
return 0, nil, 0, err
|
|
}
|
|
var charProto []byte
|
|
if err := db.QueryRow("SELECT ProtoBuf FROM CharTable WHERE id=?", charID).Scan(&charProto); err != nil {
|
|
return 0, nil, 0, err
|
|
}
|
|
next, _ := packedInts(charProto, 15)
|
|
if len(next) != 1 || next[0] == 0 {
|
|
return 0, nil, 0, fmt.Errorf("gamedata: character %d has no next stage", charID)
|
|
}
|
|
growthIDs, _ := packedInts(charProto, 1)
|
|
var growthProto []byte
|
|
if err := db.QueryRow("SELECT ProtoBuf FROM CharGrowthTable WHERE id=?", growthIDs[0]).Scan(&growthProto); err != nil {
|
|
return 0, nil, 0, err
|
|
}
|
|
types, _ := packedInts(growthProto, 4)
|
|
ids, _ := packedInts(growthProto, 3)
|
|
counts, _ := packedInts(growthProto, 2)
|
|
if len(types) == 0 || len(types) != len(ids) || len(types) != len(counts) {
|
|
return 0, nil, 0, fmt.Errorf("gamedata: invalid promotion costs for character %d", charID)
|
|
}
|
|
costs := make([]PromotionCost, len(types))
|
|
for i := range costs {
|
|
costs[i] = PromotionCost{Type: types[i], ID: ids[i], Count: counts[i]}
|
|
}
|
|
return next[0], costs, definition.maximum, nil
|
|
}
|
|
|
|
func applyGrowthExperience(db *sql.DB, definition characterGrowthDefinition, level, exp, gained uint64) (uint64, uint64, uint64, error) {
|
|
if level > definition.maximum {
|
|
return 0, 0, 0, fmt.Errorf("gamedata: level %d exceeds growth maximum %d", level, definition.maximum)
|
|
}
|
|
for gained > 0 && level < definition.maximum {
|
|
var proto []byte
|
|
if err := db.QueryRow("SELECT ProtoBuf FROM CharLevelTable WHERE GroupId=? AND id=?", definition.group, level).Scan(&proto); err != nil {
|
|
return 0, 0, 0, err
|
|
}
|
|
required, _ := packedInts(proto, 8)
|
|
if len(required) != 1 || required[0] == 0 || exp >= required[0] {
|
|
return 0, 0, 0, fmt.Errorf("gamedata: invalid level curve group %d level %d", definition.group, level)
|
|
}
|
|
need := required[0] - exp
|
|
if gained < need {
|
|
exp += gained
|
|
gained = 0
|
|
break
|
|
}
|
|
gained -= need
|
|
level++
|
|
exp = 0
|
|
}
|
|
if level == definition.maximum {
|
|
exp = 0
|
|
}
|
|
return level, exp, gained, nil
|
|
}
|
|
|
|
func characterPromotion(db *sql.DB, charID int, level, exp uint64) (uint64, []PromotionCost, error) {
|
|
if charID <= 0 || level == 0 {
|
|
return 0, nil, fmt.Errorf("gamedata: invalid promotion character or level")
|
|
}
|
|
var current []byte
|
|
if err := db.QueryRow("SELECT ProtoBuf FROM CharTable WHERE id=?", charID).Scan(¤t); err != nil {
|
|
return 0, nil, fmt.Errorf("gamedata: promotion character %d: %w", charID, err)
|
|
}
|
|
next, err := packedInts(current, 15)
|
|
if err != nil || len(next) != 1 || next[0] == 0 {
|
|
return 0, nil, fmt.Errorf("gamedata: character %d has no next stage", charID)
|
|
}
|
|
growthID, err := packedInts(current, 1)
|
|
if err != nil || len(growthID) != 1 {
|
|
return 0, nil, fmt.Errorf("gamedata: character %d has no growth definition", charID)
|
|
}
|
|
var growth []byte
|
|
if err := db.QueryRow("SELECT ProtoBuf FROM CharGrowthTable WHERE id=?", growthID[0]).Scan(&growth); err != nil {
|
|
return 0, nil, err
|
|
}
|
|
maximum, _ := packedInts(growth, 9)
|
|
group, _ := packedInts(growth, 1)
|
|
if len(maximum) != 1 || len(group) != 1 || level != maximum[0] {
|
|
return 0, nil, fmt.Errorf("gamedata: character %d has not reached its promotion level", charID)
|
|
}
|
|
var curve []byte
|
|
if err := db.QueryRow("SELECT ProtoBuf FROM CharLevelTable WHERE GroupId=? AND id=?", group[0], level).Scan(&curve); err != nil {
|
|
return 0, nil, err
|
|
}
|
|
required, _ := packedInts(curve, 8)
|
|
if len(required) != 0 && exp < required[0] {
|
|
return 0, nil, fmt.Errorf("gamedata: character %d has not reached its promotion experience", charID)
|
|
}
|
|
var nextProto []byte
|
|
if err := db.QueryRow("SELECT ProtoBuf FROM CharTable WHERE id=?", next[0]).Scan(&nextProto); err != nil {
|
|
return 0, nil, fmt.Errorf("gamedata: next character %d: %w", next[0], err)
|
|
}
|
|
nextGrowth, _ := packedInts(nextProto, 1)
|
|
if len(nextGrowth) != 1 || nextGrowth[0] == growthID[0] {
|
|
return 0, nil, fmt.Errorf("gamedata: invalid promotion %d -> %d", charID, next[0])
|
|
}
|
|
types, _ := packedInts(growth, 4)
|
|
ids, _ := packedInts(growth, 3)
|
|
counts, _ := packedInts(growth, 2)
|
|
if len(types) == 0 || len(types) != len(ids) || len(types) != len(counts) {
|
|
return 0, nil, fmt.Errorf("gamedata: invalid promotion costs for character %d", charID)
|
|
}
|
|
costs := make([]PromotionCost, len(types))
|
|
for i := range costs {
|
|
if counts[i] == 0 || (types[i] != 4 && ids[i] == 0) {
|
|
return 0, nil, fmt.Errorf("gamedata: invalid promotion cost for character %d", charID)
|
|
}
|
|
costs[i] = PromotionCost{Type: types[i], ID: ids[i], Count: counts[i]}
|
|
}
|
|
return next[0], costs, nil
|
|
}
|