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,436 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user