DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,823 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"math/bits"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protowire"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/channelcatalog"
|
||||
"longheng.io/server/internal/modules/dnf/channelinfo"
|
||||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||||
dnfproto "longheng.io/server/internal/modules/dnf/protocol"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
func TestBuildCurrentChannelResidentNoticeMatchesCurrentEXEFields(t *testing.T) {
|
||||
service := residentNoticeTestService(t)
|
||||
channel, _ := service.currentCatalog().Channel(10)
|
||||
now := time.Unix(1_721_020_000, 0)
|
||||
body, err := service.buildCurrentChannelResidentNotice(channel, now)
|
||||
if err != nil {
|
||||
t.Fatalf("build current channel notice: %v", err)
|
||||
}
|
||||
if len(body) < 4 || int(binary.LittleEndian.Uint32(body[:4])) != len(body)-4 {
|
||||
t.Fatalf("protobuf length prefix mismatch: %x", body)
|
||||
}
|
||||
varints, bytesFields := consumeChannelInfoProto(t, body[4:])
|
||||
for field, want := range map[protowire.Number]uint64{
|
||||
1: currentChannelInfoEnum,
|
||||
2: currentChannelInfoSuccess,
|
||||
7: 0,
|
||||
8: 10,
|
||||
9: currentChannelResidentControllerIndex,
|
||||
11: uint64(now.Unix()),
|
||||
13: defaultInitialUDPPort1,
|
||||
14: defaultInitialUDPPort2,
|
||||
30004: currentChannelCommandPacketCount,
|
||||
30005: currentChannelNotificationPacketCount,
|
||||
} {
|
||||
values := varints[field]
|
||||
if len(values) != 1 || values[0] != want {
|
||||
t.Fatalf("field %d = %v, want [%d]", field, values, want)
|
||||
}
|
||||
}
|
||||
if got := string(bytesFields[4][0]); got != "ch.10" {
|
||||
t.Fatalf("channel name field = %q, want ch.10", got)
|
||||
}
|
||||
if got := string(bytesFields[12][0]); got != service.options.serverIP {
|
||||
t.Fatalf("server ip field = %q, want %q", got, service.options.serverIP)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCurrentChannelResidentNoticeAcceptsOnlineDirectoryType(t *testing.T) {
|
||||
service := residentNoticeTestService(t)
|
||||
channel, _ := service.currentCatalog().Channel(10)
|
||||
channel.Type = uint8(channelinfo.OnlineTypeFor90CN(channel.ID, int(channel.Type)))
|
||||
|
||||
body, err := service.buildCurrentChannelResidentNotice(channel, time.Unix(1_721_020_000, 0))
|
||||
if err != nil {
|
||||
t.Fatalf("build online channel type notice: %v", err)
|
||||
}
|
||||
varints, _ := consumeChannelInfoProto(t, body[4:])
|
||||
if values := varints[9]; len(values) != 1 || values[0] != currentChannelResidentControllerIndex {
|
||||
t.Fatalf("online type-%d controller field = %v, want [0]", channel.Type, values)
|
||||
}
|
||||
login := service.buildLoginSuccess(channel)
|
||||
if len(login) < 3 || login[2] != channel.Type {
|
||||
t.Fatalf("endpoint success raw channel type = %v, want %d", login, channel.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGameEndpointHandshakeUsesConnectedChannelExactlyOnce(t *testing.T) {
|
||||
service := residentNoticeTestService(t)
|
||||
crack, _ := service.currentCatalog().Channel(19)
|
||||
crack.ID = 253
|
||||
crack.Name = "ch.253"
|
||||
crack.NoticeName = "ch.253"
|
||||
crack.Port = 10253
|
||||
conn := &bufferConn{}
|
||||
session := &gameSession{
|
||||
conn: conn,
|
||||
accountID: "dnf:1",
|
||||
channel: crack,
|
||||
residentChannel: crack,
|
||||
}
|
||||
now := time.Unix(1_721_020_000, 0)
|
||||
|
||||
if err := service.sendGameConnectionBootstrap(session, now); err != nil {
|
||||
t.Fatalf("send account-bound game bootstrap: %v", err)
|
||||
}
|
||||
first, rest := splitGameServerUpperPacketWithHeader(t, conn.write.Bytes(), service.gameUpperHeaderSize())
|
||||
if first.Header.Classification != 0 || first.Header.MsgID != 1 || first.Header.Seq != 0 {
|
||||
t.Fatalf("unexpected CHANNELINFO header: %+v", first.Header)
|
||||
}
|
||||
wantNotice, err := service.buildCurrentChannelResidentNotice(crack, now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if plain := decodeCurrentEXEChannelNoticeBody(first.Body); !bytes.Equal(plain, wantNotice) {
|
||||
t.Fatalf("CHANNELINFO body = %x, want %x", plain, wantNotice)
|
||||
}
|
||||
if len(rest) != 0 {
|
||||
t.Fatalf("TCP open emitted bytes after CHANNELINFO: %x", rest)
|
||||
}
|
||||
if !session.currentChannelResidentNoticeSent || session.gameEndpointSuccessSent {
|
||||
t.Fatalf("state after CHANNELINFO = notice:%t success:%t", session.currentChannelResidentNoticeSent, session.gameEndpointSuccessSent)
|
||||
}
|
||||
if session.connectionTownActorOwnerChannel != byte(crack.ID) ||
|
||||
session.townActorOwnerChannel != byte(crack.ID) {
|
||||
t.Fatalf(
|
||||
"town actor contexts after CHANNELINFO = connection:%d current:%d, want %d",
|
||||
session.connectionTownActorOwnerChannel,
|
||||
session.townActorOwnerChannel,
|
||||
crack.ID,
|
||||
)
|
||||
}
|
||||
|
||||
noticeWireLen := conn.write.Len()
|
||||
request, err := dnfproto.BuildChannelPacket(
|
||||
uint16(dnfenum.UpperMsgGameEndpoint),
|
||||
make([]byte, currentChannelReconnectDisplayProbeSize),
|
||||
1,
|
||||
dnfproto.DefaultChannelClassification,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := service.handleGameUpper(session, request); err != nil {
|
||||
t.Fatalf("handle endpoint request: %v", err)
|
||||
}
|
||||
success, trailing := splitGameServerUpperPacketWithHeader(t, conn.write.Bytes()[noticeWireLen:], service.gameUpperHeaderSize())
|
||||
if success.Header.Classification != dnfproto.DefaultChannelClassification ||
|
||||
success.Header.MsgID != uint16(dnfenum.UpperMsgGameEndpoint) ||
|
||||
success.Header.Seq != 1 {
|
||||
t.Fatalf("endpoint success header = %+v, want class1/op1 seq1", success.Header)
|
||||
}
|
||||
if want := upperSuccessBody(service.buildLoginSuccessForSession(session)); !bytes.Equal(success.Body, want) {
|
||||
t.Fatalf("endpoint success body = %x, want %x", success.Body, want)
|
||||
}
|
||||
auction0, trailing := splitGameServerUpperPacketWithHeader(t, trailing, service.gameUpperHeaderSize())
|
||||
auction1, trailing := splitGameServerUpperPacketWithHeader(t, trailing, service.gameUpperHeaderSize())
|
||||
if len(trailing) != 0 {
|
||||
t.Fatalf("endpoint request emitted unexpected trailing bytes: %x", trailing)
|
||||
}
|
||||
for index, packet := range []dnfproto.ChannelPacket{auction0, auction1} {
|
||||
if packet.Header.Classification != 0 || packet.Header.MsgID != currentAuctionServiceNoticeMsgID ||
|
||||
!bytes.Equal(packet.Body, buildAuctionServiceNotice(byte(index), currentAuctionServiceStateOpen)) {
|
||||
t.Fatalf("endpoint auction service[%d] header=%+v body=%x", index, packet.Header, packet.Body)
|
||||
}
|
||||
}
|
||||
if !session.gameEndpointSuccessSent {
|
||||
t.Fatal("endpoint success state was not committed")
|
||||
}
|
||||
|
||||
handshakeWireLen := conn.write.Len()
|
||||
repeat, err := dnfproto.BuildChannelPacket(
|
||||
uint16(dnfenum.UpperMsgGameEndpoint),
|
||||
make([]byte, reference90CNChannelReconnectDisplayProbeBodySize),
|
||||
2,
|
||||
dnfproto.DefaultChannelClassification,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := service.handleGameUpper(session, repeat); err != nil {
|
||||
t.Fatalf("handle repeated endpoint request: %v", err)
|
||||
}
|
||||
if err := service.sendCurrentChannelResidentNotice(session, now.Add(time.Second)); err != nil {
|
||||
t.Fatalf("repeat CHANNELINFO: %v", err)
|
||||
}
|
||||
if conn.write.Len() != handshakeWireLen {
|
||||
t.Fatalf("duplicate handshake added %d bytes", conn.write.Len()-handshakeWireLen)
|
||||
}
|
||||
if session.channel.ID != 253 || session.residentChannel.ID != 253 {
|
||||
t.Fatalf("connected channel changed: channel=%+v resident=%+v", session.channel, session.residentChannel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentChannelReconnectDisplayProbeBodyLengthFamily(t *testing.T) {
|
||||
for bodyLen := observed90CNShortEndpointBodySize - 1; bodyLen <= observed90CNShortEndpointMaxBodySize+1; bodyLen++ {
|
||||
want := bodyLen >= observed90CNShortEndpointBodySize &&
|
||||
bodyLen <= observed90CNShortEndpointMaxBodySize &&
|
||||
(bodyLen-observed90CNShortEndpointBodySize)%current90CNEndpointShortVariantSize == 0
|
||||
if got := isCurrentChannelReconnectDisplayProbeBodyLen(bodyLen); got != want {
|
||||
t.Fatalf("short endpoint body length %d accepted=%v, want %v", bodyLen, got, want)
|
||||
}
|
||||
}
|
||||
for _, bodyLen := range []int{
|
||||
reference90CNChannelReconnectDisplayProbeBodySize + current90CNEndpointAdapterRecordSize,
|
||||
reference90CNChannelReconnectDisplayProbeBodySize + 2*current90CNEndpointAdapterRecordSize,
|
||||
reference90CNChannelReconnectDisplayProbeBodySize + current90CNEndpointMaxAdapterRecords*current90CNEndpointAdapterRecordSize,
|
||||
} {
|
||||
if !isCurrentChannelReconnectDisplayProbeBodyLen(bodyLen) {
|
||||
t.Fatalf("valid endpoint body length %d rejected", bodyLen)
|
||||
}
|
||||
}
|
||||
for _, bodyLen := range []int{
|
||||
reference90CNChannelReconnectDisplayProbeBodySize + current90CNEndpointAdapterRecordSize - 1,
|
||||
reference90CNChannelReconnectDisplayProbeBodySize + current90CNEndpointMaxAdapterRecords*current90CNEndpointAdapterRecordSize + current90CNEndpointAdapterRecordSize,
|
||||
} {
|
||||
if isCurrentChannelReconnectDisplayProbeBodyLen(bodyLen) {
|
||||
t.Fatalf("invalid endpoint body length %d accepted", bodyLen)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountBoundChannel121PreservesSelectionChannelAndActorOwner(t *testing.T) {
|
||||
service := residentNoticeTestService(t)
|
||||
channel, _ := service.currentCatalog().Channel(19)
|
||||
channel.ID = 121
|
||||
channel.Name = "ch.121"
|
||||
channel.NoticeName = "ch.121"
|
||||
channel.Port = 10121
|
||||
conn := &bufferConn{}
|
||||
session := &gameSession{
|
||||
conn: conn,
|
||||
accountID: "dnf:1",
|
||||
channel: channel,
|
||||
residentChannel: channel,
|
||||
}
|
||||
|
||||
if err := service.sendGameConnectionBootstrap(session, time.Unix(1_721_020_000, 0)); err != nil {
|
||||
t.Fatalf("send channel-121 bootstrap: %v", err)
|
||||
}
|
||||
if session.channel.ID != 121 || session.residentChannel.ID != 121 {
|
||||
t.Fatalf("selected channel changed: channel=%d resident=%d", session.channel.ID, session.residentChannel.ID)
|
||||
}
|
||||
if session.connectionTownActorOwnerChannel != 121 || session.townActorOwnerChannel != 121 {
|
||||
t.Fatalf(
|
||||
"channel-121 actor contexts = connection:%d current:%d, want 121",
|
||||
session.connectionTownActorOwnerChannel,
|
||||
session.townActorOwnerChannel,
|
||||
)
|
||||
}
|
||||
|
||||
conn.write.Reset()
|
||||
mode0 := buildCurrentSceneObjectListBodyInContext(
|
||||
1,
|
||||
dnfrepo.CharacterRecord{CharacterID: "1", Level: 1, Name: "role"},
|
||||
true,
|
||||
"role",
|
||||
currentTownActorOwnerContext(session),
|
||||
)
|
||||
mode1 := buildCurrentActorBindingMode1BodyInContext(
|
||||
1,
|
||||
0,
|
||||
currentTownActorOwnerContext(session),
|
||||
)
|
||||
for _, packet := range []struct {
|
||||
mode byte
|
||||
body []byte
|
||||
}{
|
||||
{mode: 0, body: mode0},
|
||||
{mode: 1, body: mode1},
|
||||
} {
|
||||
if err := service.sendGameUpperRawClass(session, uint16(dnfenum.CmdPacketSetUDPIPPort), packet.body, 0); err != nil {
|
||||
t.Fatalf("send mode%d actor packet: %v", packet.mode, err)
|
||||
}
|
||||
}
|
||||
|
||||
stream := conn.write.Bytes()
|
||||
for _, want := range []byte{0, 1} {
|
||||
packet, rest := splitGameServerUpperPacketWithHeader(t, stream, service.gameUpperHeaderSize())
|
||||
stream = rest
|
||||
if packet.Header.Classification != 0 || packet.Header.MsgID != uint16(dnfenum.CmdPacketSetUDPIPPort) {
|
||||
t.Fatalf("mode%d header = %+v", want, packet.Header)
|
||||
}
|
||||
if len(packet.Body) < 5 || packet.Body[0] != want ||
|
||||
packet.Body[3] != currentSceneObjectRoute || packet.Body[4] != 121 {
|
||||
t.Fatalf("mode%d body = %x, want route/owner 00/121", want, packet.Body)
|
||||
}
|
||||
}
|
||||
if len(stream) != 0 {
|
||||
t.Fatalf("channel-121 actor packets have trailing bytes: %x", stream)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecialChannelEndpointHandshakeKeepsRawTypeOnlyInSuccess(t *testing.T) {
|
||||
service := residentNoticeTestService(t)
|
||||
special := channelcatalog.Channel{
|
||||
ServerID: 1,
|
||||
ID: 200,
|
||||
Type: 23,
|
||||
Group: "raid",
|
||||
Name: "ch.200",
|
||||
Port: 10200,
|
||||
}
|
||||
conn := &bufferConn{}
|
||||
session := &gameSession{conn: conn, channel: special, residentChannel: special}
|
||||
|
||||
if err := service.sendCurrentChannelResidentNotice(session, time.Unix(1_721_020_000, 0)); err != nil {
|
||||
t.Fatalf("send special-channel notice: %v", err)
|
||||
}
|
||||
|
||||
notice, rest := splitGameServerUpperPacketWithHeader(
|
||||
t,
|
||||
conn.write.Bytes(),
|
||||
service.gameUpperHeaderSize(),
|
||||
)
|
||||
if notice.Header.Classification != 0 || notice.Header.MsgID != 1 || notice.Header.Seq != 0 {
|
||||
t.Fatalf("special CHANNELINFO header = %+v", notice.Header)
|
||||
}
|
||||
if len(rest) != 0 {
|
||||
t.Fatalf("special TCP open emitted trailing bytes: %x", rest)
|
||||
}
|
||||
plainNotice := decodeCurrentEXEChannelNoticeBody(notice.Body)
|
||||
varints, _ := consumeChannelInfoProto(t, plainNotice[4:])
|
||||
if values := varints[9]; len(values) != 1 || values[0] != currentChannelResidentControllerIndex {
|
||||
t.Fatalf("special CHANNELINFO controller index = %v, want [0]", values)
|
||||
}
|
||||
|
||||
noticeWireLen := conn.write.Len()
|
||||
request, err := dnfproto.BuildChannelPacket(
|
||||
uint16(dnfenum.UpperMsgGameEndpoint),
|
||||
make([]byte, reference90CNChannelReconnectDisplayProbeBodySize),
|
||||
1,
|
||||
dnfproto.DefaultChannelClassification,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := service.handleGameUpper(session, request); err != nil {
|
||||
t.Fatalf("handle special endpoint request: %v", err)
|
||||
}
|
||||
packet, trailing := splitGameServerUpperPacketWithHeader(t, conn.write.Bytes()[noticeWireLen:], service.gameUpperHeaderSize())
|
||||
if packet.Header.Classification != dnfproto.DefaultChannelClassification ||
|
||||
packet.Header.MsgID != 1 ||
|
||||
packet.Header.Seq != 1 {
|
||||
t.Fatalf("special endpoint success = %+v, want class1/op1 seq1", packet.Header)
|
||||
}
|
||||
trailing = assertCurrentAuctionServiceOpenPackets(t, trailing, service.gameUpperHeaderSize())
|
||||
if len(trailing) != 0 {
|
||||
t.Fatalf("special endpoint emitted unexpected trailing bytes: %x", trailing)
|
||||
}
|
||||
want := upperSuccessBody(service.buildLoginSuccessForSession(session))
|
||||
if !bytes.Equal(packet.Body, want) {
|
||||
t.Fatalf("special endpoint body = %x, want exact type-%d body %x", packet.Body, special.Type, want)
|
||||
}
|
||||
// Success envelope byte + buildLoginSuccess channel-type offset 2.
|
||||
if len(packet.Body) <= 3 || packet.Body[3] != special.Type {
|
||||
t.Fatalf("special endpoint channel type = %d, want exact catalog type %d", packet.Body[3], special.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountBoundMaxByteChannelCommitsTownOwner(t *testing.T) {
|
||||
service := residentNoticeTestService(t)
|
||||
channel := channelcatalog.Channel{
|
||||
ServerID: 1,
|
||||
ID: 255,
|
||||
Type: 40,
|
||||
Group: "special",
|
||||
Name: "ch.255",
|
||||
NoticeName: "ch.255",
|
||||
Port: 10255,
|
||||
}
|
||||
conn := &bufferConn{}
|
||||
session := &gameSession{
|
||||
conn: conn,
|
||||
accountID: "dnf:1",
|
||||
channel: channel,
|
||||
residentChannel: channel,
|
||||
}
|
||||
|
||||
if err := service.sendGameConnectionBootstrap(session, time.Unix(1_721_020_000, 0)); err != nil {
|
||||
t.Fatalf("send max-byte channel bootstrap: %v", err)
|
||||
}
|
||||
packet, trailing := splitGameServerUpperPacketWithHeader(t, conn.write.Bytes(), service.gameUpperHeaderSize())
|
||||
if packet.Header.Classification != 0 || packet.Header.MsgID != 1 || packet.Header.Seq != 0 {
|
||||
t.Fatalf("max-byte CHANNELINFO header = %+v", packet.Header)
|
||||
}
|
||||
if len(trailing) != 0 {
|
||||
t.Fatalf("max-byte CHANNELINFO emitted trailing bytes: %x", trailing)
|
||||
}
|
||||
plain := decodeCurrentEXEChannelNoticeBody(packet.Body)
|
||||
varints, _ := consumeChannelInfoProto(t, plain[4:])
|
||||
if values := varints[8]; len(values) != 1 || values[0] != 255 {
|
||||
t.Fatalf("max-byte CHANNELINFO field8 = %v, want [255]", values)
|
||||
}
|
||||
if session.connectionTownActorOwnerChannel != 255 ||
|
||||
session.townActorOwnerChannel != 255 ||
|
||||
!session.currentChannelResidentNoticeSent ||
|
||||
session.gameEndpointSuccessSent {
|
||||
t.Fatalf(
|
||||
"max-byte bootstrap state connection=%d current=%d notice=%t success=%t",
|
||||
session.connectionTownActorOwnerChannel,
|
||||
session.townActorOwnerChannel,
|
||||
session.currentChannelResidentNoticeSent,
|
||||
session.gameEndpointSuccessSent,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleGameConnUnboundRealType23ProbeDoesNotPublishChannelInfo(t *testing.T) {
|
||||
listener, err := net.ListenTCP("tcp4", &net.TCPAddr{
|
||||
IP: net.IPv4(127, 0, 0, 1),
|
||||
Port: 0,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("listen special game endpoint: %v", err)
|
||||
}
|
||||
defer listener.Close()
|
||||
listenPort := listener.Addr().(*net.TCPAddr).Port
|
||||
|
||||
raw, err := os.ReadFile("testdata/channel_info.etc")
|
||||
if err != nil {
|
||||
t.Fatalf("read real channel catalog fixture: %v", err)
|
||||
}
|
||||
index, err := channelinfo.Parse(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("parse real channel catalog fixture: %v", err)
|
||||
}
|
||||
catalog, err := channelcatalog.New(index, channelcatalog.Options{
|
||||
ServerID: 1,
|
||||
GamePortBase: listenPort - 221,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("build real channel catalog fixture: %v", err)
|
||||
}
|
||||
special, ok := catalog.Channel(221)
|
||||
if !ok || special.Type != 23 || special.Port != listenPort {
|
||||
t.Fatalf("real special channel = %+v found=%t, want id=221 type=23 port=%d", special, ok, listenPort)
|
||||
}
|
||||
service := &Service{
|
||||
options: options{
|
||||
channelServerID: 1,
|
||||
channelAdvertiseID: 0,
|
||||
serverIP: "127.0.0.1",
|
||||
initialUDPPort1: defaultInitialUDPPort1,
|
||||
initialUDPPort2: defaultInitialUDPPort2,
|
||||
commandCount: defaultCommandCount,
|
||||
notificationCount: defaultNotificationCount,
|
||||
gameUpperHeader: gameUpperHeaderServer16,
|
||||
gameUpperBodyCodec: gameUpperBodyCodecPlain,
|
||||
maxPacketBytes: defaultMaxPacketBytes,
|
||||
},
|
||||
catalog: catalog,
|
||||
}
|
||||
handlerDone := make(chan error, 1)
|
||||
go func() {
|
||||
conn, acceptErr := listener.AcceptTCP()
|
||||
if acceptErr != nil {
|
||||
handlerDone <- acceptErr
|
||||
return
|
||||
}
|
||||
service.handleGameConn(context.Background(), conn)
|
||||
handlerDone <- nil
|
||||
}()
|
||||
|
||||
client, err := net.DialTCP("tcp4", nil, listener.Addr().(*net.TCPAddr))
|
||||
if err != nil {
|
||||
t.Fatalf("dial special game endpoint: %v", err)
|
||||
}
|
||||
endpointRequest, err := dnfproto.BuildChannelPacket(
|
||||
uint16(dnfenum.UpperMsgGameEndpoint),
|
||||
make([]byte, reference90CNChannelReconnectDisplayProbeBodySize),
|
||||
1,
|
||||
dnfproto.DefaultChannelClassification,
|
||||
)
|
||||
if err != nil {
|
||||
client.Close()
|
||||
t.Fatalf("build special endpoint request: %v", err)
|
||||
}
|
||||
if _, err := client.Write(endpointRequest); err != nil {
|
||||
client.Close()
|
||||
t.Fatalf("write special endpoint request: %v", err)
|
||||
}
|
||||
if err := client.CloseWrite(); err != nil {
|
||||
client.Close()
|
||||
t.Fatalf("close special client write side: %v", err)
|
||||
}
|
||||
if err := client.SetReadDeadline(time.Now().Add(3 * time.Second)); err != nil {
|
||||
client.Close()
|
||||
t.Fatalf("set special client read deadline: %v", err)
|
||||
}
|
||||
wire, readErr := io.ReadAll(client)
|
||||
_ = client.Close()
|
||||
if readErr != nil {
|
||||
t.Fatalf("read special endpoint handshake: %v", readErr)
|
||||
}
|
||||
select {
|
||||
case handleErr := <-handlerDone:
|
||||
if handleErr != nil {
|
||||
t.Fatalf("handle special game connection: %v", handleErr)
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("special game connection handler did not finish")
|
||||
}
|
||||
|
||||
packet, trailing := splitGameServerUpperPacketWithHeader(
|
||||
t,
|
||||
wire,
|
||||
service.gameUpperHeaderSize(),
|
||||
)
|
||||
if packet.Header.Classification != dnfproto.DefaultChannelClassification ||
|
||||
packet.Header.MsgID != 1 ||
|
||||
packet.Header.Seq != 0 {
|
||||
t.Fatalf("unbound real type-23 probe bootstrap = %+v, want class1/op1 seq0", packet.Header)
|
||||
}
|
||||
if len(trailing) != 0 {
|
||||
t.Fatalf("unbound real type-23 probe emitted extra wire bytes: %x", trailing)
|
||||
}
|
||||
if len(packet.Body) < 9 {
|
||||
t.Fatalf("real type-23 endpoint body is too short for routing identity: %x", packet.Body)
|
||||
}
|
||||
routingKey := binary.LittleEndian.Uint32(packet.Body[5:9])
|
||||
if routingKey == 0 || routingKey == service.options.gameOuterToken {
|
||||
t.Fatalf("real type-23 endpoint routing key=%#x outer token=%#x", routingKey, service.options.gameOuterToken)
|
||||
}
|
||||
want := upperSuccessBody(service.buildLoginSuccessWithRoutingKey(special, routingKey))
|
||||
if !bytes.Equal(packet.Body, want) {
|
||||
t.Fatalf("real type-23 endpoint body = %x, want %x", packet.Body, want)
|
||||
}
|
||||
if len(packet.Body) <= 3 || packet.Body[3] != special.Type {
|
||||
t.Fatalf("real type-23 endpoint channel type = %d, want %d", packet.Body[3], special.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountBoundHighNonPVPChannelFallsBackWithoutTruncatingTownOwner(t *testing.T) {
|
||||
for _, channelID := range []int{256, 501} {
|
||||
t.Run(strconv.Itoa(channelID), func(t *testing.T) {
|
||||
service := residentNoticeTestService(t)
|
||||
high := channelcatalog.Channel{
|
||||
ServerID: 1,
|
||||
ID: channelID,
|
||||
Type: 23,
|
||||
Group: "special",
|
||||
Name: "ch." + strconv.Itoa(channelID),
|
||||
Port: 10000 + channelID,
|
||||
}
|
||||
conn := &bufferConn{}
|
||||
session := &gameSession{
|
||||
conn: conn,
|
||||
accountID: "dnf:1",
|
||||
channel: high,
|
||||
residentChannel: high,
|
||||
}
|
||||
|
||||
if err := service.sendGameConnectionBootstrap(session, time.Unix(1_721_020_000, 0)); err != nil {
|
||||
t.Fatalf("send high-channel bootstrap: %v", err)
|
||||
}
|
||||
packet, trailing := splitGameServerUpperPacketWithHeader(t, conn.write.Bytes(), service.gameUpperHeaderSize())
|
||||
if packet.Header.Classification != dnfproto.DefaultChannelClassification ||
|
||||
packet.Header.MsgID != uint16(dnfenum.UpperMsgGameEndpoint) ||
|
||||
packet.Header.Seq != 0 {
|
||||
t.Fatalf("high-channel fallback = %+v, want class1/op1 seq0", packet.Header)
|
||||
}
|
||||
if len(trailing) != 0 {
|
||||
t.Fatalf("high-channel fallback emitted extra bytes: %x", trailing)
|
||||
}
|
||||
if session.currentChannelResidentNoticeSent || !session.gameEndpointSuccessSent {
|
||||
t.Fatalf(
|
||||
"high-channel state = notice:%t success:%t",
|
||||
session.currentChannelResidentNoticeSent,
|
||||
session.gameEndpointSuccessSent,
|
||||
)
|
||||
}
|
||||
if session.connectionTownActorOwnerChannel != currentSceneObjectContext ||
|
||||
session.townActorOwnerChannel != currentSceneObjectContext {
|
||||
t.Fatalf(
|
||||
"high-channel owner truncated to connection:%d current:%d",
|
||||
session.connectionTownActorOwnerChannel,
|
||||
session.townActorOwnerChannel,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func decodeCurrentEXEChannelNoticeBody(encoded []byte) []byte {
|
||||
plain := make([]byte, len(encoded))
|
||||
for i, value := range encoded {
|
||||
plain[i] = bits.RotateLeft8(value, 6) ^ 0xb5
|
||||
}
|
||||
return plain
|
||||
}
|
||||
|
||||
func residentNoticeTestService(t *testing.T) *Service {
|
||||
t.Helper()
|
||||
const fixture = `
|
||||
[dungeon]
|
||||
` + "`[crack]` `crack`" + `
|
||||
[/dungeon]
|
||||
[dungeon]
|
||||
` + "`[granfloris]` `granfloris` 1 2" + `
|
||||
[/dungeon]
|
||||
[server]
|
||||
1 19 ` + "`crack`" + ` 1 ` + "`[crack]`" + ` 0 0 10 ` + "`granfloris`" + ` 1 ` + "`[granfloris]`" + ` 0 0
|
||||
[/server]
|
||||
`
|
||||
index, err := channelinfo.Parse([]byte(fixture))
|
||||
if err != nil {
|
||||
t.Fatalf("parse resident channel fixture: %v", err)
|
||||
}
|
||||
catalog, err := channelcatalog.New(index, channelcatalog.Options{ServerID: 1})
|
||||
if err != nil {
|
||||
t.Fatalf("build resident channel fixture: %v", err)
|
||||
}
|
||||
return &Service{
|
||||
options: options{
|
||||
channelServerID: 1,
|
||||
channelAdvertiseID: 0,
|
||||
serverIP: "203.0.113.10",
|
||||
initialUDPPort1: defaultInitialUDPPort1,
|
||||
initialUDPPort2: defaultInitialUDPPort2,
|
||||
commandCount: defaultCommandCount,
|
||||
notificationCount: defaultNotificationCount,
|
||||
gameUpperHeader: gameUpperHeaderServer16,
|
||||
gameUpperBodyCodec: gameUpperBodyCodecPlain,
|
||||
},
|
||||
catalog: catalog,
|
||||
}
|
||||
}
|
||||
|
||||
func consumeChannelInfoProto(t *testing.T, raw []byte) (map[protowire.Number][]uint64, map[protowire.Number][][]byte) {
|
||||
t.Helper()
|
||||
varints := make(map[protowire.Number][]uint64)
|
||||
bytesFields := make(map[protowire.Number][][]byte)
|
||||
for len(raw) > 0 {
|
||||
field, wireType, n := protowire.ConsumeTag(raw)
|
||||
if n < 0 {
|
||||
t.Fatalf("consume channel info tag: %v", protowire.ParseError(n))
|
||||
}
|
||||
raw = raw[n:]
|
||||
switch wireType {
|
||||
case protowire.VarintType:
|
||||
value, consumed := protowire.ConsumeVarint(raw)
|
||||
if consumed < 0 {
|
||||
t.Fatalf("consume channel info field %d: %v", field, protowire.ParseError(consumed))
|
||||
}
|
||||
varints[field] = append(varints[field], value)
|
||||
raw = raw[consumed:]
|
||||
case protowire.BytesType:
|
||||
value, consumed := protowire.ConsumeBytes(raw)
|
||||
if consumed < 0 {
|
||||
t.Fatalf("consume channel info field %d: %v", field, protowire.ParseError(consumed))
|
||||
}
|
||||
bytesFields[field] = append(bytesFields[field], append([]byte(nil), value...))
|
||||
raw = raw[consumed:]
|
||||
default:
|
||||
t.Fatalf("unexpected channel info field %d wire type %d", field, wireType)
|
||||
}
|
||||
}
|
||||
return varints, bytesFields
|
||||
}
|
||||
|
||||
func TestSendPreInitialBootstrapIsWriteSilent(t *testing.T) {
|
||||
service := residentNoticeTestService(t)
|
||||
crack, _ := service.currentCatalog().Channel(19)
|
||||
conn := &bufferConn{}
|
||||
session := &gameSession{conn: conn, channel: crack}
|
||||
|
||||
if err := service.sendPreInitialBootstrap(session); err != nil {
|
||||
t.Fatalf("send pre initial bootstrap: %v", err)
|
||||
}
|
||||
firstLen := conn.write.Len()
|
||||
if firstLen != 0 {
|
||||
t.Fatalf("pre bootstrap must be write-silent: len=%d", firstLen)
|
||||
}
|
||||
if err := service.sendPreInitialBootstrap(session); err != nil {
|
||||
t.Fatalf("repeat pre initial bootstrap: %v", err)
|
||||
}
|
||||
if conn.write.Len() != firstLen {
|
||||
t.Fatalf("repeat pre bootstrap added %d bytes", conn.write.Len()-firstLen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGameEndpointRequestRequiresNoticeAndKnownShape(t *testing.T) {
|
||||
service := residentNoticeTestService(t)
|
||||
channel, _ := service.currentCatalog().Channel(19)
|
||||
conn := &bufferConn{}
|
||||
session := &gameSession{conn: conn, channel: channel, residentChannel: channel}
|
||||
|
||||
beforeNotice, err := dnfproto.BuildChannelPacket(
|
||||
uint16(dnfenum.UpperMsgGameEndpoint),
|
||||
make([]byte, currentChannelReconnectDisplayProbeSize),
|
||||
0,
|
||||
dnfproto.DefaultChannelClassification,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := service.handleGameUpper(session, beforeNotice); err != nil {
|
||||
t.Fatalf("handle endpoint request before notice: %v", err)
|
||||
}
|
||||
if conn.write.Len() != 0 {
|
||||
t.Fatalf("request before CHANNELINFO emitted bytes: %x", conn.write.Bytes())
|
||||
}
|
||||
|
||||
if err := service.sendCurrentChannelResidentNotice(session, time.Unix(1_721_020_000, 0)); err != nil {
|
||||
t.Fatalf("send CHANNELINFO: %v", err)
|
||||
}
|
||||
noticeLen := conn.write.Len()
|
||||
|
||||
for index, test := range []struct {
|
||||
class byte
|
||||
bodyLen int
|
||||
}{
|
||||
{class: dnfproto.DefaultChannelClassification, bodyLen: 0},
|
||||
{class: 2, bodyLen: currentChannelReconnectDisplayProbeSize},
|
||||
} {
|
||||
frame, err := dnfproto.BuildChannelPacket(
|
||||
uint16(dnfenum.UpperMsgGameEndpoint),
|
||||
make([]byte, test.bodyLen),
|
||||
uint16(index),
|
||||
test.class,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("build invalid upper op1/%d: %v", test.bodyLen, err)
|
||||
}
|
||||
if err := service.handleGameUpper(session, frame); err != nil {
|
||||
t.Fatalf("handle invalid upper op1/%d: %v", test.bodyLen, err)
|
||||
}
|
||||
if conn.write.Len() != noticeLen {
|
||||
t.Fatalf("invalid upper op1/%d added %d response bytes", test.bodyLen, conn.write.Len()-noticeLen)
|
||||
}
|
||||
}
|
||||
|
||||
valid, err := dnfproto.BuildChannelPacket(
|
||||
uint16(dnfenum.UpperMsgGameEndpoint),
|
||||
make([]byte, reference90CNChannelReconnectDisplayProbeBodySize),
|
||||
3,
|
||||
dnfproto.DefaultChannelClassification,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := service.handleGameUpper(session, valid); err != nil {
|
||||
t.Fatalf("handle valid endpoint request: %v", err)
|
||||
}
|
||||
handshakeLen := conn.write.Len()
|
||||
if handshakeLen <= noticeLen {
|
||||
t.Fatal("valid endpoint request did not emit class1/op1")
|
||||
}
|
||||
if err := service.handleGameCommand(
|
||||
session,
|
||||
byte(1),
|
||||
1,
|
||||
make([]byte, reference90CNChannelReconnectDisplayProbeBodySize),
|
||||
); err != nil {
|
||||
t.Fatalf("handle legacy op1: %v", err)
|
||||
}
|
||||
if conn.write.Len() != handshakeLen {
|
||||
t.Fatalf("legacy op1 added %d response bytes", conn.write.Len()-handshakeLen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGameEndpointHandshakeAcceptsCurrentRequestSizes(t *testing.T) {
|
||||
for _, bodyLen := range []int{
|
||||
observed90CNShortEndpointBodySize,
|
||||
currentChannelReconnectDisplayProbeSize,
|
||||
currentChannelReconnectDisplayProbeSize + current90CNEndpointShortVariantSize,
|
||||
observed90CNCrackEndpointBodySize,
|
||||
reference90CNChannelReconnectDisplayProbeBodySize,
|
||||
observed90CNShortEndpointMaxBodySize,
|
||||
} {
|
||||
t.Run(strconv.Itoa(bodyLen), func(t *testing.T) {
|
||||
service := residentNoticeTestService(t)
|
||||
channel, _ := service.currentCatalog().Channel(19)
|
||||
conn := &bufferConn{}
|
||||
session := &gameSession{conn: conn, channel: channel, residentChannel: channel}
|
||||
|
||||
if err := service.sendCurrentChannelResidentNotice(session, time.Unix(1_721_020_000, 0)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request, err := dnfproto.BuildChannelPacket(
|
||||
uint16(dnfenum.UpperMsgGameEndpoint),
|
||||
make([]byte, bodyLen),
|
||||
1,
|
||||
dnfproto.DefaultChannelClassification,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := service.handleGameUpper(session, request); err != nil {
|
||||
t.Fatalf("handle endpoint request: %v", err)
|
||||
}
|
||||
|
||||
first, rest := splitGameServerUpperPacketWithHeader(t, conn.write.Bytes(), service.gameUpperHeaderSize())
|
||||
if first.Header.Classification != 0 || first.Header.MsgID != 1 || first.Header.Seq != 0 {
|
||||
t.Fatalf("CHANNELINFO packet = %+v", first.Header)
|
||||
}
|
||||
second, rest := splitGameServerUpperPacketWithHeader(t, rest, service.gameUpperHeaderSize())
|
||||
if second.Header.Classification != dnfproto.DefaultChannelClassification ||
|
||||
second.Header.MsgID != 1 ||
|
||||
second.Header.Seq != 1 {
|
||||
t.Fatalf("endpoint success packet = %+v", second.Header)
|
||||
}
|
||||
rest = assertCurrentAuctionServiceOpenPackets(t, rest, service.gameUpperHeaderSize())
|
||||
if len(rest) != 0 {
|
||||
t.Fatalf("endpoint handshake emitted an unexpected packet: %x", rest)
|
||||
}
|
||||
|
||||
wireLen := conn.write.Len()
|
||||
if err := service.handleGameUpper(session, request); err != nil {
|
||||
t.Fatalf("handle repeated endpoint request: %v", err)
|
||||
}
|
||||
if conn.write.Len() != wireLen {
|
||||
t.Fatalf("repeated endpoint request added %d bytes", conn.write.Len()-wireLen)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertCurrentAuctionServiceOpenPackets(t *testing.T, data []byte, headerSize int) []byte {
|
||||
t.Helper()
|
||||
for serviceType := byte(0); serviceType < currentAuctionServiceTypeCount; serviceType++ {
|
||||
packet, rest := splitGameServerUpperPacketWithHeader(t, data, headerSize)
|
||||
if packet.Header.Classification != 0 || packet.Header.MsgID != currentAuctionServiceNoticeMsgID ||
|
||||
!bytes.Equal(packet.Body, buildAuctionServiceNotice(serviceType, currentAuctionServiceStateOpen)) {
|
||||
t.Fatalf("auction service type=%d header=%+v body=%x", serviceType, packet.Header, packet.Body)
|
||||
}
|
||||
data = rest
|
||||
}
|
||||
return data
|
||||
}
|
||||
Reference in new issue
Block a user