Files
90CNServer/server/internal/services/dnfbridge/unsupported_command_fallback_test.go
T
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

249 lines
9.5 KiB
Go

package dnfbridge
import (
"bytes"
"encoding/binary"
"testing"
"longheng.io/server/internal/modules/dnf/channelcatalog"
"longheng.io/server/internal/modules/dnf/dnfenum"
dnfproto "longheng.io/server/internal/modules/dnf/protocol"
)
func buildCurrentCommandCapabilitiesTestBody(opcodes ...uint16) []byte {
body := make([]byte, currentCommandCapabilityHeaderLength+int(currentCommandCapabilityBitmapLength))
binary.LittleEndian.PutUint32(body[0:4], currentCommandCapabilityMagic)
binary.LittleEndian.PutUint16(body[4:6], currentCommandCapabilityVersion)
binary.LittleEndian.PutUint16(body[6:8], currentCommandCapabilityEnumEnd)
binary.LittleEndian.PutUint16(body[8:10], currentCommandCapabilityBitmapLength)
for _, opcode := range opcodes {
if opcode < currentCommandCapabilityEnumEnd {
body[currentCommandCapabilityHeaderLength+int(opcode>>3)] |= byte(1 << (opcode & 7))
}
}
return body
}
func buildCurrentCommandFallbackLegacyPacket(opcode uint16, body []byte) []byte {
packet := make([]byte, dnfproto.LegacyGameHeaderSize+len(body))
packet[0] = byte(dnfenum.GameCmdCommand)
binary.LittleEndian.PutUint16(packet[1:3], opcode)
binary.LittleEndian.PutUint32(packet[3:7], uint32(len(packet)))
copy(packet[dnfproto.LegacyGameHeaderSize:], body)
return packet
}
func TestParseCurrentCommandCapabilitiesRequiresExactCurrentEnumEnvelope(t *testing.T) {
body := buildCurrentCommandCapabilitiesTestBody(
uint16(dnfenum.CmdPacketPVPChannelInfo),
uint16(dnfenum.CmdPacketRequestMatch),
)
capabilities, ok := parseCurrentCommandCapabilities(body)
if !ok {
t.Fatal("valid current command capability envelope was rejected")
}
if capabilities.handlerCount != 2 ||
capabilities.fingerprint == 0 ||
capabilities.bitmap[uint16(dnfenum.CmdPacketPVPChannelInfo)>>3] == 0 {
t.Fatalf("capabilities = %+v", capabilities)
}
for _, mutate := range []func([]byte){
func(value []byte) { value[0] ^= 0xFF },
func(value []byte) { binary.LittleEndian.PutUint16(value[4:6], 2) },
func(value []byte) { binary.LittleEndian.PutUint16(value[6:8], currentCommandCapabilityEnumEnd-1) },
func(value []byte) { binary.LittleEndian.PutUint16(value[8:10], currentCommandCapabilityBitmapLength-1) },
func(value []byte) { value[len(value)-1] |= 0x80 },
func(value []byte) {
for index := currentCommandCapabilityHeaderLength; index < len(value); index++ {
value[index] = 0
}
},
} {
candidate := append([]byte(nil), body...)
mutate(candidate)
if _, accepted := parseCurrentCommandCapabilities(candidate); accepted {
t.Fatalf("invalid capability envelope accepted: % X", candidate[:10])
}
}
if _, accepted := parseCurrentCommandCapabilities(body[:len(body)-1]); accepted {
t.Fatal("short capability envelope accepted")
}
}
func TestCurrentCommandCapabilityHandshakeMakesUnhandledUpperRequestFailSafe(t *testing.T) {
service := &Service{options: options{
gameUpperBodyCodec: gameUpperBodyCodecPlain,
gameUpperClientBodyCodec: gameUpperClientBodyCodecPlain,
}}
connection := &bufferConn{}
session := &gameSession{
conn: connection,
connID: "command-fallback-upper",
channel: channelcatalog.Channel{ID: 21, Type: 1, Name: "ch.21", Port: 10021},
}
body := buildCurrentCommandCapabilitiesTestBody(uint16(dnfenum.CmdPacketPVPChannelInfo))
handshake, err := dnfproto.BuildChannelPacket(
currentCommandCapabilityOpcode,
body,
1,
dnfproto.DefaultChannelClassification,
)
if err != nil {
t.Fatalf("build handshake: %v", err)
}
if err := service.handleGameUpper(session, handshake); err != nil {
t.Fatalf("handle handshake: %v", err)
}
if connection.write.Len() != 0 {
t.Fatalf("capability handshake received an invented ACK: % X", connection.write.Bytes())
}
if !session.commandFallback.hasClass1Handler(uint16(dnfenum.CmdPacketPVPChannelInfo)) {
t.Fatal("PVP channel response handler capability was not stored")
}
request, err := dnfproto.BuildChannelPacket(
uint16(dnfenum.CmdPacketPVPChannelInfo),
nil,
2,
dnfproto.DefaultChannelClassification,
)
if err != nil {
t.Fatalf("build request: %v", err)
}
if err := service.handleGameUpper(session, request); err != nil {
t.Fatalf("handle request: %v", err)
}
response, rest := splitPVPChannelInfoFixed15Packet(t, connection.write.Bytes())
if len(rest) != 0 ||
response.msgID != uint16(dnfenum.CmdPacketPVPChannelInfo) ||
!bytes.Equal(response.body, []byte{0, 0x15}) {
t.Fatalf("safe failure = msg %d body=% X rest=% X",
response.msgID, response.body, rest)
}
}
func TestCurrentCommandFallbackAlsoClosesLegacyInteractiveRequest(t *testing.T) {
service := &Service{options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain}}
connection := &bufferConn{}
session := &gameSession{
conn: connection,
connID: "command-fallback-legacy",
channel: channelcatalog.Channel{ID: 21, Type: 1, Name: "ch.21", Port: 10021},
}
capabilities, ok := parseCurrentCommandCapabilities(
buildCurrentCommandCapabilitiesTestBody(uint16(dnfenum.CmdPacketPVPChannelInfo)),
)
if !ok {
t.Fatal("test capability envelope rejected")
}
session.commandFallback.replace(capabilities)
request := buildCurrentCommandFallbackLegacyPacket(
uint16(dnfenum.CmdPacketPVPChannelInfo),
nil,
)
if err := service.handleLegacyGamePacket(session, request); err != nil {
t.Fatalf("handle legacy request: %v", err)
}
response, rest := splitPVPChannelInfoFixed15Packet(t, connection.write.Bytes())
if len(rest) != 0 || response.msgID != uint16(dnfenum.CmdPacketPVPChannelInfo) ||
!bytes.Equal(response.body, []byte{0, 0x15}) {
t.Fatalf("legacy safe failure = msg %d body=% X rest=% X",
response.msgID, response.body, rest)
}
}
func TestCurrentInventoryClosesMalformedConsumablePendingRouteWithoutFallback(t *testing.T) {
service := &Service{options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain}}
connection := &bufferConn{}
session := &gameSession{
conn: connection,
connID: "command-fallback-malformed-consumable",
channel: channelcatalog.Channel{ID: 21, Type: 1, Name: "ch.21", Port: 10021},
}
// op44 is exactly 15 bytes in the current client. A truncated request used
// to enter inventory's blocked parse result and then depend on the optional
// capability fallback. The inventory owner must now terminate the shared
// native item-operation state with op44's exact error-17 shape even before a
// capability bitmap is installed.
request := buildCurrentCommandFallbackLegacyPacket(
uint16(dnfenum.CmdPacketUseStackable),
[]byte{0x01},
)
if err := service.handleLegacyGamePacket(session, request); err != nil {
t.Fatalf("handle malformed consumable: %v", err)
}
response, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
if len(rest) != 0 || response.Header.MsgID != uint16(dnfenum.CmdPacketUseStackable) ||
response.Header.Classification != dnfproto.DefaultChannelClassification ||
!bytes.Equal(response.Body, []byte{0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0}) {
t.Fatalf("malformed consumable safe failure = class %d msg %d body=% X rest=% X",
response.Header.Classification, response.Header.MsgID, response.Body, rest)
}
}
func TestCurrentInventoryClosesMalformedConsumableActionPendingRouteWithoutFallback(t *testing.T) {
service := &Service{options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain}}
connection := &bufferConn{}
session := &gameSession{
conn: connection,
connID: "command-fallback-malformed-consumable-action",
channel: channelcatalog.Channel{ID: 21, Type: 1, Name: "ch.21", Port: 10021},
}
// Current sub_1E785B0 receives status/error from the class1 dispatcher and
// does not read a feature tail on failure. Therefore every malformed op515
// must still receive the exact two-byte error response without depending on
// the optional capability bitmap.
request := buildCurrentCommandFallbackLegacyPacket(
uint16(dnfenum.CmdPacketUseStackableAction),
[]byte{0x4C},
)
if err := service.handleLegacyGamePacket(session, request); err != nil {
t.Fatalf("handle malformed consumable action: %v", err)
}
response, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
if len(rest) != 0 || response.Header.MsgID != uint16(dnfenum.CmdPacketUseStackableAction) ||
response.Header.Classification != dnfproto.DefaultChannelClassification ||
!bytes.Equal(response.Body, []byte{0, 17}) {
t.Fatalf("malformed consumable action safe failure = class %d msg %d body=% X rest=% X",
response.Header.Classification, response.Header.MsgID, response.Body, rest)
}
}
func TestCurrentCommandFallbackKeepsUnregisteredPassivePacketSilent(t *testing.T) {
service := &Service{options: options{
gameUpperBodyCodec: gameUpperBodyCodecPlain,
gameUpperClientBodyCodec: gameUpperClientBodyCodecPlain,
}}
connection := &bufferConn{}
session := &gameSession{
conn: connection,
connID: "command-fallback-passive",
channel: channelcatalog.Channel{ID: 21, Type: 1, Name: "ch.21", Port: 10021},
}
capabilities, ok := parseCurrentCommandCapabilities(
buildCurrentCommandCapabilitiesTestBody(uint16(dnfenum.CmdPacketPVPChannelInfo)),
)
if !ok {
t.Fatal("test capability envelope rejected")
}
session.commandFallback.replace(capabilities)
passive, err := dnfproto.BuildChannelPacket(
uint16(dnfenum.CmdPacketSecurityStatus),
make([]byte, 8),
3,
dnfproto.DefaultChannelClassification,
)
if err != nil {
t.Fatalf("build passive packet: %v", err)
}
if err := service.handleGameUpper(session, passive); err != nil {
t.Fatalf("handle passive packet: %v", err)
}
if connection.write.Len() != 0 {
t.Fatalf("unregistered passive packet received a response: % X", connection.write.Bytes())
}
}