253 lines
6.4 KiB
Go
253 lines
6.4 KiB
Go
package gamedata
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"fmt"
|
|
"math"
|
|
"math/big"
|
|
"sort"
|
|
"sync"
|
|
)
|
|
|
|
// RewardGraph resolves random-box nodes; costume/equipment leaves retain
|
|
// their design IDs for the owning grant domain.
|
|
type RewardGraph struct {
|
|
mu sync.Mutex
|
|
boxes map[uint64]uint64
|
|
direct map[uint64]bool
|
|
special map[uint64]bool
|
|
groups map[uint64][]byte
|
|
sample func(uint64) (uint64, error)
|
|
}
|
|
|
|
func LoadRewardGraph(root, version string) (*RewardGraph, error) {
|
|
db, done, e := openStatDatabase(root, version)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
defer done()
|
|
g := &RewardGraph{boxes: map[uint64]uint64{}, direct: map[uint64]bool{}, special: map[uint64]bool{}, groups: map[uint64][]byte{}}
|
|
g.sample = func(n uint64) (uint64, error) {
|
|
if n == 0 {
|
|
return 0, fmt.Errorf("gamedata: empty reward pool")
|
|
}
|
|
v, e := rand.Int(rand.Reader, new(big.Int).SetUint64(n))
|
|
if e != nil {
|
|
return 0, e
|
|
}
|
|
return v.Uint64(), nil
|
|
}
|
|
rows, e := db.Query("SELECT id,ProtoBuf FROM RandomBoxTable")
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
for rows.Next() {
|
|
var id uint64
|
|
var raw []byte
|
|
if e = rows.Scan(&id, &raw); e != nil {
|
|
rows.Close()
|
|
return nil, e
|
|
}
|
|
gid, e := optionalScalar(raw, 9)
|
|
if e != nil {
|
|
rows.Close()
|
|
return nil, e
|
|
}
|
|
drop, err := optionalScalar(raw, 1)
|
|
if err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
// RBD_DIRECT owns the box itself; RBD_OPEN grants its selected contents.
|
|
// Design also contains special mode 10 boxes; only the verified OPEN
|
|
// mode is automatically expanded, other modes retain their entity.
|
|
if drop != 0 && drop != 1 && drop != 10 {
|
|
rows.Close()
|
|
return nil, fmt.Errorf("gamedata: unsupported random box drop type %d", drop)
|
|
}
|
|
g.direct[id] = drop != 0
|
|
g.special[id] = drop == 10
|
|
g.boxes[id] = gid
|
|
}
|
|
e = rows.Err()
|
|
rows.Close()
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
rows, e = db.Query("SELECT id,ProtoBuf FROM RewardGroupTable")
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
for rows.Next() {
|
|
var id uint64
|
|
var raw []byte
|
|
if e = rows.Scan(&id, &raw); e != nil {
|
|
rows.Close()
|
|
return nil, e
|
|
}
|
|
g.groups[id] = append([]byte(nil), raw...)
|
|
}
|
|
e = rows.Err()
|
|
rows.Close()
|
|
return g, e
|
|
}
|
|
func (g *RewardGraph) SetSampler(f func(uint64) (uint64, error)) {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
g.sample = f
|
|
}
|
|
func (g *RewardGraph) Resolve(rewards []BattleReward) ([]BattleReward, error) {
|
|
return g.resolve(rewards, true)
|
|
}
|
|
|
|
func (g *RewardGraph) resolve(rewards []BattleReward, openRoots bool) ([]BattleReward, error) {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
budget := uint64(100000)
|
|
totals := map[[2]uint64]uint64{}
|
|
seen := map[uint64]bool{}
|
|
var emit func(BattleReward, bool) error
|
|
emit = func(r BattleReward, forceOpen bool) error {
|
|
if r.Count == 0 || r.Count > math.MaxInt32 || r.Type == 0 {
|
|
return fmt.Errorf("gamedata: invalid reward graph quantity/type")
|
|
}
|
|
if budget == 0 {
|
|
return fmt.Errorf("gamedata: reward graph operation limit")
|
|
}
|
|
budget--
|
|
if r.Type == 9 {
|
|
if forceOpen && g.special[r.ID] {
|
|
return fmt.Errorf("gamedata: special random box %d cannot use generic opening", r.ID)
|
|
}
|
|
if _, ok := g.boxes[r.ID]; !ok {
|
|
return fmt.Errorf("gamedata: unknown random box %d", r.ID)
|
|
}
|
|
if _, ok := g.direct[r.ID]; !ok {
|
|
return fmt.Errorf("gamedata: missing random box drop type %d", r.ID)
|
|
}
|
|
}
|
|
if r.Type != 9 || !forceOpen && g.direct[r.ID] {
|
|
k := [2]uint64{r.Type, r.ID}
|
|
if totals[k] > math.MaxInt32-r.Count {
|
|
return fmt.Errorf("gamedata: reward graph overflow")
|
|
}
|
|
totals[k] += r.Count
|
|
return nil
|
|
}
|
|
if seen[r.ID] || len(seen) >= 32 {
|
|
return fmt.Errorf("gamedata: reward graph cycle")
|
|
}
|
|
gid, ok := g.boxes[r.ID]
|
|
if !ok {
|
|
return fmt.Errorf("gamedata: unknown random box %d", r.ID)
|
|
}
|
|
raw, ok := g.groups[gid]
|
|
if !ok {
|
|
return fmt.Errorf("gamedata: missing reward group %d", gid)
|
|
}
|
|
drop, e := optionalScalar(raw, 2)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
count, e := optionalScalar(raw, 1)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
children, e := eventGameRewards(raw, 6, 5, 4)
|
|
if e != nil || len(children) == 0 {
|
|
return fmt.Errorf("gamedata: invalid random box reward group %d", gid)
|
|
}
|
|
weights, e := packedInts(raw, 8)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
if drop > 1 || len(weights) != len(children) || drop == 0 && count == 0 {
|
|
return fmt.Errorf("gamedata: invalid reward group selection mode")
|
|
}
|
|
seen[r.ID] = true
|
|
defer delete(seen, r.ID)
|
|
for n := uint64(0); n < r.Count; n++ {
|
|
if budget == 0 {
|
|
return fmt.Errorf("gamedata: reward graph operation limit")
|
|
}
|
|
budget--
|
|
if drop == 1 {
|
|
// Local server policy: direct components independently roll their GameData percentage.
|
|
for i, child := range children {
|
|
if weights[i] > 100 {
|
|
return fmt.Errorf("gamedata: direct reward percent exceeds 100")
|
|
}
|
|
x, e := g.sample(100)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
if x >= 100 {
|
|
return fmt.Errorf("gamedata: sampler out of range")
|
|
}
|
|
if x >= weights[i] {
|
|
continue
|
|
}
|
|
if e = emit(child, false); e != nil {
|
|
return e
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
sum := uint64(0)
|
|
for _, w := range weights {
|
|
if sum > math.MaxUint64-w {
|
|
return fmt.Errorf("gamedata: reward graph weight overflow")
|
|
}
|
|
sum += w
|
|
}
|
|
if sum == 0 || g.sample == nil {
|
|
return fmt.Errorf("gamedata: empty weighted reward pool")
|
|
}
|
|
for i := uint64(0); i < count; i++ {
|
|
if budget == 0 {
|
|
return fmt.Errorf("gamedata: reward graph operation limit")
|
|
}
|
|
x, e := g.sample(sum)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
if x >= sum {
|
|
return fmt.Errorf("gamedata: random result outside reward pool")
|
|
}
|
|
for j, w := range weights {
|
|
if x < w {
|
|
if e = emit(children[j], false); e != nil {
|
|
return e
|
|
}
|
|
break
|
|
}
|
|
x -= w
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
for _, r := range rewards {
|
|
if e := emit(r, openRoots); e != nil {
|
|
return nil, e
|
|
}
|
|
}
|
|
var out []BattleReward
|
|
for k, v := range totals {
|
|
out = append(out, BattleReward{Type: k[0], ID: k[1], Count: v})
|
|
}
|
|
sort.Slice(out, func(i, j int) bool {
|
|
if out[i].Type != out[j].Type {
|
|
return out[i].Type < out[j].Type
|
|
}
|
|
return out[i].ID < out[j].ID
|
|
})
|
|
return out, nil
|
|
}
|
|
|
|
// ResolveGranted opens RBD_OPEN wrappers at every level, preserving RBD_DIRECT
|
|
// boxes as inventory items. Resolve force-opens only explicitly requested roots.
|
|
func (g *RewardGraph) ResolveGranted(rewards []BattleReward) ([]BattleReward, error) {
|
|
return g.resolve(rewards, false)
|
|
}
|