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,172 @@
package dnfbridge
import (
"testing"
"longheng.io/server/internal/modules/dnf/raid"
)
// 百科(地图构造):"雷达:该副本通关后增加所有玩家10%技能伤害,并显示地图中怪物
// 头像。(反之怪物头像显示问号)" —— 符号 81 RADAR STATE 就是那个开关。
func fiendWarRevealBattle() *raid.FiendWarBattle {
return &raid.FiendWarBattle{
Config: &raid.FiendWarBattleConfig{
Symbols: map[string]uint32{"radar state": 81},
Areas: map[int]raid.FiendWarArea{
14: {Index: 14, Dungeon: 4962}, // 普通作战区域
15: {Index: 15, Dungeon: 4963, Kind: "radar"}, // 单图设施
19: {Index: 19, Dungeon: 4967, Kind: "top outpost"}, // 三图设施(预抽 maze 2)
},
},
Symbols: map[uint32]int{81: 0},
Areas: map[int]raid.FiendWarAreaState{},
FixtureMazes: map[int]int{19: 2},
}
}
func TestFiendWarRadarGatesBoardMonster(t *testing.T) {
b := fiendWarRevealBattle()
// 雷达未通关:**ID 照发、只换脸**。
// ID —— 红框只由 unitID 决定,清零就整格空白(既没框也没问号),
// 2026-10-01 的 packet log 与现场截图双向印证过;
// 脸 —— type 换成 fiendWarFaceUnknown(= 0)。type 14/15/16 实测是"框在、里面空的",
// 只有 0 从来没发出去过:服务端过去拿 Type==0 当"这格没单位"的哨兵,
// 带 0 的行在发出去之前就被自己的守卫吃掉了 —— 所以现在用第二个返回值
// (present)来表示"有没有单位",与 Type 彻底分开。
if got, present := runtimeFiendWarRevealEnemy(b, 19, raid.FiendWarUnit{ID: 4967, Type: 7}); !present || got.ID != 110708 || got.Type != fiendWarFaceUnknown {
t.Fatalf("雷达未通关的前哨格 = %+v present=%v,want ID=110708(框要画)Type=%d(问号)", got, present, fiendWarFaceUnknown)
}
if got, present := runtimeFiendWarRevealEnemy(b, 15, raid.FiendWarUnit{ID: 4963, Type: 5}); !present || got.ID != 110973 || got.Type != fiendWarFaceUnknown {
t.Fatalf("雷达未通关的雷达格 = %+v present=%v,want ID=110973 Type=%d(问号)", got, present, fiendWarFaceUnknown)
}
if got, present := runtimeFiendWarRevealEnemy(b, 14, raid.FiendWarUnit{ID: 3, Type: 2}); !present || got.ID != 110977 || got.Type != fiendWarFaceUnknown {
t.Fatalf("雷达未通关的军团格 = %+v present=%v,want ID=110977 Type=%d(问号)", got, present, fiendWarFaceUnknown)
}
if got, present := runtimeFiendWarRevealMonster(b, 14, raid.FiendWarUnit{ID: 5, Type: 1}); !present || got.ID != 110714 || got.Type != fiendWarFaceUnknown {
t.Fatalf("雷达未通关的中立怪格 = %+v present=%v,want ID=110714 Type=%d(问号)", got, present, fiendWarFaceUnknown)
}
// 问号那个 type 必须能真的发出去:它不再是"这格没单位"的哨兵值。
if fiendWarFaceUnknown != 0 {
t.Logf("问号 type = %d(若 0 被证伪,这里就是换上的新值)", fiendWarFaceUnknown)
}
// 真的没有单位的格子依然一行都不发。
if _, present := runtimeFiendWarRevealEnemy(b, 14, raid.FiendWarUnit{}); present {
t.Fatal("空区域不该发号")
}
b.Symbols[81] = 1 // 雷达通关
// 设施格:露出这一格**固定**的那张图的领主。fixture 给 19 号格钉的是 maze 2,
// 所以露出的是 110708(4967 maze 2 的领主);脸仍然是这一格的位置 kind(7)——
// 客户端那张 kind→脸 表按位置排,而实际打到的怪也钉在同一位置,两者必然一致
// (见 TestFiendWarFixtureBoardAndEntryAreFixedTogether)。
if got, present := runtimeFiendWarRevealEnemy(b, 19, raid.FiendWarUnit{ID: 4967, Type: 7}); !present || got.ID != 110708 || got.Type != 7 {
t.Fatalf("雷达通关后的前哨格 = %+v,want ID=110708 Type=7", got)
}
// 单图设施:没有预抽记录时 maze=0,露出恒定的那只(雷达基地 = 磁力恶魔:马格内特)。
if got, present := runtimeFiendWarRevealEnemy(b, 15, raid.FiendWarUnit{ID: 4963, Type: 5}); !present || got.ID != 110973 || got.Type != 5 {
t.Fatalf("雷达通关后的雷达格 = %+v,want ID=110973 Type=5", got)
}
// 军团格:兵种 type 决定它走进哪张 maze,头像用那张图的领主。
for enemyType, want := range map[byte]uint32{1: 110979, 2: 110977, 3: 110987, 4: 110982} {
if got, _ := runtimeFiendWarRevealEnemy(b, 14, raid.FiendWarUnit{ID: uint32(enemyType), Type: enemyType}); got.ID != want {
t.Fatalf("兵种 %d 的头像 = %d,want %d", enemyType, got.ID, want)
}
}
// 中立怪:monster type 1..2 → maze 4/5 的领主。
for monsterType, want := range map[byte]uint32{1: 110714, 2: 110975} {
if got, _ := runtimeFiendWarRevealMonster(b, 14, raid.FiendWarUnit{ID: 7, Type: monsterType}); got.ID != want {
t.Fatalf("原生怪 %d 的头像 = %d,want %d", monsterType, got.ID, want)
}
}
// 空格子(没有单位)永远不发:雷达通关也不会凭空长出一只怪。
if got, present := runtimeFiendWarRevealEnemy(b, 14, raid.FiendWarUnit{}); present || got != (raid.FiendWarUnit{}) {
t.Fatalf("空区域的揭示 = %+v present=%v,want 零值且不发", got, present)
}
}
// 进图用的 maze 必须与雷达通关后战况表显示的那只是同一个:
// selectRuntimeFiendWarMaze 读的是同一张 FixtureMazes。
func TestFiendWarFixtureMazeAndBoardAgree(t *testing.T) {
b := fiendWarRevealBattle()
b.Symbols[81] = 1
maze, ok := b.MazeForArea(19)
if !ok {
t.Fatal("前哨没有预抽记录")
}
board, present := runtimeFiendWarRevealEnemy(b, 19, raid.FiendWarUnit{ID: 4967, Type: 7})
if !present {
t.Fatal("前哨格应该发号")
}
fromEntry, ok := raid.FiendWarFixtureBossID(4967, maze)
if !ok {
t.Fatal("进图侧查不到这只领主")
}
if board.ID != fromEntry {
t.Fatalf("表上 %d != 进图 %d", board.ID, fromEntry)
}
}
// 战况表上画的必须就是进图会打到的那一只 —— 而且这是**固定**的,不靠抽签对齐。
//
// 操作员口径(2026-10-01 复核历史截图):
//
// "我所说的这些格子里的怪物是固定的……雷达打完以后客户端就是这么画的,服务端也
// 理应按照客户端画的去让怪物真正百分百地出现 —— 就不再抽签了。"
//
// 客户端那张 kind→脸 的表按格子位置排(上哨=宾柏 / 中哨=薛丁恶 / 下哨=克鲁瑟,
// 兵站本营同理),而战况表发的 type 正是这个位置 kind。所以这里钉死:
// 表格子画谁,它固定用的那张 maze 的领主就必须是谁。
func TestFiendWarFixtureBoardAndEntryAreFixedTogether(t *testing.T) {
cases := []struct {
area int
kind string
maze int
face byte
boss uint32
dungeon int
}{
{19, "top outpost", 0, 7, 110981, 4967}, // 上哨 · 宾柏
{21, "mid outpost", 1, 8, 110984, 4969}, // 中哨 · 薛丁恶
{23, "bot outpost", 2, 9, 110708, 4971}, // 下哨 · 克鲁瑟
{27, "top command center", 0, 10, 110709, 4975}, // 上兵营 · 光之恶魔
{28, "mid command center", 1, 11, 110978, 4976}, // 中兵营 · 暗之恶魔
{29, "bot command center", 2, 12, 110992, 4977}, // 下兵营 · 重力恶魔
}
areas := map[int]raid.FiendWarArea{}
for _, tc := range cases {
areas[tc.area] = raid.FiendWarArea{Index: tc.area, Dungeon: tc.dungeon, Kind: tc.kind}
}
b, err := raid.NewFiendWarBattle(&raid.FiendWarBattleConfig{
Raid: &raid.AntonConfig{PhaseMax: 1, MemberMax: 8},
Rules: []raid.FiendWarRule{{}},
Areas: areas,
Symbols: map[string]uint32{"radar state": 81},
})
if err != nil {
t.Fatal(err)
}
b.Symbols[81] = 1 // 雷达通关:这一格该露脸了
for _, tc := range cases {
maze, ok := b.MazeForArea(tc.area)
if !ok {
t.Fatalf("设施 %d 没有固定图", tc.area)
}
if maze != tc.maze {
t.Fatalf("设施 %d 的固定图 = %d,want %d", tc.area, maze, tc.maze)
}
got, present := runtimeFiendWarRevealEnemy(b, tc.area, raid.FiendWarUnit{ID: uint32(tc.dungeon), Type: tc.face})
if !present {
t.Fatalf("设施 %d 应该发号", tc.area)
}
if got.ID != tc.boss || got.Type != tc.face {
t.Fatalf("设施 %d 表上画的 = %+v,want ID=%d Type=%d", tc.area, got, tc.boss, tc.face)
}
// 进图侧读的是同一张图(selectRuntimeFiendWarMaze → MazeForArea),
// 所以这里查出来的领主就是那一格真正会打到的领主。
fromEntry, ok := raid.FiendWarFixtureBossID(tc.dungeon, maze)
if !ok || fromEntry != tc.boss {
t.Fatalf("设施 %d 进图侧查到的领主 = %d(ok=%t),want %d", tc.area, fromEntry, ok, tc.boss)
}
}
}