Files
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

855 lines
26 KiB
Go

package expertjob
import (
"context"
"errors"
"fmt"
"math"
"path"
"sort"
"strings"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
)
const (
EnchanterType byte = 1
AlchemistType byte = 2
DisjointerType byte = 3
DollControllerType byte = 4
EnchanterPVFPath = "character/expertjob/enchanter.exj"
AlchemistPVFPath = "character/expertjob/alchemist.exj"
DisjointerPVFPath = "character/expertjob/disjointer.exj"
DollControllerPVFPath = "character/expertjob/doll_controller.exj"
)
var (
ErrCatalogUnavailable = errors.New("expert job catalog is unavailable")
ErrJobUnsupported = errors.New("expert job is not supported")
ErrRecipeUnavailable = errors.New("expert job recipe is unavailable")
ErrLevelTooLow = errors.New("expert job level is too low")
ErrExtractorInvalid = errors.New("expert job extractor is invalid")
ErrExtractionInvalid = errors.New("expert job extraction target is invalid")
ErrMachineEndurance = errors.New("expert job machine has no endurance")
ErrMachineInvalid = errors.New("expert job machine state is invalid")
ErrMachineGradeTooLow = errors.New("expert job machine grade is too low")
ErrInsufficientGold = errors.New("expert job gold is insufficient")
)
type randomSource interface {
Intn(int) int
}
type RandomFunc func(int) int
func (f RandomFunc) Intn(n int) int {
if n <= 1 || f == nil {
return 0
}
return f(n)
}
type itemKind byte
const (
itemStackable itemKind = iota + 1
itemEquipment
)
type itemReference struct {
kind itemKind
path string
}
type RecipeEntry struct {
ItemID int64
Count int64
}
type Recipe struct {
RecipeItemID int64
ProductItemID int64
RequiredLevel int
MinimumExperienceGain int
MaximumExperienceGain int
Materials []RecipeEntry
Output RecipeEntry
GoldCost int64
}
type CompoundRate struct {
MaximumLevelDifference int
SuccessRatePercent int
MinimumExperienceGain int
MaximumExperienceGain int
}
type Extractor struct {
ItemID int64
RequiredLevel int
MinimumExperienceGain int
MaximumExperienceGain int
}
type extractionRuleKey struct {
extractorID int64
rarity int
state int
}
type ExtractionRule struct {
ResultItemID int64
Multiplier float64
AdditionalTable int
BigWinTable int
BigWinChancePercent int
}
type SelectionRule struct {
MinimumLevel int
MaximumLevel int
ItemID int64
Weight int
QuantityMultiplier float64
}
type Config struct {
JobType byte
ExperienceThresholds []int64
GiveUpCosts []int64
AutoLearnRecipes map[int]int64
Skills map[int]int
CompoundRates []CompoundRate
Recipes map[int64]Recipe
Extractors map[int64]Extractor
ExtractionRules map[extractionRuleKey]ExtractionRule
AdditionalResults map[int][]SelectionRule
BigWinResults map[int][]SelectionRule
// ExtractionBaseConst selects the enchanter sell-price formula when it is
// positive. Alchemist and doll-controller extraction use equipment grade.
ExtractionBaseConst float64
}
func (c *Config) Level(experience int64) int {
if c == nil || len(c.ExperienceThresholds) == 0 {
return 0
}
level := 1
for _, threshold := range c.ExperienceThresholds {
if experience < threshold {
break
}
level++
}
if level > len(c.ExperienceThresholds) {
level = len(c.ExperienceThresholds)
}
return level
}
func (c *Config) AutoRecipeIDs(experience int64) []int64 {
if c == nil {
return nil
}
level := c.Level(experience)
type row struct {
level int
id int64
}
rows := make([]row, 0, len(c.AutoLearnRecipes))
for required, id := range c.AutoLearnRecipes {
if required <= level {
rows = append(rows, row{required, id})
}
}
sort.Slice(rows, func(i, j int) bool {
if rows[i].level != rows[j].level {
return rows[i].level < rows[j].level
}
return rows[i].id < rows[j].id
})
out := make([]int64, 0, len(rows))
for _, row := range rows {
out = append(out, row.id)
}
return out
}
func (c *Config) CanLearn(experience int64, recipeID int64) bool {
if c == nil {
return false
}
recipe, ok := c.Recipes[recipeID]
return ok && recipe.RequiredLevel <= c.Level(experience)+2
}
func (c *Config) compoundRate(recipeLevel, jobLevel int) CompoundRate {
if len(c.CompoundRates) == 0 {
return CompoundRate{SuccessRatePercent: 100}
}
difference := recipeLevel - jobLevel
for _, rate := range c.CompoundRates {
if rate.MaximumLevelDifference >= difference {
return rate
}
}
return c.CompoundRates[len(c.CompoundRates)-1]
}
type Catalog struct {
source dnfpvf.Source
items map[int64]itemReference
configs map[byte]*Config
equipmentSellRatePermille int64
enchanter *EnchanterConfig
disjointer *DisjointerConfig
}
func Load(ctx context.Context, source dnfpvf.Source) (*Catalog, error) {
if source == nil {
return nil, ErrCatalogUnavailable
}
if ctx == nil {
ctx = context.Background()
}
stackables, err := loadItemList(source, "stackable/stackable.lst", itemStackable)
if err != nil {
return nil, err
}
equipment, err := loadItemList(source, "equipment/equipment.lst", itemEquipment)
if err != nil {
return nil, err
}
for id, ref := range equipment {
if _, ok := stackables[id]; !ok {
stackables[id] = ref
}
}
catalog := &Catalog{source: source, items: stackables, configs: make(map[byte]*Config, 3)}
if err := catalog.loadEquipmentSellRate(); err != nil {
return nil, err
}
for _, input := range []struct {
typ byte
path, name, extraction string
}{
{EnchanterType, EnchanterPVFPath, "enchanter", "enchanter extraction"},
{AlchemistType, AlchemistPVFPath, "alchemist", "alchemist extraction"},
{DollControllerType, DollControllerPVFPath, "doll_controller", "doll_controller extraction"},
} {
if err := ctx.Err(); err != nil {
return nil, err
}
config, err := catalog.loadConfig(input.typ, input.path, input.name, input.extraction)
if err != nil {
return nil, fmt.Errorf("load expert job type=%d: %w", input.typ, err)
}
catalog.configs[input.typ] = config
}
if catalog.enchanter, err = catalog.loadEnchanterConfig(catalog.configs[EnchanterType]); err != nil {
return nil, fmt.Errorf("load enchanter config: %w", err)
}
if catalog.disjointer, err = catalog.loadDisjointerConfig(); err != nil {
return nil, fmt.Errorf("load disjointer config: %w", err)
}
return catalog, nil
}
func (c *Catalog) Config(jobType byte) (*Config, bool) {
if c == nil {
return nil, false
}
config, ok := c.configs[jobType]
return config, ok
}
func (c *Catalog) RecipeJob(recipeID int64) (byte, Recipe, bool) {
if c == nil {
return 0, Recipe{}, false
}
if c.enchanter != nil {
if recipe, ok := c.enchanter.CardRecipes[recipeID]; ok {
return EnchanterType, Recipe{RequiredLevel: recipe.RequiredLevel, Materials: append([]RecipeEntry(nil), recipe.Materials...)}, true
}
}
for _, jobType := range []byte{EnchanterType, AlchemistType, DollControllerType} {
config := c.configs[jobType]
if config == nil {
continue
}
if recipe, ok := config.Recipes[recipeID]; ok {
return jobType, recipe, true
}
}
return 0, Recipe{}, false
}
func (c *Catalog) Enchanter() (*EnchanterConfig, bool) {
if c == nil || c.enchanter == nil {
return nil, false
}
return c.enchanter, true
}
func (c *Catalog) Disjointer() (*DisjointerConfig, bool) {
if c == nil || c.disjointer == nil {
return nil, false
}
return c.disjointer, true
}
// GiveUpCosts returns the PVF-defined escalating abandonment costs for one
// expert job. The returned slice is detached from the immutable catalog.
func (c *Catalog) GiveUpCosts(jobType byte) ([]int64, bool) {
if c == nil {
return nil, false
}
if jobType == DisjointerType {
if c.disjointer == nil || len(c.disjointer.GiveUpCosts) == 0 {
return nil, false
}
return append([]int64(nil), c.disjointer.GiveUpCosts...), true
}
config := c.configs[jobType]
if config == nil || len(config.GiveUpCosts) == 0 {
return nil, false
}
return append([]int64(nil), config.GiveUpCosts...), true
}
type CompoundPlan struct {
AttemptedOutputs []RecipeEntry
Rewards []RecipeEntry
Materials []RecipeEntry
GoldCost int64
SuccessCount int
FailureCount int
ExperienceGain int64
FinalExperience int64
LevelChanged bool
}
func (c *Catalog) PlanCompound(jobType byte, experience int64, recipeID int64, count int, rng RandomFunc) (CompoundPlan, error) {
return c.PlanCompoundWithLearned(jobType, experience, recipeID, count, false, rng)
}
func (c *Catalog) PlanCompoundWithLearned(jobType byte, experience int64, recipeID int64, count int, learned bool, rng RandomFunc) (CompoundPlan, error) {
config, ok := c.Config(jobType)
if !ok {
return CompoundPlan{}, ErrJobUnsupported
}
recipe, ok := config.Recipes[recipeID]
if !ok || count <= 0 || count > math.MaxUint16 {
return CompoundPlan{}, ErrRecipeUnavailable
}
hasRecipe := learned
for _, id := range config.AutoRecipeIDs(experience) {
if id == recipeID {
hasRecipe = true
break
}
}
if !hasRecipe {
return CompoundPlan{}, ErrRecipeUnavailable
}
level := config.Level(experience)
if len(config.CompoundRates) == 0 && level < recipe.RequiredLevel {
return CompoundPlan{}, ErrLevelTooLow
}
plan := CompoundPlan{
AttemptedOutputs: []RecipeEntry{{ItemID: recipe.Output.ItemID, Count: recipe.Output.Count * int64(count)}},
Materials: multiplyEntries(recipe.Materials, int64(count)),
GoldCost: recipe.GoldCost * int64(count),
}
rate := config.compoundRate(recipe.RequiredLevel, level)
if len(config.CompoundRates) == 0 {
rate.MinimumExperienceGain = recipe.MinimumExperienceGain
rate.MaximumExperienceGain = recipe.MaximumExperienceGain
}
random := randomSource(RandomFunc(rng))
for attempt := 0; attempt < count; attempt++ {
if random.Intn(100) >= rate.SuccessRatePercent {
continue
}
plan.SuccessCount++
gain := rate.MinimumExperienceGain
if rate.MaximumExperienceGain > gain {
gain += random.Intn(rate.MaximumExperienceGain - gain + 1)
}
plan.ExperienceGain += int64(gain)
}
plan.FailureCount = count - plan.SuccessCount
if plan.SuccessCount > 0 {
plan.Rewards = []RecipeEntry{{ItemID: recipe.Output.ItemID, Count: recipe.Output.Count * int64(plan.SuccessCount)}}
}
plan.FinalExperience = saturatingAdd(experience, plan.ExperienceGain)
plan.LevelChanged = config.Level(experience) != config.Level(plan.FinalExperience)
return plan, nil
}
type EquipmentMetadata struct {
Rarity, Level, Grade, State int
SellGold int64
AttachType string
DisjointForbidden bool
}
func (c *Catalog) Equipment(itemID int64) (EquipmentMetadata, error) {
ref, ok := c.items[itemID]
if !ok || ref.kind != itemEquipment {
return EquipmentMetadata{}, ErrExtractionInvalid
}
doc, err := c.itemDocument(ref)
if err != nil {
return EquipmentMetadata{}, err
}
rarity, rarityOK := doc.Int("rarity")
level, levelOK := doc.Int("minimum level")
if !rarityOK || !levelOK || rarity < 0 || rarity > 6 || level < 0 || level > math.MaxInt32 {
return EquipmentMetadata{}, ErrExtractionInvalid
}
metadata := EquipmentMetadata{
Rarity: int(rarity),
Level: int(level),
Grade: int(level),
AttachType: normalizeTag(firstText(doc, "attach type")),
}
if grade, ok := doc.Int("grade"); ok && grade >= 0 && grade <= math.MaxInt32 {
metadata.Grade = int(grade)
}
base, baseFound := doc.Int("value")
if !baseFound || base < 0 {
base, baseFound = doc.Int("price")
}
if !baseFound || base < 0 || c.equipmentSellRatePermille <= 0 || base > math.MaxInt64/c.equipmentSellRatePermille {
return EquipmentMetadata{}, ErrExtractionInvalid
}
metadata.SellGold = base * c.equipmentSellRatePermille / 1000
if metadata.SellGold < 1 {
metadata.SellGold = 1
}
for _, content := range doc.Texts("impossible content") {
if normalizeTag(content) == "disjoint" {
metadata.DisjointForbidden = true
break
}
}
return metadata, nil
}
type ExtractionPlan struct {
Materials []RecipeEntry
ExperienceGain int64
FinalExperience int64
LevelChanged bool
}
func (c *Catalog) PlanExtraction(jobType byte, experience, extractorID int64, equipment EquipmentMetadata, rng RandomFunc) (ExtractionPlan, error) {
config, ok := c.Config(jobType)
if !ok {
return ExtractionPlan{}, ErrJobUnsupported
}
extractor, ok := config.Extractors[extractorID]
if !ok || config.Level(experience) < extractor.RequiredLevel {
return ExtractionPlan{}, ErrExtractorInvalid
}
rule, ok := config.ExtractionRules[extractionRuleKey{extractorID, equipment.Rarity, equipment.State}]
if !ok {
return ExtractionPlan{}, ErrExtractionInvalid
}
random := randomSource(RandomFunc(rng))
equipmentGrade := equipment.Grade
if equipmentGrade <= 0 {
equipmentGrade = equipment.Level
}
baseInput := float64(max(1, equipmentGrade))
if config.ExtractionBaseConst > 0 {
baseInput = float64(max(int64(1), equipment.SellGold)) / config.ExtractionBaseConst
}
base := int64(math.Floor(baseInput * rule.Multiplier))
if base < 1 {
base = 1
}
plan := ExtractionPlan{Materials: []RecipeEntry{{ItemID: rule.ResultItemID, Count: base}}}
bigWin := random.Intn(10000) < max(0, min(100, rule.BigWinChancePercent))*100
table := rule.AdditionalTable
selections := config.AdditionalResults
if bigWin {
table = rule.BigWinTable
selections = config.BigWinResults
}
if selected, ok := selectExtractionRule(selections[table], equipmentGrade, random); ok {
quantity := int64(math.Floor(float64(equipmentGrade) * selected.QuantityMultiplier))
if quantity < 1 {
quantity = 1
}
plan.Materials = mergeEntry(plan.Materials, RecipeEntry{ItemID: selected.ItemID, Count: quantity})
}
gain := extractor.MinimumExperienceGain
if extractor.MaximumExperienceGain > gain {
gain += random.Intn(extractor.MaximumExperienceGain - gain + 1)
}
plan.ExperienceGain = int64(gain)
plan.FinalExperience = saturatingAdd(experience, plan.ExperienceGain)
plan.LevelChanged = config.Level(experience) != config.Level(plan.FinalExperience)
return plan, nil
}
func (c *Catalog) loadConfig(jobType byte, pvfPath, jobName, extractionSection string) (*Config, error) {
doc, err := readDocument(c.source, pvfPath)
if err != nil {
return nil, err
}
config := &Config{JobType: jobType, AutoLearnRecipes: map[int]int64{}, Skills: map[int]int{}, Recipes: map[int64]Recipe{}, Extractors: map[int64]Extractor{}, ExtractionRules: map[extractionRuleKey]ExtractionRule{}, AdditionalResults: map[int][]SelectionRule{}, BigWinResults: map[int][]SelectionRule{}}
thresholds, found := doc.Section("expertness exp")
if !found || len(thresholds) == 0 || len(thresholds)%3 != 0 {
return nil, fmt.Errorf("%s [expertness exp] row width tokens=%d", pvfPath, len(thresholds))
}
for i := 0; i < len(thresholds); i += 3 {
if thresholds[i].Kind != dnfpvf.TokenInt {
return nil, fmt.Errorf("%s [expertness exp] threshold row=%d", pvfPath, i/3)
}
config.ExperienceThresholds = append(config.ExperienceThresholds, thresholds[i].Int)
}
for _, cost := range doc.Ints("giveup cost") {
if cost < 0 {
return nil, fmt.Errorf("%s [giveup cost] contains negative value %d", pvfPath, cost)
}
config.GiveUpCosts = append(config.GiveUpCosts, cost)
}
if len(config.GiveUpCosts) == 0 {
return nil, fmt.Errorf("%s [giveup cost] is empty", pvfPath)
}
if err := parsePairs(doc.Ints("auto learn recipe"), func(k, v int64) { config.AutoLearnRecipes[int(k)] = v }); err != nil {
return nil, fmt.Errorf("%s [auto learn recipe]: %w", pvfPath, err)
}
if err := parsePairs(doc.Ints("skill"), func(k, v int64) { config.Skills[int(k)] = int(v) }); err != nil {
return nil, fmt.Errorf("%s [skill]: %w", pvfPath, err)
}
rates := doc.Ints("compound rate")
if len(rates)%4 != 0 {
return nil, fmt.Errorf("%s [compound rate] row width", pvfPath)
}
for i := 0; i < len(rates); i += 4 {
config.CompoundRates = append(config.CompoundRates, CompoundRate{int(rates[i]), int(rates[i+1]), int(rates[i+2]), int(rates[i+3])})
}
extractExp, err := parseRanges(doc.Ints("extract exp"))
if err != nil {
return nil, fmt.Errorf("%s [extract exp]: %w", pvfPath, err)
}
productExp := map[int64][2]int{}
if values := doc.Ints("product exp"); len(values) > 0 {
productExp, err = parseRanges(values)
if err != nil {
return nil, fmt.Errorf("%s [product exp]: %w", pvfPath, err)
}
}
items := doc.Ints("items")
if len(items) == 0 || len(items)%4 != 0 {
return nil, fmt.Errorf("%s [items] row width", pvfPath)
}
for i := 0; i < len(items); i += 4 {
recipeID, productID, required := items[i+1], items[i+2], int(items[i+3])
experienceRange := productExp[recipeID]
if recipe, recipeErr := c.loadRecipe(recipeID, productID, required, experienceRange, config.Skills); recipeErr == nil {
config.Recipes[recipeID] = recipe
}
if experienceRange, ok := extractExp[productID]; ok {
if extractorErr := c.validateExtractor(productID, required, jobName, extractionSection); extractorErr != nil {
return nil, extractorErr
}
config.Extractors[productID] = Extractor{productID, required, experienceRange[0], experienceRange[1]}
}
}
if len(config.Recipes) == 0 || len(config.Extractors) == 0 {
return nil, fmt.Errorf("%s has no recipes or extractors", pvfPath)
}
values := sectionNumbers(doc, "extraction result")
if len(values) == 0 || len(values)%8 != 0 {
return nil, fmt.Errorf("%s [extraction result] row width", pvfPath)
}
for i := 0; i < len(values); i += 8 {
key := extractionRuleKey{int64(values[i]), int(values[i+1]), int(values[i+2])}
config.ExtractionRules[key] = ExtractionRule{int64(values[i+3]), values[i+4], int(values[i+5]), int(values[i+6]), int(values[i+7])}
}
if err := parseSelections(sectionNumbers(doc, "additional result"), config.AdditionalResults); err != nil {
return nil, err
}
if err := parseSelections(sectionNumbers(doc, "big win result"), config.BigWinResults); err != nil {
return nil, err
}
if jobType == EnchanterType {
base := doc.Ints("enchanter extraction result item")
if len(base) != 2 || base[0] <= 0 || base[1] <= 0 {
return nil, fmt.Errorf("%s [enchanter extraction result item] is invalid", pvfPath)
}
config.ExtractionBaseConst = float64(base[1])
}
return config, nil
}
func (c *Catalog) loadRecipe(recipeID, productID int64, required int, experienceRange [2]int, skills map[int]int) (Recipe, error) {
ref, ok := c.items[recipeID]
if !ok || ref.kind != itemStackable {
return Recipe{}, ErrRecipeUnavailable
}
doc, err := c.itemDocument(ref)
if err != nil {
return Recipe{}, err
}
category := normalizeTag(strings.Join(doc.Texts("item category"), " "))
if !strings.Contains(normalizeTag(firstText(doc, "stackable type")), "recipe") || category != "expertjob recipe" {
return Recipe{}, ErrRecipeUnavailable
}
needSkill := doc.Ints("need skill")
if len(needSkill) < 2 || needSkill[1] <= 0 {
return Recipe{}, ErrRecipeUnavailable
}
if _, ok := skills[int(needSkill[0])]; !ok {
return Recipe{}, ErrRecipeUnavailable
}
materials, outputs, gold := parseRecipeEntries(doc)
if len(materials) == 0 || len(outputs) != 1 || outputs[0].ItemID != productID {
return Recipe{}, ErrRecipeUnavailable
}
return Recipe{
RecipeItemID: recipeID, ProductItemID: productID, RequiredLevel: required,
MinimumExperienceGain: experienceRange[0], MaximumExperienceGain: experienceRange[1],
Materials: materials, Output: outputs[0], GoldCost: gold,
}, nil
}
func (c *Catalog) validateExtractor(itemID int64, required int, jobName, extractionSection string) error {
ref, ok := c.items[itemID]
if !ok || ref.kind != itemStackable {
return ErrExtractorInvalid
}
doc, err := c.itemDocument(ref)
if err != nil {
return err
}
jobOnly := normalizeTag(firstText(doc, "expertjob only"))
level, levelOK := doc.Int("expertjob only")
extraction, extractionOK := doc.Int(extractionSection)
if jobOnly != normalizeTag(jobName) || !levelOK || int(level) != required || !extractionOK || extraction < 0 {
return fmt.Errorf("%w: item=%d job=%q level=%d level_found=%t required=%d extraction=%d extraction_found=%t section=%q", ErrExtractorInvalid, itemID, jobOnly, level, levelOK, required, extraction, extractionOK, extractionSection)
}
return nil
}
func (c *Catalog) itemDocument(ref itemReference) (*dnfpvf.Document, error) {
root := "stackable"
if ref.kind == itemEquipment {
root = "equipment"
}
clean := cleanPath(ref.path)
candidates := []string{clean}
if !strings.HasPrefix(strings.ToLower(clean), root+"/") {
candidates = append(candidates, path.Join(root, clean))
}
for _, candidate := range append([]string(nil), candidates...) {
dir, base := path.Split(candidate)
if base != "" && !strings.HasPrefix(strings.ToLower(base), "(r)") {
candidates = append(candidates, dir+"(r)"+base)
}
}
var last error
for _, candidate := range candidates {
doc, err := readDocument(c.source, candidate)
if err == nil {
return doc, nil
}
last = err
}
return nil, last
}
func loadItemList(source dnfpvf.Source, pvfPath string, kind itemKind) (map[int64]itemReference, error) {
doc, err := readDocument(source, pvfPath)
if err != nil {
return nil, err
}
out := make(map[int64]itemReference)
for _, entry := range dnfpvf.ParseList(doc) {
if entry.ID > 0 {
out[entry.ID] = itemReference{kind, cleanPath(entry.Path)}
}
}
return out, nil
}
func readDocument(source dnfpvf.Source, pvfPath string) (*dnfpvf.Document, error) {
text, err := source.ReadText(pvfPath)
if err != nil {
return nil, fmt.Errorf("read %s: %w", pvfPath, err)
}
doc, err := dnfpvf.Parse(pvfPath, text)
if err != nil {
return nil, fmt.Errorf("parse %s: %w", pvfPath, err)
}
return doc, nil
}
func (c *Catalog) loadEquipmentSellRate() error {
if c == nil || c.source == nil {
return ErrCatalogUnavailable
}
doc, err := readDocument(c.source, "equipment/pricetable.tbl")
if err != nil {
return err
}
rates := doc.Ints("rate")
if len(rates) == 0 || rates[0] <= 0 || rates[0] > math.MaxInt32 {
return errors.New("equipment/pricetable.tbl [rate] is invalid")
}
c.equipmentSellRatePermille = rates[0]
return nil
}
func cleanPath(value string) string {
return strings.TrimPrefix(strings.TrimPrefix(strings.TrimSpace(strings.ReplaceAll(value, "\\", "/")), "./"), "/")
}
func normalizeTag(value string) string {
return strings.ToLower(strings.Trim(strings.TrimSpace(strings.ReplaceAll(value, "`", "")), "[]"))
}
func firstText(doc *dnfpvf.Document, name string) string { value, _ := doc.Text(name); return value }
func parsePairs(values []int64, add func(int64, int64)) error {
if len(values) == 0 || len(values)%2 != 0 {
return errors.New("row width is not 2")
}
seen := map[int64]struct{}{}
for i := 0; i < len(values); i += 2 {
if values[i] <= 0 || values[i+1] <= 0 {
return errors.New("non-positive pair")
}
if _, ok := seen[values[i]]; ok {
return errors.New("duplicate key")
}
seen[values[i]] = struct{}{}
add(values[i], values[i+1])
}
return nil
}
func parseRanges(values []int64) (map[int64][2]int, error) {
if len(values) == 0 || len(values)%3 != 0 {
return nil, errors.New("row width is not 3")
}
out := map[int64][2]int{}
for i := 0; i < len(values); i += 3 {
if values[i] <= 0 || values[i+1] < 0 || values[i+2] < values[i+1] {
return nil, errors.New("invalid range")
}
out[values[i]] = [2]int{int(values[i+1]), int(values[i+2])}
}
return out, nil
}
func sectionNumbers(doc *dnfpvf.Document, name string) []float64 { return doc.Numbers(name) }
func parseSelections(values []float64, target map[int][]SelectionRule) error {
if len(values)%6 != 0 {
return errors.New("expert job selection row width is not 6")
}
for i := 0; i < len(values); i += 6 {
table := int(values[i])
rule := SelectionRule{int(values[i+1]), int(values[i+2]), int64(values[i+3]), int(values[i+4]), values[i+5]}
if table <= 0 || rule.MinimumLevel < 0 || rule.MaximumLevel < rule.MinimumLevel || rule.ItemID <= 0 || rule.Weight <= 0 || rule.QuantityMultiplier <= 0 {
return errors.New("invalid expert job selection")
}
target[table] = append(target[table], rule)
}
return nil
}
func parseRecipeEntries(doc *dnfpvf.Document) ([]RecipeEntry, []RecipeEntry, int64) {
values := doc.Ints("int data")
if len(values) > 0 {
position, materialCount := 1, int(values[0])
if materialCount >= 0 && len(values) >= position+materialCount*2 {
materials := make([]RecipeEntry, 0, materialCount)
for i := 0; i < materialCount; i++ {
materials = append(materials, RecipeEntry{values[position], values[position+1]})
position += 2
}
if position < len(values) {
outputCount := int(values[position])
position++
if outputCount >= 0 && len(values) >= position+outputCount*2 {
outputs := make([]RecipeEntry, 0, outputCount)
for i := 0; i < outputCount; i++ {
outputs = append(outputs, RecipeEntry{values[position], values[position+1]})
position += 2
}
return materials, outputs, 0
}
}
}
}
input := doc.Ints("input item")
output := doc.Ints("output item")
materials := make([]RecipeEntry, 0, len(input)/2)
var gold int64
for i := 0; i+1 < len(input); i += 2 {
if input[i] == 0 {
gold = max(gold, input[i+1])
} else if input[i] > 0 && input[i+1] > 0 {
materials = append(materials, RecipeEntry{input[i], input[i+1]})
}
}
outputs := make([]RecipeEntry, 0, len(output)/2)
for i := 0; i+1 < len(output); i += 2 {
if output[i] > 0 && output[i+1] > 0 {
outputs = append(outputs, RecipeEntry{output[i], output[i+1]})
}
}
return materials, outputs, gold
}
func multiplyEntries(values []RecipeEntry, multiplier int64) []RecipeEntry {
out := make([]RecipeEntry, 0, len(values))
for _, value := range values {
out = append(out, RecipeEntry{value.ItemID, value.Count * multiplier})
}
return out
}
func mergeEntry(values []RecipeEntry, entry RecipeEntry) []RecipeEntry {
for i := range values {
if values[i].ItemID == entry.ItemID {
values[i].Count += entry.Count
return values
}
}
return append(values, entry)
}
func saturatingAdd(left, right int64) int64 {
if right > 0 && left > math.MaxInt64-right {
return math.MaxInt64
}
return left + right
}
func selectExtractionRule(values []SelectionRule, level int, rng randomSource) (SelectionRule, bool) {
total := 0
eligible := make([]SelectionRule, 0, len(values))
for _, value := range values {
if level >= value.MinimumLevel && level <= value.MaximumLevel {
eligible = append(eligible, value)
total += value.Weight
}
}
if total <= 0 {
return SelectionRule{}, false
}
roll := rng.Intn(total)
for _, value := range eligible {
if roll < value.Weight {
return value, true
}
roll -= value.Weight
}
return SelectionRule{}, false
}