服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
300 lines
10 KiB
Go
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
|
|
}
|