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.
138 lines
4.4 KiB
Go
138 lines
4.4 KiB
Go
package gamedata
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
// RandomBoxDesign contains only boxes whose GameData reward graph is wholly
|
|
// deterministic. The client asks the server to open every RandomBox, but an
|
|
// offline server must not invent an outcome for weighted boxes. Deterministic
|
|
// material boxes (including the engraving-scroll and essence boxes) are safe
|
|
// to resolve directly from the authoritative RewardGroupTable.
|
|
type RandomBoxDesign struct {
|
|
rewards map[uint64][]BattleReward
|
|
}
|
|
|
|
// LoadRandomBoxDesign reads RandomBoxTable -> RewardGroupTable from the
|
|
// installed common GameData. A box is deliberately omitted if its group is
|
|
// empty, weighted, malformed, cyclic, or resolves to a non-ItemDBInfo reward;
|
|
// callers then fail closed when a client tries to open it.
|
|
func LoadRandomBoxDesign(root, version string) (*RandomBoxDesign, error) {
|
|
db, release, err := OpenDatabase(root, version, "common")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer release()
|
|
|
|
groups := map[uint64][]BattleReward{}
|
|
rows, err := db.Query("SELECT id, ProtoBuf FROM RewardGroupTable")
|
|
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
|
|
}
|
|
ids, err := packedInts(raw, 5)
|
|
if err != nil {
|
|
rows.Close()
|
|
return nil, fmt.Errorf("gamedata: random box reward group %d item IDs: %w", id, err)
|
|
}
|
|
types, err := packedInts(raw, 6)
|
|
if err != nil {
|
|
rows.Close()
|
|
return nil, fmt.Errorf("gamedata: random box reward group %d item types: %w", id, err)
|
|
}
|
|
counts, err := packedInts(raw, 4)
|
|
if err != nil {
|
|
rows.Close()
|
|
return nil, fmt.Errorf("gamedata: random box reward group %d item counts: %w", id, err)
|
|
}
|
|
ratios, err := packedInts(raw, 8)
|
|
if err != nil {
|
|
rows.Close()
|
|
return nil, fmt.Errorf("gamedata: random box reward group %d ratios: %w", id, err)
|
|
}
|
|
// A single reward is deterministic. Ratios are presentation/probability
|
|
// metadata in that case; a multi-entry group is intentionally not used.
|
|
if len(ids) != 1 || len(types) != 1 || len(counts) != 1 || ids[0] == 0 || !itemDBInfoRewardType(types[0]) || counts[0] == 0 || (len(ratios) != 0 && len(ratios) != 1) {
|
|
continue
|
|
}
|
|
groups[id] = []BattleReward{{Type: types[0], ID: ids[0], Count: counts[0]}}
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
design := &RandomBoxDesign{rewards: map[uint64][]BattleReward{}}
|
|
rows, err = db.Query("SELECT id, ProtoBuf FROM RandomBoxTable")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for rows.Next() {
|
|
var rowID uint64
|
|
var raw []byte
|
|
if err := rows.Scan(&rowID, &raw); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
ids, err := packedInts(raw, 4)
|
|
if err != nil || len(ids) != 1 || ids[0] != rowID {
|
|
rows.Close()
|
|
return nil, fmt.Errorf("gamedata: malformed random box %d", rowID)
|
|
}
|
|
groupIDs, err := packedInts(raw, 9)
|
|
if err != nil || len(groupIDs) != 1 || groupIDs[0] == 0 {
|
|
rows.Close()
|
|
return nil, fmt.Errorf("gamedata: random box %d reward group: %w", rowID, err)
|
|
}
|
|
if rewards, ok := groups[groupIDs[0]]; ok {
|
|
design.rewards[rowID] = append([]BattleReward(nil), rewards...)
|
|
}
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
return design, nil
|
|
}
|
|
|
|
// itemDBInfoRewardType is the ItemDBInfo part of DataManager.GetItemInfo for
|
|
// the static tables this server persists in Inventory. Equipment, characters,
|
|
// costumes and currency each have a different domain store/response field, so
|
|
// a RandomBox resolving to one of them is intentionally not opened here.
|
|
func itemDBInfoRewardType(itemType uint64) bool {
|
|
switch itemType {
|
|
case 5, 7, 8, 9, 13, 14, 17, 27, 29:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Open returns the exact aggregate for count opens. It never samples a
|
|
// weighted group, which avoids silently fabricating a random result.
|
|
func (d *RandomBoxDesign) Open(boxID, count uint64) ([]BattleReward, error) {
|
|
if d == nil || count == 0 {
|
|
return nil, errors.New("gamedata: invalid random box open")
|
|
}
|
|
rewards, ok := d.rewards[boxID]
|
|
if !ok {
|
|
return nil, fmt.Errorf("gamedata: random box %d is not deterministic ItemDBInfo loot", boxID)
|
|
}
|
|
result := make([]BattleReward, len(rewards))
|
|
for i, reward := range rewards {
|
|
if reward.Count > ^uint64(0)/count {
|
|
return nil, errors.New("gamedata: random box reward count overflow")
|
|
}
|
|
result[i] = reward
|
|
result[i].Count *= count
|
|
}
|
|
return result, nil
|
|
}
|