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,53 @@
package mail
import (
"errors"
"fmt"
"testing"
)
// 这组用例钉住"锁定"这条拒绝在 op95 上到底回什么码,以及它**不能**被算成别的语义。
//
// 现场证据(2026-09-15,线上包 packet log,玩家在游戏里点领取那一刻):
//
// SEND kind=game-upper msg_id=95 body=0004
//
// 服务端确实回了 ackType 0x005F 的码 4,而客户端**没有画任何东西**。所以:
// - 码 4 在 op95 表里是可达的(老注释若说它渲染"背包已满"就与本机客户端不符),
// - 无论回哪个码,都不能再假设"客户端一定会自己画提示"。
//
// 这个用例只保证服务端这一侧稳定:锁定必须走锁定那条错误,且码值集中在一个 switch 里,
// 将来客户端码表反查清楚后只改一处。
func TestMailboxClaimErrorCodeKeepsLockedDistinct(t *testing.T) {
cases := []struct {
name string
err error
want byte
why string
}{
{"nil", nil, 0, "成功不回失败码"},
{"locked", ErrMailboxItemLocked, 4, "锁定走'放不进去'这一格(当前客户端不渲染,安全性不受影响)"},
{"locked wrapped", fmt.Errorf("%w: list=0 slot=65 item=2001", ErrMailboxItemLocked), 4, "带上文包装后仍要认出锁定"},
{"inventory full", ErrMailboxInventoryFull, 4, "背包放不下与锁定共用同一格"},
{"mail gone", ErrMailboxMailNotFound, 21, "信件不存在"},
{"already claimed", ErrMailboxAlreadyClaimed, 21, "已领取"},
{"expired", ErrMailboxExpired, 21, "已过期"},
{"gold", ErrMailboxGoldInsufficient, 10, "金币携带上限"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := mailboxClaimErrorCode(tc.err); got != tc.want {
t.Fatalf("code=%d want=%d (%s)", got, tc.want, tc.why)
}
})
}
// 锁定永远不能被折叠成"成功"或"0":静默成功会把锁定附件真的发出去。
body := buildErrorAck(mailboxClaimErrorCode(fmt.Errorf("%w: x", ErrMailboxItemLocked)))
if len(body) != 2 || body[0] != 0x00 || body[1] == 0 {
t.Fatalf("锁定拒绝帧必须是非零失败码,实际 % X", body)
}
// 而且 must 不等于"没有错误"这条路径。
if errors.Is(ErrMailboxItemLocked, ErrMailboxInventoryFull) {
t.Fatal("锁定与背包已满是两个不同原因,不能互为别名")
}
}