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

263 lines
8.3 KiB
Go

// 本文件把 DNF PVF 角色成长表接入 dnfbridge 的 USERINFO 拼包流程。
package dnfbridge
import (
"context"
"fmt"
"strconv"
"strings"
dnfcharstat "longheng.io/server/internal/modules/dnf/charstat"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
const (
csharpPremiumHPBonus int64 = 9800
csharpPremiumMPBonus int64 = 10500
)
func (s *Service) preloadCharacterStatTable(ctx context.Context) error {
table, err := s.loadCharacterStatTable(ctx)
if err != nil {
return err
}
s.logPacketEvent("dnf-character-stat-pvf-loaded", "jobs", table.Snapshot().Jobs)
return nil
}
func (s *Service) loadCharacterStatTable(ctx context.Context) (*dnfcharstat.Table, error) {
if s == nil {
return nil, fmt.Errorf("dnfbridge service is nil")
}
s.characterStatsMu.Lock()
if s.characterStats != nil {
table := s.characterStats
s.characterStatsMu.Unlock()
return table, nil
}
if s.characterStatsLoadErr != nil {
err := s.characterStatsLoadErr
s.characterStatsMu.Unlock()
return nil, err
}
s.characterStatsMu.Unlock()
s.initialEquipmentMu.Lock()
archive, err := s.initialEquipmentArchiveLocked()
s.initialEquipmentMu.Unlock()
if err != nil {
return nil, fmt.Errorf("load character stat pvf archive: %w", err)
}
table, err := dnfcharstat.Load(ctx, archive, dnfcharstat.Options{})
s.characterStatsMu.Lock()
defer s.characterStatsMu.Unlock()
if err != nil {
s.characterStatsLoadErr = fmt.Errorf("load character stat pvf table: %w", err)
return nil, s.characterStatsLoadErr
}
s.characterStats = table
return table, nil
}
func (s *Service) characterPVFStatsForUserInfo(ctx context.Context, session *gameSession, character dnfrepo.CharacterRecord, hasCharacter bool) (dnfcharstat.Vector, bool) {
if !hasCharacter {
return dnfcharstat.Vector{}, false
}
jobText := strings.TrimSpace(character.Job)
if jobText == "" {
s.logCharacterStatMissing(session, character, "job_missing", nil)
return dnfcharstat.Vector{}, false
}
job, err := strconv.Atoi(jobText)
if err != nil || job < 0 || job > 0xff {
s.logCharacterStatMissing(session, character, "job_invalid", err)
return dnfcharstat.Vector{}, false
}
level := character.Level
if level <= 0 {
level = 1
}
growType := byte(numericCharacterStatValue(character, "grow_type"))
s.characterStatsMu.Lock()
table := s.characterStats
loadErr := s.characterStatsLoadErr
s.characterStatsMu.Unlock()
if table == nil {
s.logCharacterStatMissing(session, character, "pvf_table_not_preloaded", loadErr)
return dnfcharstat.Vector{}, false
}
stats, err := table.Compute(byte(job), growType, level)
if err != nil {
s.logCharacterStatMissing(session, character, "pvf_compute_failed", err)
return dnfcharstat.Vector{}, false
}
pvfHPMax := stats.HPMax
pvfMPMax := stats.MPMax
stats = applyCSharpHPMPCompatibility(stats)
if bonus, bonusErr := s.currentCollectboxBonus(ctx, session); bonusErr != nil {
s.logGameEvent(session, "game-collectbox-bonus-deferred", "char_id", character.CharacterID, "reason", bonusErr)
} else {
stats = applyCurrentCollectboxBonus(stats, bonus)
}
s.logPacketEvent("dnf-character-stat-pvf-computed",
"conn_id", sessionConnID(session),
"character_id", character.CharacterID,
"job", job,
"grow_type", growType,
"level", level,
"hp_max", pvfHPMax,
"mp_max", pvfMPMax,
"userinfo_hp_max", stats.HPMax,
"userinfo_mp_max", stats.MPMax,
"hp_compatibility_bonus", csharpPremiumHPBonus,
"mp_compatibility_bonus", csharpPremiumMPBonus,
"strength", stats.Strength,
"physical_attack", stats.PhysicalAttack,
"source", "pvf_character_growth_base_plus_csharp_hpmp_compatibility")
return stats, true
}
func applyCSharpHPMPCompatibility(stats dnfcharstat.Vector) dnfcharstat.Vector {
// 86JP CharacterStatComputer.BuildAdditionalInfo starts from a freshly
// computed PVF vector and then adds these two fixed compatibility values.
// Keeping the operation on the fresh vector makes repeated recalculation
// idempotent; no other stat is changed here.
stats.HPMax += csharpPremiumHPBonus
stats.MPMax += csharpPremiumMPBonus
return stats
}
func isPreCSharpHPMPCompatibilityStat(column string, storedValue int64, computedValue int64) bool {
switch column {
case "stat_hp_max":
return computedValue >= csharpPremiumHPBonus && storedValue == computedValue-csharpPremiumHPBonus
case "stat_mp_max":
return computedValue >= csharpPremiumMPBonus && storedValue == computedValue-csharpPremiumMPBonus
default:
return false
}
}
func (s *Service) applyCharacterPVFStatsForCreate(ctx context.Context, record *dnfrepo.CharacterRecord) {
if record == nil {
return
}
stats, ok := s.characterPVFStatsForUserInfo(ctx, nil, *record, true)
if !ok {
return
}
applyCharacterPVFStats(record, stats)
}
func characterPVFStatsNeedRepair(record dnfrepo.CharacterRecord, stats dnfcharstat.Vector) bool {
if _, ok := record.Stats["stat_level"]; !ok {
return true
}
for key, value := range characterPVFStatValues(stats) {
if record.Stats[key] != value {
return true
}
}
return false
}
func repairSelectedCharacterPVFStats(
ctx context.Context,
repository dnfrepo.CharacterRepository,
record dnfrepo.CharacterRecord,
) error {
return dnfrepo.SaveCharacterFields(ctx, repository, record, dnfrepo.CharacterFieldStats)
}
func applyCharacterPVFStats(record *dnfrepo.CharacterRecord, stats dnfcharstat.Vector) {
if record == nil {
return
}
if record.Stats == nil {
record.Stats = make(map[string]int64)
}
for key, value := range characterPVFStatValues(stats) {
record.Stats[key] = value
}
if _, ok := record.Stats["stat_level"]; !ok {
record.Stats["stat_level"] = csharpSubtype1ProtocolStatLevel
}
}
func characterPVFStatValues(stats dnfcharstat.Vector) map[string]int64 {
return map[string]int64{
"stat_hp_max": stats.HPMax,
"stat_mp_max": stats.MPMax,
"stat_strength": stats.Strength,
"stat_intelligence": stats.Intelligence,
"stat_vitality": stats.Vitality,
"stat_spirit": stats.Spirit,
"stat_physical_attack": stats.PhysicalAttack,
"stat_physical_defense": stats.PhysicalDefense,
"stat_magical_attack": stats.MagicalAttack,
"stat_magical_defense": stats.MagicalDefense,
"stat_independent_attack": stats.IndependentAttack,
"stat_fire_resistance": stats.FireResistance,
"stat_water_resistance": stats.WaterResistance,
"stat_dark_resistance": stats.DarkResistance,
"stat_light_resistance": stats.LightResistance,
"stat_inventory_limit": stats.InventoryLimit,
"stat_hp_regen_speed": stats.HPRegenSpeed,
"stat_mp_regen_speed": stats.MPRegenSpeed,
"stat_move_speed": stats.MoveSpeed,
"stat_attack_speed": stats.AttackSpeed,
"stat_cast_speed": stats.CastSpeed,
"stat_hit_recovery": stats.HitRecovery,
"stat_jump_power": stats.JumpPower,
"stat_weight": stats.Weight,
}
}
func (s *Service) characterPVFStatValuesForLevel(character dnfrepo.CharacterRecord, level int) (map[string]int64, error) {
jobText := strings.TrimSpace(character.Job)
job, err := strconv.Atoi(jobText)
if err != nil || job < 0 || job > 0xff {
return nil, fmt.Errorf("compute character level stats: invalid job %q", character.Job)
}
s.characterStatsMu.Lock()
table := s.characterStats
loadErr := s.characterStatsLoadErr
s.characterStatsMu.Unlock()
if table == nil {
if loadErr == nil {
table, loadErr = s.loadCharacterStatTable(context.Background())
}
if loadErr != nil {
return nil, fmt.Errorf("compute character level stats: %w", loadErr)
}
}
stats, err := table.Compute(byte(job), byte(numericCharacterStatValue(character, "grow_type")), level)
if err != nil {
return nil, fmt.Errorf("compute character level stats: %w", err)
}
return characterPVFStatValues(applyCSharpHPMPCompatibility(stats)), nil
}
func (s *Service) logCharacterStatMissing(session *gameSession, character dnfrepo.CharacterRecord, reason string, err error) {
fields := []any{
"conn_id", sessionConnID(session),
"character_id", character.CharacterID,
"job", character.Job,
"level", character.Level,
"grow_type", numericCharacterStatValue(character, "grow_type"),
"reason", reason,
}
if err != nil {
fields = append(fields, "error", err)
}
s.logPacketEvent("dnf-character-stat-pvf-missing", fields...)
}
func sessionConnID(session *gameSession) string {
if session == nil {
return ""
}
return session.connID
}