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

437 lines
16 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package expertjob
import (
"errors"
"math"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
)
type disjointerResultKey struct {
MachineGrade int
Rarity int
State int
}
type DisjointerResultRule struct {
ItemID int64
Multiplier float64
AdditionalTable int
BigWinTable int
BigWinChancePercent int
}
type DisjointerSelectionRule struct {
MinimumGrade int
MaximumGrade int
ItemID int64
Weight int
CountDivisor float64
}
type DisjointerConfig struct {
InitialEndurance int64
MaximumStoreCharge int64
GiveUpCosts []int64
BaseConst float64
EnduranceReduceMin int
EnduranceReduceMax int
ExperienceGainMin int
ExperienceGainMax int
SelfServiceChance int
SelfServiceItemID int64
SelfServiceItemCount int64
Results map[disjointerResultKey]DisjointerResultRule
AdditionalResults map[int][]DisjointerSelectionRule
BigWinResults map[int][]DisjointerSelectionRule
RepairRules []RepairRule
ExperienceThresholds []int64
UpgradeCosts map[int]int64
CharacterLevelLimits map[int]int
// ChronicleSetItemID / ChronicleSetCount are [chronicle set disjoint
// result]: the fixed refund of 异界 (chronicle, rarity 5) equipment
// (异次元碎片 ×20 in the live PVF). The bridge grants it when
// reinforcement or amplification destroys the piece. Zero when the file
// has no such section.
ChronicleSetItemID int64
ChronicleSetCount int64
}
// DisjointerChronicleRarity is the PVF rarity index of 异界 equipment.
const DisjointerChronicleRarity = 5
// ChronicleSetResult returns the fixed [chronicle set disjoint result] refund.
func (c *DisjointerConfig) ChronicleSetResult() (itemID, count int64, ok bool) {
if c == nil || c.ChronicleSetItemID <= 0 || c.ChronicleSetCount <= 0 {
return 0, 0, false
}
return c.ChronicleSetItemID, c.ChronicleSetCount, true
}
// The disjointer.exj [disjoint result] state column: 0 ordinary equipment, 1
// equipment carrying an otherverse aura (红字), 2 boss-unique equipment. The
// otherverse aura rolls [additional result] table 6 (浓缩的异界精髓 /
// 矛盾的结晶体 in the live PVF).
const (
DisjointerOtherverseState = 1
DisjointerOtherverseTable = 6
)
// ResultRule returns the [disjoint result] row for one machine-grade column
// (the file's first column, 0 for the lowest machine), equipment rarity and
// state.
func (c *DisjointerConfig) ResultRule(machineGradeColumn, rarity, state int) (DisjointerResultRule, bool) {
if c == nil {
return DisjointerResultRule{}, false
}
rule, found := c.Results[disjointerResultKey{machineGradeColumn, rarity, state}]
return rule, found
}
// SelectAdditional picks one weighted [additional result] row of a table
// whose level range covers level.
func (c *DisjointerConfig) SelectAdditional(table, level int, rng RandomFunc) (DisjointerSelectionRule, bool) {
if c == nil {
return DisjointerSelectionRule{}, false
}
return selectDisjointerRule(c.AdditionalResults[table], level, randomSource(rng))
}
// SelectBigWin picks one weighted [big win result] row of a table whose level
// range covers level.
func (c *DisjointerConfig) SelectBigWin(table, level int, rng RandomFunc) (DisjointerSelectionRule, bool) {
if c == nil {
return DisjointerSelectionRule{}, false
}
return selectDisjointerRule(c.BigWinResults[table], level, randomSource(rng))
}
// LevelDivisorCount mirrors 86JP PlayerDisjointResultCalculator.
// RollLevelDivisorCount: floor(level / divisor) plus one more on a fractional
// roll, never below zero. A divisor of 100 therefore yields one item on
// level% of the rolls and nothing otherwise.
func LevelDivisorCount(level int, divisor float64, rng RandomFunc) int64 {
return levelDivisorCount(level, divisor, randomSource(rng))
}
func levelDivisorCount(level int, divisor float64, random randomSource) int64 {
if level <= 0 || divisor <= 0 || random == nil {
return 0
}
value := float64(level) / divisor
count := math.Floor(value)
if fraction := value - count; fraction > 0 && float64(random.Intn(10000)) < fraction*10000 {
count++
}
if count < 0 {
return 0
}
return int64(count)
}
func (c *DisjointerConfig) 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++
}
return min(level, len(c.ExperienceThresholds))
}
func (c *DisjointerConfig) RepairRule(grade int) (RepairRule, bool) {
if c == nil || grade <= 0 || grade > len(c.RepairRules) {
return RepairRule{}, false
}
return c.RepairRules[grade-1], true
}
type DisjointerPlan struct {
Materials []RecipeEntry
EnduranceReduction int64
ExperienceGain int64
FinalExperience int64
LevelChanged bool
}
func (c *Catalog) PlanDisjointer(experience int64, machineGrade int, equipment EquipmentMetadata, selfService bool, rng RandomFunc) (DisjointerPlan, error) {
config, ok := c.Disjointer()
if !ok || machineGrade <= 0 {
return DisjointerPlan{}, ErrMachineInvalid
}
rule, found := config.Results[disjointerResultKey{machineGrade - 1, equipment.Rarity, equipment.State}]
if !found && equipment.State == 1 {
return DisjointerPlan{}, ErrMachineGradeTooLow
}
if !found || rule.ItemID <= 0 || rule.Multiplier <= 0 || config.BaseConst <= 0 {
return DisjointerPlan{}, ErrExtractionInvalid
}
random := randomSource(RandomFunc(rng))
equipmentGrade := equipment.Grade
if equipmentGrade <= 0 {
equipmentGrade = equipment.Level
}
adjustedSellGold := math.Max(1, math.Floor(float64(max(int64(1), equipment.SellGold))*1.1))
base := int64(math.Max(1, math.Floor(adjustedSellGold*rule.Multiplier/config.BaseConst)))
plan := DisjointerPlan{Materials: []RecipeEntry{{rule.ItemID, 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, selectedOK := selectDisjointerRule(selections[table], equipmentGrade, random); selectedOK {
quantity := int64(1)
if selected.CountDivisor > 0 {
quantity = int64(math.Floor(float64(equipmentGrade) / selected.CountDivisor))
if quantity < 1 {
quantity = 1
}
}
plan.Materials = mergeEntry(plan.Materials, RecipeEntry{selected.ItemID, quantity})
}
// Equipment carrying an otherverse aura (红字) additionally rolls the
// [additional result] otherverse table, unless that table was already the
// ordinary (non big-win) selection (86JP
// PlayerDisjointResultCalculator.Calculate).
if equipment.State == DisjointerOtherverseState && (bigWin || table != DisjointerOtherverseTable) {
if selected, selectedOK := selectDisjointerRule(config.AdditionalResults[DisjointerOtherverseTable], equipmentGrade, random); selectedOK {
if quantity := levelDivisorCount(equipmentGrade, selected.CountDivisor, random); quantity > 0 {
plan.Materials = mergeEntry(plan.Materials, RecipeEntry{selected.ItemID, quantity})
}
}
}
if selfService && equipment.Rarity > 1 && config.SelfServiceItemID > 0 && config.SelfServiceItemCount > 0 && random.Intn(100) < config.SelfServiceChance {
plan.Materials = mergeEntry(plan.Materials, RecipeEntry{config.SelfServiceItemID, config.SelfServiceItemCount})
}
plan.EnduranceReduction = int64(config.EnduranceReduceMin)
if config.EnduranceReduceMax > config.EnduranceReduceMin {
plan.EnduranceReduction += int64(random.Intn(config.EnduranceReduceMax - config.EnduranceReduceMin + 1))
}
plan.ExperienceGain = int64(config.ExperienceGainMin)
if config.ExperienceGainMax > config.ExperienceGainMin {
plan.ExperienceGain += int64(random.Intn(config.ExperienceGainMax - config.ExperienceGainMin + 1))
}
plan.FinalExperience = saturatingAdd(experience, plan.ExperienceGain)
plan.LevelChanged = config.Level(experience) != config.Level(plan.FinalExperience)
return plan, nil
}
type MachineRepairPlan struct {
Gold int64
Endurance int64
Cost int64
}
func PlanMachineRepair(gold, currentEndurance int64, rule RepairRule) (MachineRepairPlan, error) {
if gold < 0 || currentEndurance < 0 || rule.MaxEndurance <= 0 || rule.FullRepairCost <= 0 {
return MachineRepairPlan{}, ErrMachineInvalid
}
current := min(currentEndurance, rule.MaxEndurance)
cost := rule.FullRepairCost - current*rule.FullRepairCost/rule.MaxEndurance
if cost <= 0 {
return MachineRepairPlan{}, ErrMachineInvalid
}
if gold < cost {
unit := rule.FullRepairCost / rule.MaxEndurance
if unit <= 0 {
return MachineRepairPlan{}, ErrMachineInvalid
}
cost = gold - gold%unit
repaired := cost / unit
if cost <= 0 || repaired <= 0 {
return MachineRepairPlan{}, ErrMachineInvalid
}
current = min(rule.MaxEndurance, current+repaired)
} else {
current = rule.MaxEndurance
}
return MachineRepairPlan{Gold: gold - cost, Endurance: current, Cost: cost}, nil
}
type DisjointerUpgradePlan struct {
Gold int64
Grade int
Endurance int64
Cost int64
}
func (c *DisjointerConfig) PlanUpgrade(gold, experience, endurance int64, machineGrade, characterLevel int) (DisjointerUpgradePlan, error) {
if c == nil || gold < 0 || machineGrade <= 0 || characterLevel <= 0 {
return DisjointerUpgradePlan{}, ErrMachineInvalid
}
current, currentOK := c.RepairRule(machineGrade)
targetGrade := machineGrade + 1
target, targetOK := c.RepairRule(targetGrade)
cost, costOK := c.UpgradeCosts[targetGrade]
minimumCharacterLevel, levelOK := c.CharacterLevelLimits[targetGrade]
if !currentOK || !targetOK || !costOK || cost <= 0 || !levelOK || endurance != current.MaxEndurance || c.Level(experience) < targetGrade {
return DisjointerUpgradePlan{}, ErrMachineInvalid
}
if characterLevel < minimumCharacterLevel {
return DisjointerUpgradePlan{}, ErrLevelTooLow
}
if gold < cost {
return DisjointerUpgradePlan{}, ErrInsufficientGold
}
return DisjointerUpgradePlan{Gold: gold - cost, Grade: targetGrade, Endurance: target.MaxEndurance, Cost: cost}, nil
}
func (c *Catalog) loadDisjointerConfig() (*DisjointerConfig, error) {
doc, err := readDocument(c.source, DisjointerPVFPath)
if err != nil {
return nil, err
}
config := &DisjointerConfig{
Results: map[disjointerResultKey]DisjointerResultRule{}, AdditionalResults: map[int][]DisjointerSelectionRule{},
BigWinResults: map[int][]DisjointerSelectionRule{}, UpgradeCosts: map[int]int64{}, CharacterLevelLimits: map[int]int{},
}
config.InitialEndurance, _ = doc.Int("endurance initial value")
config.MaximumStoreCharge, _ = doc.Int("limit store charge")
for _, cost := range doc.Ints("giveup cost") {
if cost < 0 {
return nil, errors.New("disjointer [giveup cost] contains a negative value")
}
config.GiveUpCosts = append(config.GiveUpCosts, cost)
}
if value, ok := doc.Int("base const"); ok {
config.BaseConst = float64(value)
}
reduce := doc.Ints("endurance reduce")
gain := doc.Ints("gain exp")
self := doc.Ints("disjoint self service")
if len(reduce) != 2 || len(gain) != 2 || len(self) < 3 {
return nil, errors.New("disjointer scalar rules are invalid")
}
config.EnduranceReduceMin, config.EnduranceReduceMax = int(reduce[0]), int(reduce[1])
config.ExperienceGainMin, config.ExperienceGainMax = int(gain[0]), int(gain[1])
config.SelfServiceChance, config.SelfServiceItemID, config.SelfServiceItemCount = int(self[0]), self[1], self[2]
results := doc.Numbers("disjoint result")
if len(results) == 0 || len(results)%8 != 0 {
return nil, errors.New("disjointer [disjoint result] is invalid")
}
for i := 0; i < len(results); i += 8 {
key := disjointerResultKey{int(results[i]), int(results[i+1]), int(results[i+2])}
rule := DisjointerResultRule{int64(results[i+3]), results[i+4], int(results[i+5]), int(results[i+6]), int(results[i+7])}
if key.MachineGrade < 0 || key.Rarity < 0 || key.State < 0 || rule.ItemID <= 0 || rule.Multiplier <= 0 {
return nil, errors.New("disjointer result rule is invalid")
}
if _, duplicate := config.Results[key]; duplicate {
return nil, errors.New("disjointer result rule is duplicated")
}
config.Results[key] = rule
}
if err := parseDisjointerSelections(doc.Numbers("additional result"), config.AdditionalResults); err != nil {
return nil, err
}
if err := parseDisjointerSelections(doc.Numbers("big win result"), config.BigWinResults); err != nil {
return nil, err
}
if chronicleSet := doc.Ints("chronicle set disjoint result"); len(chronicleSet) != 0 {
if len(chronicleSet) != 2 || chronicleSet[0] <= 0 || chronicleSet[1] <= 0 {
return nil, errors.New("disjointer [chronicle set disjoint result] is invalid")
}
config.ChronicleSetItemID, config.ChronicleSetCount = chronicleSet[0], chronicleSet[1]
}
repairs := doc.Ints("endurance repair cost")
if len(repairs) == 0 || len(repairs)%2 != 0 {
return nil, errors.New("disjointer repair rules are invalid")
}
for i := 0; i < len(repairs); i += 2 {
if repairs[i] <= 0 || repairs[i+1] <= 0 {
return nil, errors.New("disjointer repair rule is invalid")
}
config.RepairRules = append(config.RepairRules, RepairRule{repairs[i], repairs[i+1]})
}
thresholdTokens, found := doc.Section("expertness exp")
if !found || len(thresholdTokens) == 0 || len(thresholdTokens)%3 != 0 {
return nil, errors.New("disjointer experience thresholds are invalid")
}
for i := 0; i < len(thresholdTokens); i += 3 {
if thresholdTokens[i].Kind != dnfpvf.TokenInt {
return nil, errors.New("disjointer experience threshold is invalid")
}
config.ExperienceThresholds = append(config.ExperienceThresholds, thresholdTokens[i].Int)
}
if err := parseInt64Pairs(doc.Ints("upgrade cost"), config.UpgradeCosts); err != nil {
return nil, err
}
etc, err := readDocument(c.source, "character/expertjob.etc")
if err != nil {
return nil, err
}
limits := map[int]int64{}
if err := parseInt64Pairs(etc.Ints("expertjob level limit"), limits); err != nil {
return nil, err
}
for grade, level := range limits {
config.CharacterLevelLimits[grade] = int(level)
}
if config.InitialEndurance <= 0 || config.MaximumStoreCharge < 0 || len(config.GiveUpCosts) == 0 || config.BaseConst <= 0 || config.EnduranceReduceMin < 0 ||
config.EnduranceReduceMax < config.EnduranceReduceMin || config.ExperienceGainMin < 0 || config.ExperienceGainMax < config.ExperienceGainMin ||
len(config.Results) == 0 || len(config.RepairRules) == 0 || len(config.RepairRules) != len(config.ExperienceThresholds) ||
len(config.UpgradeCosts) != len(config.RepairRules) || len(config.CharacterLevelLimits) < len(config.RepairRules) {
return nil, errors.New("disjointer configuration is incomplete")
}
return config, nil
}
func parseDisjointerSelections(values []float64, target map[int][]DisjointerSelectionRule) error {
if len(values)%6 != 0 {
return errors.New("disjointer selection row width is invalid")
}
for i := 0; i < len(values); i += 6 {
table := int(values[i])
rule := DisjointerSelectionRule{int(values[i+1]), int(values[i+2]), int64(values[i+3]), int(values[i+4]), values[i+5]}
if table <= 0 || rule.MinimumGrade < 0 || rule.MaximumGrade < rule.MinimumGrade || rule.ItemID <= 0 || rule.Weight <= 0 || rule.CountDivisor <= 0 {
return errors.New("disjointer selection is invalid")
}
target[table] = append(target[table], rule)
}
return nil
}
func parseInt64Pairs(values []int64, target map[int]int64) error {
if len(values) == 0 || len(values)%2 != 0 {
return errors.New("expert job pair row width is invalid")
}
for i := 0; i < len(values); i += 2 {
key, value := int(values[i]), values[i+1]
if key <= 0 || value < 0 {
return errors.New("expert job pair is invalid")
}
if _, duplicate := target[key]; duplicate {
return errors.New("expert job pair is duplicated")
}
target[key] = value
}
return nil
}
func selectDisjointerRule(values []DisjointerSelectionRule, grade int, rng randomSource) (DisjointerSelectionRule, bool) {
total := 0
eligible := make([]DisjointerSelectionRule, 0, len(values))
for _, value := range values {
if grade >= value.MinimumGrade && grade <= value.MaximumGrade {
eligible = append(eligible, value)
total += value.Weight
}
}
if total <= 0 {
return DisjointerSelectionRule{}, false
}
roll := rng.Intn(total)
for _, value := range eligible {
if roll < value.Weight {
return value, true
}
roll -= value.Weight
}
return DisjointerSelectionRule{}, false
}