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

453 lines
19 KiB
Go

package dnfbridge
import (
"errors"
"fmt"
"math"
"strings"
"longheng.io/server/internal/modules/dnf/alignedcmd"
"longheng.io/server/internal/modules/dnf/dnfenum"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
)
const currentForgingPVFPath = "etc/upgrade_separate.etc"
type currentForgingLevel struct {
TargetLevel byte
IndependentAttack int64
SuccessWeight int
MaterialMultiplier float64
}
type currentForgingMaterial struct {
ItemID int64
BaseCount int64
}
type currentForgingConfig struct {
MaxLevel byte
Levels []currentForgingLevel
LevelWeight float64
MaterialsByGrade map[int64]currentForgingMaterial
ItemRarityWeights []float64
StatRarityWeights []float64
WeaponWeights []float64
}
type currentForgingTicket struct {
TargetLevel int64
SuccessWeight int
Mode string
ApplyValue int64
}
func (s *Service) alignedForgingResolverForCommand(opcode dnfenum.CmdPacket) (alignedcmd.ForgingResolver, error) {
if opcode != dnfenum.CmdPacketUpgradeItemSeparate {
return nil, nil
}
catalog, err := s.currentPVFItemCatalog()
if err != nil {
return nil, err
}
if catalog == nil {
return nil, errDungeonDropSourceRequired
}
s.initialEquipmentMu.Lock()
source, err := s.initialEquipmentArchiveLocked()
s.initialEquipmentMu.Unlock()
if err != nil {
return nil, err
}
config, err := currentForgingConfigFromSource(source)
if err != nil {
return nil, err
}
return func(materialItemID int64, targetItemID int64, currentLevel byte) (alignedcmd.ForgingResolution, error) {
return resolveCurrentForgingRequestMetadata(catalog, config, source, materialItemID, targetItemID, currentLevel)
}, nil
}
func currentForgingConfigFromSource(source dnfpvf.Source) (*currentForgingConfig, error) {
if source == nil {
return nil, dnfpvf.ErrSourceRequired
}
text, err := source.ReadText(currentForgingPVFPath)
if err != nil {
return nil, fmt.Errorf("read %s: %w", currentForgingPVFPath, err)
}
document, err := dnfpvf.Parse(currentForgingPVFPath, text)
if err != nil {
return nil, fmt.Errorf("parse %s: %w", currentForgingPVFPath, err)
}
config, err := parseCurrentForgingConfig(document)
if err != nil {
return nil, fmt.Errorf("parse %s config: %w", currentForgingPVFPath, err)
}
return config, nil
}
func parseCurrentForgingConfig(document *dnfpvf.Document) (*currentForgingConfig, error) {
if document == nil {
return nil, fmt.Errorf("document required")
}
table := document.Numbers("table")
if len(table) == 0 || len(table)%4 != 0 {
return nil, fmt.Errorf("[table] numeric width %d is not a positive multiple of 4", len(table))
}
config := &currentForgingConfig{MaterialsByGrade: make(map[int64]currentForgingMaterial)}
for offset := 0; offset < len(table); offset += 4 {
independentAttack, ok := exactForgingInteger(table[offset])
if !ok || independentAttack < 0 {
return nil, fmt.Errorf("[table] row %d independent attack %.6f invalid", offset/4+1, table[offset])
}
successWeight, ok := exactForgingInteger(table[offset+2])
if !ok || successWeight < 0 || successWeight > 10000 {
return nil, fmt.Errorf("[table] row %d success weight %.6f invalid", offset/4+1, table[offset+2])
}
multiplier := table[offset+3]
if math.IsNaN(multiplier) || math.IsInf(multiplier, 0) || multiplier <= 0 {
return nil, fmt.Errorf("[table] row %d material multiplier %.6f invalid", offset/4+1, multiplier)
}
config.Levels = append(config.Levels, currentForgingLevel{
TargetLevel: byte(len(config.Levels) + 1),
IndependentAttack: independentAttack,
SuccessWeight: int(successWeight),
MaterialMultiplier: multiplier,
})
}
maxValues := document.Numbers("separate upgrade max")
if len(maxValues) != 1 {
return nil, fmt.Errorf("[separate upgrade max] count %d, want 1", len(maxValues))
}
maxLevel, ok := exactForgingInteger(maxValues[0])
if !ok || maxLevel <= 0 || maxLevel > int64(len(config.Levels)) || maxLevel > math.MaxUint8 {
return nil, fmt.Errorf("[separate upgrade max] %.6f outside parsed table", maxValues[0])
}
config.MaxLevel = byte(maxLevel)
levelValues := document.Numbers("level")
if len(levelValues) != 1 || math.IsNaN(levelValues[0]) || math.IsInf(levelValues[0], 0) || levelValues[0] <= 0 {
return nil, fmt.Errorf("[level] must contain one positive value")
}
config.LevelWeight = levelValues[0]
materialValues := document.Numbers("item weights by grade")
if len(materialValues) == 0 || len(materialValues)%3 != 0 {
return nil, fmt.Errorf("[item weights by grade] numeric width %d is not a positive multiple of 3", len(materialValues))
}
for offset := 0; offset < len(materialValues); offset += 3 {
grade, gradeOK := exactForgingInteger(materialValues[offset])
itemID, itemOK := exactForgingInteger(materialValues[offset+1])
baseCount, countOK := exactForgingInteger(materialValues[offset+2])
if !gradeOK || !itemOK || !countOK || grade <= 0 || itemID <= 0 || baseCount <= 0 {
return nil, fmt.Errorf("[item weights by grade] row %d invalid", offset/3+1)
}
if _, duplicate := config.MaterialsByGrade[grade]; duplicate {
return nil, fmt.Errorf("[item weights by grade] duplicate grade %d", grade)
}
config.MaterialsByGrade[grade] = currentForgingMaterial{ItemID: itemID, BaseCount: baseCount}
}
config.ItemRarityWeights = append([]float64(nil), document.Numbers("item weights by rarity")...)
if len(config.ItemRarityWeights) == 0 {
return nil, fmt.Errorf("[item weights by rarity] is empty")
}
for rarity, weight := range config.ItemRarityWeights {
if math.IsNaN(weight) || math.IsInf(weight, 0) || weight <= 0 {
return nil, fmt.Errorf("[item weights by rarity] index %d value %.6f invalid", rarity, weight)
}
}
config.StatRarityWeights = append([]float64(nil), document.Numbers("stat weights by rarity")...)
if len(config.StatRarityWeights) == 0 {
return nil, fmt.Errorf("[stat weights by rarity] is empty")
}
for rarity, weight := range config.StatRarityWeights {
if math.IsNaN(weight) || math.IsInf(weight, 0) || weight <= 0 {
return nil, fmt.Errorf("[stat weights by rarity] index %d value %.6f invalid", rarity, weight)
}
}
config.WeaponWeights = append([]float64(nil), document.Numbers("weapon weights")...)
if len(config.WeaponWeights) == 0 {
return nil, fmt.Errorf("[weapon weights] is empty")
}
for index, weight := range config.WeaponWeights {
if math.IsNaN(weight) || math.IsInf(weight, 0) || weight <= 0 {
return nil, fmt.Errorf("[weapon weights] index %d value %.6f invalid", index, weight)
}
}
return config, nil
}
func parseCurrentForgingTicket(document *dnfpvf.Document) (currentForgingTicket, bool, error) {
const section = "equipment separate reinforcement ticket"
if document == nil {
return currentForgingTicket{}, false, nil
}
if _, found := document.Section(section); !found {
return currentForgingTicket{}, false, nil
}
values := document.Ints(section)
texts := document.Texts(section)
if len(values) < 2 || len(texts) == 0 || values[0] <= 0 || values[0] > math.MaxUint8 || values[1] < 0 || values[1] > 100 {
return currentForgingTicket{}, true, fmt.Errorf("invalid [%s]", section)
}
mode := strings.ToLower(strings.Trim(strings.TrimSpace(texts[0]), "` []"))
if mode != "fixed" && mode != "additional" {
return currentForgingTicket{}, true, fmt.Errorf("unsupported mode %q in [%s]", texts[0], section)
}
applyValue := int64(-1)
if len(values) >= 3 {
applyValue = values[2]
}
return currentForgingTicket{
TargetLevel: values[0],
SuccessWeight: int(values[1] * 100),
Mode: mode,
ApplyValue: applyValue,
}, true, nil
}
func resolveCurrentForgingMetadata(catalog *pvfDungeonDropCatalog, config *currentForgingConfig, targetItemID int64, currentLevel byte) (alignedcmd.ForgingResolution, error) {
resolution := alignedcmd.ForgingResolution{}
if catalog == nil || config == nil {
return resolution, errDungeonDropSourceRequired
}
if targetItemID <= 0 || targetItemID > math.MaxUint32 {
return resolution, nil
}
definition, err := catalog.ResolveItem(uint32(targetItemID))
if err != nil {
if errors.Is(err, errDungeonDropItemUnresolved) {
return resolution, nil
}
return resolution, fmt.Errorf("resolve forging target item=%d: %w", targetItemID, err)
}
resolution.TargetKind = string(definition.Kind)
resolution.TargetPVFPath = definition.PVFPath
resolution.TargetEquipmentType = definition.EquipmentType
resolution.TargetDurability = definition.Durability
resolution.MaxLevel = config.MaxLevel
if definition.Kind != dungeonDropItemEquipment || !currentForgingWeaponType(definition.EquipmentType) || currentLevel >= config.MaxLevel {
return resolution, nil
}
targetLevel := int(currentLevel) + 1
if targetLevel <= 0 || targetLevel > len(config.Levels) {
return resolution, nil
}
level := config.Levels[targetLevel-1]
material, ok := config.MaterialsByGrade[definition.Grade]
if !ok || definition.Rarity < 0 || definition.Rarity >= int64(len(config.ItemRarityWeights)) || definition.Rarity >= int64(len(config.StatRarityWeights)) {
return resolution, nil
}
rawCount := float64(material.BaseCount)*config.ItemRarityWeights[definition.Rarity]*level.MaterialMultiplier + 0.0000001
if math.IsNaN(rawCount) || math.IsInf(rawCount, 0) || rawCount > math.MaxInt64 {
return alignedcmd.ForgingResolution{}, fmt.Errorf("forging material cost item=%d grade=%d rarity=%d invalid", targetItemID, definition.Grade, definition.Rarity)
}
materialCount := int64(rawCount)
if materialCount < 1 {
materialCount = 1
}
resolution.TargetLevel = level.TargetLevel
independentAttack, err := calculateCurrentForgingIndependentAttack(config, definition.MinimumLevel, definition.Rarity, level.TargetLevel)
if err != nil {
return alignedcmd.ForgingResolution{}, fmt.Errorf("forging independent attack item=%d level=%d: %w", targetItemID, level.TargetLevel, err)
}
resolution.IndependentAttack = independentAttack
resolution.SuccessWeight = level.SuccessWeight
resolution.MaterialItemID = material.ItemID
resolution.MaterialCount = materialCount
return resolution, nil
}
// resolveCurrentForgingRequestMetadata overlays the separate-upgrade ticket
// carried by the request's material slot on the ordinary target metadata.
// The current PVF declares this coupon family through
// [equipment separate reinforcement ticket]:
//
// <level> <success-percent> `<fixed|additional>` <source-level-cap>
//
// Fixed coupons write the declared forging level even when it is lower than
// the current level. Additional coupons add the declared amount and honor the
// optional source-level cap. Both consume exactly one coupon per accepted
// attempt and use the native op438 probability base of 10000.
//
// A second family declares itself through [enchant random] plus
// [action type] `[random upgrade separate]` and is handled by
// resolveCurrentForgingRandomTicketMetadata.
func resolveCurrentForgingRequestMetadata(
catalog *pvfDungeonDropCatalog,
config *currentForgingConfig,
source dnfpvf.Source,
materialItemID int64,
targetItemID int64,
currentLevel byte,
) (alignedcmd.ForgingResolution, error) {
resolution, err := resolveCurrentForgingMetadata(catalog, config, targetItemID, currentLevel)
if err != nil {
return alignedcmd.ForgingResolution{}, err
}
if source == nil || materialItemID <= 0 || materialItemID > math.MaxUint32 {
return resolution, nil
}
materialDefinition, err := catalog.ResolveItem(uint32(materialItemID))
if err != nil {
if errors.Is(err, errDungeonDropItemUnresolved) {
return resolution, nil
}
return alignedcmd.ForgingResolution{}, fmt.Errorf("resolve forging request material item=%d: %w", materialItemID, err)
}
if materialDefinition.Kind != dungeonDropItemStackable {
return resolution, nil
}
materialDocument, err := parseDungeonCardPVFDocument(source, materialDefinition.PVFPath)
if err != nil {
return alignedcmd.ForgingResolution{}, fmt.Errorf("parse forging request material item=%d path=%s: %w", materialItemID, materialDefinition.PVFPath, err)
}
if randomTicket, found := parseEnchantRandomTicket(materialDocument); found &&
currentEnchantRandomForgingActionType(materialDocument) {
return resolveCurrentForgingRandomTicketMetadata(catalog, config, resolution, randomTicket, materialDefinition, targetItemID)
}
if action, found := materialDocument.Text("action type"); found &&
strings.EqualFold(strings.TrimSpace(strings.ReplaceAll(action, "`", "")), "[portable genuine damage upgrade upgrede]") {
levels := materialDocument.Ints("check usable itemlevel")
if len(levels) != 2 || levels[0] < 0 || levels[1] < levels[0] {
return alignedcmd.ForgingResolution{}, fmt.Errorf("portable forging item=%d has invalid [check usable itemlevel]", materialItemID)
}
targetDefinition, resolveErr := catalog.ResolveItem(uint32(targetItemID))
if resolveErr != nil {
return alignedcmd.ForgingResolution{}, fmt.Errorf("resolve portable forging target item=%d: %w", targetItemID, resolveErr)
}
if targetDefinition.Kind != dungeonDropItemEquipment || targetDefinition.MinimumLevel < levels[0] || targetDefinition.MinimumLevel > levels[1] {
return alignedcmd.ForgingResolution{}, fmt.Errorf("portable forging target item=%d level=%d outside %d..%d", targetItemID, targetDefinition.MinimumLevel, levels[0], levels[1])
}
resolution.MaterialItemID = materialItemID
resolution.MaterialCount = 1
return resolution, nil
}
ticket, found, err := parseCurrentForgingTicket(materialDocument)
if err != nil {
return alignedcmd.ForgingResolution{}, fmt.Errorf("forging ticket item=%d path=%s: %w", materialItemID, materialDefinition.PVFPath, err)
}
if !found {
return resolution, nil
}
resolution.TicketMode = ticket.Mode
resolution.TicketApplyValue = ticket.ApplyValue
resolution.TicketPVFPath = materialDefinition.PVFPath
resolution.MaterialItemID = materialItemID
resolution.MaterialCount = 1
resolution.SuccessWeight = ticket.SuccessWeight
resolution.TargetLevel = byte(ticket.TargetLevel)
if ticket.Mode == "additional" {
maximumSourceLevel := int64(config.MaxLevel) - ticket.TargetLevel
if ticket.ApplyValue >= 0 && ticket.ApplyValue < maximumSourceLevel {
maximumSourceLevel = ticket.ApplyValue
}
if int64(currentLevel) > maximumSourceLevel || int64(currentLevel)+ticket.TargetLevel > int64(config.MaxLevel) {
resolution.TargetLevel = 0
return resolution, nil
}
resolution.TargetLevel = byte(int64(currentLevel) + ticket.TargetLevel)
}
if resolution.TargetLevel == 0 || resolution.TargetLevel > config.MaxLevel {
return resolution, nil
}
targetDefinition, err := catalog.ResolveItem(uint32(targetItemID))
if err != nil {
if errors.Is(err, errDungeonDropItemUnresolved) {
return resolution, nil
}
return alignedcmd.ForgingResolution{}, fmt.Errorf("resolve forging ticket target item=%d: %w", targetItemID, err)
}
independentAttack, err := calculateCurrentForgingIndependentAttack(config, targetDefinition.MinimumLevel, targetDefinition.Rarity, resolution.TargetLevel)
if err != nil {
return alignedcmd.ForgingResolution{}, fmt.Errorf("forging ticket independent attack item=%d level=%d: %w", targetItemID, resolution.TargetLevel, err)
}
resolution.IndependentAttack = independentAttack
return resolution, nil
}
// resolveCurrentForgingRandomTicketMetadata overlays the [enchant random] +
// [action type] `[random upgrade separate]` coupon family (e.g. +6锻造券
// 2683788) on the ordinary target metadata. Unlike the fixed/additional
// coupons this family has no single declared level: the er_enchant candidates
// and their er_slot / er_grade / er_seal restrictions travel to the inventory
// domain, which owns the weighted roll. Only the per-candidate independent
// attack is pre-computed here, because the upgrade_separate.etc table lives on
// this side of the boundary.
func resolveCurrentForgingRandomTicketMetadata(
catalog *pvfDungeonDropCatalog,
config *currentForgingConfig,
resolution alignedcmd.ForgingResolution,
ticket alignedcmd.EnchantRandomTicket,
materialDefinition dungeonDropItemDefinition,
targetItemID int64,
) (alignedcmd.ForgingResolution, error) {
if len(ticket.Entries) == 0 {
return resolution, nil
}
targetDefinition, err := catalog.ResolveItem(uint32(targetItemID))
if err != nil {
if errors.Is(err, errDungeonDropItemUnresolved) {
return resolution, nil
}
return alignedcmd.ForgingResolution{}, fmt.Errorf("resolve random forging ticket target item=%d: %w", targetItemID, err)
}
resolution.TicketRandom = true
resolution.RandomTicket = ticket
resolution.TargetRarity = targetDefinition.Rarity
resolution.TicketPVFPath = materialDefinition.PVFPath
resolution.MaterialItemID = int64(materialDefinition.ItemID)
resolution.MaterialCount = 1
// The domain picks the level, so neither of these may carry the ordinary
// next-level forging plan computed above.
resolution.TargetLevel = 0
resolution.SuccessWeight = 0
resolution.IndependentAttack = 0
if targetDefinition.Kind != dungeonDropItemEquipment {
return resolution, nil
}
attacks := make(map[int64]int64, len(ticket.Entries))
for _, entry := range ticket.Entries {
if entry.TargetLevel <= 0 || entry.TargetLevel > int64(config.MaxLevel) {
continue
}
attack, attackErr := calculateCurrentForgingIndependentAttack(config, targetDefinition.MinimumLevel, targetDefinition.Rarity, byte(entry.TargetLevel))
if attackErr != nil {
return alignedcmd.ForgingResolution{}, fmt.Errorf("random forging ticket independent attack item=%d level=%d: %w", targetItemID, entry.TargetLevel, attackErr)
}
attacks[entry.TargetLevel] = attack
}
resolution.RandomIndependentAttacks = attacks
return resolution, nil
}
func calculateCurrentForgingIndependentAttack(config *currentForgingConfig, minimumLevel, rarity int64, forgingLevel byte) (int64, error) {
if config == nil || forgingLevel == 0 || int(forgingLevel) > len(config.Levels) {
return 0, nil
}
if config.LevelWeight <= 0 || rarity < 0 || rarity >= int64(len(config.StatRarityWeights)) {
return 0, fmt.Errorf("invalid level or rarity weights")
}
level := config.Levels[int(forgingLevel)-1]
raw := (1 + float64(max(minimumLevel, int64(0)))/config.LevelWeight) * float64(level.IndependentAttack) * config.StatRarityWeights[rarity]
if math.IsNaN(raw) || math.IsInf(raw, 0) || raw < 0 || raw > math.MaxInt64 {
return 0, fmt.Errorf("calculated value %.6f outside int64", raw)
}
return int64(math.Floor(raw)), nil
}
func currentForgingWeaponType(raw string) bool {
value := strings.ToLower(strings.TrimSpace(strings.ReplaceAll(raw, "`", "")))
return value == "[weapon]" || value == "weapon"
}
func exactForgingInteger(value float64) (int64, bool) {
if math.IsNaN(value) || math.IsInf(value, 0) || value < math.MinInt64 || value > math.MaxInt64 {
return 0, false
}
integer := int64(value)
return integer, float64(integer) == value
}