355 lines
11 KiB
Go
355 lines
11 KiB
Go
package gamedata
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"math"
|
|
)
|
|
|
|
const equipmentMakingTalentClass = 9
|
|
|
|
type EquipmentCraftRecipe struct {
|
|
ID uint64
|
|
ResultCount uint64
|
|
TalentLevel uint64
|
|
Costs []PromotionCost
|
|
Results []WeightedEquipment
|
|
}
|
|
|
|
type equipmentMakingTalent struct {
|
|
SkillGroup uint64
|
|
GrowthGroup uint64
|
|
}
|
|
|
|
type equipmentMakingTalentSkill struct {
|
|
Experience uint64
|
|
Catalyst uint64
|
|
}
|
|
|
|
type GeneratedEquipment struct {
|
|
Design EquipmentDesign
|
|
Main []EquipmentOptionChoice
|
|
Sub []EquipmentOptionChoice
|
|
Private *EquipmentOptionChoice
|
|
}
|
|
|
|
type EquipmentCraftDesign struct {
|
|
Recipes map[uint64]EquipmentCraftRecipe
|
|
Equipment map[uint64]EquipmentDesign
|
|
CharTalent map[uint64]equipmentMakingTalent
|
|
Skills map[[2]uint64]equipmentMakingTalentSkill
|
|
TalentNeed map[[2]uint64]uint64
|
|
draw func(uint64) (uint64, error)
|
|
}
|
|
|
|
func LoadEquipmentCraftDesign(root, version string) (*EquipmentCraftDesign, error) {
|
|
db, closeDB, err := openStatDatabase(root, version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer closeDB()
|
|
return loadEquipmentCraftDesign(db)
|
|
}
|
|
|
|
func loadEquipmentCraftDesign(db *sql.DB) (*EquipmentCraftDesign, error) {
|
|
design := &EquipmentCraftDesign{
|
|
Recipes: make(map[uint64]EquipmentCraftRecipe), Equipment: make(map[uint64]EquipmentDesign),
|
|
CharTalent: make(map[uint64]equipmentMakingTalent), Skills: make(map[[2]uint64]equipmentMakingTalentSkill),
|
|
TalentNeed: make(map[[2]uint64]uint64), draw: cryptoDraw,
|
|
}
|
|
rows, err := db.Query("SELECT id,ProtoBuf FROM EquipmentMakingTable ORDER BY id")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for rows.Next() {
|
|
var id uint64
|
|
var raw []byte
|
|
if err := rows.Scan(&id, &raw); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
counts, _ := packedInts(raw, 4)
|
|
materialIDs, _ := packedInts(raw, 5)
|
|
materialTypes, _ := packedInts(raw, 6)
|
|
resultCounts, _ := packedInts(raw, 7)
|
|
boxIDs, _ := packedInts(raw, 8)
|
|
resultTypes, _ := packedInts(raw, 9)
|
|
talentLevels, _ := packedInts(raw, 11)
|
|
if len(counts) == 0 || len(counts) != len(materialIDs) || len(counts) != len(materialTypes) ||
|
|
len(resultCounts) != 1 || resultCounts[0] != 1 || len(boxIDs) != 1 ||
|
|
len(resultTypes) != 1 || resultTypes[0] != 9 || len(talentLevels) != 1 || talentLevels[0] == 0 {
|
|
rows.Close()
|
|
return nil, fmt.Errorf("gamedata: equipment making recipe %d malformed", id)
|
|
}
|
|
var box []byte
|
|
if err := db.QueryRow("SELECT ProtoBuf FROM RandomBoxTable WHERE id=?", boxIDs[0]).Scan(&box); err != nil {
|
|
rows.Close()
|
|
return nil, fmt.Errorf("gamedata: equipment making recipe %d random box: %w", id, err)
|
|
}
|
|
groups, _ := packedInts(box, 9)
|
|
if len(groups) != 1 || groups[0] == 0 {
|
|
rows.Close()
|
|
return nil, fmt.Errorf("gamedata: equipment making random box %d malformed", boxIDs[0])
|
|
}
|
|
results, equipmentOnly, err := classifyEquipmentRewardPool(db, groups[0])
|
|
if err != nil || !equipmentOnly || len(results) == 0 {
|
|
rows.Close()
|
|
return nil, fmt.Errorf("gamedata: equipment making recipe %d reward group: %w", id, err)
|
|
}
|
|
recipe := EquipmentCraftRecipe{ID: id, ResultCount: resultCounts[0], TalentLevel: talentLevels[0], Results: results}
|
|
for i := range counts {
|
|
if counts[i] == 0 || materialIDs[i] == 0 || materialTypes[i] == 0 {
|
|
rows.Close()
|
|
return nil, fmt.Errorf("gamedata: equipment making recipe %d invalid material", id)
|
|
}
|
|
recipe.Costs = append(recipe.Costs, PromotionCost{Type: materialTypes[i], ID: materialIDs[i], Count: counts[i]})
|
|
}
|
|
if err := design.loadEquipmentPool(db, results); err != nil {
|
|
rows.Close()
|
|
return nil, fmt.Errorf("gamedata: equipment making recipe %d: %w", id, err)
|
|
}
|
|
design.Recipes[id] = recipe
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
talents := make(map[uint64]equipmentMakingTalent)
|
|
rows, err = db.Query("SELECT id,ProtoBuf FROM TalentTable")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for rows.Next() {
|
|
var id uint64
|
|
var raw []byte
|
|
if err := rows.Scan(&id, &raw); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
classes, _ := packedInts(raw, 4)
|
|
growthGroups, _ := packedInts(raw, 6)
|
|
skillGroups, _ := packedInts(raw, 18)
|
|
if len(classes) == 1 && classes[0] == equipmentMakingTalentClass && len(growthGroups) == 1 && len(skillGroups) == 1 {
|
|
talents[id] = equipmentMakingTalent{SkillGroup: skillGroups[0], GrowthGroup: growthGroups[0]}
|
|
}
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
rows, err = db.Query("SELECT id,ProtoBuf FROM CharTable")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for rows.Next() {
|
|
var id uint64
|
|
var raw []byte
|
|
if err := rows.Scan(&id, &raw); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
talentIDs, _ := packedInts(raw, 18)
|
|
if len(talentIDs) == 1 {
|
|
if talent, ok := talents[talentIDs[0]]; ok {
|
|
design.CharTalent[id] = talent
|
|
}
|
|
}
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
rows, err = db.Query("SELECT groupId,id,ProtoBuf FROM TalentSkillTable WHERE classType=?", equipmentMakingTalentClass)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for rows.Next() {
|
|
var group, level uint64
|
|
var raw []byte
|
|
if err := rows.Scan(&group, &level, &raw); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
catalysts, _ := packedInts(raw, 1)
|
|
experience, _ := packedInts(raw, 5)
|
|
if len(catalysts) > 1 || len(experience) > 1 {
|
|
rows.Close()
|
|
return nil, fmt.Errorf("gamedata: equipment making talent skill %d/%d malformed", group, level)
|
|
}
|
|
var skill equipmentMakingTalentSkill
|
|
if len(experience) == 1 {
|
|
skill.Experience = experience[0]
|
|
}
|
|
if len(catalysts) == 1 {
|
|
skill.Catalyst = catalysts[0]
|
|
}
|
|
design.Skills[[2]uint64{group, level}] = skill
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
rows, err = db.Query("SELECT groupId,id,ProtoBuf FROM TalentGrowthTable")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for rows.Next() {
|
|
var group, level uint64
|
|
var raw []byte
|
|
if err := rows.Scan(&group, &level, &raw); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
need, _ := packedInts(raw, 6)
|
|
if len(need) == 1 {
|
|
design.TalentNeed[[2]uint64{group, level}] = need[0]
|
|
}
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
return design, nil
|
|
}
|
|
|
|
func (d *EquipmentCraftDesign) loadEquipmentPool(db *sql.DB, pool []WeightedEquipment) error {
|
|
for _, result := range pool {
|
|
if result.ID == 0 {
|
|
if len(result.Children) == 0 {
|
|
return fmt.Errorf("empty nested equipment reward")
|
|
}
|
|
if err := d.loadEquipmentPool(db, result.Children); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
if len(result.Children) != 0 {
|
|
return fmt.Errorf("equipment reward %d also has children", result.ID)
|
|
}
|
|
if _, exists := d.Equipment[result.ID]; exists {
|
|
continue
|
|
}
|
|
equipment, err := loadEquipmentDesign(db, result.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
d.Equipment[result.ID] = equipment
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *EquipmentCraftDesign) Recipe(id uint64) (EquipmentCraftRecipe, bool) {
|
|
if d == nil {
|
|
return EquipmentCraftRecipe{}, false
|
|
}
|
|
recipe, ok := d.Recipes[id]
|
|
return recipe, ok
|
|
}
|
|
|
|
func (d *EquipmentCraftDesign) Generate(recipeID uint64) (GeneratedEquipment, error) {
|
|
if d == nil {
|
|
return GeneratedEquipment{}, fmt.Errorf("gamedata: equipment crafting unavailable")
|
|
}
|
|
recipe, ok := d.Recipes[recipeID]
|
|
if !ok {
|
|
return GeneratedEquipment{}, fmt.Errorf("gamedata: unknown equipment making recipe %d", recipeID)
|
|
}
|
|
draw := d.draw
|
|
if draw == nil {
|
|
draw = cryptoDraw
|
|
}
|
|
id, err := rollEquipmentChoiceWith(recipe.Results, draw)
|
|
if err != nil {
|
|
return GeneratedEquipment{}, err
|
|
}
|
|
equipment, ok := d.Equipment[id]
|
|
if !ok {
|
|
return GeneratedEquipment{}, fmt.Errorf("gamedata: equipment making result %d missing", id)
|
|
}
|
|
main, sub, private, err := rollEquipmentOptionsWith(equipment, draw)
|
|
if err != nil {
|
|
return GeneratedEquipment{}, err
|
|
}
|
|
return GeneratedEquipment{Design: equipment, Main: main, Sub: sub, Private: private}, nil
|
|
}
|
|
|
|
func rollEquipmentOptionsWith(design EquipmentDesign, draw func(uint64) (uint64, error)) (main, sub []EquipmentOptionChoice, private *EquipmentOptionChoice, err error) {
|
|
pick := func(groups []OptionGroup) ([]EquipmentOptionChoice, error) {
|
|
out := make([]EquipmentOptionChoice, 0, len(groups))
|
|
for _, group := range groups {
|
|
pool := make([]WeightedEquipment, 0, len(group.Choices))
|
|
for _, choice := range group.Choices {
|
|
pool = append(pool, WeightedEquipment{ID: choice.ID, Weight: choice.Weight})
|
|
}
|
|
id, err := rollEquipmentChoiceWith(pool, draw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, EquipmentOptionChoice{GroupID: group.ID, ID: id})
|
|
}
|
|
return out, nil
|
|
}
|
|
if main, err = pick(design.Main); err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
if sub, err = pick(design.Sub); err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
choices, err := pick(design.Private)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
if len(choices) > 1 {
|
|
return nil, nil, nil, fmt.Errorf("gamedata: equipment %d has %d private option groups", design.ID, len(choices))
|
|
}
|
|
if len(choices) == 1 {
|
|
choice := choices[0]
|
|
private = &choice
|
|
}
|
|
return main, sub, private, nil
|
|
}
|
|
|
|
func (d *EquipmentCraftDesign) Talent(characterID, characterLevel, recipeLevel, count, currentExperience uint64) (gain, catalyst, maximum uint64, err error) {
|
|
if d == nil || characterID == 0 || characterLevel == 0 || recipeLevel == 0 || count == 0 {
|
|
return 0, 0, 0, fmt.Errorf("gamedata: invalid equipment making talent request")
|
|
}
|
|
talent, ok := d.CharTalent[characterID]
|
|
if !ok {
|
|
return 0, 0, 0, fmt.Errorf("gamedata: character %d has no equipment making talent", characterID)
|
|
}
|
|
if characterLevel < recipeLevel {
|
|
return 0, 0, 0, fmt.Errorf("gamedata: equipment making talent level %d is below recipe level %d", characterLevel, recipeLevel)
|
|
}
|
|
recipeSkill, ok := d.Skills[[2]uint64{talent.SkillGroup, recipeLevel}]
|
|
if !ok {
|
|
return 0, 0, 0, fmt.Errorf("gamedata: equipment making talent skill %d/%d missing", talent.SkillGroup, recipeLevel)
|
|
}
|
|
currentSkill, ok := d.Skills[[2]uint64{talent.SkillGroup, characterLevel}]
|
|
if !ok {
|
|
return 0, 0, 0, fmt.Errorf("gamedata: equipment making talent skill %d/%d missing", talent.SkillGroup, characterLevel)
|
|
}
|
|
if currentSkill.Experience != 0 && count > math.MaxUint64/currentSkill.Experience {
|
|
return 0, 0, 0, fmt.Errorf("gamedata: equipment making talent experience overflow")
|
|
}
|
|
if recipeSkill.Catalyst != 0 && count > math.MaxUint64/recipeSkill.Catalyst {
|
|
return 0, 0, 0, fmt.Errorf("gamedata: equipment making catalyst overflow")
|
|
}
|
|
for level := uint64(1); level <= characterLevel; level++ {
|
|
need, exists := d.TalentNeed[[2]uint64{talent.GrowthGroup, level}]
|
|
if !exists {
|
|
return 0, 0, 0, fmt.Errorf("gamedata: equipment making talent growth %d/%d missing", talent.GrowthGroup, level)
|
|
}
|
|
if maximum > math.MaxUint64-need {
|
|
return 0, 0, 0, fmt.Errorf("gamedata: equipment making talent threshold overflow")
|
|
}
|
|
maximum += need
|
|
}
|
|
wanted := currentSkill.Experience * count
|
|
if currentExperience >= maximum {
|
|
wanted = 0
|
|
} else if wanted > maximum-currentExperience {
|
|
wanted = maximum - currentExperience
|
|
}
|
|
return wanted, recipeSkill.Catalyst * count, maximum, nil
|
|
}
|