DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

@@ -0,0 +1,97 @@
package dnfbridge
import (
"fmt"
"longheng.io/server/internal/modules/dnf/worldmap"
)
// 深渊派对入场消耗(邀请函数量)的取值规则。
//
// 客户端副本提示里的 "进入条件 ·消耗N个深渊派对邀请函 (0)" 是客户端自己算的,
// N 来自副本自己的 [minimum required level]:
//
// N = (40*Lv - 1800) / 100 + 10 // 整数除法,向零截断
//
// 实测(2026-09-26,角色 测试x Lv90,客户端内逐个悬停读数):
//
// 86 列车上的海贼 Lv71 -> 20 87 夺回西部线 Lv72 -> 20
// 92 雾都赫伊斯 Lv73 -> 21 93 阿登高地 Lv74 -> 21
// 193 能源熔炉 Lv86 -> 26
//
// 五个读数全部命中该式,而且区间边界也吻合(71/72 同为 20,73/74 同为 21)。
// 换用 [basis level](74/75/76/77/89)会得到 21/22/22/22/27,全部不符;换用角色
// 等级也不符(同一个角色读到 20/20/21/21 四个不同的数)。旧树 changlog 里
// "(40·90−1800)/100+10 = 28" 是同一个式子在 Lv90 上的取值。
//
// 真 PVF 里只有 11/205 个深渊区域副本写了 [hell party required item count];剩下 194
// 个没写,其中 187 个的所属区域有恰好一条 [item condition](就是 3330 邀请函),
// 7 个(镜像阿拉德 8000 与伤城 8500 一系)连 [item condition] 都没有。没有声明值时
// 必须按上面这条等级式推导:否则这些副本的入场计划建立不起来,玩家点下"深渊派对"
// 之后服务端给不出任何回执(2026-09-26 现场:193 号副本卡在选择页 26 秒)。
const (
currentHellAdmissionLevelScale = 40
currentHellAdmissionLevelOffset = 1800
currentHellAdmissionLevelDivisor = 100
currentHellAdmissionBaseCount = 10
currentHellAdmissionMinCount = 1
)
// currentHellAdmissionCostSource* 记录这次消耗数量是从哪儿来的,只进日志:
// 有 PVF 声明值和按等级式推导,出问题时必须能一眼分清。
const (
currentHellAdmissionCostSourceDeclared = "dungeon_hell_party_required_item_count"
currentHellAdmissionCostSourceLevel = "dungeon_minimum_required_level_formula"
)
// currentHellAdmissionInvitationCountForLevel is the client's own invitation
// count rule: (40*Lv-1800)/100+10 with C-style truncating division. A level that
// would price the entry at zero or below still costs one invitation.
func currentHellAdmissionInvitationCountForLevel(level int64) int64 {
if level <= 0 {
return 0
}
count := (currentHellAdmissionLevelScale*level-currentHellAdmissionLevelOffset)/
currentHellAdmissionLevelDivisor + currentHellAdmissionBaseCount
if count < currentHellAdmissionMinCount {
count = currentHellAdmissionMinCount
}
return count
}
// currentDungeonHellAdmissionInvitationCount returns how many invitations a hell
// entry into this dungeon consumes, preferring the dungeon's own PVF value
// ([hell party required item count]) and otherwise deriving it from
// [minimum required level] exactly as the client tooltip does. The second result
// names which of the two was used.
func currentDungeonHellAdmissionInvitationCount(dungeon worldmap.Dungeon) (int64, string, error) {
if declared := dungeon.Metadata.HellPartyRequiredItemCount; declared.Set && declared.Value > 0 {
return declared.Value, currentHellAdmissionCostSourceDeclared, nil
}
level := dungeon.Metadata.MinimumRequiredLevel
if !level.Set || level.Value <= 0 {
return 0, "", fmt.Errorf(
"dungeon %d has neither [hell party required item count] nor [minimum required level]",
dungeon.ID,
)
}
return currentHellAdmissionInvitationCountForLevel(level.Value), currentHellAdmissionCostSourceLevel, nil
}
// currentDungeonHellEntryRequiresInvitations reports whether this entry's
// admission item is the hell area's [item condition] -- the two branches the
// admission plan prices with the invitation count above. Those are exactly the
// entries whose client text lives in the op16 failure reason-27 block
// (0x01CFBD8B: "队伍中有队员的%s不足,无法进入深渊派对。" / 27156
// "部分队员邀请函不足,无法进入该地下城。"), so they get reason 27 instead of the
// generic reason 17. Particular-hell (魔界裂缝) entries are excluded: their plan
// comes from the area's particular-hell free pass, not from the item condition.
func currentDungeonHellEntryRequiresInvitations(runtime *runtimeDungeonState) bool {
if runtime == nil {
return false
}
if runtime.Dungeon.Metadata.InfernoDungeon {
return true
}
return currentDungeonExplicitHellPartyRequest(runtime.Request, runtime.Dungeon, runtime.MazeIndex)
}