DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,404 @@
|
||||
package expertjob
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
||||
)
|
||||
|
||||
type RepairRule struct {
|
||||
FullRepairCost int64
|
||||
MaxEndurance int64
|
||||
}
|
||||
|
||||
type EnchanterCardRecipe struct {
|
||||
Qualification int
|
||||
RecipeItemID int64
|
||||
RequiredLevel int
|
||||
Materials []RecipeEntry
|
||||
}
|
||||
|
||||
type EnchanterCard struct {
|
||||
Qualification int
|
||||
BindChance int
|
||||
}
|
||||
|
||||
type EnchanterCardExperienceRule struct {
|
||||
Level int
|
||||
SuccessRates [5]int
|
||||
ExtraRate int
|
||||
MinimumExperienceGain int
|
||||
MaximumExperienceGain int
|
||||
}
|
||||
|
||||
type EnchanterConfig struct {
|
||||
Recipes *Config
|
||||
MaximumStoreCharge int64
|
||||
InitialEndurance int64
|
||||
EnduranceReduction int64
|
||||
EnduranceMinimumLevel int
|
||||
RepairRules []RepairRule
|
||||
QualificationRequiredLevel map[int]int
|
||||
CardRecipes map[int64]EnchanterCardRecipe
|
||||
Cards map[int64]EnchanterCard
|
||||
BeadByCard map[int64]int64
|
||||
CardExperienceByLevel map[int]EnchanterCardExperienceRule
|
||||
}
|
||||
|
||||
func (c *EnchanterConfig) Qualifications(experience int64) []byte {
|
||||
if c == nil || c.Recipes == nil {
|
||||
return nil
|
||||
}
|
||||
level := c.Recipes.Level(experience)
|
||||
values := make([]int, 0, len(c.QualificationRequiredLevel))
|
||||
for qualification, required := range c.QualificationRequiredLevel {
|
||||
if qualification >= 0 && qualification <= math.MaxUint8 && required <= level {
|
||||
values = append(values, qualification)
|
||||
}
|
||||
}
|
||||
sort.Ints(values)
|
||||
out := make([]byte, len(values))
|
||||
for i, value := range values {
|
||||
out[i] = byte(value)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (c *EnchanterConfig) StoreSkills(experience int64) []byte {
|
||||
if c == nil || c.Recipes == nil {
|
||||
return nil
|
||||
}
|
||||
level := c.Recipes.Level(experience)
|
||||
values := make([]int, 0, len(c.Recipes.Skills))
|
||||
for skill, required := range c.Recipes.Skills {
|
||||
if skill > 0 && skill <= math.MaxUint8 && required <= level {
|
||||
values = append(values, skill)
|
||||
}
|
||||
}
|
||||
sort.Ints(values)
|
||||
out := make([]byte, len(values))
|
||||
for i, value := range values {
|
||||
out[i] = byte(value)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (c *EnchanterConfig) RepairRule(experience int64) (RepairRule, bool) {
|
||||
if c == nil || c.Recipes == nil {
|
||||
return RepairRule{}, false
|
||||
}
|
||||
index := c.Recipes.Level(experience) - 1
|
||||
if index < 0 || index >= len(c.RepairRules) {
|
||||
return RepairRule{}, false
|
||||
}
|
||||
return c.RepairRules[index], true
|
||||
}
|
||||
|
||||
func (c *EnchanterConfig) KnowsRecipe(recipeID int64) bool {
|
||||
if c == nil {
|
||||
return false
|
||||
}
|
||||
if _, ok := c.CardRecipes[recipeID]; ok {
|
||||
return true
|
||||
}
|
||||
_, ok := c.Recipes.Recipes[recipeID]
|
||||
return ok
|
||||
}
|
||||
|
||||
type EnchanterBeadPlan struct {
|
||||
Recipe EnchanterCardRecipe
|
||||
Card EnchanterCard
|
||||
BeadItemID int64
|
||||
Success bool
|
||||
ExperienceGain int64
|
||||
FinalExperience int64
|
||||
LevelChanged bool
|
||||
}
|
||||
|
||||
func (c *Catalog) PlanEnchanterBead(experience, recipeID, cardID int64, rng RandomFunc) (EnchanterBeadPlan, error) {
|
||||
config, ok := c.Enchanter()
|
||||
if !ok {
|
||||
return EnchanterBeadPlan{}, ErrJobUnsupported
|
||||
}
|
||||
recipe, recipeOK := config.CardRecipes[recipeID]
|
||||
card, cardOK := config.Cards[cardID]
|
||||
bead, beadOK := config.BeadByCard[cardID]
|
||||
level := config.Recipes.Level(experience)
|
||||
rule, ruleOK := config.CardExperienceByLevel[level]
|
||||
rateIndex, rateOK := enchanterCardSuccessRateIndex(recipe.Qualification, len(rule.SuccessRates))
|
||||
if !recipeOK || !cardOK || !beadOK || !ruleOK || recipe.Qualification != card.Qualification ||
|
||||
level < recipe.RequiredLevel || !rateOK {
|
||||
return EnchanterBeadPlan{}, ErrRecipeUnavailable
|
||||
}
|
||||
random := randomSource(RandomFunc(rng))
|
||||
plan := EnchanterBeadPlan{Recipe: recipe, Card: card, BeadItemID: bead}
|
||||
plan.Success = random.Intn(100) < rule.SuccessRates[rateIndex]
|
||||
if plan.Success {
|
||||
gain := rule.MinimumExperienceGain
|
||||
if rule.MaximumExperienceGain > gain {
|
||||
gain += random.Intn(rule.MaximumExperienceGain - gain + 1)
|
||||
}
|
||||
plan.ExperienceGain = int64(gain)
|
||||
}
|
||||
plan.FinalExperience = saturatingAdd(experience, plan.ExperienceGain)
|
||||
plan.LevelChanged = config.Recipes.Level(experience) != config.Recipes.Level(plan.FinalExperience)
|
||||
return plan, nil
|
||||
}
|
||||
|
||||
type EnchanterStorePlan struct {
|
||||
Recipe EnchanterCardRecipe
|
||||
Success bool
|
||||
EnduranceReduction int64
|
||||
ExperienceGain int64
|
||||
FinalExperience int64
|
||||
LevelChanged bool
|
||||
}
|
||||
|
||||
func (c *Catalog) PlanEnchanterStore(experience, recipeID, cardID, endurance int64, rng RandomFunc) (EnchanterStorePlan, error) {
|
||||
config, ok := c.Enchanter()
|
||||
if !ok {
|
||||
return EnchanterStorePlan{}, ErrJobUnsupported
|
||||
}
|
||||
recipe, recipeOK := config.CardRecipes[recipeID]
|
||||
card, cardOK := config.Cards[cardID]
|
||||
level := config.Recipes.Level(experience)
|
||||
rule, ruleOK := config.CardExperienceByLevel[level]
|
||||
rateIndex, rateOK := enchanterCardSuccessRateIndex(recipe.Qualification, len(rule.SuccessRates))
|
||||
if !recipeOK || !cardOK || !ruleOK || recipe.Qualification != card.Qualification || level < recipe.RequiredLevel || !rateOK {
|
||||
return EnchanterStorePlan{}, ErrRecipeUnavailable
|
||||
}
|
||||
reduction := int64(0)
|
||||
if level >= config.EnduranceMinimumLevel {
|
||||
reduction = config.EnduranceReduction
|
||||
}
|
||||
if endurance < reduction {
|
||||
return EnchanterStorePlan{}, ErrMachineEndurance
|
||||
}
|
||||
random := randomSource(RandomFunc(rng))
|
||||
plan := EnchanterStorePlan{Recipe: recipe, EnduranceReduction: reduction}
|
||||
plan.Success = random.Intn(100) < rule.SuccessRates[rateIndex]
|
||||
if plan.Success {
|
||||
gain := rule.MinimumExperienceGain
|
||||
if rule.MaximumExperienceGain > gain {
|
||||
gain += random.Intn(rule.MaximumExperienceGain - gain + 1)
|
||||
}
|
||||
plan.ExperienceGain = int64(gain)
|
||||
}
|
||||
plan.FinalExperience = saturatingAdd(experience, plan.ExperienceGain)
|
||||
plan.LevelChanged = config.Recipes.Level(experience) != config.Recipes.Level(plan.FinalExperience)
|
||||
return plan, nil
|
||||
}
|
||||
|
||||
// Current Script.pvf has five success-rate columns but uses both
|
||||
// qualification 4 and the later high presentation grade 6. Only that exact
|
||||
// active-PVF extension shares the final column; unknown future qualification
|
||||
// values remain fail closed while recipe/card qualification equality stays
|
||||
// exact.
|
||||
func enchanterCardSuccessRateIndex(qualification, width int) (int, bool) {
|
||||
if qualification >= 0 && qualification < width {
|
||||
return qualification, true
|
||||
}
|
||||
if width == 5 && qualification == 6 {
|
||||
return 4, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (c *Catalog) loadEnchanterConfig(recipes *Config) (*EnchanterConfig, error) {
|
||||
if recipes == nil {
|
||||
return nil, ErrCatalogUnavailable
|
||||
}
|
||||
doc, err := readDocument(c.source, EnchanterPVFPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config := &EnchanterConfig{
|
||||
Recipes: recipes, QualificationRequiredLevel: map[int]int{}, CardRecipes: map[int64]EnchanterCardRecipe{},
|
||||
Cards: map[int64]EnchanterCard{}, BeadByCard: map[int64]int64{}, CardExperienceByLevel: map[int]EnchanterCardExperienceRule{},
|
||||
}
|
||||
if value, ok := doc.Int("limit store charge"); ok {
|
||||
config.MaximumStoreCharge = value
|
||||
}
|
||||
if value, ok := doc.Int("endurance initial value"); ok {
|
||||
config.InitialEndurance = value
|
||||
}
|
||||
reduction := doc.Ints("endurance reduce")
|
||||
if len(reduction) != 2 {
|
||||
return nil, errors.New("enchanter [endurance reduce] is invalid")
|
||||
}
|
||||
config.EnduranceMinimumLevel, config.EnduranceReduction = int(reduction[0]), reduction[1]
|
||||
repairs := doc.Ints("endurance repair cost")
|
||||
if len(repairs) == 0 || len(repairs)%2 != 0 {
|
||||
return nil, errors.New("enchanter [endurance repair cost] is invalid")
|
||||
}
|
||||
for i := 0; i < len(repairs); i += 2 {
|
||||
if repairs[i] <= 0 || repairs[i+1] <= 0 {
|
||||
return nil, errors.New("enchanter repair rule is invalid")
|
||||
}
|
||||
config.RepairRules = append(config.RepairRules, RepairRule{repairs[i], repairs[i+1]})
|
||||
}
|
||||
if err := c.parseEnchanterRarityRecipes(doc, config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bind := doc.Ints("monstercard bind info")
|
||||
if len(bind) == 0 || len(bind)%2 != 0 {
|
||||
return nil, fmt.Errorf("enchanter [monstercard bind info] is invalid: values=%d", len(bind))
|
||||
}
|
||||
for i := 0; i < len(bind); i += 2 {
|
||||
qualification, chance := int(bind[i]), int(bind[i+1])
|
||||
if qualification < 0 || chance < 0 || chance > 1000 {
|
||||
return nil, fmt.Errorf("enchanter bind rule is invalid: qualification=%d chance=%d", qualification, chance)
|
||||
}
|
||||
}
|
||||
rules := doc.Ints("monstercard exp")
|
||||
if len(rules) == 0 || len(rules)%9 != 0 {
|
||||
return nil, errors.New("enchanter [monstercard exp] is invalid")
|
||||
}
|
||||
for i := 0; i < len(rules); i += 9 {
|
||||
rule := EnchanterCardExperienceRule{Level: int(rules[i]), ExtraRate: int(rules[i+6]), MinimumExperienceGain: int(rules[i+7]), MaximumExperienceGain: int(rules[i+8])}
|
||||
for j := range rule.SuccessRates {
|
||||
rule.SuccessRates[j] = int(rules[i+1+j])
|
||||
if rule.SuccessRates[j] < 0 || rule.SuccessRates[j] > 100 {
|
||||
return nil, errors.New("enchanter card success rate is invalid")
|
||||
}
|
||||
}
|
||||
if rule.Level <= 0 || rule.ExtraRate < 0 || rule.ExtraRate > 100 || rule.MinimumExperienceGain < 0 || rule.MaximumExperienceGain < rule.MinimumExperienceGain {
|
||||
return nil, errors.New("enchanter card experience rule is invalid")
|
||||
}
|
||||
config.CardExperienceByLevel[rule.Level] = rule
|
||||
}
|
||||
if config.MaximumStoreCharge < 0 || config.InitialEndurance <= 0 || config.EnduranceReduction <= 0 || config.EnduranceMinimumLevel <= 0 ||
|
||||
len(config.RepairRules) != len(recipes.ExperienceThresholds) || len(config.CardRecipes) == 0 || len(config.Cards) == 0 ||
|
||||
len(config.BeadByCard) == 0 || len(config.CardExperienceByLevel) != len(recipes.ExperienceThresholds) {
|
||||
return nil, errors.New("enchanter configuration is incomplete")
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (c *Catalog) parseEnchanterRarityRecipes(doc *dnfpvf.Document, config *EnchanterConfig) error {
|
||||
active := false
|
||||
for index := 0; index < len(doc.Sections); index++ {
|
||||
name := strings.ToLower(strings.TrimSpace(doc.Sections[index].Name))
|
||||
switch name {
|
||||
case "rarity recipe":
|
||||
active = true
|
||||
continue
|
||||
case "/rarity recipe":
|
||||
return nil
|
||||
}
|
||||
if !active || name != "rarity" {
|
||||
continue
|
||||
}
|
||||
qualificationValues := sectionInts(doc, doc.Sections[index])
|
||||
if len(qualificationValues) != 1 {
|
||||
return errors.New("enchanter [rarity] is invalid")
|
||||
}
|
||||
qualification := int(qualificationValues[0])
|
||||
var recipeID, required int64
|
||||
var baseResults []int64
|
||||
for index++; index < len(doc.Sections); index++ {
|
||||
childName := strings.ToLower(strings.TrimSpace(doc.Sections[index].Name))
|
||||
if childName == "/rarity" {
|
||||
break
|
||||
}
|
||||
values := sectionInts(doc, doc.Sections[index])
|
||||
switch childName {
|
||||
case "recipe":
|
||||
if len(values) == 1 {
|
||||
recipeID = values[0]
|
||||
}
|
||||
case "expert job level":
|
||||
if len(values) == 1 {
|
||||
required = values[0]
|
||||
}
|
||||
case "base result":
|
||||
baseResults = values
|
||||
}
|
||||
}
|
||||
if qualification < 0 || qualification > math.MaxUint8 || recipeID <= 0 || required <= 0 || len(baseResults) == 0 || len(baseResults)%2 != 0 {
|
||||
return errors.New("enchanter rarity recipe is invalid")
|
||||
}
|
||||
ref, found := c.items[recipeID]
|
||||
if !found || ref.kind != itemStackable {
|
||||
return fmt.Errorf("enchanter card recipe item=%d is unavailable", recipeID)
|
||||
}
|
||||
recipeDoc, err := c.itemDocument(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
jobOnly := normalizeTag(firstText(recipeDoc, "expertjob only"))
|
||||
jobLevel, levelFound := recipeDoc.Int("expertjob only")
|
||||
needSkill := recipeDoc.Ints("need skill")
|
||||
// Card-to-bead recipes split their costs across [input item] and
|
||||
// [bead item]. The latter is the rarity soul/essence cost (for example
|
||||
// unique recipes consume 2610041 x3); omitting it let the compound
|
||||
// succeed without consuming the PVF-authoritative soul material.
|
||||
materialValues := append([]int64(nil), recipeDoc.Ints("input item")...)
|
||||
materialValues = append(materialValues, recipeDoc.Ints("bead item")...)
|
||||
materials := positivePairs(materialValues)
|
||||
if !strings.Contains(normalizeTag(firstText(recipeDoc, "stackable type")), "recipe") || jobOnly != "enchanter" || !levelFound || jobLevel != required ||
|
||||
len(needSkill) < 2 || needSkill[1] <= 0 || config.Recipes.Skills[int(needSkill[0])] == 0 || len(materials) == 0 {
|
||||
return errors.New("enchanter card recipe metadata is invalid")
|
||||
}
|
||||
config.QualificationRequiredLevel[qualification] = int(required)
|
||||
config.CardRecipes[recipeID] = EnchanterCardRecipe{qualification, recipeID, int(required), materials}
|
||||
for i := 0; i < len(baseResults); i += 2 {
|
||||
cardID, beadID := baseResults[i], baseResults[i+1]
|
||||
if cardID <= 0 || beadID <= 0 {
|
||||
return errors.New("enchanter bead result is invalid")
|
||||
}
|
||||
if _, duplicate := config.BeadByCard[cardID]; duplicate {
|
||||
return errors.New("enchanter bead result is duplicated")
|
||||
}
|
||||
ref, found := c.items[cardID]
|
||||
config.BeadByCard[cardID] = beadID
|
||||
if found && ref.kind == itemStackable {
|
||||
cardDoc, loadErr := c.itemDocument(ref)
|
||||
if loadErr == nil && normalizeTag(strings.Join(cardDoc.Texts("item category"), " ")) == "monster card" {
|
||||
config.Cards[cardID] = EnchanterCard{Qualification: qualification}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors.New("enchanter [rarity recipe] is not closed")
|
||||
}
|
||||
|
||||
func sectionInts(doc *dnfpvf.Document, section dnfpvf.Section) []int64 {
|
||||
if doc == nil || section.Start < 0 || section.End > len(doc.Tokens) || section.Start > section.End {
|
||||
return nil
|
||||
}
|
||||
values := make([]int64, 0, section.End-section.Start)
|
||||
for _, token := range doc.Tokens[section.Start:section.End] {
|
||||
if token.Kind == dnfpvf.TokenInt {
|
||||
values = append(values, token.Int)
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func positivePairs(values []int64) []RecipeEntry {
|
||||
if len(values) == 0 || len(values)%2 != 0 {
|
||||
return nil
|
||||
}
|
||||
merged := map[int64]int64{}
|
||||
for i := 0; i < len(values); i += 2 {
|
||||
if values[i] <= 0 || values[i+1] <= 0 || merged[values[i]] > math.MaxInt64-values[i+1] {
|
||||
return nil
|
||||
}
|
||||
merged[values[i]] += values[i+1]
|
||||
}
|
||||
keys := make([]int64, 0, len(merged))
|
||||
for itemID := range merged {
|
||||
keys = append(keys, itemID)
|
||||
}
|
||||
sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })
|
||||
out := make([]RecipeEntry, 0, len(keys))
|
||||
for _, itemID := range keys {
|
||||
out = append(out, RecipeEntry{itemID, merged[itemID]})
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user