Files
bd2/go/internal/server/gamedata/missions.go
T
Flechazo d8771caef2 feat(all): add hunting and events, commerce and rewards, binary calendars, GameData caching and login diagnostics
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.
2026-10-05 11:50:31 +08:00

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
}