Files
90CNServer/server/internal/modules/dnf/party/body_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

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))
}
}