Generate equipment loot atomically, grant random boxes and prevent repeat claims. Read gacha pools, pity, products, tickets, missions, presets, chapter rewards and inventory costs from GameData; extend both collaboration UR ticket pools. Sync owned recipes, configure startup state and calendars in versioned seeds, and validate unlock tickets and revival talents.
105 lines
3.3 KiB
Go
105 lines
3.3 KiB
Go
package gamedata
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
)
|
|
|
|
// Cash product tickets are the deterministic CashProductTable -> RandomBoxTable
|
|
// -> RewardGroupTable grant. Their IDs and quantities are version facts.
|
|
func loadGachaCashRewards(db *sql.DB, group GachaGroupDesign) ([]BattleReward, error) {
|
|
var raw []byte
|
|
if err := db.QueryRow("SELECT ProtoBuf FROM CashProductTable WHERE GroupId=? AND id=? AND saleGroup=?", group.CashProductGroupID, group.CashProductID, group.CashSalesGroup).Scan(&raw); err != nil {
|
|
return nil, err
|
|
}
|
|
box, err := optionalScalar(raw, 14)
|
|
if err != nil || box == 0 {
|
|
return nil, fmt.Errorf("gamedata: gacha cash product has no random box")
|
|
}
|
|
if err := db.QueryRow("SELECT ProtoBuf FROM RandomBoxTable WHERE id=?", box).Scan(&raw); err != nil {
|
|
return nil, err
|
|
}
|
|
rewardID, err := optionalScalar(raw, 9)
|
|
if err != nil || rewardID == 0 {
|
|
return nil, fmt.Errorf("gamedata: gacha cash box has no reward group")
|
|
}
|
|
return deterministicGachaCashRewards(db, rewardID, map[uint64]bool{})
|
|
}
|
|
|
|
func deterministicGachaCashRewards(db *sql.DB, id uint64, visiting map[uint64]bool) ([]BattleReward, error) {
|
|
if visiting[id] {
|
|
return nil, fmt.Errorf("gamedata: cyclic cash reward group %d", id)
|
|
}
|
|
visiting[id] = true
|
|
defer delete(visiting, id)
|
|
var raw []byte
|
|
if err := db.QueryRow("SELECT ProtoBuf FROM RewardGroupTable WHERE id=?", id).Scan(&raw); err != nil {
|
|
return nil, err
|
|
}
|
|
dropType, err := optionalScalar(raw, 2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ids, err := packedInts(raw, 5)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
types, err := packedInts(raw, 6)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
counts, err := packedInts(raw, 4)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ratios, err := packedInts(raw, 8)
|
|
if err != nil || len(ratios) != len(ids) {
|
|
return nil, fmt.Errorf("gamedata: malformed cash reward ratio")
|
|
}
|
|
for _, ratio := range ratios {
|
|
if dropType == 0 && ratio == 0 || dropType == 1 && ratio != 100 {
|
|
return nil, fmt.Errorf("gamedata: cash reward is not deterministic")
|
|
}
|
|
}
|
|
if len(ids) == 0 || len(ids) != len(types) || len(ids) != len(counts) || dropType > 1 || dropType == 0 && len(ids) != 1 {
|
|
return nil, fmt.Errorf("gamedata: cash reward %d is not deterministic", id)
|
|
}
|
|
dropCount, err := optionalScalar(raw, 1)
|
|
if err != nil || dropType == 0 && dropCount == 0 {
|
|
return nil, fmt.Errorf("gamedata: cash reward has invalid drop count")
|
|
}
|
|
if dropType == 0 {
|
|
for i := range counts {
|
|
if counts[i] > ^uint64(0)/dropCount {
|
|
return nil, fmt.Errorf("gamedata: cash reward count overflow")
|
|
}
|
|
counts[i] *= dropCount
|
|
}
|
|
}
|
|
var out []BattleReward
|
|
for i, itemID := range ids {
|
|
if counts[i] == 0 {
|
|
return nil, fmt.Errorf("gamedata: cash reward %d has zero count", id)
|
|
}
|
|
if types[i] == 9 {
|
|
children, err := deterministicGachaCashRewards(db, itemID, visiting)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, child := range children {
|
|
if child.Count > ^uint64(0)/counts[i] {
|
|
return nil, fmt.Errorf("gamedata: cash reward overflow")
|
|
}
|
|
child.Count *= counts[i]
|
|
out = append(out, child)
|
|
}
|
|
} else {
|
|
if (!itemDBInfoRewardType(types[i]) && types[i] != 19) || itemID == 0 {
|
|
return nil, fmt.Errorf("gamedata: unsupported cash reward type %d", types[i])
|
|
}
|
|
out = append(out, BattleReward{Type: types[i], ID: itemID, Count: counts[i]})
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|