feat(server): add colored logs, level rewards, hunting dispatch and fiend hunt
Persist claims, AP, teams and presets with session receipts and atomic request rollback. Read rewards and growth from GameData; replace fixed Fiend Hunt user state and preserve the account initialization contract.
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
package gamedata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type AchievementLevel struct {
|
||||
ID, NeedEXP uint64
|
||||
Rewards []Reward
|
||||
}
|
||||
type AchievementLevelDesign struct{ Levels []AchievementLevel }
|
||||
|
||||
func LoadAchievementLevelDesign(root, version string) (*AchievementLevelDesign, error) {
|
||||
db, closeDB, err := openStatDatabase(root, version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer closeDB()
|
||||
rows, err := db.Query("SELECT ProtoBuf FROM AchievementLevelTable")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
d := &AchievementLevelDesign{}
|
||||
for rows.Next() {
|
||||
var raw []byte
|
||||
if err := rows.Scan(&raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id, err := optionalScalar(raw, 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
exp, err := optionalScalar(raw, 2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rewards, err := parallelRewards(raw, 5, 4, 3)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
types, err := packedInts(raw, 5)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rewards) != len(types) {
|
||||
return nil, fmt.Errorf("gamedata: achievement level reward arrays mismatch")
|
||||
}
|
||||
d.Levels = append(d.Levels, AchievementLevel{id, exp, rewards})
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Slice(d.Levels, func(i, j int) bool { return d.Levels[i].ID < d.Levels[j].ID })
|
||||
if err := d.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
func (d *AchievementLevelDesign) Validate() error {
|
||||
if d == nil || len(d.Levels) == 0 {
|
||||
return fmt.Errorf("gamedata: empty achievement level design")
|
||||
}
|
||||
var previous, total uint64
|
||||
for _, l := range d.Levels {
|
||||
if l.ID <= previous || l.ID > math.MaxInt32 || l.NeedEXP == 0 || l.NeedEXP > math.MaxInt32 || total > math.MaxUint64-l.NeedEXP {
|
||||
return fmt.Errorf("gamedata: invalid achievement level design")
|
||||
}
|
||||
previous = l.ID
|
||||
total += l.NeedEXP
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Level follows the client: the first cumulative requirement strictly above EXP.
|
||||
func (d *AchievementLevelDesign) Level(exp uint64) uint64 {
|
||||
var total uint64
|
||||
for _, l := range d.Levels {
|
||||
total += l.NeedEXP
|
||||
if total > exp {
|
||||
return l.ID
|
||||
}
|
||||
}
|
||||
return d.Levels[len(d.Levels)-1].ID
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package gamedata
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type DispatchDesign struct {
|
||||
Group, ID, Pack, Difficulty, TypeGroup, TypeID, AP, ClearTime, Growth uint64
|
||||
GroundID uint64
|
||||
Rewards []BattleReward
|
||||
boxes map[uint64]*dispatchGroup
|
||||
}
|
||||
type dispatchEntry struct {
|
||||
Reward BattleReward
|
||||
Weight uint64
|
||||
Child *dispatchGroup
|
||||
}
|
||||
type dispatchGroup struct {
|
||||
Drop, Count uint64
|
||||
Entries []dispatchEntry
|
||||
}
|
||||
|
||||
func LoadDispatchDesign(root, version string, group, id uint64) (*DispatchDesign, error) {
|
||||
db, done, err := openStatDatabase(root, version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer done()
|
||||
var raw []byte
|
||||
if err = db.QueryRow("SELECT ProtoBuf FROM HuntDispatchTable WHERE groupId=? AND id=?", group, id).Scan(&raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d := &DispatchDesign{boxes: map[uint64]*dispatchGroup{}}
|
||||
for n, p := range map[int]*uint64{1: &d.AP, 4: &d.ClearTime, 5: &d.Difficulty, 7: &d.Group, 8: &d.ID, 10: &d.Pack, 12: &d.Growth, 13: &d.TypeGroup, 14: &d.TypeID} {
|
||||
*p, err = optionalScalar(raw, n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if d.AP == 0 || d.Pack == 0 || d.Group != group || d.ID != id {
|
||||
return nil, fmt.Errorf("gamedata: invalid dispatch")
|
||||
}
|
||||
pdb, closePack, err := openPackDatabase(root, version, int(d.Pack))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer closePack()
|
||||
var ids []uint64
|
||||
var boss uint64
|
||||
if d.TypeGroup == 0 {
|
||||
if err = pdb.QueryRow("SELECT ProtoBuf FROM HuntingGroundTable WHERE difficulty=?", d.Difficulty).Scan(&raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids, err = packedInts(raw, 5)
|
||||
boss, _ = optionalScalar(raw, 1)
|
||||
d.GroundID, _ = optionalScalar(raw, 3)
|
||||
} else {
|
||||
if err = db.QueryRow("SELECT ProtoBuf FROM SkyWayFieldTable WHERE groupId=? AND id=?", d.TypeGroup, d.TypeID).Scan(&raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids, err = packedInts(raw, 15)
|
||||
boss, _ = optionalScalar(raw, 4)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids = append(ids, boss)
|
||||
for _, monster := range ids {
|
||||
if err = pdb.QueryRow("SELECT ProtoBuf FROM FieldMonsterTable WHERE id=?", monster).Scan(&raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decks, e := packedInts(raw, 2)
|
||||
if e != nil || len(decks) == 0 {
|
||||
return nil, fmt.Errorf("gamedata: dispatch monster has no deck")
|
||||
}
|
||||
if err = pdb.QueryRow("SELECT ProtoBuf FROM BattleDeckTable WHERE id=?", decks[0]).Scan(&raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
types, e := packedInts(raw, 36)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
rewardIDs, e := packedInts(raw, 34)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
counts, e := packedInts(raw, 33)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if len(types) != len(rewardIDs) || len(types) != len(counts) {
|
||||
return nil, fmt.Errorf("gamedata: dispatch reward arrays mismatch")
|
||||
}
|
||||
rs, e := parallelRewards(raw, 36, 34, 33)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
for _, r := range rs {
|
||||
d.Rewards = append(d.Rewards, BattleReward{r.Type, r.ID, r.Count})
|
||||
if r.Type == 9 {
|
||||
g, e := loadDispatchBox(db, r.ID, map[uint64]bool{})
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
d.boxes[r.ID] = g
|
||||
}
|
||||
}
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
func loadDispatchBox(db *sql.DB, id uint64, seen map[uint64]bool) (*dispatchGroup, error) {
|
||||
if seen[id] || len(seen) > 32 {
|
||||
return nil, fmt.Errorf("gamedata: dispatch reward cycle")
|
||||
}
|
||||
seen[id] = true
|
||||
defer delete(seen, id)
|
||||
var raw []byte
|
||||
if err := db.QueryRow("SELECT ProtoBuf FROM RandomBoxTable WHERE id=?", id).Scan(&raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gid, err := optionalScalar(raw, 9)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = db.QueryRow("SELECT ProtoBuf FROM RewardGroupTable WHERE id=?", gid).Scan(&raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g := &dispatchGroup{}
|
||||
g.Drop, _ = optionalScalar(raw, 2)
|
||||
g.Count, _ = optionalScalar(raw, 1)
|
||||
fields := map[int][]uint64{}
|
||||
for _, n := range []int{4, 5, 6, 8} {
|
||||
fields[n], err = packedInts(raw, n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(fields[5]) == 0 || len(fields[4]) != len(fields[5]) || len(fields[6]) != len(fields[5]) || len(fields[8]) != len(fields[5]) || g.Drop > 1 || (g.Drop == 0 && g.Count == 0) {
|
||||
return nil, fmt.Errorf("gamedata: malformed dispatch reward group %d", gid)
|
||||
}
|
||||
for i, item := range fields[5] {
|
||||
e := dispatchEntry{Reward: BattleReward{fields[6][i], item, fields[4][i]}, Weight: fields[8][i]}
|
||||
if e.Reward.Count == 0 {
|
||||
return nil, fmt.Errorf("gamedata: empty dispatch reward")
|
||||
}
|
||||
if e.Reward.Type == 9 {
|
||||
e.Child, err = loadDispatchBox(db, item, seen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
g.Entries = append(g.Entries, e)
|
||||
}
|
||||
return g, nil
|
||||
}
|
||||
|
||||
// Roll uses the local server's random source; GameData supplies every weight and quantity.
|
||||
func (d *DispatchDesign) Roll(count uint64, draw func(uint64) (uint64, error)) ([]BattleReward, error) {
|
||||
if d == nil || count == 0 || count > 20 {
|
||||
return nil, fmt.Errorf("gamedata: invalid dispatch count")
|
||||
}
|
||||
var budget uint64 = 100000
|
||||
var out []BattleReward
|
||||
var run func(*dispatchGroup) error
|
||||
emit := func(e dispatchEntry) error {
|
||||
if e.Child != nil {
|
||||
for i := uint64(0); i < e.Reward.Count; i++ {
|
||||
if err := run(e.Child); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if len(out) >= 100000 {
|
||||
return fmt.Errorf("gamedata: dispatch reward count exceeds operation limit")
|
||||
}
|
||||
out = append(out, e.Reward)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
run = func(g *dispatchGroup) error {
|
||||
if g == nil || budget == 0 {
|
||||
return fmt.Errorf("gamedata: dispatch reward program exceeds operation limit")
|
||||
}
|
||||
budget--
|
||||
if g.Drop == 1 {
|
||||
for _, e := range g.Entries {
|
||||
if err := emit(e); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var sum uint64
|
||||
for _, e := range g.Entries {
|
||||
if e.Weight == 0 || math.MaxUint64-sum < e.Weight {
|
||||
return fmt.Errorf("gamedata: invalid dispatch weight")
|
||||
}
|
||||
sum += e.Weight
|
||||
}
|
||||
for i := uint64(0); i < g.Count; i++ {
|
||||
v, err := draw(sum)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if v >= sum {
|
||||
return fmt.Errorf("gamedata: random out of range")
|
||||
}
|
||||
for _, e := range g.Entries {
|
||||
if v < e.Weight {
|
||||
if err := emit(e); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
v -= e.Weight
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for i := uint64(0); i < count; i++ {
|
||||
for _, r := range d.Rewards {
|
||||
if r.Type == 9 {
|
||||
for n := uint64(0); n < r.Count; n++ {
|
||||
if err := run(d.boxes[r.ID]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
aggregate := map[[2]uint64]uint64{}
|
||||
for _, r := range out {
|
||||
k := [2]uint64{r.Type, r.ID}
|
||||
if r.Count > math.MaxInt32-aggregate[k] {
|
||||
return nil, fmt.Errorf("gamedata: dispatch reward overflow")
|
||||
}
|
||||
aggregate[k] += r.Count
|
||||
}
|
||||
out = nil
|
||||
for k, n := range aggregate {
|
||||
out = append(out, BattleReward{k[0], k[1], n})
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package gamedata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDispatchWeightedNestedRewards(t *testing.T) {
|
||||
leaf := &dispatchGroup{Drop: 1, Entries: []dispatchEntry{{Reward: BattleReward{Type: 8, ID: 44, Count: 3}}}}
|
||||
weighted := &dispatchGroup{Drop: 0, Count: 2, Entries: []dispatchEntry{{Reward: BattleReward{Type: 9, Count: 1}, Weight: 90, Child: leaf}, {Reward: BattleReward{Type: 4, Count: 5}, Weight: 10}}}
|
||||
d := &DispatchDesign{Rewards: []BattleReward{{Type: 9, ID: 7, Count: 1}}, boxes: map[uint64]*dispatchGroup{7: weighted}}
|
||||
draws := 0
|
||||
r, err := d.Roll(2, func(n uint64) (uint64, error) {
|
||||
if n != 100 {
|
||||
return 0, fmt.Errorf("limit %d", n)
|
||||
}
|
||||
draws++
|
||||
if draws%2 == 0 {
|
||||
return 95, nil
|
||||
}
|
||||
return 0, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
totals := map[uint64]uint64{}
|
||||
for _, v := range r {
|
||||
totals[v.Type] += v.Count
|
||||
}
|
||||
if totals[8] != 6 || totals[4] != 10 || draws != 4 {
|
||||
t.Fatalf("rewards %+v draws %d", r, draws)
|
||||
}
|
||||
if _, err = d.Roll(1, func(n uint64) (uint64, error) { return n, nil }); err == nil {
|
||||
t.Fatal("out of range RNG accepted")
|
||||
}
|
||||
}
|
||||
func TestDispatchRewardOverflow(t *testing.T) {
|
||||
d := &DispatchDesign{Rewards: []BattleReward{{Type: 4, Count: 2147483647}}}
|
||||
if _, err := d.Roll(2, nil); err == nil {
|
||||
t.Fatal("overflow accepted")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package gamedata
|
||||
|
||||
import "fmt"
|
||||
|
||||
type HuntingMonster struct {
|
||||
ID, Type uint64
|
||||
Decks []uint64
|
||||
Rewards map[uint64][]BattleReward
|
||||
}
|
||||
type HuntingGround struct {
|
||||
ID, MapID, Difficulty, BossID uint64
|
||||
Monsters []uint64
|
||||
}
|
||||
type HuntingPack struct {
|
||||
Grounds []HuntingGround
|
||||
Monsters map[uint64]HuntingMonster
|
||||
NormalAP, BossAP uint64
|
||||
}
|
||||
|
||||
func LoadHuntingPack(root, version string, pack int) (*HuntingPack, error) {
|
||||
db, cleanup, err := openPackDatabase(root, version, pack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cleanup()
|
||||
common, closeCommon, err := openStatDatabase(root, version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer closeCommon()
|
||||
d := &HuntingPack{Monsters: map[uint64]HuntingMonster{}}
|
||||
var defaults []byte
|
||||
if err := common.QueryRow("SELECT ProtoBuf FROM GameDefaultTable").Scan(&defaults); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.BossAP, err = optionalScalar(defaults, 121)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.NormalAP, err = optionalScalar(defaults, 122)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := db.Query("SELECT ProtoBuf FROM HuntingGroundTable ORDER BY id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for rows.Next() {
|
||||
var raw []byte
|
||||
if err := rows.Scan(&raw); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
g := HuntingGround{}
|
||||
for field, target := range map[int]*uint64{1: &g.BossID, 2: &g.Difficulty, 3: &g.ID, 4: &g.MapID} {
|
||||
v, e := optionalScalar(raw, field)
|
||||
if e != nil {
|
||||
rows.Close()
|
||||
return nil, e
|
||||
}
|
||||
*target = v
|
||||
}
|
||||
g.Monsters, err = packedInts(raw, 5)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
if g.ID == 0 || g.MapID == 0 || g.BossID == 0 {
|
||||
rows.Close()
|
||||
return nil, fmt.Errorf("gamedata: invalid hunting ground")
|
||||
}
|
||||
d.Grounds = append(d.Grounds, g)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
rows.Close()
|
||||
for _, g := range d.Grounds {
|
||||
ids := append(append([]uint64(nil), g.Monsters...), g.BossID)
|
||||
for _, id := range ids {
|
||||
if _, ok := d.Monsters[id]; ok {
|
||||
continue
|
||||
}
|
||||
var raw []byte
|
||||
if err := db.QueryRow("SELECT ProtoBuf FROM FieldMonsterTable WHERE id=?", id).Scan(&raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := HuntingMonster{ID: id, Rewards: map[uint64][]BattleReward{}}
|
||||
m.Type, err = optionalScalar(raw, 30)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.Decks, err = packedInts(raw, 2)
|
||||
if err != nil || len(m.Decks) == 0 {
|
||||
return nil, fmt.Errorf("gamedata: missing hunting monster deck")
|
||||
}
|
||||
for _, deck := range m.Decks {
|
||||
var b []byte
|
||||
if err := db.QueryRow("SELECT ProtoBuf FROM BattleDeckTable WHERE id=?", deck).Scan(&b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r, e := parallelRewards(b, 36, 34, 33)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
types, e := packedInts(b, 36)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if len(r) != len(types) {
|
||||
return nil, fmt.Errorf("gamedata: hunting reward arrays mismatch")
|
||||
}
|
||||
for _, reward := range r {
|
||||
m.Rewards[deck] = append(m.Rewards[deck], BattleReward{reward.Type, reward.ID, reward.Count})
|
||||
}
|
||||
}
|
||||
d.Monsters[id] = m
|
||||
}
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
package gamedata
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
type MonsterHuntRewards struct{ Clear, Daily []BattleReward }
|
||||
type MonsterHuntRankReward struct {
|
||||
ID, Type uint64
|
||||
Ranking float64
|
||||
Rewards []BattleReward
|
||||
}
|
||||
|
||||
// MonsterHunt is the immutable common database design. Hunt decks live in
|
||||
// common.db, as BattleDeckInfo.GetBattleDeckInfo(id, true) specifies.
|
||||
type MonsterHunt struct {
|
||||
ID, PackID, MonsterID, DeckID, RewardGroupID, MaxLevel, RewardLevel, ChallengeableLevel uint64
|
||||
TeamOpenLevels []uint64
|
||||
Rewards map[uint64]MonsterHuntRewards
|
||||
Ranks map[uint64][]MonsterHuntRankReward
|
||||
baseHP, healthRate, healthSlope, stage2Ratio, stage3Ratio float64
|
||||
stage2Level, stage3Level uint64
|
||||
}
|
||||
|
||||
func LoadMonsterHunt(root, version string, id uint64) (*MonsterHunt, error) {
|
||||
db, done, err := openStatDatabase(root, version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer done()
|
||||
return loadMonsterHunt(db, id)
|
||||
}
|
||||
func loadMonsterHunt(db *sql.DB, id uint64) (*MonsterHunt, error) {
|
||||
if id == 0 {
|
||||
return nil, fmt.Errorf("gamedata: zero monster hunt")
|
||||
}
|
||||
var raw []byte
|
||||
if err := db.QueryRow("SELECT ProtoBuf FROM MonsterHuntTable WHERE id=?", id).Scan(&raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d := &MonsterHunt{Rewards: map[uint64]MonsterHuntRewards{}, Ranks: map[uint64][]MonsterHuntRankReward{}}
|
||||
for n, p := range map[int]*uint64{8: &d.ID, 18: &d.PackID, 16: &d.MonsterID, 1: &d.DeckID, 22: &d.RewardGroupID, 23: &d.RewardLevel, 14: &d.ChallengeableLevel, 26: &d.stage2Level, 28: &d.stage3Level} {
|
||||
v, e := optionalScalar(raw, n)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
*p = v
|
||||
}
|
||||
for n, p := range map[int]*float64{11: &d.healthRate, 12: &d.healthSlope, 27: &d.stage2Ratio, 29: &d.stage3Ratio} {
|
||||
v, _, e := fixed64Double(raw, n)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
*p = v
|
||||
}
|
||||
var err error
|
||||
d.TeamOpenLevels, err = packedInts(raw, 32)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
group, err := optionalScalar(raw, 19)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := db.Query("SELECT ProtoBuf FROM MonsterHuntPartsTable WHERE groupId=? ORDER BY id", group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var body uint64
|
||||
for rows.Next() {
|
||||
var part []byte
|
||||
if err = rows.Scan(&part); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
v, e := optionalScalar(part, 7)
|
||||
if e != nil {
|
||||
rows.Close()
|
||||
return nil, e
|
||||
}
|
||||
if body == 0 && v > 0 {
|
||||
body = v
|
||||
}
|
||||
}
|
||||
err = rows.Err()
|
||||
rows.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if body == 0 {
|
||||
return nil, fmt.Errorf("gamedata: hunt %d missing body", id)
|
||||
}
|
||||
var deck []byte
|
||||
if err = db.QueryRow("SELECT ProtoBuf FROM BattleDeckTable WHERE id=?", d.DeckID).Scan(&deck); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chars, err := packedInts(deck, 14)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
levels, err := packedInts(deck, 30)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(chars) != len(levels) {
|
||||
return nil, fmt.Errorf("gamedata: hunt deck level arrays differ")
|
||||
}
|
||||
level := uint64(1)
|
||||
for i, c := range chars {
|
||||
if c == body {
|
||||
level = levels[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
stats, err := loadCharacterStatDesign(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
base, err := stats.BaseStats(body, level)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deckRate, _, err := fixed64Double(deck, 28)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.baseHP = math.Trunc(base.Health*deckRate*100) / 100
|
||||
rows, err = db.Query("SELECT level, ProtoBuf FROM MonsterHuntRewardTable WHERE groupId=? ORDER BY level", d.RewardGroupID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for rows.Next() {
|
||||
var lv uint64
|
||||
var r []byte
|
||||
if err = rows.Scan(&lv, &r); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
clear, e := monsterHuntRewardArrays(r, 8, 7, 6)
|
||||
if e != nil {
|
||||
rows.Close()
|
||||
return nil, e
|
||||
}
|
||||
daily, e := monsterHuntRewardArrays(r, 3, 2, 1)
|
||||
if e != nil {
|
||||
rows.Close()
|
||||
return nil, e
|
||||
}
|
||||
d.Rewards[lv] = MonsterHuntRewards{clear, daily}
|
||||
if lv > d.MaxLevel {
|
||||
d.MaxLevel = lv
|
||||
}
|
||||
}
|
||||
err = rows.Err()
|
||||
rows.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err = db.Query("SELECT groupId,id,ProtoBuf FROM MonsterHuntRankTable ORDER BY groupId,id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for rows.Next() {
|
||||
var g uint64
|
||||
var r MonsterHuntRankReward
|
||||
var p []byte
|
||||
if err = rows.Scan(&g, &r.ID, &p); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
r.Type, err = optionalScalar(p, 6)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
r.Ranking, _, err = fixed64Double(p, 5)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
r.Rewards, err = monsterHuntRewardArrays(p, 9, 8, 7)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
d.Ranks[g] = append(d.Ranks[g], r)
|
||||
}
|
||||
err = rows.Err()
|
||||
rows.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if d.ID != id || d.PackID == 0 || d.DeckID == 0 || d.MaxLevel == 0 || d.baseHP <= 0 {
|
||||
return nil, fmt.Errorf("gamedata: invalid hunt %d design", id)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// HP matches MonsterHuntInfo.ApplyMonsterHuntStatRate, including banker
|
||||
// rounding and truncation to three significant decimal digits.
|
||||
func (d *MonsterHunt) HP(level uint64) (uint64, error) {
|
||||
if d == nil || level == 0 || level > d.MaxLevel {
|
||||
return 0, fmt.Errorf("gamedata: invalid hunt level %d", level)
|
||||
}
|
||||
ratio := 1.0
|
||||
if level > d.stage2Level {
|
||||
ratio = d.stage2Ratio
|
||||
if level > d.stage3Level {
|
||||
ratio = d.stage3Ratio
|
||||
}
|
||||
}
|
||||
rate := 1 + float64(level-1)*d.healthRate*.01*math.Pow(float64(level), d.healthSlope)*ratio
|
||||
hp := math.RoundToEven(d.baseHP * rate)
|
||||
if math.IsNaN(hp) || math.IsInf(hp, 0) || hp < 1 || hp >= float64(math.MaxInt64) {
|
||||
return 0, fmt.Errorf("gamedata: hunt HP overflow")
|
||||
}
|
||||
value := uint64(hp)
|
||||
scale := uint64(1)
|
||||
for value/scale >= 1000 {
|
||||
scale *= 10
|
||||
}
|
||||
return value / scale * scale, nil
|
||||
}
|
||||
func monsterHuntRewardArrays(raw []byte, typeField, idField, countField int) ([]BattleReward, error) {
|
||||
types, e := packedInts(raw, typeField)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
ids, e := packedInts(raw, idField)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
counts, e := packedInts(raw, countField)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if len(types) != len(ids) || len(ids) != len(counts) {
|
||||
return nil, fmt.Errorf("gamedata: hunt reward arrays differ")
|
||||
}
|
||||
var result []BattleReward
|
||||
for i, t := range types {
|
||||
if t > 0 && counts[i] > 0 {
|
||||
result = append(result, BattleReward{Type: t, ID: ids[i], Count: counts[i]})
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func LoadMonsterHuntPresetDesign(root, version string) (*PresetDesign, error) {
|
||||
db, done, err := openStatDatabase(root, version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer done()
|
||||
d, err := loadPresetDesign(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var raw []byte
|
||||
if err = db.QueryRow("SELECT ProtoBuf FROM GameDefaultTable WHERE id=0").Scan(&raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.BaseCount, err = optionalScalar(raw, 84)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.Maximum, err = optionalScalar(raw, 85)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, d.Validate()
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package gamedata
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMonsterHuntHPStageBoundariesAndSignificantDigits(t *testing.T) {
|
||||
d := &MonsterHunt{MaxLevel: 30, baseHP: 6000, healthRate: 400, healthSlope: 2.6, stage2Level: 12, stage2Ratio: 1.02, stage3Level: 14, stage3Ratio: .86}
|
||||
// Independently evaluated client formula at the two strict greater-than
|
||||
// stage boundaries; truncation keeps three significant decimal digits.
|
||||
for level, want := range map[uint64]uint64{1: 6000, 2: 151000, 12: 168000000, 13: 231000000, 14: 303000000, 15: 330000000} {
|
||||
got, err := d.HP(level)
|
||||
if err != nil || got != want {
|
||||
t.Fatalf("level %d hp=%d err=%v want=%d", level, got, err, want)
|
||||
}
|
||||
}
|
||||
if _, err := d.HP(0); err == nil {
|
||||
t.Fatal("zero level accepted")
|
||||
}
|
||||
if _, err := d.HP(31); err == nil {
|
||||
t.Fatal("outside design level accepted")
|
||||
}
|
||||
}
|
||||
func TestMonsterHuntInstalledDesign23510(t *testing.T) {
|
||||
root := os.Getenv("BD2_REAL_GAMEDATA")
|
||||
if root == "" {
|
||||
t.Skip("BD2_REAL_GAMEDATA not configured")
|
||||
}
|
||||
d, err := LoadMonsterHunt(root, "20260923193640", 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if d.PackID != 1001 || d.DeckID != 10001 || d.MaxLevel != 30 || len(d.Rewards) != 30 {
|
||||
t.Fatalf("unexpected installed design %+v", d)
|
||||
}
|
||||
hp, err := d.HP(1)
|
||||
if err != nil || hp != 6600 {
|
||||
t.Fatalf("base HP %d %v", hp, err)
|
||||
}
|
||||
r := d.Rewards[1]
|
||||
if len(r.Clear) != 1 || r.Clear[0].Type != 8 || r.Clear[0].Count != 5 || len(r.Daily) != 1 || r.Daily[0].Type != 4 || r.Daily[0].Count != 20000 {
|
||||
t.Fatalf("unexpected rewards %+v", r)
|
||||
}
|
||||
p, err := LoadMonsterHuntPresetDesign(root, "20260923193640")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p.BaseCount != 5 || p.Maximum != 10 || p.Price != 2000 || p.PriceType != 4 {
|
||||
t.Fatalf("unexpected hunt presets %+v", p)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user