Files
90CNServer/server/internal/services/dnfbridge/equipment_stats.go
T
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

300 lines
10 KiB
Go

package dnfbridge
import (
"context"
"fmt"
"strconv"
"strings"
dnfcharstat "longheng.io/server/internal/modules/dnf/charstat"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
func (s *Service) preloadEquipmentStatIndex(ctx context.Context) error {
if s == nil {
return fmt.Errorf("dnfbridge service is nil")
}
s.equipmentStatsMu.Lock()
if s.equipmentStatPaths != nil {
s.equipmentStatsMu.Unlock()
return nil
}
if s.equipmentStatsLoadErr != nil {
err := s.equipmentStatsLoadErr
s.equipmentStatsMu.Unlock()
return err
}
s.equipmentStatsMu.Unlock()
s.initialEquipmentMu.Lock()
archive, err := s.initialEquipmentArchiveLocked()
s.initialEquipmentMu.Unlock()
if err != nil {
return fmt.Errorf("load equipment stat pvf archive: %w", err)
}
paths, err := initialEquipmentPathMap(archive)
s.equipmentStatsMu.Lock()
defer s.equipmentStatsMu.Unlock()
if err != nil {
s.equipmentStatsLoadErr = fmt.Errorf("load equipment stat pvf index: %w", err)
return s.equipmentStatsLoadErr
}
s.equipmentStatPaths = paths
s.equipmentStats = make(map[int64]dnfcharstat.Vector)
s.equipmentExpAdvantages = make(map[int64]int64)
s.logPacketEvent("dnf-equipment-stat-pvf-index-loaded", "items", len(paths))
return nil
}
func (s *Service) addEquipmentPVFStatsForUserInfo(ctx context.Context, session *gameSession, character dnfrepo.CharacterRecord, base dnfcharstat.Vector) dnfcharstat.Vector {
if s == nil || strings.TrimSpace(character.CharacterID) == "" {
return base
}
repos, ok := s.repositoryGroup()
if !ok || repos.Equipment == nil {
return base
}
equipment, found, err := repos.Equipment.Load(ctx, character.CharacterID)
if err != nil {
s.logPacketEvent("dnf-equipment-stat-load-failed",
"conn_id", sessionConnID(session),
"character_id", character.CharacterID,
"error", err)
return base
}
if !found || len(equipment.Entries) == 0 {
return base
}
out := base
count := 0
var physAtk, physDef int64
for _, entry := range equipment.Entries {
if entry.ItemID <= 0 {
continue
}
stats, ok := s.equipmentPVFStat(ctx, entry.ItemID, entry.Extra)
if !ok {
s.logPacketEvent("dnf-equipment-stat-pvf-missing",
"conn_id", sessionConnID(session),
"character_id", character.CharacterID,
"item_id", entry.ItemID,
"slot", entry.SlotIndex)
continue
}
if forgingAttack, forgingOK := s.currentEquippedWeaponForgingIndependentAttack(ctx, entry); forgingOK {
stats.IndependentAttack += forgingAttack
}
out.Add(stats)
count++
physAtk += stats.PhysicalAttack
physDef += stats.PhysicalDefense
}
if count > 0 {
s.logPacketEvent("dnf-equipment-stat-pvf-applied",
"conn_id", sessionConnID(session),
"character_id", character.CharacterID,
"count", count,
"physical_attack_add", physAtk,
"physical_defense_add", physDef)
}
return out
}
const currentRepositoryMainWeaponSlot int16 = 11
func (s *Service) currentEquippedWeaponForgingIndependentAttack(ctx context.Context, entry dnfrepo.EquipmentEntry) (int64, bool) {
level := currentEquipmentForgingLevelForStats(entry)
if s == nil || entry.ItemID <= 0 || level == 0 {
return 0, false
}
catalog, err := s.currentPVFItemCatalog()
if err != nil || catalog == nil {
s.logPacketEvent("dnf-equipment-forging-stat-catalog-missing", "item_id", entry.ItemID, "error", err)
return 0, false
}
definition, err := catalog.ResolveItem(uint32(entry.ItemID))
if err != nil || definition.Kind != dungeonDropItemEquipment || !currentForgingWeaponType(definition.EquipmentType) {
s.logPacketEvent("dnf-equipment-forging-stat-target-invalid", "item_id", entry.ItemID, "error", err)
return 0, false
}
if err := ctx.Err(); err != nil {
return 0, false
}
s.initialEquipmentMu.Lock()
source, err := s.initialEquipmentArchiveLocked()
s.initialEquipmentMu.Unlock()
if err != nil {
s.logPacketEvent("dnf-equipment-forging-stat-pvf-missing", "item_id", entry.ItemID, "error", err)
return 0, false
}
config, err := currentForgingConfigFromSource(source)
if err != nil {
s.logPacketEvent("dnf-equipment-forging-stat-config-invalid", "item_id", entry.ItemID, "forging_level", level, "error", err)
return 0, false
}
if level > config.MaxLevel {
s.logPacketEvent("dnf-equipment-forging-stat-config-invalid", "item_id", entry.ItemID, "forging_level", level, "error", "level exceeds PVF maximum")
return 0, false
}
attack, err := calculateCurrentForgingIndependentAttack(config, definition.MinimumLevel, definition.Rarity, level)
if err != nil {
s.logPacketEvent("dnf-equipment-forging-stat-calculate-failed", "item_id", entry.ItemID, "forging_level", level, "error", err)
return 0, false
}
return attack, true
}
func currentEquipmentForgingLevelForStats(entry dnfrepo.EquipmentEntry) byte {
if entry.SlotIndex != currentRepositoryMainWeaponSlot {
return 0
}
return currentEquipmentForgingLevel(entry)
}
func currentEquipmentForgingLevel(entry dnfrepo.EquipmentEntry) byte {
if raw := strings.TrimSpace(entry.Extra["forging_level"]); raw != "" {
if value, err := strconv.ParseUint(raw, 0, 8); err == nil {
return byte(value)
}
}
if len(entry.RawEntry) == currentItemListEntryWireSize {
return entry.RawEntry[currentItemListForgingLevelOffset]
}
return 0
}
func (s *Service) equipmentPVFStat(ctx context.Context, itemID int64, extra map[string]string) (dnfcharstat.Vector, bool) {
if s == nil || itemID <= 0 {
return dnfcharstat.Vector{}, false
}
s.equipmentStatsMu.Lock()
var cachedStats dnfcharstat.Vector
statsCached := false
if s.equipmentStats != nil {
cachedStats, statsCached = s.equipmentStats[itemID]
}
_, expAdvantageCached := s.equipmentExpAdvantages[itemID]
if statsCached && expAdvantageCached {
s.equipmentStatsMu.Unlock()
return cachedStats, true
}
refPath := ""
if s.equipmentStatPaths != nil {
refPath = s.equipmentStatPaths[itemID]
}
s.equipmentStatsMu.Unlock()
if refPath == "" && extra != nil {
refPath = strings.TrimSpace(extra["pvf_path"])
}
if refPath == "" {
return dnfcharstat.Vector{}, false
}
if err := ctx.Err(); err != nil {
s.logPacketEvent("dnf-equipment-stat-pvf-canceled", "item_id", itemID, "error", err)
return dnfcharstat.Vector{}, false
}
s.initialEquipmentMu.Lock()
archive, err := s.initialEquipmentArchiveLocked()
s.initialEquipmentMu.Unlock()
if err != nil {
s.logPacketEvent("dnf-equipment-stat-pvf-archive-missing", "item_id", itemID, "error", err)
return dnfcharstat.Vector{}, false
}
text, actualPath, err := readInitialPVFText(archive, initialPVFPath("equipment", refPath), refPath)
if err != nil {
s.logPacketEvent("dnf-equipment-stat-pvf-read-failed", "item_id", itemID, "path", refPath, "error", err)
return dnfcharstat.Vector{}, false
}
doc, err := dnfpvf.Parse(actualPath, text)
if err != nil {
s.logPacketEvent("dnf-equipment-stat-pvf-parse-failed", "item_id", itemID, "path", actualPath, "error", err)
return dnfcharstat.Vector{}, false
}
stats := equipmentStatVector(doc)
expAdvantage := equipmentStatNumber(doc, "exp advantage")
if expAdvantage < 0 {
expAdvantage = 0
}
s.equipmentStatsMu.Lock()
if s.equipmentStats == nil {
s.equipmentStats = make(map[int64]dnfcharstat.Vector)
}
if s.equipmentExpAdvantages == nil {
s.equipmentExpAdvantages = make(map[int64]int64)
}
s.equipmentStats[itemID] = stats
s.equipmentExpAdvantages[itemID] = expAdvantage
s.equipmentStatsMu.Unlock()
return stats, true
}
func (s *Service) equipmentPVFExpAdvantage(ctx context.Context, itemID int64, extra map[string]string) (int64, bool) {
if s == nil || itemID <= 0 {
return 0, false
}
s.equipmentStatsMu.Lock()
percent, cached := s.equipmentExpAdvantages[itemID]
s.equipmentStatsMu.Unlock()
if cached {
return percent, true
}
if _, ok := s.equipmentPVFStat(ctx, itemID, extra); !ok {
return 0, false
}
s.equipmentStatsMu.Lock()
percent, cached = s.equipmentExpAdvantages[itemID]
s.equipmentStatsMu.Unlock()
return percent, cached
}
func equipmentStatVector(doc *dnfpvf.Document) dnfcharstat.Vector {
return dnfcharstat.Vector{
HPMax: equipmentStatNumber(doc, "HP MAX", "hp max"),
MPMax: equipmentStatNumber(doc, "MP MAX", "mp max"),
Strength: equipmentStatNumber(doc, "strength"),
Intelligence: equipmentStatNumber(doc, "intelligence"),
Vitality: equipmentStatNumber(doc, "vitality"),
Spirit: equipmentStatNumber(doc, "spirit"),
PhysicalAttack: equipmentStatNumber(doc, "physical attack", "physical weapon attack", "equipment physical attack"),
PhysicalDefense: equipmentStatNumber(doc, "physical defense", "equipment physical defense"),
MagicalAttack: equipmentStatNumber(doc, "magical attack", "magical weapon attack", "equipment magical attack"),
MagicalDefense: equipmentStatNumber(doc, "magical defense", "equipment magical defense"),
IndependentAttack: equipmentStatNumber(doc, "independent attack", "separate attack"),
FireResistance: equipmentStatNumber(doc, "fire resistance"),
WaterResistance: equipmentStatNumber(doc, "water resistance"),
DarkResistance: equipmentStatNumber(doc, "dark resistance"),
LightResistance: equipmentStatNumber(doc, "light resistance"),
InventoryLimit: equipmentStatNumber(doc, "inventory limit"),
HPRegenSpeed: equipmentStatNumber(doc, "HP regen speed", "hp regen speed"),
MPRegenSpeed: equipmentStatNumber(doc, "MP regen speed", "mp regen speed"),
MoveSpeed: equipmentStatNumber(doc, "move speed"),
AttackSpeed: equipmentStatNumber(doc, "attack speed"),
CastSpeed: equipmentStatNumber(doc, "cast speed"),
HitRecovery: equipmentStatNumber(doc, "hit recovery"),
JumpPower: equipmentStatNumber(doc, "jump power"),
}
}
func equipmentStatNumber(doc *dnfpvf.Document, names ...string) int64 {
if doc == nil {
return 0
}
for _, name := range names {
if values := doc.Numbers(name); len(values) > 0 {
return int64(values[0])
}
if texts := doc.Texts(name); len(texts) > 0 {
value, err := strconv.ParseFloat(strings.TrimSpace(texts[0]), 64)
if err == nil {
return int64(value)
}
}
}
return 0
}