服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
113 lines
5.0 KiB
Go
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)
|
|
}
|
|
}
|