Files
bd2/go/internal/server/gamedata/quests.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

134 lines
4.2 KiB
Go

package gamedata
import (
"database/sql"
"fmt"
_ "modernc.org/sqlite"
)
// QuestDesign is the authoritative reward definition from QuestTable<pack>.
// Reward slot zero is normal story difficulty; later slots are separate
// difficulty tiers and must never be added to the same clear.
type QuestDesign struct {
ID int
Type int
PriorQuestID int
NextQuestID int
GiveQuestItemIDs []uint64
CollectionRewards []Reward
Rewards [5][]Reward
}
func LoadQuestDesign(root, version string, packID int) (map[int]QuestDesign, error) {
if packID <= 0 {
return nil, fmt.Errorf("gamedata: invalid quest pack %d", packID)
}
db, release, err := OpenDatabase(root, version, "common")
if err != nil {
return nil, err
}
defer release()
return loadQuestDesignDB(db, packID)
}
func loadQuestDesignDB(db *sql.DB, packID int) (map[int]QuestDesign, error) {
if db == nil || packID <= 0 {
return nil, fmt.Errorf("gamedata: invalid quest design configuration")
}
rows, err := db.Query(fmt.Sprintf("SELECT id,ProtoBuf FROM QuestTable%d ORDER BY id", packID))
if err != nil {
return nil, fmt.Errorf("gamedata: query QuestTable%d: %w", packID, err)
}
defer rows.Close()
type questRow struct {
id int
proto []byte
}
var questRows []questRow
for rows.Next() {
var row questRow
if err := rows.Scan(&row.id, &row.proto); err != nil {
return nil, err
}
questRows = append(questRows, row)
}
if err := rows.Err(); err != nil {
return nil, err
}
if err := rows.Close(); err != nil {
return nil, err
}
// Collection lookups reuse the database's single connection after the
// quest result set is closed.
result := make(map[int]QuestDesign)
for _, row := range questRows {
id, proto := row.id, row.proto
if id <= 0 {
continue
}
entry := QuestDesign{ID: id}
collectionIDs, err := packedInts(proto, 6)
if err != nil {
return nil, fmt.Errorf("gamedata: quest%d collections: %w", id, err)
}
for _, collectionID := range collectionIDs {
var collection []byte
if err := db.QueryRow("SELECT ProtoBuf FROM CollectionTable WHERE id=?", collectionID).Scan(&collection); err != nil {
return nil, fmt.Errorf("gamedata: quest%d collection%d: %w", id, collectionID, err)
}
itemID, err := requiredScalar(collection, 3)
if err != nil {
return nil, err
}
if itemID != collectionID {
return nil, fmt.Errorf("gamedata: quest%d collection%d identity mismatch%d", id, collectionID, itemID)
}
entry.CollectionRewards = append(entry.CollectionRewards, Reward{Type: 17, ID: collectionID, Count: 1})
}
entry.GiveQuestItemIDs, err = packedInts(proto, 29)
if err != nil {
return nil, fmt.Errorf("gamedata: quest%d give items: %w", id, err)
}
for field, dst := range map[int]*int{63: &entry.Type, 37: &entry.PriorQuestID, 35: &entry.NextQuestID} {
values, err := packedInts(proto, field)
if err != nil || len(values) > 1 || (len(values) == 1 && values[0] > uint64(^uint32(0)>>1)) {
return nil, fmt.Errorf("gamedata: invalid quest %d field%d", id, field)
}
if len(values) == 1 {
*dst = int(values[0])
}
}
for slot := 0; slot < len(entry.Rewards); slot++ {
types, err := packedInts(proto, 56+slot)
if err != nil {
return nil, fmt.Errorf("gamedata: quest %d reward slot %d type: %w", id, slot, err)
}
ids, err := packedInts(proto, 51+slot)
if err != nil {
return nil, fmt.Errorf("gamedata: quest %d reward slot %d id: %w", id, slot, err)
}
counts, err := packedInts(proto, 46+slot)
if err != nil {
return nil, fmt.Errorf("gamedata: quest %d reward slot %d count: %w", id, slot, err)
}
if len(types) != len(ids) || len(ids) != len(counts) {
return nil, fmt.Errorf("gamedata: quest %d reward slot %d has mismatched arrays", id, slot)
}
for i := range types {
// Currency has id zero; non-stackable costume/equipment rewards
// have count zero. Both are valid GameData representations.
if types[i] == 0 {
return nil, fmt.Errorf("gamedata: quest %d reward slot %d has zero type", id, slot)
}
entry.Rewards[slot] = append(entry.Rewards[slot], Reward{Type: types[i], ID: ids[i], Count: counts[i]})
}
}
result[id] = entry
}
if err := rows.Err(); err != nil {
return nil, err
}
return result, nil
}