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

858 lines
33 KiB
Go

package protocol
import (
"bytes"
"encoding/binary"
"strconv"
"testing"
)
func TestSplitLatestGameStreamAcceptsUpperAndTransport(t *testing.T) {
upper, err := BuildChannelPacket(4, []byte{1, 0}, 7, DefaultChannelClassification)
if err != nil {
t.Fatalf("build upper packet: %v", err)
}
transport, err := BuildLatestGameTCP(1, 1, []byte{9, 8, 7}, TransportOptions{Sequence: 3})
if err != nil {
t.Fatalf("build transport: %v", err)
}
stream := append(append([]byte(nil), upper...), transport...)
packets, remaining, skipped, err := SplitLatestGameStream(stream, 1024)
if err != nil {
t.Fatalf("split stream: %v", err)
}
if skipped != 0 || len(remaining) != 0 {
t.Fatalf("unexpected split state skipped=%d remaining=%d", skipped, len(remaining))
}
if len(packets) != 2 {
t.Fatalf("packet count = %d, want 2", len(packets))
}
if packets[0].Kind != LatestGameStreamUpper {
t.Fatalf("first packet kind = %d, want upper", packets[0].Kind)
}
if packets[1].Kind != LatestGameStreamTransport {
t.Fatalf("second packet kind = %d, want transport", packets[1].Kind)
}
}
func TestSplitLatestGameStreamClassifiesNativeDprotoBeforeLegacy(t *testing.T) {
dproto, err := BuildDprotoClientEnvelope([]byte{0x00, 0xad, 0x00, 0x10, 0x00, 1, 2, 3}, 55)
if err != nil {
t.Fatalf("build dproto: %v", err)
}
heartbeat, err := BuildChannelPacket(1276, nil, 56, DefaultChannelClassification)
if err != nil {
t.Fatalf("build heartbeat: %v", err)
}
stream := append(append([]byte(nil), dproto...), heartbeat...)
packets, remaining, skipped, err := SplitLatestGameStream(stream, 1024)
if err != nil {
t.Fatalf("split stream: %v", err)
}
if skipped != 0 || len(remaining) != 0 || len(packets) != 2 {
t.Fatalf("packets=%d skipped=%d remaining=%d", len(packets), skipped, len(remaining))
}
if packets[0].Kind != LatestGameStreamDproto || packets[1].Kind != LatestGameStreamUpper {
t.Fatalf("kinds=%d,%d", packets[0].Kind, packets[1].Kind)
}
}
func TestSplitLatestGameStreamClassifies90CNChannelReconnectLifecycleBeforeLegacy(t *testing.T) {
tests := []struct {
name string
msgID uint16
bodyLen int
}{
{name: "target channel probe", msgID: 2, bodyLen: currentChannelReconnectProbeSize},
{name: "observed 90cn short bootstrap endpoint", msgID: 1, bodyLen: observed90CNShortEndpointBodySize},
{name: "current exe post commit display rebind", msgID: 1, bodyLen: currentChannelDisplayRebindSize},
{name: "observed 90cn intermediate endpoint", msgID: 1, bodyLen: currentChannelDisplayRebindSize + current90CNEndpointShortVariantSize},
{name: "90cn reference post commit display rebind", msgID: 1, bodyLen: reference90CNChannelDisplayRebindBodySize},
{name: "observed 90cn crack endpoint", msgID: 1, bodyLen: observed90CNCrackEndpointBodySize},
{name: "observed 90cn short endpoint maximum", msgID: 1, bodyLen: observed90CNShortEndpointMaxBodySize},
{name: "current bootstrap endpoint with one additional adapter", msgID: 1, bodyLen: reference90CNChannelDisplayRebindBodySize + current90CNEndpointAdapterRecordSize},
{name: "current bootstrap endpoint with two additional adapters", msgID: 1, bodyLen: reference90CNChannelDisplayRebindBodySize + 2*current90CNEndpointAdapterRecordSize},
{name: "current bootstrap endpoint at bounded adapter maximum", msgID: 1, bodyLen: reference90CNChannelDisplayRebindBodySize + current90CNEndpointMaxAdapterRecords*current90CNEndpointAdapterRecordSize},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
body := make([]byte, test.bodyLen)
if test.msgID == 1 {
body = current90CNEndpointTestBody(test.bodyLen)
}
lifecycle, err := BuildChannelPacket(
test.msgID,
body,
55,
DefaultChannelClassification,
)
if err != nil {
t.Fatal(err)
}
heartbeat, err := BuildChannelPacket(1276, nil, 56, DefaultChannelClassification)
if err != nil {
t.Fatal(err)
}
stream := append(append([]byte(nil), lifecycle...), heartbeat...)
packets, remaining, skipped, err := SplitLatestGameStream(stream, 4096)
if err != nil {
t.Fatal(err)
}
if skipped != 0 || len(remaining) != 0 || len(packets) != 2 {
t.Fatalf(
"msg%d split packets=%d skipped=%d remaining=%d",
test.msgID,
len(packets),
skipped,
len(remaining),
)
}
if packets[0].Kind != LatestGameStreamUpper ||
packets[1].Kind != LatestGameStreamUpper {
t.Fatalf("msg%d kinds=%d/%d want upper/upper", test.msgID, packets[0].Kind, packets[1].Kind)
}
parsed, err := ParseChannelPacketUnchecked(packets[0].Data)
if err != nil {
t.Fatal(err)
}
if parsed.Header.MsgID != test.msgID || len(parsed.Body) != test.bodyLen {
t.Fatalf("msg%d parsed header=%+v body_len=%d", test.msgID, parsed.Header, len(parsed.Body))
}
})
}
}
func TestKnown90CNChannelReconnectLifecycleRejectsAdjacentLengths(t *testing.T) {
for _, test := range []struct {
msgID uint16
bodyLen int
}{
{msgID: 1, bodyLen: observed90CNShortEndpointBodySize - 1},
{msgID: 1, bodyLen: observed90CNShortEndpointBodySize + 1},
{msgID: 1, bodyLen: currentChannelDisplayRebindSize - 1},
{msgID: 1, bodyLen: currentChannelDisplayRebindSize + 1},
{msgID: 1, bodyLen: observed90CNCrackEndpointBodySize - 1},
{msgID: 1, bodyLen: observed90CNCrackEndpointBodySize + 1},
{msgID: 1, bodyLen: observed90CNShortEndpointMaxBodySize - 1},
{msgID: 1, bodyLen: observed90CNShortEndpointMaxBodySize + 1},
{msgID: 1, bodyLen: reference90CNChannelDisplayRebindBodySize - 1},
{msgID: 1, bodyLen: reference90CNChannelDisplayRebindBodySize + 1},
{msgID: 1, bodyLen: reference90CNChannelDisplayRebindBodySize + current90CNEndpointAdapterRecordSize - 1},
{msgID: 1, bodyLen: reference90CNChannelDisplayRebindBodySize + current90CNEndpointAdapterRecordSize + 1},
{msgID: 1, bodyLen: reference90CNChannelDisplayRebindBodySize + current90CNEndpointMaxAdapterRecords*current90CNEndpointAdapterRecordSize + current90CNEndpointAdapterRecordSize},
{msgID: 2, bodyLen: currentChannelReconnectProbeSize - 1},
{msgID: 2, bodyLen: currentChannelReconnectProbeSize + 1},
} {
body := make([]byte, test.bodyLen)
if test.msgID == 1 && test.bodyLen >= reference90CNChannelDisplayRebindBodySize+current90CNEndpointAdapterRecordSize-1 {
body = current90CNEndpointTestBody(test.bodyLen)
}
if isKnownClientUpperMsg(test.msgID, body) {
t.Fatalf("msg%d adjacent body length %d accepted", test.msgID, test.bodyLen)
}
}
}
func TestKnown90CNShortEndpointLengthFamily(t *testing.T) {
for bodyLen := observed90CNShortEndpointBodySize - 1; bodyLen <= observed90CNShortEndpointMaxBodySize+1; bodyLen++ {
valid := current90CNEndpointTestBody(bodyLen)
want := bodyLen >= observed90CNShortEndpointBodySize &&
bodyLen <= observed90CNShortEndpointMaxBodySize &&
(bodyLen-observed90CNShortEndpointBodySize)%current90CNEndpointShortVariantSize == 0
if got := isKnownClientUpperMsg(1, valid); got != want {
t.Fatalf("endpoint body length %d accepted=%v, want %v", bodyLen, got, want)
}
}
}
func TestKnown90CNEndpointRejectsMalformedFixedEnvelope(t *testing.T) {
bodyLens := []int{
observed90CNShortEndpointBodySize,
currentChannelDisplayRebindSize,
currentChannelDisplayRebindSize + current90CNEndpointShortVariantSize,
observed90CNCrackEndpointBodySize,
reference90CNChannelDisplayRebindBodySize,
observed90CNShortEndpointMaxBodySize,
reference90CNChannelDisplayRebindBodySize + 2*current90CNEndpointAdapterRecordSize,
}
tests := []struct {
name string
offset int
value byte
}{
{name: "wrong version", offset: 0, value: 2},
{name: "wrong fixed prefix", offset: 4, value: 0x31},
{name: "uppercase token", offset: 9, value: 'A'},
{name: "missing token terminator", offset: 41, value: 1},
{name: "wrong token length", offset: 42, value: 7},
{name: "wrong client-spec marker", offset: 59, value: 2},
}
for _, bodyLen := range bodyLens {
t.Run(strconv.Itoa(bodyLen), func(t *testing.T) {
valid := current90CNEndpointTestBody(bodyLen)
if !isKnownClientUpperMsg(1, valid) {
t.Fatal("valid endpoint body rejected")
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
body := append([]byte(nil), valid...)
body[test.offset] = test.value
if isKnownClientUpperMsg(1, body) {
t.Fatalf("malformed endpoint body accepted after byte %d changed", test.offset)
}
})
}
})
}
}
func TestKnown90CNEndpointAcceptsObservedClientSpecTailVariants(t *testing.T) {
for _, bodyLen := range []int{observed90CNShortEndpointBodySize, reference90CNChannelDisplayRebindBodySize, observed90CNCrackEndpointBodySize, observed90CNShortEndpointMaxBodySize} {
for _, value := range []byte{0x16, 0x1c, 0x2e} {
body := current90CNEndpointTestBody(bodyLen)
body[61] = value
if !isKnownClientUpperMsg(1, body) {
t.Fatalf("observed endpoint len %d client-spec tail %#x rejected", bodyLen, value)
}
}
body := current90CNEndpointTestBody(bodyLen)
body[61] = 0x17
if isKnownClientUpperMsg(1, body) {
t.Fatalf("unobserved endpoint len %d client-spec tail 0x17 accepted", bodyLen)
}
}
}
func current90CNEndpointTestBody(bodyLen int) []byte {
body := make([]byte, bodyLen)
binary.LittleEndian.PutUint32(body[0:4], 1)
body[4] = 0x30
body[5] = 0x20
copy(body[9:41], []byte("de509f65e9ccaae621cb7278fc2b8e6c"))
binary.LittleEndian.PutUint32(body[42:46], 8)
copy(body[54:62], []byte{0, 2, 0, 0, 1, 1, 0, 0x1c})
return body
}
func TestSplitLatestGameStreamClassifiesExactChangeCharacterSlotBeforeLegacy(t *testing.T) {
body := []byte{0, 0, 0, 0, 1, 0, 0, 0}
changeSlot, err := BuildChannelPacket(295, body, 55, DefaultChannelClassification)
if err != nil {
t.Fatal(err)
}
heartbeat, err := BuildChannelPacket(1276, nil, 56, DefaultChannelClassification)
if err != nil {
t.Fatal(err)
}
stream := append(append([]byte(nil), changeSlot...), heartbeat...)
packets, remaining, skipped, err := SplitLatestGameStream(stream, 1024)
if err != nil {
t.Fatal(err)
}
if skipped != 0 || len(remaining) != 0 || len(packets) != 2 {
t.Fatalf("packets=%d skipped=%d remaining=%d", len(packets), skipped, len(remaining))
}
if packets[0].Kind != LatestGameStreamUpper || packets[1].Kind != LatestGameStreamUpper {
t.Fatalf("kinds=%d/%d want upper/upper", packets[0].Kind, packets[1].Kind)
}
parsed, err := ParseChannelPacketUnchecked(packets[0].Data)
if err != nil {
t.Fatal(err)
}
if parsed.Header.MsgID != 295 || !bytes.Equal(parsed.Body, body) {
t.Fatalf("change-slot header=%+v body=%x", parsed.Header, parsed.Body)
}
for _, adjacentLength := range []int{7, 9} {
if isKnownClientUpperMsg(295, make([]byte, adjacentLength)) {
t.Fatalf("op295 adjacent body length %d accepted", adjacentLength)
}
}
}
func TestSplitLatestGameStreamAcceptsCurrentDungeonDeathMoveSettlementBossStatisticTutorialMissionAndReturnUpper(t *testing.T) {
townMove, err := BuildChannelPacket(36, make([]byte, currentTownSetUserAreaPlainBodySize), 6, DefaultChannelClassification)
if err != nil {
t.Fatalf("build town-move upper: %v", err)
}
deathBody := make([]byte, 86)
deathBody[22] = 2
death, err := BuildChannelPacket(39, deathBody, 7, DefaultChannelClassification)
if err != nil {
t.Fatalf("build death upper: %v", err)
}
characterDeath, err := BuildChannelPacket(40, make([]byte, currentDieCharacterPlainBodySize), 7, DefaultChannelClassification)
if err != nil {
t.Fatalf("build character-death upper: %v", err)
}
useCoin, err := BuildChannelPacket(41, make([]byte, currentUseCoinPlainBodySize), 7, DefaultChannelClassification)
if err != nil {
t.Fatalf("build use-coin upper: %v", err)
}
move, err := BuildChannelPacket(45, make([]byte, currentDungeonMoveMapPlainBodySize), 8, DefaultChannelClassification)
if err != nil {
t.Fatalf("build move upper: %v", err)
}
playResult, err := BuildChannelPacket(46, make([]byte, currentPlayResultBaseBodySize+currentPlayResultDynamicRowSize), 9, DefaultChannelClassification)
if err != nil {
t.Fatalf("build play-result upper: %v", err)
}
giveupGame, err := BuildChannelPacket(42, nil, 10, DefaultChannelClassification)
if err != nil {
t.Fatalf("build giveup-game upper: %v", err)
}
bossDieCheck, err := BuildChannelPacket(117, make([]byte, currentBossDieCheckPlainBodySize), 10, DefaultChannelClassification)
if err != nil {
t.Fatalf("build boss-die-check upper: %v", err)
}
characterStatistic, err := BuildChannelPacket(123, make([]byte, 24), 11, DefaultChannelClassification)
if err != nil {
t.Fatalf("build character-statistic upper: %v", err)
}
missionCheck, err := BuildChannelPacket(560, nil, 11, DefaultChannelClassification)
if err != nil {
t.Fatalf("build mission-check upper: %v", err)
}
backToVillage, err := BuildChannelPacket(132, nil, 12, DefaultChannelClassification)
if err != nil {
t.Fatalf("build return-to-village upper: %v", err)
}
tutorialFlag, err := BuildChannelPacket(143, []byte{0, 31, 0, 0, 0, 1}, 13, DefaultChannelClassification)
if err != nil {
t.Fatalf("build tutorial-flag upper: %v", err)
}
stream := append(append(append(append(append(append(append(append(append(append(append(append([]byte(nil), townMove...), death...), characterDeath...), useCoin...), move...), playResult...), giveupGame...), bossDieCheck...), characterStatistic...), tutorialFlag...), missionCheck...), backToVillage...)
packets, remaining, skipped, err := SplitLatestGameStream(stream, 4096)
if err != nil {
t.Fatalf("split dungeon stream: %v", err)
}
if skipped != 0 || len(remaining) != 0 || len(packets) != 12 {
t.Fatalf("split dungeon stream packets=%d skipped=%d remaining=%d", len(packets), skipped, len(remaining))
}
for index, packet := range packets {
if packet.Kind != LatestGameStreamUpper {
t.Fatalf("packet[%d] kind=%d, want upper", index, packet.Kind)
}
}
}
func TestKnownCurrentDungeonUpperRejectsUnprovenBoundaries(t *testing.T) {
if !isKnownClientUpperMsg(39, make([]byte, 62)) {
t.Fatal("fixed 62-byte op39 rejected")
}
variable := make([]byte, 86)
variable[22] = 2
if !isKnownClientUpperMsg(39, variable) {
t.Fatal("variable 86-byte op39 rejected")
}
variable[22] = 3
if isKnownClientUpperMsg(39, variable) {
t.Fatal("op39 count/body mismatch accepted")
}
if !isKnownClientUpperMsg(132, nil) {
t.Fatal("bodyless op132 rejected")
}
if !isKnownClientUpperMsg(36, make([]byte, currentTownSetUserAreaPlainBodySize)) {
t.Fatal("exact 16-byte town op36 rejected")
}
if !isKnownClientUpperMsg(31, []byte{0x1f, 0x00, 0x49, 0x0c}) {
t.Fatal("exact accept-quest op31 rejected")
}
if !isKnownClientUpperMsg(42, nil) {
t.Fatal("bodyless op42 rejected")
}
if !isKnownClientUpperMsg(40, make([]byte, currentDieCharacterPlainBodySize)) {
t.Fatal("two-u16 op40 rejected")
}
if !isKnownClientUpperMsg(41, make([]byte, currentUseCoinPlainBodySize)) {
t.Fatal("single-u16 op41 rejected")
}
if !isKnownClientUpperMsg(123, make([]byte, 24)) {
t.Fatal("six-u32 op123 rejected")
}
for rows := 0; rows <= currentPlayResultMaximumDynamicRows; rows++ {
for _, bodyLen := range []int{
currentPlayResultBaseBodySize + rows*currentPlayResultDynamicRowSize,
currentPlayResultOptionalBodySize + rows*currentPlayResultDynamicRowSize,
} {
if !isKnownClientUpperMsg(46, make([]byte, bodyLen)) {
t.Fatalf("plaintext op46 rows=%d body_len=%d rejected", rows, bodyLen)
}
}
}
if !isKnownClientUpperMsg(117, make([]byte, currentBossDieCheckPlainBodySize)) {
t.Fatal("plaintext 39-byte op117 rejected")
}
if !isKnownClientUpperMsg(143, make([]byte, currentTutorialFlagPlainBodySize)) {
t.Fatal("plaintext six-byte op143 rejected")
}
if !isKnownClientUpperMsg(560, nil) {
t.Fatal("bodyless op560 rejected")
}
if isKnownClientUpperMsg(39, make([]byte, 63)) ||
isKnownClientUpperMsg(31, []byte{0x1f, 0x00, 0x49}) ||
isKnownClientUpperMsg(31, []byte{0x20, 0x00, 0x49, 0x0c}) ||
isKnownClientUpperMsg(36, make([]byte, currentTownSetUserAreaPlainBodySize-1)) ||
isKnownClientUpperMsg(36, make([]byte, currentTownSetUserAreaPlainBodySize+1)) ||
isKnownClientUpperMsg(45, make([]byte, currentDungeonMoveMapPlainBodySize-1)) ||
isKnownClientUpperMsg(45, make([]byte, currentDungeonMoveMapPlainBodySize+1)) ||
isKnownClientUpperMsg(45, make([]byte, 112)) ||
isKnownClientUpperMsg(40, make([]byte, currentDieCharacterPlainBodySize-1)) ||
isKnownClientUpperMsg(40, make([]byte, currentDieCharacterPlainBodySize+1)) ||
isKnownClientUpperMsg(41, make([]byte, currentUseCoinPlainBodySize-1)) ||
isKnownClientUpperMsg(41, make([]byte, currentUseCoinPlainBodySize+1)) ||
isKnownClientUpperMsg(46, make([]byte, currentPlayResultBaseBodySize-1)) ||
isKnownClientUpperMsg(46, make([]byte, currentPlayResultBaseBodySize+1)) ||
isKnownClientUpperMsg(46, make([]byte, currentPlayResultOptionalBodySize+currentPlayResultMaximumDynamicRows*currentPlayResultDynamicRowSize+1)) ||
isKnownClientUpperMsg(117, make([]byte, currentBossDieCheckPlainBodySize-1)) ||
isKnownClientUpperMsg(117, make([]byte, currentBossDieCheckPlainBodySize+1)) ||
isKnownClientUpperMsg(143, make([]byte, currentTutorialFlagPlainBodySize-1)) ||
isKnownClientUpperMsg(143, make([]byte, 16)) ||
isKnownClientUpperMsg(42, []byte{0}) ||
isKnownClientUpperMsg(123, make([]byte, 23)) ||
isKnownClientUpperMsg(123, make([]byte, 25)) ||
isKnownClientUpperMsg(132, []byte{0}) ||
isKnownClientUpperMsg(560, []byte{0}) {
t.Fatal("unproven dungeon upper boundary accepted")
}
}
func TestSplitLatestGameStreamClassifiesExactAcceptQuestAsUpper(t *testing.T) {
packet, err := BuildChannelPacket(31, []byte{0x1f, 0x00, 0x49, 0x0c}, 34, DefaultChannelClassification)
if err != nil {
t.Fatal(err)
}
packets, remaining, skipped, err := SplitLatestGameStream(packet, 1024)
if err != nil {
t.Fatal(err)
}
if skipped != 0 || len(remaining) != 0 || len(packets) != 1 {
t.Fatalf("op31 split packets=%d skipped=%d remaining=%d", len(packets), skipped, len(remaining))
}
if packets[0].Kind != LatestGameStreamUpper {
t.Fatalf("op31 kind=%d, want upper", packets[0].Kind)
}
parsed, err := ParseChannelPacketUnchecked(packets[0].Data)
if err != nil {
t.Fatal(err)
}
if parsed.Header.MsgID != 31 || !bytes.Equal(parsed.Body, []byte{0x1f, 0x00, 0x49, 0x0c}) {
t.Fatalf("op31 packet header=%+v body=%x", parsed.Header, parsed.Body)
}
}
func TestSplitLatestGameStreamClassifiesExactSetQuestTriggerAsUpper(t *testing.T) {
body := []byte{0x21, 0x00, 0x78, 0x12, 0x00, 0x00}
packet, err := BuildChannelPacket(33, body, 35, DefaultChannelClassification)
if err != nil {
t.Fatal(err)
}
packets, remaining, skipped, err := SplitLatestGameStream(packet, 1024)
if err != nil {
t.Fatal(err)
}
if skipped != 0 || len(remaining) != 0 || len(packets) != 1 || packets[0].Kind != LatestGameStreamUpper {
t.Fatalf("op33 split packets=%d skipped=%d remaining=%d kind=%d", len(packets), skipped, len(remaining), packets[0].Kind)
}
parsed, err := ParseChannelPacketUnchecked(packets[0].Data)
if err != nil {
t.Fatal(err)
}
if parsed.Header.MsgID != 33 || !bytes.Equal(parsed.Body, body) {
t.Fatalf("op33 packet header=%+v body=%x", parsed.Header, parsed.Body)
}
}
func TestSplitLatestGameStreamKeepsSetQuestTriggerCloneTrailerLegacy(t *testing.T) {
body := []byte{0x21, 0x00, 0x78, 0x12, 0x00, 0x00, 0xaa, 0xbb, 0xcc, 0xdd}
packet, err := BuildChannelPacket(33, body, 36, DefaultChannelClassification)
if err != nil {
t.Fatal(err)
}
packets, remaining, skipped, err := SplitLatestGameStream(packet, 1024)
if err != nil {
t.Fatal(err)
}
if skipped != 0 || len(remaining) != 0 || len(packets) != 1 || packets[0].Kind != LatestGameStreamLegacy {
t.Fatalf("op33 clone split packets=%d skipped=%d remaining=%d kind=%d", len(packets), skipped, len(remaining), packets[0].Kind)
}
}
func TestSplitLatestGameStreamClassifiesBodylessMissionCheckAsUpperNotLegacy(t *testing.T) {
packet, err := BuildChannelPacket(560, nil, 12, DefaultChannelClassification)
if err != nil {
t.Fatal(err)
}
packets, remaining, skipped, err := SplitLatestGameStream(packet, 1024)
if err != nil {
t.Fatal(err)
}
if skipped != 0 || len(remaining) != 0 || len(packets) != 1 {
t.Fatalf("op560 split packets=%d skipped=%d remaining=%d", len(packets), skipped, len(remaining))
}
if packets[0].Kind != LatestGameStreamUpper {
t.Fatalf("op560 kind=%d want upper", packets[0].Kind)
}
}
func TestSplitLatestGameStreamKeepsProtectedFortyByteBossDieCheckLegacy(t *testing.T) {
packet := buildLegacyGamePacketForTest(1, 117, 8, make([]byte, 40))
packets, remaining, skipped, err := SplitLatestGameStream(packet, 1024)
if err != nil {
t.Fatalf("split protected boss-die-check: %v", err)
}
if skipped != 0 || len(remaining) != 0 || len(packets) != 1 {
t.Fatalf("split protected boss-die-check packets=%d skipped=%d remaining=%d", len(packets), skipped, len(remaining))
}
if packets[0].Kind != LatestGameStreamLegacy {
t.Fatalf("protected 40-byte op117 kind=%d want legacy", packets[0].Kind)
}
parsed, err := ParseLegacyGamePacket(packets[0].Data)
if err != nil {
t.Fatal(err)
}
if parsed.Header.Type != 117 || len(parsed.Body) != 40 {
t.Fatalf("protected op117 type/body=%d/%d", parsed.Header.Type, len(parsed.Body))
}
}
func TestSplitLatestGameStreamWaitsForPartialUpper(t *testing.T) {
upper, err := BuildChannelPacket(4, []byte{1, 0}, 7, DefaultChannelClassification)
if err != nil {
t.Fatalf("build upper packet: %v", err)
}
packets, remaining, skipped, err := SplitLatestGameStream(upper[:len(upper)-1], 1024)
if err != nil {
t.Fatalf("split partial upper: %v", err)
}
if len(packets) != 0 || skipped != 0 {
t.Fatalf("partial upper should wait, packets=%d skipped=%d", len(packets), skipped)
}
if len(remaining) != len(upper)-1 {
t.Fatalf("remaining = %d, want %d", len(remaining), len(upper)-1)
}
}
func TestSplitLatestGameStreamAcceptsLegacyGamePackets(t *testing.T) {
first := buildLegacyGamePacketForTest(1, 0x00ab, 1, []byte{1, 2, 3})
second := buildLegacyGamePacketForTest(1, 0x01dc, 2, []byte{4, 5})
stream := append(append([]byte(nil), first...), second...)
packets, remaining, skipped, err := SplitLatestGameStream(stream, 1024)
if err != nil {
t.Fatalf("split stream: %v", err)
}
if skipped != 0 || len(remaining) != 0 {
t.Fatalf("unexpected split state skipped=%d remaining=%d", skipped, len(remaining))
}
if len(packets) != 2 {
t.Fatalf("packet count = %d, want 2", len(packets))
}
if packets[0].Kind != LatestGameStreamLegacy || packets[1].Kind != LatestGameStreamLegacy {
t.Fatalf("packet kinds = %d/%d, want legacy/legacy", packets[0].Kind, packets[1].Kind)
}
parsed, err := ParseLegacyGamePacket(packets[0].Data)
if err != nil {
t.Fatalf("parse legacy packet: %v", err)
}
if parsed.Header.Type != 0x00ab || len(parsed.Body) != 3 {
t.Fatalf("parsed legacy packet type=0x%04x body=%d", parsed.Header.Type, len(parsed.Body))
}
}
func TestSplitLatestGameStreamAcceptsPostCreateUpperChain(t *testing.T) {
for _, msgID := range []uint16{0x02b1, 0x02b2, 0x02b5, 0x000f, 0x0010, 0x0007, 0x02a7} {
upper, err := BuildChannelPacket(msgID, []byte{1, 2, 3, 4}, 7, DefaultChannelClassification)
if err != nil {
t.Fatalf("build upper packet 0x%04x: %v", msgID, err)
}
packets, remaining, skipped, err := SplitLatestGameStream(upper, 1024)
if err != nil {
t.Fatalf("split upper 0x%04x: %v", msgID, err)
}
if skipped != 0 || len(remaining) != 0 || len(packets) != 1 {
t.Fatalf("split upper 0x%04x state packets=%d skipped=%d remaining=%d", msgID, len(packets), skipped, len(remaining))
}
if packets[0].Kind != LatestGameStreamUpper {
t.Fatalf("upper 0x%04x kind = %d, want upper", msgID, packets[0].Kind)
}
}
}
func TestSplitLatestGameStreamAcceptsObservedInitUpperChain(t *testing.T) {
cases := []struct {
msgID uint16
bodyLen int
}{
{msgID: 171, bodyLen: 48},
{msgID: 476, bodyLen: 16},
{msgID: 477, bodyLen: 8},
{msgID: 1320, bodyLen: 32},
{msgID: 1262, bodyLen: 32},
{msgID: 593, bodyLen: 24},
{msgID: 8, bodyLen: 8},
{msgID: 415, bodyLen: 0},
{msgID: 441, bodyLen: 8},
{msgID: 645, bodyLen: 0},
{msgID: 1276, bodyLen: 0},
{msgID: 1518, bodyLen: 96},
{msgID: 1518, bodyLen: 112},
{msgID: 1518, bodyLen: 152},
{msgID: 1516, bodyLen: 32},
{msgID: 1516, bodyLen: 48},
{msgID: 1516, bodyLen: 64},
{msgID: 1516, bodyLen: 80},
}
for _, tc := range cases {
upper, err := BuildChannelPacket(tc.msgID, make([]byte, tc.bodyLen), 7, DefaultChannelClassification)
if err != nil {
t.Fatalf("build observed upper %d: %v", tc.msgID, err)
}
packets, remaining, skipped, err := SplitLatestGameStream(upper, 1024)
if err != nil {
t.Fatalf("split observed upper %d: %v", tc.msgID, err)
}
if skipped != 0 || len(remaining) != 0 || len(packets) != 1 {
t.Fatalf("split observed upper %d state packets=%d skipped=%d remaining=%d", tc.msgID, len(packets), skipped, len(remaining))
}
if packets[0].Kind != LatestGameStreamUpper {
t.Fatalf("observed upper %d kind = %d, want upper", tc.msgID, packets[0].Kind)
}
}
}
func TestSplitLatestGameStreamAcceptsObservedPostMsg1UpperChain(t *testing.T) {
cases := []struct {
msgID uint16
bodyLen int
}{
{msgID: 194, bodyLen: 368},
{msgID: 1286, bodyLen: 16},
{msgID: 1287, bodyLen: 8},
{msgID: 250, bodyLen: 64},
{msgID: 279, bodyLen: 16},
{msgID: 593, bodyLen: 8},
{msgID: 3, bodyLen: 16},
}
for _, tc := range cases {
upper, err := BuildChannelPacket(tc.msgID, make([]byte, tc.bodyLen), 7, DefaultChannelClassification)
if err != nil {
t.Fatalf("build post-msg1 upper %d: %v", tc.msgID, err)
}
packets, remaining, skipped, err := SplitLatestGameStream(upper, 1024)
if err != nil {
t.Fatalf("split post-msg1 upper %d: %v", tc.msgID, err)
}
if skipped != 0 || len(remaining) != 0 || len(packets) != 1 {
t.Fatalf("split post-msg1 upper %d state packets=%d skipped=%d remaining=%d", tc.msgID, len(packets), skipped, len(remaining))
}
if packets[0].Kind != LatestGameStreamUpper {
t.Fatalf("post-msg1 upper %d kind = %d, want upper", tc.msgID, packets[0].Kind)
}
}
}
func TestSplitLatestGameStreamAcceptsCodecUpperWithoutChecksum(t *testing.T) {
raw := mustHex(t, "01b2021d00000038ab3cdc000015b101e6d58515bd9dbb3490ac32e75a")
packets, remaining, skipped, err := SplitLatestGameStream(raw, 1024)
if err != nil {
t.Fatalf("split codec upper: %v", err)
}
if skipped != 0 || len(remaining) != 0 || len(packets) != 1 {
t.Fatalf("split state packets=%d skipped=%d remaining=%d", len(packets), skipped, len(remaining))
}
if packets[0].Kind != LatestGameStreamUpper {
t.Fatalf("packet kind = %d, want upper", packets[0].Kind)
}
}
func TestSplitLatestGameStreamCutsHookedShortCreateBeforeHeartbeat(t *testing.T) {
create := mustHex(t, "010500250000009585a5c916000008000000b8d5b8d5b7a2b8f8000000000000ff00")
heartbeat, err := BuildChannelPacket(1276, nil, 23, DefaultChannelClassification)
if err != nil {
t.Fatalf("build heartbeat upper: %v", err)
}
stream := append(append([]byte(nil), create...), heartbeat...)
packets, remaining, skipped, err := SplitLatestGameStream(stream, 1024)
if err != nil {
t.Fatalf("split hooked create stream: %v", err)
}
if skipped != 0 || len(remaining) != 0 {
t.Fatalf("unexpected split state skipped=%d remaining=%x", skipped, remaining)
}
if len(packets) != 2 {
t.Fatalf("packet count = %d, want 2", len(packets))
}
first, err := ParseChannelPacketUnchecked(packets[0].Data)
if err != nil {
t.Fatalf("parse create upper: %v", err)
}
second, err := ParseChannelPacketUnchecked(packets[1].Data)
if err != nil {
t.Fatalf("parse heartbeat upper: %v", err)
}
if first.Header.MsgID != 5 || len(first.Body) != 21 {
t.Fatalf("first packet msg/body = %d/%d", first.Header.MsgID, len(first.Body))
}
if second.Header.MsgID != 1276 || len(second.Body) != 0 {
t.Fatalf("second packet msg/body = %d/%d", second.Header.MsgID, len(second.Body))
}
}
func TestSplitLatestGameStreamCutsCurrentHookedShortCreateBeforeHeartbeat(t *testing.T) {
create := mustHex(t, "010500250000003514fb8014000106000000b7a2b7a2b7a2000000000000ff00")
heartbeat, err := BuildChannelPacket(1276, nil, 21, DefaultChannelClassification)
if err != nil {
t.Fatalf("build heartbeat upper: %v", err)
}
stream := append(append([]byte(nil), create...), heartbeat...)
packets, remaining, skipped, err := SplitLatestGameStream(stream, 1024)
if err != nil {
t.Fatalf("split current hooked create stream: %v", err)
}
if skipped != 0 || len(remaining) != 0 {
t.Fatalf("unexpected split state skipped=%d remaining=%x", skipped, remaining)
}
if len(packets) != 2 {
t.Fatalf("packet count = %d, want 2", len(packets))
}
first, err := ParseChannelPacketUnchecked(packets[0].Data)
if err != nil {
t.Fatalf("parse create upper: %v", err)
}
second, err := ParseChannelPacketUnchecked(packets[1].Data)
if err != nil {
t.Fatalf("parse heartbeat upper: %v", err)
}
if first.Header.MsgID != 5 || len(first.Body) != 19 {
t.Fatalf("first packet msg/body = %d/%d", first.Header.MsgID, len(first.Body))
}
if second.Header.MsgID != 1276 || len(second.Body) != 0 {
t.Fatalf("second packet msg/body = %d/%d", second.Header.MsgID, len(second.Body))
}
}
func TestSplitLatestGameStreamClassifiesExactLengthShortCreateAsUpper(t *testing.T) {
body := mustHex(t, "0003000000363636000000000000ff00")
create, err := BuildChannelPacket(5, body, 20, DefaultChannelClassification)
if err != nil {
t.Fatalf("build exact-length create upper: %v", err)
}
packets, remaining, skipped, err := SplitLatestGameStream(create, 1024)
if err != nil {
t.Fatalf("split exact-length create upper: %v", err)
}
if skipped != 0 || len(remaining) != 0 {
t.Fatalf("unexpected split state skipped=%d remaining=%x", skipped, remaining)
}
if len(packets) != 1 {
t.Fatalf("packet count = %d, want 1", len(packets))
}
if packets[0].Kind != LatestGameStreamUpper {
t.Fatalf("packet kind = %d, want upper", packets[0].Kind)
}
parsed, err := ParseChannelPacketUnchecked(packets[0].Data)
if err != nil {
t.Fatalf("parse exact-length create upper: %v", err)
}
if parsed.Header.MsgID != 5 || !bytes.Equal(parsed.Body, body) {
t.Fatalf("create packet msg/body = %d/%x, want 5/%x", parsed.Header.MsgID, parsed.Body, body)
}
}
func TestSplitLatestGameStreamCutsCurrentHookedSelectBeforeFollowingLegacyPacket(t *testing.T) {
selectDungeon := mustHex(t, "0110002d000000a46efcb81800e91b00000000000000ffff00000000000000000000")
changeTutorial := mustHex(t, "018f001d00000064d69b8b19004cfb5fc3405889ac4e528f72fe7249ea")
stream := append(append([]byte(nil), selectDungeon...), changeTutorial...)
packets, remaining, skipped, err := SplitLatestGameStream(stream, 1024)
if err != nil {
t.Fatalf("split hooked select stream: %v", err)
}
if skipped != 0 || len(remaining) != 0 {
t.Fatalf("unexpected split state skipped=%d remaining=%x", skipped, remaining)
}
if len(packets) != 2 {
t.Fatalf("packet count = %d, want 2", len(packets))
}
first, err := ParseChannelPacketUnchecked(packets[0].Data)
if err != nil {
t.Fatalf("parse select upper: %v", err)
}
second, err := ParseLegacyGamePacket(packets[1].Data)
if err != nil {
t.Fatalf("parse following legacy packet: %v", err)
}
if first.Header.MsgID != 16 || len(first.Body) != 21 {
t.Fatalf("first packet msg/body = %d/%d", first.Header.MsgID, len(first.Body))
}
if second.Header.Type != 143 || len(second.Body) != 16 {
t.Fatalf("second packet type/body = %d/%d", second.Header.Type, len(second.Body))
}
}
func TestSplitLatestGameStreamPrefersLegacyForUpperLikeCommand(t *testing.T) {
packet := buildLegacyGamePacketForTest(1, 0x0301, 8, nil)
packets, remaining, skipped, err := SplitLatestGameStream(packet, 1024)
if err != nil {
t.Fatalf("split stream: %v", err)
}
if skipped != 0 || len(remaining) != 0 {
t.Fatalf("unexpected split state skipped=%d remaining=%d", skipped, len(remaining))
}
if len(packets) != 1 {
t.Fatalf("packet count = %d, want 1", len(packets))
}
if packets[0].Kind != LatestGameStreamLegacy {
t.Fatalf("packet kind = %d, want legacy", packets[0].Kind)
}
parsed, err := ParseLegacyGamePacket(packets[0].Data)
if err != nil {
t.Fatalf("parse legacy packet: %v", err)
}
if parsed.Header.Type != 0x0301 {
t.Fatalf("legacy type = 0x%04x, want 0x0301", parsed.Header.Type)
}
}
func TestSplitLatestGameStreamKeepsLegacyGetUserInfoWithoutUpperBody(t *testing.T) {
packet := buildLegacyGamePacketForTest(1, 8, 8, nil)
packets, remaining, skipped, err := SplitLatestGameStream(packet, 1024)
if err != nil {
t.Fatalf("split stream: %v", err)
}
if skipped != 0 || len(remaining) != 0 {
t.Fatalf("unexpected split state skipped=%d remaining=%d", skipped, len(remaining))
}
if len(packets) != 1 {
t.Fatalf("packet count = %d, want 1", len(packets))
}
if packets[0].Kind != LatestGameStreamLegacy {
t.Fatalf("packet kind = %d, want legacy", packets[0].Kind)
}
}
func buildLegacyGamePacketForTest(cmd byte, typ uint16, seq uint16, body []byte) []byte {
packet := make([]byte, LegacyGameHeaderSize+len(body))
packet[0] = cmd
packet[1] = byte(typ)
packet[2] = byte(typ >> 8)
length := uint32(len(packet))
packet[3] = byte(length)
packet[4] = byte(length >> 8)
packet[5] = byte(length >> 16)
packet[6] = byte(length >> 24)
packet[11] = byte(seq)
packet[12] = byte(seq >> 8)
copy(packet[LegacyGameHeaderSize:], body)
return packet
}