366 lines
11 KiB
Go
366 lines
11 KiB
Go
package gamedata
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
)
|
|
|
|
// FirstGachaDesign is the free starter reroll referenced by GameDefaultTable.
|
|
// It is separate from ordinary and cash-product gachas because its reward
|
|
// program emits both costumes and equipment and has no purchase currency.
|
|
type FirstGachaDesign struct {
|
|
Group GachaGroupDesign
|
|
GachaID uint64
|
|
Count int
|
|
RewardGroup *FirstGachaRewardGroup
|
|
characters map[uint64]CharacterDesign
|
|
equipment *EquipmentGachaCatalog
|
|
}
|
|
|
|
type FirstGachaReward struct{ Type, ID uint64 }
|
|
type FirstGachaRewardGroup struct {
|
|
ID, DropCount, DropType uint64
|
|
Entries []FirstGachaRewardEntry
|
|
}
|
|
type FirstGachaRewardEntry struct {
|
|
ItemType, ItemID, Count, Weight uint64
|
|
Group *FirstGachaRewardGroup
|
|
}
|
|
|
|
func (d *FirstGachaDesign) Character(id uint64) (CharacterDesign, bool) {
|
|
v, ok := d.characters[id]
|
|
return v, ok
|
|
}
|
|
func (d *FirstGachaDesign) EquipmentCatalog() *EquipmentGachaCatalog { return d.equipment }
|
|
|
|
// CostumeCatalog supplies character and duplicate-upgrade metadata only. It
|
|
// deliberately has no purchasable products, so it cannot enable a zero-price
|
|
// ordinary draw through the normal catalog.
|
|
func (d *FirstGachaDesign) CostumeCatalog() *RegularGachaCatalog {
|
|
return &RegularGachaCatalog{characters: d.characters}
|
|
}
|
|
|
|
func NewFirstGachaDesign(group GachaGroupDesign, id uint64, count int, program *FirstGachaRewardGroup, characters map[uint64]CharacterDesign, equipment map[uint64]EquipmentDesign) (*FirstGachaDesign, error) {
|
|
if group.ID == 0 || group.GachaSubType != 3 || group.GachaType != 0 || group.TenTimeGachaID != id || id == 0 || count <= 0 || count > 100 {
|
|
return nil, errors.New("gamedata: invalid first gacha identity")
|
|
}
|
|
n, err := firstGachaRewardCount(program, map[*FirstGachaRewardGroup]bool{})
|
|
if err != nil || n != uint64(count) {
|
|
return nil, fmt.Errorf("gamedata: first gacha reward count=%d want=%d: %v", n, count, err)
|
|
}
|
|
d := &FirstGachaDesign{Group: group, GachaID: id, Count: count, RewardGroup: program, characters: map[uint64]CharacterDesign{}, equipment: &EquipmentGachaCatalog{equipment: map[uint64]EquipmentDesign{}}}
|
|
var visit func(*FirstGachaRewardGroup) error
|
|
visit = func(g *FirstGachaRewardGroup) error {
|
|
for _, e := range g.Entries {
|
|
switch e.ItemType {
|
|
case 9:
|
|
if err := visit(e.Group); err != nil {
|
|
return err
|
|
}
|
|
case 11:
|
|
c, ok := characters[e.ItemID]
|
|
if !ok || c.ID == 0 || c.HP == 0 {
|
|
return fmt.Errorf("gamedata: first gacha costume %d has no character", e.ItemID)
|
|
}
|
|
d.characters[e.ItemID] = c
|
|
case 10:
|
|
v, ok := equipment[e.ItemID]
|
|
if !ok || v.ID != e.ItemID || v.Grade == 0 {
|
|
return fmt.Errorf("gamedata: first gacha equipment %d has no design", e.ItemID)
|
|
}
|
|
d.equipment.equipment[e.ItemID] = v
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if err := visit(program); err != nil {
|
|
return nil, err
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
func LoadFirstGacha(root, version string) (*FirstGachaDesign, error) {
|
|
db, closeDB, err := openStatDatabase(root, version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer closeDB()
|
|
return loadFirstGacha(db)
|
|
}
|
|
|
|
func loadFirstGacha(db *sql.DB) (*FirstGachaDesign, error) {
|
|
read := func(table string, id uint64) ([]byte, error) {
|
|
var b []byte
|
|
err := db.QueryRow("SELECT ProtoBuf FROM "+table+" WHERE id=?", id).Scan(&b)
|
|
return b, err
|
|
}
|
|
defaults, err := read("GameDefaultTable", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
first, err := packedInts(defaults, 46)
|
|
if err != nil || len(first) != 1 || first[0] == 0 {
|
|
return nil, errors.New("gamedata: missing FirstLimitGachaId")
|
|
}
|
|
raw, err := read("GachaGroupTable", first[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
value := func(b []byte, n int) (uint64, error) {
|
|
v, err := packedInts(b, n)
|
|
if err != nil || len(v) > 1 {
|
|
return 0, fmt.Errorf("gamedata: malformed first gacha field %d", n)
|
|
}
|
|
if len(v) == 0 {
|
|
return 0, nil
|
|
}
|
|
return v[0], nil
|
|
}
|
|
id, err := value(raw, 18)
|
|
if err != nil || id != first[0] {
|
|
return nil, errors.New("gamedata: first gacha group identity mismatch")
|
|
}
|
|
for field, want := range map[int]uint64{16: 3, 17: 0, 28: 0, 3: 0, 4: 0, 5: 0, 24: 0} {
|
|
v, e := value(raw, field)
|
|
if e != nil || v != want {
|
|
return nil, fmt.Errorf("gamedata: first gacha group %d has unsupported field %d=%d", id, field, v)
|
|
}
|
|
}
|
|
gachaID, err := value(raw, 33)
|
|
if err != nil || gachaID == 0 {
|
|
return nil, errors.New("gamedata: first gacha product missing")
|
|
}
|
|
product, err := read("GachaTable", gachaID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for field, want := range map[int]uint64{9: gachaID, 1: 0, 2: 0, 3: 0, 4: 0, 10: 0, 11: 0, 12: 0} {
|
|
v, e := value(product, field)
|
|
if e != nil || v != want {
|
|
return nil, fmt.Errorf("gamedata: first gacha product has unsupported field %d=%d", field, v)
|
|
}
|
|
}
|
|
tickets, err := packedInts(product, 8)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, id := range tickets {
|
|
if id != 0 {
|
|
return nil, errors.New("gamedata: first gacha cannot consume tickets")
|
|
}
|
|
}
|
|
count, err := value(product, 5)
|
|
if err != nil || count == 0 || count > 100 {
|
|
return nil, errors.New("gamedata: first gacha count invalid")
|
|
}
|
|
rewardID, err := value(product, 7)
|
|
if err != nil || rewardID == 0 {
|
|
return nil, errors.New("gamedata: first gacha reward missing")
|
|
}
|
|
program, err := loadFirstGachaRewardGroup(db, rewardID, map[uint64]bool{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
characters := map[uint64]CharacterDesign{}
|
|
equipment := map[uint64]EquipmentDesign{}
|
|
var load func(*FirstGachaRewardGroup) error
|
|
load = func(g *FirstGachaRewardGroup) error {
|
|
for _, e := range g.Entries {
|
|
switch e.ItemType {
|
|
case 9:
|
|
if err := load(e.Group); err != nil {
|
|
return err
|
|
}
|
|
case 11:
|
|
if _, ok := characters[e.ItemID]; !ok {
|
|
v, err := loadGachaCharacterDesign(db, e.ItemID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
characters[e.ItemID] = v
|
|
}
|
|
case 10:
|
|
if _, ok := equipment[e.ItemID]; !ok {
|
|
v, err := loadEquipmentDesign(db, e.ItemID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
equipment[e.ItemID] = v
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if err := load(program); err != nil {
|
|
return nil, err
|
|
}
|
|
points, err := value(raw, 27)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
group := GachaGroupDesign{ID: id, GachaSubType: 3, TenTimeGachaID: gachaID, PointCount: points}
|
|
return NewFirstGachaDesign(group, gachaID, int(count), program, characters, equipment)
|
|
}
|
|
|
|
func loadFirstGachaRewardGroup(db *sql.DB, id uint64, visiting map[uint64]bool) (*FirstGachaRewardGroup, error) {
|
|
if visiting[id] || len(visiting) > 32 {
|
|
return nil, fmt.Errorf("gamedata: first gacha reward cycle/depth at %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
|
|
}
|
|
fields := map[int][]uint64{}
|
|
for _, n := range []int{1, 2, 4, 5, 6, 8} {
|
|
v, err := packedInts(raw, n)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fields[n] = v
|
|
}
|
|
if len(fields[1]) != 1 || len(fields[2]) > 1 {
|
|
return nil, errors.New("gamedata: malformed first reward execution")
|
|
}
|
|
g := &FirstGachaRewardGroup{ID: id, DropCount: fields[1][0]}
|
|
if len(fields[2]) == 1 {
|
|
g.DropType = fields[2][0]
|
|
}
|
|
ids := fields[5]
|
|
if len(ids) == 0 || len(ids) != len(fields[4]) || len(ids) != len(fields[6]) || len(ids) != len(fields[8]) {
|
|
return nil, errors.New("gamedata: malformed first reward entries")
|
|
}
|
|
for i, id := range ids {
|
|
e := FirstGachaRewardEntry{ItemID: id, ItemType: fields[6][i], Count: fields[4][i], Weight: fields[8][i]}
|
|
if e.ItemType == 9 {
|
|
child, err := loadFirstGachaRewardGroup(db, id, visiting)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
e.Group = child
|
|
}
|
|
g.Entries = append(g.Entries, e)
|
|
}
|
|
if _, err := firstGachaRewardCount(g, map[*FirstGachaRewardGroup]bool{}); err != nil {
|
|
return nil, err
|
|
}
|
|
return g, nil
|
|
}
|
|
|
|
func firstGachaRewardCount(g *FirstGachaRewardGroup, visiting map[*FirstGachaRewardGroup]bool) (uint64, error) {
|
|
if g == nil || g.ID == 0 || g.DropCount == 0 || g.DropCount > 100 || len(g.Entries) == 0 || g.DropType > 1 || visiting[g] || len(visiting) > 32 {
|
|
return 0, errors.New("gamedata: invalid first reward program")
|
|
}
|
|
visiting[g] = true
|
|
defer delete(visiting, g)
|
|
var total, each, weight uint64
|
|
for _, e := range g.Entries {
|
|
if e.ItemID == 0 || e.Count == 0 || e.Count > 100 || e.Weight == 0 || math.MaxUint64-weight < e.Weight {
|
|
return 0, errors.New("gamedata: invalid first reward entry")
|
|
}
|
|
weight += e.Weight
|
|
n := e.Count
|
|
switch e.ItemType {
|
|
case 9:
|
|
if e.Group == nil || e.Group.ID != e.ItemID {
|
|
return 0, errors.New("gamedata: first reward child mismatch")
|
|
}
|
|
child, err := firstGachaRewardCount(e.Group, visiting)
|
|
if err != nil || child > 100/e.Count {
|
|
return 0, errors.New("gamedata: invalid first reward child count")
|
|
}
|
|
n = child * e.Count
|
|
case 10, 11:
|
|
if e.Group != nil {
|
|
return 0, errors.New("gamedata: direct first reward has a child")
|
|
}
|
|
default:
|
|
return 0, fmt.Errorf("gamedata: unsupported first reward type %d", e.ItemType)
|
|
}
|
|
if g.DropType == 0 {
|
|
if each == 0 {
|
|
each = n
|
|
} else if each != n {
|
|
return 0, errors.New("gamedata: weighted first reward cardinality differs")
|
|
}
|
|
} else {
|
|
total += n
|
|
}
|
|
}
|
|
if g.DropType == 0 {
|
|
total = each * g.DropCount
|
|
}
|
|
if total > 100 {
|
|
return 0, errors.New("gamedata: first reward count exceeds bound")
|
|
}
|
|
return total, nil
|
|
}
|
|
|
|
func (d *FirstGachaDesign) Roll() ([]FirstGachaReward, error) { return d.roll(cryptoDraw) }
|
|
func (d *FirstGachaDesign) roll(draw func(uint64) (uint64, error)) ([]FirstGachaReward, error) {
|
|
if d == nil {
|
|
return nil, errors.New("gamedata: missing first gacha")
|
|
}
|
|
if n, err := firstGachaRewardCount(d.RewardGroup, map[*FirstGachaRewardGroup]bool{}); err != nil || n != uint64(d.Count) {
|
|
return nil, errors.New("gamedata: first reward program changed")
|
|
}
|
|
var out []FirstGachaReward
|
|
var run func(*FirstGachaRewardGroup) error
|
|
var emit func(FirstGachaRewardEntry) error
|
|
emit = func(e FirstGachaRewardEntry) error {
|
|
for n := uint64(0); n < e.Count; n++ {
|
|
if e.ItemType == 9 {
|
|
if err := run(e.Group); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
out = append(out, FirstGachaReward{Type: e.ItemType, ID: e.ItemID})
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
run = func(g *FirstGachaRewardGroup) error {
|
|
if g.DropType == 1 {
|
|
for _, e := range g.Entries {
|
|
if err := emit(e); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
var sum uint64
|
|
for _, e := range g.Entries {
|
|
sum += e.Weight
|
|
}
|
|
for n := uint64(0); n < g.DropCount; n++ {
|
|
v, err := draw(sum)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if v >= sum {
|
|
return errors.New("gamedata: first reward random out of bounds")
|
|
}
|
|
for _, e := range g.Entries {
|
|
if v < e.Weight {
|
|
if err := emit(e); err != nil {
|
|
return err
|
|
}
|
|
break
|
|
}
|
|
v -= e.Weight
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if err := run(d.RewardGroup); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(out) != d.Count {
|
|
return nil, errors.New("gamedata: first reward count mismatch")
|
|
}
|
|
return out, nil
|
|
}
|