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

226 lines
6.7 KiB
Go

package gamedata
import (
"database/sql"
"fmt"
"sort"
)
// IncludeCollaborationURWeapons extends only the UR equipment ticket pool.
// Limited costumes identify eligible owners; equipment IDs remain version data.
func (c *EquipmentGachaCatalog) IncludeCollaborationURWeapons(root, version string) error {
db, release, err := OpenDatabase(root, version, "common")
if err != nil {
return err
}
defer release()
return c.includeCollaborationURWeapons(db)
}
func (c *EquipmentGachaCatalog) includeCollaborationURWeapons(db *sql.DB) error {
if c == nil || c.equipment == nil {
return fmt.Errorf("gamedata: missing equipment catalog")
}
// Select guaranteed weapon products from their existing equipment definitions,
// rather than tying eligibility to a product or resource-ticket number.
var targets []uint64
for id, g := range c.Gachas {
if !g.TicketOnly || len(g.TicketIDs) == 0 || len(g.Pool) == 0 {
continue
}
eligible := true
for _, branch := range g.Pool {
if branch.ID != 0 || branch.Weight == 0 || len(branch.Children) == 0 {
eligible = false
break
}
for _, item := range branch.Children {
if item.ID == 0 || len(item.Children) != 0 {
eligible = false
break
}
}
}
if !eligible {
continue
}
var visit func([]WeightedEquipment) error
visit = func(pool []WeightedEquipment) error {
for _, entry := range pool {
if entry.ID == 0 {
if len(entry.Children) == 0 {
eligible = false
}
if err := visit(entry.Children); err != nil {
return err
}
continue
}
var raw []byte
if err := db.QueryRow("SELECT ProtoBuf FROM EquipmentTable WHERE id=?", entry.ID).Scan(&raw); err != nil {
return err
}
grade, e1 := packedInts(raw, 3)
quality, e2 := packedInts(raw, 18)
owner, e3 := packedInts(raw, 16)
if e1 != nil || e2 != nil || e3 != nil {
return fmt.Errorf("gamedata: malformed ticket equipment %d", entry.ID)
}
if len(grade) != 1 || grade[0] != 4 || len(quality) != 1 || quality[0] != 3 || len(owner) != 1 || owner[0] == 0 {
eligible = false
}
}
return nil
}
if err := visit(g.Pool); err != nil {
return err
}
if eligible {
targets = append(targets, id)
}
}
sort.Slice(targets, func(i, j int) bool { return targets[i] < targets[j] })
// Publish only after all selected products validate successfully.
clone := *c
clone.Gachas = make(map[uint64]EquipmentGacha, len(c.Gachas))
for id, g := range c.Gachas {
clone.Gachas[id] = g
}
clone.equipment = make(map[uint64]EquipmentDesign, len(c.equipment))
for id, d := range c.equipment {
clone.equipment[id] = d
}
for _, id := range targets {
if err := clone.includeCollaborationWeaponPool(db, id); err != nil {
return err
}
}
c.Gachas, c.equipment = clone.Gachas, clone.equipment
return nil
}
func (c *EquipmentGachaCatalog) includeCollaborationWeaponPool(db *sql.DB, id uint64) error {
g := c.Gachas[id]
readRows := func(query string) (map[uint64][]byte, error) {
rows, err := db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
out := map[uint64][]byte{}
for rows.Next() {
var id uint64
var raw []byte
if err := rows.Scan(&id, &raw); err != nil {
return nil, err
}
out[id] = raw
}
return out, rows.Err()
}
scalar := func(raw []byte, field int) (uint64, error) {
v, err := packedInts(raw, field)
if err != nil || len(v) != 1 || v[0] == 0 {
return 0, fmt.Errorf("gamedata: invalid collaboration equipment field %d", field)
}
return v[0], nil
}
owners, err := readRows("SELECT DISTINCT c.useUniqueCharId,c.ProtoBuf FROM LimitedCostumeTable l JOIN CostumeTable c ON c.id=l.id ORDER BY c.useUniqueCharId")
if err != nil {
return err
}
chars, err := readRows("SELECT id,ProtoBuf FROM CharTable WHERE uniqueCharId IN (SELECT privateUniqueCharId FROM EquipmentTable WHERE privateUniqueCharId>0) ORDER BY id")
if err != nil {
return err
}
grades := map[uint64]uint64{}
for _, raw := range chars {
owner, err := scalar(raw, 20)
if err != nil {
return err
}
grade, err := scalar(raw, 9)
if err != nil {
return err
}
if previous, exists := grades[owner]; exists && previous != grade {
return fmt.Errorf("gamedata: conflicting character grade for %d", owner)
}
grades[owner] = grade
}
equipment, err := readRows("SELECT id,ProtoBuf FROM EquipmentTable ORDER BY id")
if err != nil {
return err
}
pool := make([]WeightedEquipment, len(g.Pool))
branchForGrade := map[uint64]int{}
seen := map[uint64]bool{}
for index, branch := range g.Pool {
if branch.ID != 0 || branch.Weight == 0 || len(branch.Children) == 0 {
return fmt.Errorf("gamedata: malformed UR ticket branch")
}
pool[index] = WeightedEquipment{Weight: branch.Weight, Children: append([]WeightedEquipment(nil), branch.Children...)}
var branchGrade uint64
for _, item := range branch.Children {
if item.ID == 0 || item.Weight != 1 || len(item.Children) != 0 || seen[item.ID] {
return fmt.Errorf("gamedata: malformed or duplicate UR ticket candidate %d", item.ID)
}
seen[item.ID] = true
owner, err := scalar(equipment[item.ID], 16)
if err != nil {
return err
}
grade := grades[owner]
if grade == 0 || (branchGrade != 0 && branchGrade != grade) {
return fmt.Errorf("gamedata: unknown or mixed UR ticket character tier")
}
branchGrade = grade
}
if _, exists := branchForGrade[branchGrade]; exists {
return fmt.Errorf("gamedata: duplicate UR ticket tier")
}
branchForGrade[branchGrade] = index
}
// Collect designs before publishing the cloned pool, so a failed load leaves
// the catalog unchanged. Order by ID to keep repeated loads deterministic.
designs := map[uint64]EquipmentDesign{}
ids := make([]uint64, 0, len(equipment))
for id := range equipment {
ids = append(ids, id)
}
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
for _, id := range ids {
raw := equipment[id]
grade, _ := packedInts(raw, 3)
quality, _ := packedInts(raw, 18)
owner, _ := packedInts(raw, 16)
if len(grade) != 1 || grade[0] != 4 || len(quality) != 1 || quality[0] != 3 || len(owner) != 1 {
continue
}
if _, eligible := owners[owner[0]]; !eligible || seen[id] {
continue
}
index, exists := branchForGrade[grades[owner[0]]]
if !exists {
return fmt.Errorf("gamedata: unknown collaboration equipment tier for %d", id)
}
design, err := loadEquipmentDesign(db, id)
if err != nil {
return err
}
designs[id] = design
pool[index].Children = append(pool[index].Children, WeightedEquipment{ID: id, Weight: 1})
seen[id] = true
}
for id, design := range designs {
c.equipment[id] = design
}
g.Pool = pool
g.Grades = make(map[uint64]uint64, len(seen))
for id := range seen {
g.Grades[id] = c.equipment[id].Grade
}
c.Gachas[g.ID] = g
return nil
}