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.
281 lines
9.5 KiB
Go
281 lines
9.5 KiB
Go
package gamedata
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"math"
|
|
)
|
|
|
|
// EquipmentSmeltingDesign contains only static 2.35.10 GameData facts. It does
|
|
// not decide whether a lower-score result is applied or how mileage is paid:
|
|
// those are server transactions and require an official response capture.
|
|
type EquipmentSmeltingDesign struct {
|
|
Equipment map[uint64]EquipmentSmeltingItem
|
|
Ranks map[[2]uint64]EquipmentSmeltingRank
|
|
Grades map[uint64][]PromotionCost
|
|
Mileage EquipmentSmeltingMileage
|
|
MaxStreak uint64
|
|
}
|
|
|
|
type EquipmentSmeltingItem struct {
|
|
Grade, RankGroup, MaxLevel uint64
|
|
}
|
|
|
|
type EquipmentSmeltingRank struct {
|
|
// GrowthPoint is the per-rank battle-power contribution. Values is the
|
|
// separate 1..4 score used to compare a refinement candidate.
|
|
GrowthPoint []uint64
|
|
Values []uint64
|
|
Ratio []float64
|
|
}
|
|
|
|
type EquipmentSmeltingMileage struct {
|
|
UseType, UseID, UseCount uint64
|
|
RewardType, RewardID, RewardCount uint64
|
|
}
|
|
|
|
func LoadEquipmentSmeltingDesign(root, version string) (*EquipmentSmeltingDesign, error) {
|
|
db, release, err := OpenDatabase(root, version, "common")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer release()
|
|
return loadEquipmentSmeltingDesign(db)
|
|
}
|
|
|
|
func loadEquipmentSmeltingDesign(db *sql.DB) (*EquipmentSmeltingDesign, error) {
|
|
d := &EquipmentSmeltingDesign{
|
|
Equipment: make(map[uint64]EquipmentSmeltingItem),
|
|
Ranks: make(map[[2]uint64]EquipmentSmeltingRank),
|
|
Grades: make(map[uint64][]PromotionCost),
|
|
}
|
|
rows, err := db.Query("SELECT id,ProtoBuf FROM EquipmentTable")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for rows.Next() {
|
|
var id uint64
|
|
var proto []byte
|
|
if err := rows.Scan(&id, &proto); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
grade, _ := packedInts(proto, 3)
|
|
maximum, _ := packedInts(proto, 13)
|
|
rankGroup, _ := packedInts(proto, 19)
|
|
if len(grade) != 1 || len(maximum) != 1 || len(rankGroup) != 1 || grade[0] == 0 || maximum[0] == 0 || rankGroup[0] == 0 {
|
|
rows.Close()
|
|
return nil, fmt.Errorf("gamedata: equipment %d has invalid smelting design", id)
|
|
}
|
|
d.Equipment[id] = EquipmentSmeltingItem{Grade: grade[0], RankGroup: rankGroup[0], MaxLevel: maximum[0]}
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
rows, err = db.Query("SELECT id,ProtoBuf FROM EquipmentGradeTable")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for rows.Next() {
|
|
var id uint64
|
|
var proto []byte
|
|
if err := rows.Scan(&id, &proto); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
counts, _ := packedInts(proto, 4)
|
|
ids, _ := packedInts(proto, 5)
|
|
types, _ := packedInts(proto, 6)
|
|
if len(counts) == 0 || len(counts) != len(ids) || len(counts) != len(types) {
|
|
rows.Close()
|
|
return nil, fmt.Errorf("gamedata: equipment grade %d has invalid smelting costs", id)
|
|
}
|
|
for i := range counts {
|
|
if counts[i] == 0 || (types[i] != 4 && types[i] != 8) || (types[i] == 4 && ids[i] != 0) || (types[i] == 8 && ids[i] == 0) {
|
|
rows.Close()
|
|
return nil, fmt.Errorf("gamedata: equipment grade %d has invalid smelting material", id)
|
|
}
|
|
d.Grades[id] = append(d.Grades[id], PromotionCost{Type: types[i], ID: ids[i], Count: counts[i]})
|
|
}
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
rows, err = db.Query("SELECT groupId,id,ProtoBuf FROM EquipmentRankTable")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for rows.Next() {
|
|
var group, slot uint64
|
|
var proto []byte
|
|
if err := rows.Scan(&group, &slot, &proto); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
ratios, err := fixed32Floats(proto, 4)
|
|
growth, growthErr := packedInts(proto, 2)
|
|
values, valueErr := packedInts(proto, 5)
|
|
if err != nil || growthErr != nil || valueErr != nil || slot < 1 || slot > 3 || len(ratios) != 4 || len(growth) != 4 || len(values) != 4 {
|
|
rows.Close()
|
|
return nil, fmt.Errorf("gamedata: equipment rank %d/%d has invalid smelting distribution", group, slot)
|
|
}
|
|
var total float64
|
|
for i := range ratios {
|
|
if ratios[i] < 0 || ratios[i] > 1 || math.IsNaN(ratios[i]) || values[i] == 0 || growth[i] == 0 {
|
|
rows.Close()
|
|
return nil, fmt.Errorf("gamedata: equipment rank %d/%d has invalid smelting value", group, slot)
|
|
}
|
|
total += ratios[i]
|
|
if i != 0 && (values[i] <= values[i-1] || growth[i] <= growth[i-1]) {
|
|
rows.Close()
|
|
return nil, fmt.Errorf("gamedata: equipment rank %d/%d is not increasing", group, slot)
|
|
}
|
|
}
|
|
if math.Abs(total-1) > 1e-5 {
|
|
rows.Close()
|
|
return nil, fmt.Errorf("gamedata: equipment rank %d/%d ratio total %.8f", group, slot, total)
|
|
}
|
|
d.Ranks[[2]uint64{group, slot}] = EquipmentSmeltingRank{GrowthPoint: growth, Values: values, Ratio: ratios}
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
for id, equipment := range d.Equipment {
|
|
if len(d.Grades[equipment.Grade]) == 0 {
|
|
return nil, fmt.Errorf("gamedata: equipment %d has no grade costs", id)
|
|
}
|
|
for slot := uint64(1); slot <= 3; slot++ {
|
|
if len(d.Ranks[[2]uint64{equipment.RankGroup, slot}].Values) != 4 {
|
|
return nil, fmt.Errorf("gamedata: equipment %d has no rank slot %d", id, slot)
|
|
}
|
|
}
|
|
}
|
|
var mileageProto []byte
|
|
if err := db.QueryRow("SELECT ProtoBuf FROM EquipmentMileageTable WHERE id=0").Scan(&mileageProto); err != nil {
|
|
return nil, fmt.Errorf("gamedata: equipment mileage: %w", err)
|
|
}
|
|
mileageCount, _ := packedInts(mileageProto, 2)
|
|
mileageID, _ := packedInts(mileageProto, 3)
|
|
mileageType, _ := packedInts(mileageProto, 4)
|
|
useCount, _ := packedInts(mileageProto, 5)
|
|
useID, _ := packedInts(mileageProto, 6)
|
|
useType, _ := packedInts(mileageProto, 7)
|
|
if len(mileageCount) != 1 || len(mileageID) > 1 || len(mileageType) != 1 || len(useCount) != 1 || len(useID) != 1 || len(useType) != 1 || mileageCount[0] == 0 || useCount[0] == 0 {
|
|
return nil, fmt.Errorf("gamedata: invalid equipment mileage definition")
|
|
}
|
|
if len(mileageID) == 1 {
|
|
d.Mileage.RewardID = mileageID[0]
|
|
}
|
|
d.Mileage.RewardCount, d.Mileage.RewardType = mileageCount[0], mileageType[0]
|
|
d.Mileage.UseCount, d.Mileage.UseID, d.Mileage.UseType = useCount[0], useID[0], useType[0]
|
|
if d.Mileage.RewardType == 0 || d.Mileage.UseType == 0 || d.Mileage.UseID == 0 {
|
|
return nil, fmt.Errorf("gamedata: invalid equipment mileage item types")
|
|
}
|
|
for grade, costs := range d.Grades {
|
|
found := false
|
|
for _, cost := range costs {
|
|
if cost.Type == d.Mileage.UseType && cost.ID == d.Mileage.UseID {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return nil, fmt.Errorf("gamedata: equipment grade %d does not consume mileage material", grade)
|
|
}
|
|
}
|
|
var defaults []byte
|
|
if err := db.QueryRow("SELECT ProtoBuf FROM GameDefaultTable WHERE id=0").Scan(&defaults); err != nil {
|
|
return nil, fmt.Errorf("gamedata: equipment smelting limit: %w", err)
|
|
}
|
|
maxStreak, _ := packedInts(defaults, 5)
|
|
if len(maxStreak) != 1 || maxStreak[0] == 0 {
|
|
return nil, fmt.Errorf("gamedata: invalid maximum smelting streak")
|
|
}
|
|
d.MaxStreak = maxStreak[0]
|
|
return d, nil
|
|
}
|
|
|
|
func (d *EquipmentSmeltingDesign) Cost(equipmentID uint64) ([]PromotionCost, error) {
|
|
if d == nil {
|
|
return nil, fmt.Errorf("gamedata: equipment smelting design unavailable")
|
|
}
|
|
equipment, ok := d.Equipment[equipmentID]
|
|
if !ok || len(d.Grades[equipment.Grade]) == 0 {
|
|
return nil, fmt.Errorf("gamedata: equipment %d has no smelting cost", equipmentID)
|
|
}
|
|
return append([]PromotionCost(nil), d.Grades[equipment.Grade]...), nil
|
|
}
|
|
|
|
func (d *EquipmentSmeltingDesign) Score(equipmentID uint64, ranks []uint64) (uint64, error) {
|
|
if d == nil || len(ranks) != 3 {
|
|
return 0, fmt.Errorf("gamedata: invalid equipment rank score input")
|
|
}
|
|
equipment, ok := d.Equipment[equipmentID]
|
|
if !ok {
|
|
return 0, fmt.Errorf("gamedata: equipment %d has no smelting design", equipmentID)
|
|
}
|
|
var score uint64
|
|
for i, rank := range ranks {
|
|
values := d.Ranks[[2]uint64{equipment.RankGroup, uint64(i + 1)}].Values
|
|
if rank < 1 || rank > uint64(len(values)) || values[rank-1] > ^uint64(0)-score {
|
|
return 0, fmt.Errorf("gamedata: invalid equipment rank %d in slot %d", rank, i+1)
|
|
}
|
|
score += values[rank-1]
|
|
}
|
|
return score, nil
|
|
}
|
|
|
|
func (d *EquipmentSmeltingDesign) MaximumRanks(equipmentID uint64) ([]uint64, error) {
|
|
if d == nil {
|
|
return nil, fmt.Errorf("gamedata: equipment smelting design unavailable")
|
|
}
|
|
equipment, ok := d.Equipment[equipmentID]
|
|
if !ok {
|
|
return nil, fmt.Errorf("gamedata: equipment %d has no smelting design", equipmentID)
|
|
}
|
|
ranks := make([]uint64, 3)
|
|
for slot := uint64(1); slot <= 3; slot++ {
|
|
values := d.Ranks[[2]uint64{equipment.RankGroup, slot}].Values
|
|
if len(values) == 0 {
|
|
return nil, fmt.Errorf("gamedata: equipment %d has no rank slot %d", equipmentID, slot)
|
|
}
|
|
ranks[slot-1] = uint64(len(values))
|
|
}
|
|
return ranks, nil
|
|
}
|
|
|
|
// RollCandidate independently rolls all three rank slots from the official
|
|
// EquipmentRankTable distributions. Whether the candidate replaces the
|
|
// current ranks is a player-state transaction, not a GameData concern.
|
|
func (d *EquipmentSmeltingDesign) RollCandidate(equipmentID uint64) ([]uint64, error) {
|
|
if d == nil {
|
|
return nil, fmt.Errorf("gamedata: equipment smelting design unavailable")
|
|
}
|
|
equipment, ok := d.Equipment[equipmentID]
|
|
if !ok {
|
|
return nil, fmt.Errorf("gamedata: equipment %d has no smelting design", equipmentID)
|
|
}
|
|
result := make([]uint64, 3)
|
|
for slot := uint64(1); slot <= 3; slot++ {
|
|
rank := d.Ranks[[2]uint64{equipment.RankGroup, slot}]
|
|
value, err := cryptoRankRoll(rank.Ratio)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("gamedata: roll equipment %d rank slot %d: %w", equipmentID, slot, err)
|
|
}
|
|
result[slot-1] = value
|
|
}
|
|
return result, nil
|
|
}
|