服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
604 lines
21 KiB
Go
604 lines
21 KiB
Go
package party
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"longheng.io/server/internal/modules/dnf/alignedcmd"
|
|
"longheng.io/server/internal/modules/dnf/dnfenum"
|
|
dnfproto "longheng.io/server/internal/modules/dnf/protocol"
|
|
)
|
|
|
|
func TestDecodeSetPartyInfoRequestCurrentPresetLayout(t *testing.T) {
|
|
body := []byte{
|
|
0x00, 0x05,
|
|
0x04,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x01,
|
|
0x00, 0x00,
|
|
0x00,
|
|
0x01,
|
|
0xe9, 0x12,
|
|
}
|
|
got, err := DecodeSetPartyInfoRequest(body)
|
|
if err != nil {
|
|
t.Fatalf("DecodeSetPartyInfoRequest error = %v", err)
|
|
}
|
|
if got.Prefix0 != 0 || got.Prefix1 != 5 ||
|
|
got.MemberSelectCode != 4 || got.MaxMembers != 4 ||
|
|
got.SelectionID != 0 || got.SelectionCode != 1 || got.SelectionValue != 0 ||
|
|
got.RecruitFlag != 0 || got.TargetMode != 1 || got.TargetDungeonID != 0x12e9 {
|
|
t.Fatalf("decoded preset request = %+v", got)
|
|
}
|
|
if len(got.NameBytes) == 0 {
|
|
t.Fatalf("preset name was not resolved")
|
|
}
|
|
}
|
|
|
|
func TestDecodeSetPartyInfoRequestCurrentSpecialChannelLayout(t *testing.T) {
|
|
body := []byte{
|
|
0x00, 0x02,
|
|
0x04,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x05,
|
|
0x00, 0x00,
|
|
0x00,
|
|
0x00,
|
|
0x00, 0x00,
|
|
}
|
|
got, err := DecodeSetPartyInfoRequest(body)
|
|
if err != nil {
|
|
t.Fatalf("DecodeSetPartyInfoRequest error = %v", err)
|
|
}
|
|
if got.SelectionCode != 5 || got.TargetMode != 0 || got.TargetDungeonID != 0 {
|
|
t.Fatalf("decoded special-channel request = %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestDecodeSetPartyInfoRequestCurrentCustomNameLayout(t *testing.T) {
|
|
body := []byte{
|
|
0x00, 0x00,
|
|
0x03, 0x00, 0x00, 0x00,
|
|
'a', 'b', 'c',
|
|
0x04,
|
|
0x44, 0x33, 0x22, 0x11,
|
|
0x02,
|
|
0x66, 0x55,
|
|
0x01,
|
|
0x02,
|
|
0xaa, 0x99,
|
|
}
|
|
got, err := DecodeSetPartyInfoRequest(body)
|
|
if err != nil {
|
|
t.Fatalf("DecodeSetPartyInfoRequest error = %v", err)
|
|
}
|
|
if string(got.NameBytes) != "abc" || got.SelectionID != 0x11223344 ||
|
|
got.SelectionCode != 2 || got.SelectionValue != 0x5566 ||
|
|
got.RecruitFlag != 1 || got.TargetMode != 2 || got.TargetDungeonID != 0x99aa {
|
|
t.Fatalf("decoded custom-name request = %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestDecodeSetPartyInfoRequestRejectsLegacyOrTrailingLayout(t *testing.T) {
|
|
for _, body := range [][]byte{
|
|
{0, 3, 4, 0x9c, 0, 5, 0},
|
|
{0, 3, 4, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0x9c, 0, 0},
|
|
} {
|
|
if _, err := DecodeSetPartyInfoRequest(body); err == nil {
|
|
t.Fatalf("DecodeSetPartyInfoRequest accepted invalid layout: % X", body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildCurrentPartyStateSupportsFourRealMembers(t *testing.T) {
|
|
state := alignedcmd.PartyState{
|
|
PartyID: 1,
|
|
UserID: 1001,
|
|
NameBytes: []byte("party"),
|
|
MaxMembers: 4,
|
|
TargetDungeonID: 0x009c,
|
|
Members: []alignedcmd.PartyMemberState{
|
|
{UserID: 1001, Slot: 0, UserState: 1, HPPercent: 100, MPPercent: 100},
|
|
{UserID: 1002, Slot: 1, UserState: 1, HPPercent: 90, MPPercent: 80},
|
|
{UserID: 1003, Slot: 2, UserState: 1, HPPercent: 70, MPPercent: 60},
|
|
{UserID: 1004, Slot: 3, UserState: 1, HPPercent: 50, MPPercent: 40},
|
|
},
|
|
}
|
|
|
|
realtime := BuildSingleMemberRealtimeInfo(state)
|
|
if realtime[0] != 4 {
|
|
t.Fatalf("realtime count = %d, want 4", realtime[0])
|
|
}
|
|
for i, want := range []uint16{1001, 1002, 1003, 1004} {
|
|
offset := 1 + i*5
|
|
if got := binary.LittleEndian.Uint16(realtime[offset : offset+2]); got != want {
|
|
t.Fatalf("realtime member %d userID = %d, want %d", i, got, want)
|
|
}
|
|
// +2 is the HP percentage, byte-identical to the member's own value
|
|
// (0 falls back to full health, exactly like the pre-S1 behaviour).
|
|
if got, want := realtime[offset+2], []byte{100, 90, 70, 50}[i]; got != want {
|
|
t.Fatalf("realtime member %d hpPercent = %d, want %d", i, got, want)
|
|
}
|
|
if got := realtime[offset+3]; got != 0 {
|
|
t.Fatalf("realtime member %d helper flag = %d, want 0", i, got)
|
|
}
|
|
// +4 is the native party slot, so the four rows must stay distinct.
|
|
if got := realtime[offset+4]; got != byte(i) {
|
|
t.Fatalf("realtime member %d slot = %d, want %d", i, got, byte(i))
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestBuildSingleMemberRealtimeInfoCarriesHpPercentAndSlot pins the two bytes
|
|
// S1 corrected against df_game_r CParty::get_party_realtime_info @0x0859CBAC:
|
|
// +2 = HP percent, +4 = native slot index. A slot left unset by the caller is
|
|
// filled with the first free slot so a multi-member body never repeats slot 0.
|
|
func TestBuildSingleMemberRealtimeInfoCarriesHpPercentAndSlot(t *testing.T) {
|
|
state := alignedcmd.PartyState{
|
|
PartyID: 7,
|
|
UserID: 1001,
|
|
MaxMembers: 4,
|
|
Members: []alignedcmd.PartyMemberState{
|
|
{UserID: 1001, Slot: 2, UserState: 1, HPPercent: 90, MPPercent: 90},
|
|
{UserID: 1002, UserState: 1, HPPercent: 100, MPPercent: 100},
|
|
},
|
|
}
|
|
got := BuildSingleMemberRealtimeInfo(state)
|
|
want := []byte{2, 0xe9, 0x03, 90, 0, 2, 0xea, 0x03, 100, 0, 0}
|
|
if !bytes.Equal(got, want) {
|
|
t.Fatalf("realtime body = % X, want % X", got, want)
|
|
}
|
|
if full := BuildSingleMemberRealtimeInfo(alignedcmd.PartyState{
|
|
PartyID: 7, UserID: 1001, MaxMembers: 4,
|
|
Members: []alignedcmd.PartyMemberState{{UserID: 1001, UserState: 1}},
|
|
}); !bytes.Equal(full, []byte{1, 0xe9, 0x03, 100, 0, 0}) {
|
|
t.Fatalf("single-member body = % X, want 01 E9 03 64 00 00", full)
|
|
}
|
|
}
|
|
|
|
func TestBuildPeerEndpointInfoCurrentTwentyTwoByteMemberLayout(t *testing.T) {
|
|
got := BuildPeerEndpointInfo([]PeerEndpoint{
|
|
{UserID: 1001, InnerIPv4: [4]byte{192, 168, 1, 10}, OuterIPv4: [4]byte{192, 168, 1, 10}, Port: 10000, RoutingKey: 11, PathMTU: 512, UserState: 100},
|
|
{UserID: 1002, InnerIPv4: [4]byte{127, 0, 0, 1}, OuterIPv4: [4]byte{127, 0, 0, 1}, Port: 2311, RoutingKey: 22, NATType: 1, PathMTU: 1472, UserState: 100},
|
|
})
|
|
if len(got) != 1+2*partyPeerEndpointSize {
|
|
t.Fatalf("peer endpoint body len=%d want=%d body=%x", len(got), 1+2*partyPeerEndpointSize, got)
|
|
}
|
|
want := []byte{
|
|
2,
|
|
0xe9, 0x03,
|
|
192, 168, 1, 10,
|
|
192, 168, 1, 10,
|
|
0x27, 0x10,
|
|
0x0b, 0x00, 0x00, 0x00,
|
|
0,
|
|
0x00, 0x02, 0x00, 0x00,
|
|
100,
|
|
0xea, 0x03,
|
|
127, 0, 0, 1,
|
|
127, 0, 0, 1,
|
|
0x09, 0x07,
|
|
0x16, 0x00, 0x00, 0x00,
|
|
1,
|
|
0xc0, 0x05, 0x00, 0x00,
|
|
100,
|
|
}
|
|
if !bytes.Equal(got, want) {
|
|
t.Fatalf("peer endpoint body=%x want=%x", got, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildDirectorySnapshotCurrentOp87Layout(t *testing.T) {
|
|
got := BuildDirectorySnapshot([]DirectoryRecord{
|
|
{
|
|
PartyID: 1001,
|
|
SelectionID: 0x12e9,
|
|
MemberIDs: []uint16{1001, 1002},
|
|
},
|
|
})
|
|
if len(got) != 2+28 {
|
|
t.Fatalf("directory body len = %d, want 30", len(got))
|
|
}
|
|
wantPrefix := []byte{
|
|
1, 0,
|
|
0xe9, 0x03,
|
|
0,
|
|
2,
|
|
0,
|
|
0xff,
|
|
0xe9, 0x12, 0, 0,
|
|
0, 0xe9, 0x03,
|
|
0, 0xea, 0x03,
|
|
}
|
|
if !reflect.DeepEqual(got[:len(wantPrefix)], wantPrefix) {
|
|
t.Fatalf("directory prefix = % X, want % X", got[:len(wantPrefix)], wantPrefix)
|
|
}
|
|
for offset := len(wantPrefix); offset < len(got); offset += 3 {
|
|
if !reflect.DeepEqual(got[offset:offset+3], []byte{0xff, 0xff, 0xff}) {
|
|
t.Fatalf("directory empty slot at %d = % X", offset, got[offset:offset+3])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildDirectorySnapshotEmpty(t *testing.T) {
|
|
if got := BuildDirectorySnapshot(nil); !reflect.DeepEqual(got, []byte{0, 0}) {
|
|
t.Fatalf("empty directory body = % X, want 00 00", got)
|
|
}
|
|
}
|
|
|
|
func TestSetPartyInfoDefersCurrentPartyFrameProjectionToBridge(t *testing.T) {
|
|
state := alignedcmd.PartyState{}
|
|
got, err := NewHandler().Handle(context.Background(), alignedcmd.Request{
|
|
Opcode: uint16(dnfenum.CmdPacketSetPartyInfo),
|
|
Body: []byte{0, 3, 4, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0x9c, 0},
|
|
SelectedCharacterID: 1001,
|
|
Party: &state,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Handle(SetPartyInfo) error = %v", err)
|
|
}
|
|
if !got.Handled || !got.ResponseAllowed {
|
|
t.Fatalf("handled=%t responseAllowed=%t", got.Handled, got.ResponseAllowed)
|
|
}
|
|
if len(got.UpperResponses) != 1 {
|
|
t.Fatalf("response count = %d, want ACK before bridge projection", len(got.UpperResponses))
|
|
}
|
|
for _, response := range got.UpperResponses {
|
|
if response.Classification == 0 && response.MsgID == 0x0009 {
|
|
t.Fatalf("unsafe class0/op9 scene record emitted: % X", response.Body)
|
|
}
|
|
}
|
|
ack := got.UpperResponses[0]
|
|
if ack.Classification != dnfproto.DefaultChannelClassification ||
|
|
ack.MsgID != uint16(dnfenum.CmdPacketSetPartyInfo) ||
|
|
!reflect.DeepEqual(ack.Body, []byte{1}) {
|
|
t.Fatalf("ack = class %d msg %d body % X", ack.Classification, ack.MsgID, ack.Body)
|
|
}
|
|
if !reflect.DeepEqual(got.PostActions, []alignedcmd.PostAction{alignedcmd.PostActionRefreshSelectedPartyFrame}) {
|
|
t.Fatalf("post actions = %v, want party frame refresh", got.PostActions)
|
|
}
|
|
if state.PartyID != 1001 {
|
|
t.Fatalf("party ID = %d, want current leader character ID 1001", state.PartyID)
|
|
}
|
|
}
|
|
|
|
func TestQuickPartyAckOnlyCommands(t *testing.T) {
|
|
h := NewHandler()
|
|
for _, opcode := range []dnfenum.CmdPacket{
|
|
dnfenum.CmdPacketCancelQuickParty,
|
|
dnfenum.CmdPacketDirectEntranceDungeonQuickParty,
|
|
} {
|
|
t.Run(dnfenum.CmdPacketName(uint16(opcode)), func(t *testing.T) {
|
|
got, err := h.Handle(context.Background(), alignedcmd.Request{Opcode: uint16(opcode)})
|
|
if err != nil {
|
|
t.Fatalf("Handle(%d) error = %v", opcode, err)
|
|
}
|
|
if !got.Handled || !got.ResponseAllowed {
|
|
t.Fatalf("Handle(%d) handled=%v responseAllowed=%v", opcode, got.Handled, got.ResponseAllowed)
|
|
}
|
|
if len(got.UpperResponses) != 1 {
|
|
t.Fatalf("Handle(%d) response count = %d, want 1", opcode, len(got.UpperResponses))
|
|
}
|
|
resp := got.UpperResponses[0]
|
|
if resp.MsgID != uint16(opcode) || !reflect.DeepEqual(resp.Body, []byte{1}) {
|
|
t.Fatalf("Handle(%d) response msg=%d body=% X", opcode, resp.MsgID, resp.Body)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRegisterQuickPartyPending(t *testing.T) {
|
|
body := make([]byte, 4+5*2)
|
|
binary.LittleEndian.PutUint32(body[:4], 2)
|
|
binary.LittleEndian.PutUint32(body[4:8], 1001)
|
|
body[8] = 1
|
|
binary.LittleEndian.PutUint32(body[9:13], 1002)
|
|
body[13] = 0
|
|
|
|
got, err := NewHandler().Handle(context.Background(), alignedcmd.Request{
|
|
Opcode: uint16(dnfenum.CmdPacketRegisterQuickParty),
|
|
Body: body,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Handle(register quick party) error = %v", err)
|
|
}
|
|
if !got.Handled || got.ResponseAllowed || len(got.UpperResponses) != 0 {
|
|
t.Fatalf("register quick party handled=%v responseAllowed=%v responses=%d",
|
|
got.Handled, got.ResponseAllowed, len(got.UpperResponses))
|
|
}
|
|
}
|
|
|
|
func TestReserveLeavePartyAck(t *testing.T) {
|
|
state := alignedcmd.PartyState{}
|
|
got, err := NewHandler().Handle(context.Background(), alignedcmd.Request{
|
|
Opcode: uint16(dnfenum.CmdPacketReserveLeaveParty),
|
|
Body: []byte{1},
|
|
SelectedCharacterID: 1001,
|
|
Party: &state,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Handle(reserve leave party) error = %v", err)
|
|
}
|
|
if !got.Handled || !got.ResponseAllowed {
|
|
t.Fatalf("reserve leave party handled=%v responseAllowed=%v", got.Handled, got.ResponseAllowed)
|
|
}
|
|
if state.ReserveLeaveFlag != 1 {
|
|
t.Fatalf("ReserveLeaveFlag = %d, want 1", state.ReserveLeaveFlag)
|
|
}
|
|
if len(got.UpperResponses) != 1 {
|
|
t.Fatalf("response count = %d, want 1", len(got.UpperResponses))
|
|
}
|
|
want := []byte{1, 1, 0xe9, 0x03}
|
|
resp := got.UpperResponses[0]
|
|
if resp.MsgID != uint16(dnfenum.CmdPacketReserveLeaveParty) || !reflect.DeepEqual(resp.Body, want) {
|
|
t.Fatalf("reserve response msg=%d body=% X, want msg=%d body=% X",
|
|
resp.MsgID, resp.Body, dnfenum.CmdPacketReserveLeaveParty, want)
|
|
}
|
|
}
|
|
|
|
func TestEntryIntoPartyRequestAndAck(t *testing.T) {
|
|
body := make([]byte, 4)
|
|
binary.LittleEndian.PutUint32(body, 1002)
|
|
parsed, err := DecodeEntryIntoPartyRequest(body)
|
|
if err != nil {
|
|
t.Fatalf("DecodeEntryIntoPartyRequest error = %v", err)
|
|
}
|
|
if parsed.TargetID != 1002 {
|
|
t.Fatalf("target = %d, want 1002", parsed.TargetID)
|
|
}
|
|
want := []byte{1, 0xea, 0x03, 0x00, 0x00, 0xe9, 0x03, 0x00, 0x00}
|
|
if got := BuildEntryIntoPartyAck(1002, 1001); !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("entry ack = % X, want % X", got, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildAssaultInvite(t *testing.T) {
|
|
got := BuildAssaultInvite(1001, 13)
|
|
want := []byte{0xe9, 0x03, 13}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("quick party invite = % X, want % X", got, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildRequestPeerNoticesUseCurrentModeSpecificShapes(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
req RequestPeerRequest
|
|
stats PeerNoticeStats
|
|
want []byte
|
|
}{
|
|
{
|
|
// The ordinary party invite renders the same prompt widget as the
|
|
// raid application, so it writes the same three value rows and
|
|
// keeps value1 only as the 抗魔值 source. Its trailing u32 is the
|
|
// one thing this arm does not compute and stays the request's.
|
|
name: "party",
|
|
req: RequestPeerRequest{Mode: 0, Value0: 0x11223344, Value1: 0x5566, Value2: 0x778899aa},
|
|
stats: PeerNoticeStats{Fatigue: 156, Weakness: 100, Exorcism: 0x5566},
|
|
want: []byte{
|
|
0xe9, 0x03, 0,
|
|
0x44, 0x33, 0x22, 0x11, // 复活币 0x11223344
|
|
0x9c, 0, // 疲劳值 156
|
|
0x64, 0, // 虚弱状态 100 (healthy)
|
|
0x66, 0x55, // 抗魔值 0x5566
|
|
0xaa, 0x99, 0x88, 0x77, // value2, unchanged
|
|
},
|
|
},
|
|
{
|
|
name: "trade",
|
|
req: RequestPeerRequest{Mode: 1, Value0: 0x11223344, Value2: 0x55667788},
|
|
want: []byte{0xe9, 0x03, 1, 0x44, 0x33, 0x22, 0x11, 0x88, 0x77, 0x66, 0x55},
|
|
},
|
|
{
|
|
name: "quick party",
|
|
req: RequestPeerRequest{Mode: 13, Value0: 0x11223344},
|
|
want: []byte{0xe9, 0x03, 13, 0x44, 0x33, 0x22, 0x11},
|
|
},
|
|
{
|
|
// Raid admission. The reply tail is 疲劳值/虚弱状态/抗魔值, which is
|
|
// NOT the request tail (复活币/抗魔值/0): char 22 asked with
|
|
// Value1=2828 for a character whose stored fatigue was 156, so
|
|
// Value1 is 抗魔值 and the two middle rows must be supplied by the
|
|
// server. Offsets pinned on the wire by the 2026-09-26 marker
|
|
// probe -- 0x1111/0x2222/0x3333 at 7/9/11 rendered under exactly
|
|
// those three labels, and every marker past offset 11 was ignored.
|
|
name: "raid admission",
|
|
req: RequestPeerRequest{Mode: 10, Value0: 10, Value1: 2828},
|
|
stats: PeerNoticeStats{Fatigue: 156, Weakness: 100, Exorcism: 2828},
|
|
want: []byte{
|
|
0xe9, 0x03, 10,
|
|
0x0a, 0, 0, 0, // 复活币 10
|
|
0x9c, 0, // 疲劳值 156
|
|
0x64, 0, // 虚弱状态 100 (healthy)
|
|
0x0c, 0x0b, // 抗魔值 2828
|
|
},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if got := BuildRequestPeerNotice(1001, test.req, test.stats); !bytes.Equal(got, test.want) {
|
|
t.Fatalf("notice=% X want=% X", got, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildResponsePeerBodies(t *testing.T) {
|
|
if got, want := BuildResponsePeerAckPayload(1001, 0), []byte{0xe9, 0x03, 0}; !bytes.Equal(got, want) {
|
|
t.Fatalf("party ack payload=% X want=% X", got, want)
|
|
}
|
|
if got, want := BuildResponsePeerAckPayload(1001, 1), []byte{0xe9, 0x03, 1, 0}; !bytes.Equal(got, want) {
|
|
t.Fatalf("trade ack payload=% X want=% X", got, want)
|
|
}
|
|
if got, want := BuildResponsePeerNotice(1002, 1, 0), []byte{0xea, 0x03, 1, 0, 0, 0, 0}; !bytes.Equal(got, want) {
|
|
t.Fatalf("response notice=% X want=% X", got, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildWalkoutPartyMemberNotice(t *testing.T) {
|
|
got := BuildWalkoutPartyMemberNotice(1, 0)
|
|
want := []byte{1, 0}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("walkout party member notice = % X, want % X", got, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildChangePartyMemberPositionAck(t *testing.T) {
|
|
got := BuildChangePartyMemberPositionAck(1, 0)
|
|
want := []byte{1, 1, 0}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("change party member position ack = % X, want % X", got, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildRequestPeerAck(t *testing.T) {
|
|
if got := BuildRequestPeerAck(); !reflect.DeepEqual(got, []byte{1}) {
|
|
t.Fatalf("request peer ack = % X, want 01", got)
|
|
}
|
|
}
|
|
|
|
func TestBuildRequestPeerSelectionNotice(t *testing.T) {
|
|
partyless := BuildRequestPeerSelectionNotice(1002, 0x11223344, false)
|
|
wantPartyless := []byte{0xea, 0x03, 15, 0x44, 0x33, 0x22, 0x11, 0xff, 0xff}
|
|
if !reflect.DeepEqual(partyless, wantPartyless) {
|
|
t.Fatalf("partyless request peer selection notice = % X, want % X", partyless, wantPartyless)
|
|
}
|
|
active := BuildRequestPeerSelectionNotice(1002, 0x11223344, true)
|
|
wantActive := []byte{0xea, 0x03, 15, 0x44, 0x33, 0x22, 0x11, 0, 0}
|
|
if !reflect.DeepEqual(active, wantActive) {
|
|
t.Fatalf("active request peer selection notice = % X, want % X", active, wantActive)
|
|
}
|
|
}
|
|
|
|
func TestDecodeRequestPeerRequest(t *testing.T) {
|
|
body := []byte{
|
|
0xea, 0x03,
|
|
15,
|
|
0x44, 0x33, 0x22, 0x11,
|
|
0x66, 0x55,
|
|
0xaa, 0x99, 0x88, 0x77,
|
|
}
|
|
got, err := DecodeRequestPeerRequest(body)
|
|
if err != nil {
|
|
t.Fatalf("DecodeRequestPeerRequest error = %v", err)
|
|
}
|
|
if got.TargetID != 1002 || got.Mode != 15 ||
|
|
got.Value0 != 0x11223344 || got.Value1 != 0x5566 || got.Value2 != 0x778899aa {
|
|
t.Fatalf("request peer = %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestDecodeResponsePeerRequest(t *testing.T) {
|
|
got, err := DecodeResponsePeerRequest([]byte{0xea, 0x03, 13, 1, 0, 0, 0})
|
|
if err != nil {
|
|
t.Fatalf("DecodeResponsePeerRequest error = %v", err)
|
|
}
|
|
if got.TargetID != 1002 || got.Mode != 13 || got.Value != 1 {
|
|
t.Fatalf("response peer = %+v, want target=1002 mode=13 value=1", got)
|
|
}
|
|
}
|
|
|
|
func TestDecodeRequestPeerModeSpecificLengths(t *testing.T) {
|
|
partyBody := []byte{0xea, 0x03, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
|
partyReq, err := DecodeRequestPeerRequest(partyBody)
|
|
if err != nil || partyReq.TargetID != 1002 || partyReq.Mode != 0 || partyReq.Value1 != 0x0605 || partyReq.Value2 != 0x0a090807 {
|
|
t.Fatalf("party request=%+v err=%v", partyReq, err)
|
|
}
|
|
tradeBody := []byte{0xea, 0x03, 1, 1, 2, 3, 4, 5, 6, 7, 8}
|
|
tradeReq, err := DecodeRequestPeerRequest(tradeBody)
|
|
if err != nil || tradeReq.TargetID != 1002 || tradeReq.Mode != 1 || tradeReq.Value2 != 0x08070605 {
|
|
t.Fatalf("trade request=%+v err=%v", tradeReq, err)
|
|
}
|
|
}
|
|
|
|
func TestDecodeWalkoutPartyMemberRequest(t *testing.T) {
|
|
got, err := DecodeWalkoutPartyMemberRequest([]byte{1})
|
|
if err != nil {
|
|
t.Fatalf("DecodeWalkoutPartyMemberRequest error = %v", err)
|
|
}
|
|
if got.Slot != 1 {
|
|
t.Fatalf("slot = %d, want 1", got.Slot)
|
|
}
|
|
}
|
|
|
|
func TestDecodeChangePartyMemberPositionRequest(t *testing.T) {
|
|
got, err := DecodeChangePartyMemberPositionRequest([]byte{2, 3})
|
|
if err != nil {
|
|
t.Fatalf("DecodeChangePartyMemberPositionRequest error = %v", err)
|
|
}
|
|
if got.Slot != 2 || got.Position != 3 {
|
|
t.Fatalf("change position = %+v, want slot=2 position=3", got)
|
|
}
|
|
if _, err := DecodeChangePartyMemberPositionRequest([]byte{2, 2}); err == nil {
|
|
t.Fatalf("DecodeChangePartyMemberPositionRequest accepted invalid position")
|
|
}
|
|
}
|
|
|
|
func TestPartyFixedRequestDecodersRejectTrailingBytes(t *testing.T) {
|
|
quick := make([]byte, 10)
|
|
binary.LittleEndian.PutUint32(quick[:4], 1)
|
|
tests := []struct {
|
|
name string
|
|
body []byte
|
|
call func([]byte) error
|
|
}{
|
|
{name: "register quick party", body: quick, call: func(body []byte) error { _, err := DecodeRegisterQuickPartyRequest(body); return err }},
|
|
{name: "reserve leave", body: []byte{1, 0}, call: func(body []byte) error { _, err := DecodeReserveLeavePartyRequest(body); return err }},
|
|
{name: "entry into party", body: []byte{1, 0, 0, 0, 0}, call: func(body []byte) error { _, err := DecodeEntryIntoPartyRequest(body); return err }},
|
|
{name: "request peer", body: make([]byte, 14), call: func(body []byte) error { _, err := DecodeRequestPeerRequest(body); return err }},
|
|
{name: "response peer", body: []byte{1, 0, 13, 1, 0, 0, 0, 0}, call: func(body []byte) error { _, err := DecodeResponsePeerRequest(body); return err }},
|
|
{name: "walkout", body: []byte{1, 0}, call: func(body []byte) error { _, err := DecodeWalkoutPartyMemberRequest(body); return err }},
|
|
{name: "change position", body: []byte{1, 3, 0}, call: func(body []byte) error { _, err := DecodeChangePartyMemberPositionRequest(body); return err }},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if err := tc.call(tc.body); err == nil {
|
|
t.Fatalf("decoder accepted trailing byte: % X", tc.body)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEntryIntoPartyFinishAckRequiresPartyContext(t *testing.T) {
|
|
state := alignedcmd.PartyState{
|
|
PartyID: 1,
|
|
Members: []alignedcmd.PartyMemberState{
|
|
{UserID: 1001},
|
|
{UserID: 1002},
|
|
},
|
|
}
|
|
got, err := NewHandler().Handle(context.Background(), alignedcmd.Request{
|
|
Opcode: uint16(dnfenum.CmdPacketEntryIntoPartyFinish),
|
|
Party: &state,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Handle(entry finish) error = %v", err)
|
|
}
|
|
if !got.Handled || !got.ResponseAllowed || len(got.UpperResponses) != 1 {
|
|
t.Fatalf("entry finish handled=%v responseAllowed=%v responses=%d",
|
|
got.Handled, got.ResponseAllowed, len(got.UpperResponses))
|
|
}
|
|
resp := got.UpperResponses[0]
|
|
if resp.MsgID != uint16(dnfenum.CmdPacketEntryIntoPartyFinish) ||
|
|
resp.Classification != 0 || resp.AllowCodec ||
|
|
!reflect.DeepEqual(resp.Body, []byte{1, 0}) {
|
|
t.Fatalf("entry finish response msg=%d body=% X", resp.MsgID, resp.Body)
|
|
}
|
|
}
|
|
|
|
func TestEntryIntoPartyFinishWithoutContextDoesNotForgeClassOneFailure(t *testing.T) {
|
|
got, err := NewHandler().Handle(context.Background(), alignedcmd.Request{
|
|
Opcode: uint16(dnfenum.CmdPacketEntryIntoPartyFinish),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Handle(entry finish without context) error = %v", err)
|
|
}
|
|
if !got.Handled || got.ResponseAllowed || len(got.UpperResponses) != 0 {
|
|
t.Fatalf("entry finish without context handled=%v responseAllowed=%v responses=%d",
|
|
got.Handled, got.ResponseAllowed, len(got.UpperResponses))
|
|
}
|
|
}
|