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

113 lines
5.0 KiB
Go

package dnfbridge
import (
"bytes"
"testing"
"longheng.io/server/internal/modules/dnf/alignedcmd"
"longheng.io/server/internal/modules/dnf/dnfenum"
dnfproto "longheng.io/server/internal/modules/dnf/protocol"
)
func newNoticeRoutingTest() (*Service, *gameSession, *bufferConn) {
connection := &bufferConn{}
service := &Service{options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain}}
return service, &gameSession{conn: connection, selectedCharacterID: 17}, connection
}
func TestCurrentRandomOptionGoldInsufficientNoticeText(t *testing.T) {
tests := []struct {
operation string
want string
}{
{operation: "unseal_random_option", want: "金币不足,无法解除魔法封印。"},
{operation: "change_random_option", want: "金币不足,无法变更魔法封印属性。"},
}
for _, test := range tests {
got := currentRandomOptionGoldInsufficientNoticeText(test.operation)
if got != test.want {
t.Fatalf("operation %q text = %q, want %q", test.operation, got, test.want)
}
if _, err := buildCurrentNativeNoticeBody(currentRandomOptionGoldInsufficientNoticeSurface.Type, 0, 0, got); err != nil {
t.Fatalf("operation %q text does not fit the client bound: %v", test.operation, err)
}
}
}
// 【op455 成功提示的顺序契约】
//
// 客户端 op455 处理器没有成功分支(0x2137820 只在 result==0 时按错误码弹提示),
// 所以成功提示只能由服务端补。顺序必须是:先 op14 道具行(装备 + 调整箱),再一次
// class-1 {0x01} 解开 UI 等待,最后才是系统页提示 —— 否则提示会早于词条刷新出现。
func TestRandomOptionResetSuccessAppendsVisibleNotice(t *testing.T) {
service, session, connection := newNoticeRoutingTest()
result := alignedcmd.Result{
Operation: "reset_random_option",
UpperResponses: []alignedcmd.UpperResponse{
// class0/op14 增量道具行,与 op401/op437 成功回包同一条通道。
{MsgID: 0x000E, Classification: 0, Body: []byte{0, 0, 0}},
{MsgID: uint16(dnfenum.CmdPacketResetRandomOption), Classification: 1, Body: []byte{1}},
},
PostActions: []alignedcmd.PostAction{alignedcmd.PostActionNotifyRandomOptionResetSuccess},
}
if err := service.sendAlignedUpperResponses(session, result); err != nil {
t.Fatal(err)
}
update, remaining := splitGameServerUpperPacket(t, connection.write.Bytes())
if update.Header.MsgID != 0x000E || update.Header.Classification != 0 {
t.Fatalf("first packet header=%+v, want the op14 item rows", update.Header)
}
ack, remaining := splitGameServerUpperPacket(t, remaining)
if ack.Header.MsgID != uint16(dnfenum.CmdPacketResetRandomOption) || ack.Header.Classification != 1 || !bytes.Equal(ack.Body, []byte{1}) {
t.Fatalf("ACK header=%+v body=%x", ack.Header, ack.Body)
}
text := currentRandomOptionResetSuccessNoticeText()
if text != "已成功重调该物品的封印属性品级。" {
t.Fatalf("notice text = %q", text)
}
want, err := buildCurrentNativeNoticeBody(currentRandomOptionResetSuccessNoticeSurface.Type, 0, 0, text)
if err != nil {
t.Fatalf("notice text does not fit the client bound: %v", err)
}
notice, trailing := splitGameServerUpperPacketWithHeader(t, remaining, dnfproto.GameServerUpperHeaderSize16)
if notice.Header.MsgID != currentNativeNoticeMsgID || notice.Header.Classification != 0 || !bytes.Equal(notice.Body, want) || len(trailing) != 0 {
t.Fatalf("notice header=%+v body=%x want=%x trailing=%x", notice.Header, notice.Body, want, trailing)
}
if currentRandomOptionResetSuccessNoticeSurface != noticeSurfaceSystemTab {
t.Fatalf("success notice surface = %+v, want the system tab like the op81 message", currentRandomOptionResetSuccessNoticeSurface)
}
}
func TestRandomOptionFailureACKPrecedesVisibleNotice(t *testing.T) {
service, session, connection := newNoticeRoutingTest()
result := alignedcmd.Result{
Operation: "unseal_random_option",
UpperResponses: []alignedcmd.UpperResponse{{
MsgID: uint16(dnfenum.CmdPacketUnsealRandomOption),
Classification: 1,
Body: []byte{0},
}},
PostActions: []alignedcmd.PostAction{alignedcmd.PostActionNotifyRandomOptionGoldInsufficient},
}
if err := service.sendAlignedUpperResponses(session, result); err != nil {
t.Fatal(err)
}
ack, remaining := splitGameServerUpperPacket(t, connection.write.Bytes())
if ack.Header.MsgID != uint16(dnfenum.CmdPacketUnsealRandomOption) || ack.Header.Classification != 1 || !bytes.Equal(ack.Body, []byte{0}) {
t.Fatalf("ACK header=%+v body=%x", ack.Header, ack.Body)
}
text := currentRandomOptionGoldInsufficientNoticeText(result.Operation)
notice, trailing := splitGameServerUpperPacketWithHeader(t, remaining, dnfproto.GameServerUpperHeaderSize16)
want, err := buildCurrentNativeNoticeBody(currentRandomOptionGoldInsufficientNoticeSurface.Type, 0, 0, text)
if err != nil {
t.Fatal(err)
}
if notice.Header.MsgID != currentNativeNoticeMsgID || notice.Header.Classification != 0 || !bytes.Equal(notice.Body, want) || len(trailing) != 0 {
t.Fatalf("notice header=%+v body=%x want=%x trailing=%x", notice.Header, notice.Body, want, trailing)
}
}