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.
284 lines
8.5 KiB
Go
284 lines
8.5 KiB
Go
package gamedata
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"math"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
// Reward is an immutable GameData reward definition. Type, ID and Count are
|
|
// the exact parallel arrays stored by the client design tables; it is not a
|
|
// player inventory row.
|
|
type Reward struct {
|
|
Type uint64
|
|
ID uint64
|
|
Count uint64
|
|
}
|
|
|
|
type MissionKey struct{ GroupType, GroupID, ID uint64 }
|
|
type SectionRewardKey struct{ GroupType, ID uint64 }
|
|
type AchievementKey struct{ ContentsGroup, GroupID, ID uint64 }
|
|
|
|
type MissionDesign struct {
|
|
Missions map[MissionKey][]Reward
|
|
Conditions map[MissionKey]MissionCondition
|
|
Sections map[SectionRewardKey]SectionRewardDesign
|
|
Achievements map[AchievementKey]AchievementDesign
|
|
}
|
|
|
|
type MissionCondition struct {
|
|
Type uint64
|
|
SubType uint64
|
|
SubTypeComparison uint64
|
|
Params []uint64
|
|
TargetValue uint64
|
|
UnlockPack uint64
|
|
UnlockQuest uint64
|
|
}
|
|
|
|
type SectionRewardDesign struct {
|
|
SectionValue uint64
|
|
Rewards []Reward
|
|
}
|
|
|
|
type AchievementDesign struct {
|
|
Target float64
|
|
CounterGroup uint64
|
|
AddExp uint64
|
|
Rewards []Reward
|
|
}
|
|
|
|
// LoadMissionDesign loads the three non-event design tables from the shared
|
|
// current database. Event missions intentionally remain outside this reader:
|
|
// their eligibility depends on an active server schedule, not just GameData.
|
|
func LoadMissionDesign(root, version string) (*MissionDesign, error) {
|
|
db, release, err := OpenDatabase(root, version, "common")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer release()
|
|
return loadMissionDesignDB(db)
|
|
}
|
|
|
|
func loadMissionDesignDB(db *sql.DB) (*MissionDesign, error) {
|
|
if db == nil {
|
|
return nil, fmt.Errorf("gamedata: nil mission database")
|
|
}
|
|
design := &MissionDesign{
|
|
Missions: map[MissionKey][]Reward{}, Conditions: map[MissionKey]MissionCondition{}, Sections: map[SectionRewardKey]SectionRewardDesign{},
|
|
Achievements: map[AchievementKey]AchievementDesign{},
|
|
}
|
|
if err := loadMissionRows(db, design); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := loadSectionRows(db, design); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := loadAchievementRows(db, design); err != nil {
|
|
return nil, err
|
|
}
|
|
return design, nil
|
|
}
|
|
|
|
func loadMissionRows(db *sql.DB, design *MissionDesign) error {
|
|
rows, err := db.Query("SELECT ProtoBuf FROM MissionTable")
|
|
if err != nil {
|
|
return fmt.Errorf("gamedata: query MissionTable: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var proto []byte
|
|
if err := rows.Scan(&proto); err != nil {
|
|
return err
|
|
}
|
|
groupID, err := optionalScalar(proto, 6)
|
|
if err != nil {
|
|
return fmt.Errorf("gamedata: MissionTable group id: %w", err)
|
|
}
|
|
groupType, err := optionalScalar(proto, 7)
|
|
if err != nil {
|
|
return fmt.Errorf("gamedata: MissionTable group type: %w", err)
|
|
}
|
|
id, err := optionalScalar(proto, 8)
|
|
if err != nil {
|
|
return fmt.Errorf("gamedata: MissionTable id: %w", err)
|
|
}
|
|
// GroupType zero is DAILY, a valid proto3 default. Rows without a
|
|
// group/id identity remain non-claimable templates.
|
|
if groupID == 0 || id == 0 {
|
|
continue
|
|
}
|
|
rewards, err := parallelRewards(proto, 15, 14, 13)
|
|
if err != nil {
|
|
return fmt.Errorf("gamedata: MissionTable %d/%d/%d: %w", groupType, groupID, id, err)
|
|
}
|
|
key := MissionKey{groupType, groupID, id}
|
|
if _, exists := design.Missions[key]; exists {
|
|
return fmt.Errorf("gamedata: duplicate MissionTable key %+v", key)
|
|
}
|
|
design.Missions[key] = rewards
|
|
conditionType, err := optionalScalar(proto, 3)
|
|
if err != nil {
|
|
return fmt.Errorf("gamedata: MissionTable condition type: %w", err)
|
|
}
|
|
conditionSubType, err := optionalScalar(proto, 1)
|
|
if err != nil {
|
|
return fmt.Errorf("gamedata: MissionTable condition subtype: %w", err)
|
|
}
|
|
params, err := packedInts(proto, 2)
|
|
if err != nil {
|
|
return fmt.Errorf("gamedata: MissionTable params: %w", err)
|
|
}
|
|
target, err := optionalScalar(proto, 4)
|
|
if err != nil {
|
|
return fmt.Errorf("gamedata: MissionTable target: %w", err)
|
|
}
|
|
unlockPack, err := optionalScalar(proto, 19)
|
|
if err != nil {
|
|
return fmt.Errorf("gamedata: MissionTable unlock pack: %w", err)
|
|
}
|
|
unlockQuest, err := optionalScalar(proto, 20)
|
|
if err != nil {
|
|
return fmt.Errorf("gamedata: MissionTable unlock quest: %w", err)
|
|
}
|
|
subTypeMore, err := optionalScalar(proto, 9)
|
|
if err != nil {
|
|
return fmt.Errorf("gamedata: MissionTable subtype comparison invalid")
|
|
}
|
|
design.Conditions[key] = MissionCondition{SubTypeComparison: subTypeMore, Type: conditionType, SubType: conditionSubType, Params: params, TargetValue: target, UnlockPack: unlockPack, UnlockQuest: unlockQuest}
|
|
}
|
|
return rows.Err()
|
|
}
|
|
|
|
func loadSectionRows(db *sql.DB, design *MissionDesign) error {
|
|
rows, err := db.Query("SELECT ProtoBuf FROM MissionSectionRewardTable")
|
|
if err != nil {
|
|
return fmt.Errorf("gamedata: query MissionSectionRewardTable: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var proto []byte
|
|
if err := rows.Scan(&proto); err != nil {
|
|
return err
|
|
}
|
|
groupType, err := optionalScalar(proto, 1)
|
|
if err != nil {
|
|
return fmt.Errorf("gamedata: MissionSectionRewardTable group type: %w", err)
|
|
}
|
|
id, err := optionalScalar(proto, 2)
|
|
if err != nil {
|
|
return fmt.Errorf("gamedata: MissionSectionRewardTable id: %w", err)
|
|
}
|
|
if id == 0 {
|
|
continue
|
|
}
|
|
rewards, err := parallelRewards(proto, 5, 4, 3)
|
|
if err != nil {
|
|
return fmt.Errorf("gamedata: MissionSectionRewardTable %d/%d: %w", groupType, id, err)
|
|
}
|
|
key := SectionRewardKey{groupType, id}
|
|
if _, exists := design.Sections[key]; exists {
|
|
return fmt.Errorf("gamedata: duplicate MissionSectionRewardTable key %+v", key)
|
|
}
|
|
sectionValue, err := optionalScalar(proto, 6)
|
|
if err != nil {
|
|
return fmt.Errorf("gamedata: MissionSectionRewardTable section value: %w", err)
|
|
}
|
|
design.Sections[key] = SectionRewardDesign{SectionValue: sectionValue, Rewards: rewards}
|
|
}
|
|
return rows.Err()
|
|
}
|
|
|
|
func loadAchievementRows(db *sql.DB, design *MissionDesign) error {
|
|
rows, err := db.Query("SELECT ProtoBuf FROM AchievementTable")
|
|
if err != nil {
|
|
return fmt.Errorf("gamedata: query AchievementTable: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var proto []byte
|
|
if err := rows.Scan(&proto); err != nil {
|
|
return err
|
|
}
|
|
contents, err := optionalScalar(proto, 4)
|
|
if err != nil {
|
|
return fmt.Errorf("gamedata: AchievementTable contents group: %w", err)
|
|
}
|
|
groupID, err := optionalScalar(proto, 9)
|
|
if err != nil {
|
|
return fmt.Errorf("gamedata: AchievementTable group id: %w", err)
|
|
}
|
|
id, err := optionalScalar(proto, 11)
|
|
if err != nil {
|
|
return fmt.Errorf("gamedata: AchievementTable id: %w", err)
|
|
}
|
|
if groupID == 0 || id == 0 {
|
|
continue
|
|
}
|
|
exp, err := optionalScalar(proto, 8)
|
|
if err != nil {
|
|
return fmt.Errorf("gamedata: AchievementTable exp: %w", err)
|
|
}
|
|
rewards, err := parallelRewards(proto, 16, 15, 14)
|
|
if err != nil {
|
|
return fmt.Errorf("gamedata: AchievementTable %d/%d/%d: %w", contents, groupID, id, err)
|
|
}
|
|
parent, err := optionalScalar(proto, 13)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
useType, err := optionalScalar(proto, 21)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if parent != 0 || useType != 0 {
|
|
continue
|
|
}
|
|
key := AchievementKey{contents, groupID, id}
|
|
if _, exists := design.Achievements[key]; exists {
|
|
return fmt.Errorf("gamedata: duplicate AchievementTable key %+v", key)
|
|
}
|
|
target, found, err := fixed64Double(proto, 3)
|
|
if err != nil || !found || target < 0 || math.IsNaN(target) || math.IsInf(target, 0) {
|
|
return fmt.Errorf("gamedata: invalid achievement target")
|
|
}
|
|
counter := groupID
|
|
design.Achievements[key] = AchievementDesign{Target: target, CounterGroup: counter, AddExp: exp, Rewards: rewards}
|
|
}
|
|
return rows.Err()
|
|
}
|
|
|
|
func parallelRewards(proto []byte, typeField, idField, countField int) ([]Reward, error) {
|
|
types, err := packedInts(proto, typeField)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ids, err := packedInts(proto, idField)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
counts, err := packedInts(proto, countField)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(ids) == 0 && len(types) == len(counts) {
|
|
ids = make([]uint64, len(types))
|
|
}
|
|
if len(types) != len(ids) || len(ids) != len(counts) {
|
|
// A design row may be a mission marker with only display/condition
|
|
// values populated. Keeping an empty reward list preserves the
|
|
// claimable identity without inventing an item from partial arrays.
|
|
return nil, nil
|
|
}
|
|
rewards := make([]Reward, len(types))
|
|
for i := range rewards {
|
|
if types[i] == 0 || counts[i] == 0 {
|
|
return nil, fmt.Errorf("invalid reward at %d", i)
|
|
}
|
|
rewards[i] = Reward{Type: types[i], ID: ids[i], Count: counts[i]}
|
|
}
|
|
return rewards, nil
|
|
}
|