服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
966 lines
30 KiB
Go
966 lines
30 KiB
Go
// 本文件验证攻坚队包体解析、0x24F 成员刷新和 889 检查结果构造。
|
|
package raid
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"longheng.io/server/internal/modules/dnf/alignedcmd"
|
|
"longheng.io/server/internal/modules/dnf/dnfenum"
|
|
)
|
|
|
|
func TestDecodeManagerWorkRequest(t *testing.T) {
|
|
body := make([]byte, 12)
|
|
binary.LittleEndian.PutUint32(body[0:4], 0)
|
|
binary.LittleEndian.PutUint32(body[4:8], 1001)
|
|
binary.LittleEndian.PutUint32(body[8:12], 3)
|
|
|
|
got, err := DecodeManagerWorkRequest(body)
|
|
if err != nil {
|
|
t.Fatalf("DecodeManagerWorkRequest error = %v", err)
|
|
}
|
|
if got.ActionOrMode != 0 || got.MemberCharKey != 1001 || got.TargetGroup != 3 {
|
|
t.Fatalf("manager work = %+v, want action=0 member=1001 group=3", got)
|
|
}
|
|
}
|
|
|
|
func TestBuildCreateAndLeaveRaidResultBodies(t *testing.T) {
|
|
if got, want := BuildCreateRaidResultBody(0x12345678), []byte{1, 0x78, 0x56, 0x34, 0x12}; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("create raid result body = % X, want % X", got, want)
|
|
}
|
|
for _, tc := range []struct {
|
|
name string
|
|
raidEnded bool
|
|
want []byte
|
|
}{
|
|
{name: "member left", raidEnded: false, want: []byte{1, 0}},
|
|
{name: "raid ended", raidEnded: true, want: []byte{1, 1}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := BuildLeaveRaidResultBody(tc.raidEnded); !reflect.DeepEqual(got, tc.want) {
|
|
t.Fatalf("leave raid result body = % X, want % X", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCurrentRaidNotificationIDs(t *testing.T) {
|
|
if RaidObjectMsgID != 0x024f || RaidMemberRefreshMsgID != 0x024f {
|
|
t.Fatalf("raid object/roster msg ids = %#04x/%#04x, want 0x024f", RaidObjectMsgID, RaidMemberRefreshMsgID)
|
|
}
|
|
if RaidTimerMsgID != 0x0254 || RaidRemainTimeMsgID != 0x0255 {
|
|
t.Fatalf("raid timer msg ids = %#04x/%#04x, want 0x0254/0x0255", RaidTimerMsgID, RaidRemainTimeMsgID)
|
|
}
|
|
if RaidWaitingChangeMsgID != 0x0251 || RaidPrepareStartMsgID != 0x0252 {
|
|
t.Fatalf("raid waiting/prepare msg ids = %#04x/%#04x, want 0x0251/0x0252", RaidWaitingChangeMsgID, RaidPrepareStartMsgID)
|
|
}
|
|
}
|
|
|
|
func TestBuildCurrentRaidCreationBodies(t *testing.T) {
|
|
member := MemberRecord{
|
|
CharKey: 0x1234,
|
|
Field4: 1,
|
|
RawNameUTF16LE: []byte{'L', 0},
|
|
Field40: 2,
|
|
Field44: 3,
|
|
GroupIndex: 1,
|
|
SlotOrder: 0,
|
|
Field48: 9,
|
|
Field52: 4,
|
|
Field53: 5,
|
|
Field56: 6,
|
|
Field60: 10,
|
|
Field64: 11,
|
|
Field66BoolSource: 1,
|
|
}
|
|
got, err := BuildRaidObjectMode0Body(RaidObjectMode0{
|
|
RaidKey: 0x11223344,
|
|
TitleBytes: []byte{'R', '1'},
|
|
ObjectField24: 3,
|
|
ObjectField28: 0,
|
|
ObjectField30: 0xff,
|
|
ObjectField34: 5,
|
|
ObjectField94: 1,
|
|
Leader: member,
|
|
Members: []MemberRecord{member},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("BuildRaidObjectMode0Body error = %v", err)
|
|
}
|
|
wantMember := []byte{
|
|
0x34, 0x12, 1,
|
|
2, 0, 0, 0, 'L', 0,
|
|
2, 3, 1, 0,
|
|
9, 0, 0, 0,
|
|
4, 5, 6,
|
|
10, 0, 0, 0,
|
|
11, 0,
|
|
1, 0, 0, 0,
|
|
}
|
|
want := []byte{
|
|
0x44, 0x33, 0x22, 0x11,
|
|
0, 0, 0, 0,
|
|
0x44, 0x33, 0x22, 0x11,
|
|
2, 0, 0, 0, 'R', '1',
|
|
3, 0, 0xff,
|
|
5, 0, 0, 0,
|
|
1,
|
|
}
|
|
want = append(want, wantMember...)
|
|
want = append(want, 1)
|
|
want = append(want, wantMember...)
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("mode0 raid object body = % X, want % X", got, want)
|
|
}
|
|
if got := BuildEmptyRaidBuffBody(); !reflect.DeepEqual(got, []byte{0}) {
|
|
t.Fatalf("empty buff body = % X, want 00", got)
|
|
}
|
|
}
|
|
|
|
func TestBuildCurrentRaidStateNotificationBodies(t *testing.T) {
|
|
symbols, err := BuildRaidSymbolBody([]RaidSymbolRow{
|
|
{Key: 0x11223344, Value: 0x55667788},
|
|
{Key: 0x01020304, Value: 0xaabbccdd},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("BuildRaidSymbolBody error = %v", err)
|
|
}
|
|
wantSymbols := []byte{
|
|
2,
|
|
0x44, 0x33, 0x22, 0x11, 0x88, 0x77, 0x66, 0x55,
|
|
0x04, 0x03, 0x02, 0x01, 0xdd, 0xcc, 0xbb, 0xaa,
|
|
}
|
|
if !reflect.DeepEqual(symbols, wantSymbols) {
|
|
t.Fatalf("raid symbol body = % X, want % X", symbols, wantSymbols)
|
|
}
|
|
|
|
participation, err := BuildRaidParticipationBody([]RaidParticipationRow{
|
|
{DungeonID: 0x11223344, Op: 5, MemberKeys: []uint32{0x1234, 0xabcd}},
|
|
{DungeonID: 0x01020304},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("BuildRaidParticipationBody error = %v", err)
|
|
}
|
|
wantParticipation := []byte{
|
|
2,
|
|
0x44, 0x33, 0x22, 0x11, 5, 2, 0x34, 0x12, 0xcd, 0xab,
|
|
0x04, 0x03, 0x02, 0x01, 0, 0,
|
|
}
|
|
if !reflect.DeepEqual(participation, wantParticipation) {
|
|
t.Fatalf("raid participation body = % X, want % X", participation, wantParticipation)
|
|
}
|
|
|
|
dungeonState, err := BuildRaidDungeonStateBody(RaidDungeonStateSnapshot{
|
|
Unknown0: 7,
|
|
Rows: []RaidDungeonStateRow{
|
|
{DungeonID: 0x11223344, State: 1},
|
|
{DungeonID: 0x01020304, State: 9},
|
|
},
|
|
TrailingValue: 0xaabbccdd,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("BuildRaidDungeonStateBody error = %v", err)
|
|
}
|
|
wantDungeonState := []byte{
|
|
7, 2,
|
|
0x44, 0x33, 0x22, 0x11, 1,
|
|
0x04, 0x03, 0x02, 0x01, 9,
|
|
0xdd, 0xcc, 0xbb, 0xaa,
|
|
}
|
|
if !reflect.DeepEqual(dungeonState, wantDungeonState) {
|
|
t.Fatalf("raid dungeon state body = % X, want % X", dungeonState, wantDungeonState)
|
|
}
|
|
|
|
if got, want := BuildRaidDungeonChangeBody(RaidDungeonChange{
|
|
DungeonID: 0x11223344,
|
|
State: 3,
|
|
Argument: 0x80,
|
|
}), []byte{0x44, 0x33, 0x22, 0x11, 3, 0x80}; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("raid dungeon change body = % X, want % X", got, want)
|
|
}
|
|
if got, want := BuildRaidStateBody(RaidState{State: 9, Argument: -2}), []byte{9, 0xfe}; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("raid state body = % X, want % X", got, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildCurrentRaidTimerMovieAndResultBodies(t *testing.T) {
|
|
if got, want := BuildRaidTimerBody(RaidTimer{
|
|
TimerType: 2,
|
|
Value0: 0x11223344,
|
|
Value1: 0x55667788,
|
|
Value2: 0xaabbccdd,
|
|
}), []byte{
|
|
2,
|
|
0x44, 0x33, 0x22, 0x11,
|
|
0x88, 0x77, 0x66, 0x55,
|
|
0xdd, 0xcc, 0xbb, 0xaa,
|
|
}; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("raid timer body = % X, want % X", got, want)
|
|
}
|
|
if got, want := BuildRaidRemainTimeBody(RaidRemainTime{
|
|
TimerType: 3,
|
|
Seconds: 0x01020304,
|
|
}), []byte{3, 0x04, 0x03, 0x02, 0x01}; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("raid remain-time body = % X, want % X", got, want)
|
|
}
|
|
if got, want := BuildRaidMovieBody(), []byte{}; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("raid movie body = % X, want empty", got)
|
|
}
|
|
if got, want := BuildRaidResultBody(RaidResult{
|
|
ResultType: 2,
|
|
Field1: 3,
|
|
Field2: 0x11223344,
|
|
Field3: 0x5566,
|
|
Field4: 7,
|
|
Field5: 8,
|
|
}), []byte{2, 3, 0x44, 0x33, 0x22, 0x11, 0x66, 0x55, 7, 8}; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("raid result body = % X, want % X", got, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildCurrentRaidRewardAndBuffBodies(t *testing.T) {
|
|
rewards, err := BuildRaidRewardListBody(RaidRewardList{
|
|
ResultType: 1,
|
|
Groups: []RaidRewardGroup{
|
|
{
|
|
MemberKey: 0x1234,
|
|
Field0: 5,
|
|
Entries: []RaidRewardEntry{
|
|
{Field1: 7, ItemOrValue: 0x11223344, CountOrValue: 0x5566},
|
|
{Field1: 8, ItemOrValue: 0xaabbccdd, CountOrValue: 0xeeff},
|
|
},
|
|
},
|
|
{MemberKey: 0xabcd},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("BuildRaidRewardListBody error = %v", err)
|
|
}
|
|
wantRewards := []byte{
|
|
1, 2,
|
|
0x34, 0x12, 5, 2,
|
|
7, 0x44, 0x33, 0x22, 0x11, 0x66, 0x55,
|
|
8, 0xdd, 0xcc, 0xbb, 0xaa, 0xff, 0xee,
|
|
0xcd, 0xab, 0, 0,
|
|
}
|
|
if !reflect.DeepEqual(rewards, wantRewards) {
|
|
t.Fatalf("raid reward-list body = % X, want % X", rewards, wantRewards)
|
|
}
|
|
|
|
buffs, err := BuildRaidBuffBody([]RaidBuffGroup{
|
|
{
|
|
Field0: 1,
|
|
Entries: []RaidBuffEntry{
|
|
{
|
|
PartyIndex: 0x0203,
|
|
MemberKey: 0x1234,
|
|
Value0: 0x11223344,
|
|
Value1: 0x55667788,
|
|
},
|
|
},
|
|
},
|
|
{Field0: 9},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("BuildRaidBuffBody error = %v", err)
|
|
}
|
|
wantBuffs := []byte{
|
|
2,
|
|
1, 1,
|
|
0x03, 0x02, 0x34, 0x12,
|
|
0x44, 0x33, 0x22, 0x11,
|
|
0x88, 0x77, 0x66, 0x55,
|
|
9, 0,
|
|
}
|
|
if !reflect.DeepEqual(buffs, wantBuffs) {
|
|
t.Fatalf("raid buff body = % X, want % X", buffs, wantBuffs)
|
|
}
|
|
emptyBuffs, err := BuildRaidBuffBody(nil)
|
|
if err != nil {
|
|
t.Fatalf("BuildRaidBuffBody empty error = %v", err)
|
|
}
|
|
if got, want := emptyBuffs, BuildEmptyRaidBuffBody(); !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("empty raid buff body = % X, want % X", got, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildCurrentRaidMonsterStateBody(t *testing.T) {
|
|
got, err := BuildRaidMonsterStateBody([]RaidMonsterStateGroup{
|
|
{
|
|
PartyIndex: 0x0203,
|
|
Members: []uint16{0x1234, 0xabcd},
|
|
Value0: 0x11223344,
|
|
Value1: 0x55667788,
|
|
Values: []uint32{0x01020304, 0xaabbccdd},
|
|
},
|
|
{
|
|
PartyIndex: 0x0405,
|
|
Value1: 0x10203040,
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("BuildRaidMonsterStateBody error = %v", err)
|
|
}
|
|
want := []byte{
|
|
2,
|
|
0x03, 0x02, 2, 0x34, 0x12, 0xcd, 0xab,
|
|
0x44, 0x33, 0x22, 0x11,
|
|
0x88, 0x77, 0x66, 0x55,
|
|
2, 0x04, 0x03, 0x02, 0x01, 0xdd, 0xcc, 0xbb, 0xaa,
|
|
0x05, 0x04, 0,
|
|
0, 0, 0, 0,
|
|
0x40, 0x30, 0x20, 0x10,
|
|
0,
|
|
}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("raid monster-state body = % X, want % X", got, want)
|
|
}
|
|
empty, err := BuildRaidMonsterStateBody(nil)
|
|
if err != nil {
|
|
t.Fatalf("BuildRaidMonsterStateBody empty error = %v", err)
|
|
}
|
|
if got, want := empty, BuildEmptyRaidMonsterStateBody(); !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("empty raid monster-state body = % X, want % X", got, want)
|
|
}
|
|
}
|
|
|
|
func TestRaidByteSizedCountsRejectTheTwoHundredFiftySixthRow(t *testing.T) {
|
|
tooManyMembers := make([]uint16, maxRaidByteCount+1)
|
|
tooManyValues := make([]uint32, maxRaidByteCount+1)
|
|
tooManySymbols := make([]RaidSymbolRow, maxRaidByteCount+1)
|
|
tests := []struct {
|
|
name string
|
|
call func() error
|
|
}{
|
|
{
|
|
name: "outer count",
|
|
call: func() error {
|
|
_, err := BuildRaidSymbolBody(tooManySymbols)
|
|
return err
|
|
},
|
|
},
|
|
{
|
|
name: "participation members",
|
|
call: func() error {
|
|
_, err := BuildRaidParticipationBody([]RaidParticipationRow{{Members: tooManyMembers}})
|
|
return err
|
|
},
|
|
},
|
|
{
|
|
name: "reward entries",
|
|
call: func() error {
|
|
_, err := BuildRaidRewardListBody(RaidRewardList{Groups: []RaidRewardGroup{{
|
|
Entries: make([]RaidRewardEntry, maxRaidByteCount+1),
|
|
}}})
|
|
return err
|
|
},
|
|
},
|
|
{
|
|
name: "buff entries",
|
|
call: func() error {
|
|
_, err := BuildRaidBuffBody([]RaidBuffGroup{{
|
|
Entries: make([]RaidBuffEntry, maxRaidByteCount+1),
|
|
}})
|
|
return err
|
|
},
|
|
},
|
|
{
|
|
name: "monster members",
|
|
call: func() error {
|
|
_, err := BuildRaidMonsterStateBody([]RaidMonsterStateGroup{{Members: tooManyMembers}})
|
|
return err
|
|
},
|
|
},
|
|
{
|
|
name: "monster values",
|
|
call: func() error {
|
|
_, err := BuildRaidMonsterStateBody([]RaidMonsterStateGroup{{Values: tooManyValues}})
|
|
return err
|
|
},
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if err := tc.call(); err != errRaidByteCountOverflow {
|
|
t.Fatalf("error = %v, want %v", err, errRaidByteCountOverflow)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildCurrentRaidEntryCostAndWaitingBodies(t *testing.T) {
|
|
if got, want := BuildRaidCommandSuccessBody(), []byte{1}; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("raid success body = % X, want % X", got, want)
|
|
}
|
|
if got, want := BuildEntryCostBody([]EntryCostRow{
|
|
{MemberKey: 0x1234, Field: 1, Value: 7},
|
|
{MemberKey: 0xabcd, Field: 0, Value: 0},
|
|
}), []byte{
|
|
2, 0, 0, 0,
|
|
0x34, 0x12, 1, 7, 0,
|
|
0xcd, 0xab, 0, 0, 0,
|
|
}; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("entry cost body = % X, want % X", got, want)
|
|
}
|
|
rows := []WaitingRow{{MemberKey: 0x1234, State: 2}, {MemberKey: 0xabcd, State: 0}}
|
|
if got, want := BuildWaitingListBody(rows), []byte{2, 0, 0x34, 0x12, 2, 0xcd, 0xab, 0}; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("waiting list body = % X, want % X", got, want)
|
|
}
|
|
if got, want := BuildWaitingChangeBody(0, rows[0]), []byte{0, 0x34, 0x12, 2}; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("waiting add body = % X, want % X", got, want)
|
|
}
|
|
if got, want := BuildWaitingChangeBody(1, rows[1]), []byte{1, 0xcd, 0xab}; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("waiting remove body = % X, want % X", got, want)
|
|
}
|
|
}
|
|
|
|
func TestDecodeCreateAndModifyRaidInfoRequest(t *testing.T) {
|
|
body := buildRaidInfoBodyForTest(2, []byte("anton"), 7)
|
|
|
|
create, err := DecodeCreateRaidRequest(body)
|
|
if err != nil {
|
|
t.Fatalf("DecodeCreateRaidRequest error = %v", err)
|
|
}
|
|
modify, err := DecodeModifyRaidInfoRequest(body)
|
|
if err != nil {
|
|
t.Fatalf("DecodeModifyRaidInfoRequest error = %v", err)
|
|
}
|
|
for name, got := range map[string]InfoRequest{"create": create, "modify": modify} {
|
|
if got.RouteOrRaidType != 2 || !bytesEqual(got.NameBytes, []byte("anton")) || got.TailFlag != 7 {
|
|
t.Fatalf("%s request = %+v, want route=2 name=anton tail=7", name, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDecodeRequestRaidInfoRequest(t *testing.T) {
|
|
body := []byte{2, 0x78, 0x56, 0x34, 0x12}
|
|
got, err := DecodeRequestRaidInfoRequest(body)
|
|
if err != nil {
|
|
t.Fatalf("DecodeRequestRaidInfoRequest error = %v", err)
|
|
}
|
|
if !bytesEqual(got.Raw[:], body) {
|
|
t.Fatalf("request raid info = %+v", got)
|
|
}
|
|
for _, size := range []int{0, 4, 6} {
|
|
if _, err := DecodeRequestRaidInfoRequest(make([]byte, size)); err == nil {
|
|
t.Fatalf("DecodeRequestRaidInfoRequest accepted %d-byte body", size)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildRaidInfoResponseBodyIsEmpty(t *testing.T) {
|
|
if got := BuildRaidInfoResponseBody(); len(got) != 0 {
|
|
t.Fatalf("raid info response body = % X, want empty", got)
|
|
}
|
|
}
|
|
|
|
func TestBuildRaidDirectoryReadyBodyHasReservedByte(t *testing.T) {
|
|
if got := BuildRaidDirectoryReadyBody(); len(got) != 1 || got[0] != 0 {
|
|
t.Fatalf("raid directory ready body = % X, want 00", got)
|
|
}
|
|
}
|
|
|
|
func TestDecodeCreateRaidInfoRequestRejectsTruncatedName(t *testing.T) {
|
|
body := []byte{1, 5, 0, 0, 0, 'a', 'b'}
|
|
if _, err := DecodeCreateRaidRequest(body); err == nil {
|
|
t.Fatalf("DecodeCreateRaidRequest accepted truncated dstr")
|
|
}
|
|
}
|
|
|
|
func TestRaidRequestDecodersRejectTrailingBytes(t *testing.T) {
|
|
info := append(buildRaidInfoBodyForTest(2, []byte("anton"), 7), 0)
|
|
if _, err := DecodeCreateRaidRequest(info); err == nil {
|
|
t.Fatalf("DecodeCreateRaidRequest accepted trailing byte")
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
body []byte
|
|
call func([]byte) error
|
|
}{
|
|
{name: "leave", body: []byte{1, 0, 0}, call: func(body []byte) error { _, err := DecodeLeaveRaidRequest(body); return err }},
|
|
{name: "rejoin", body: []byte{1, 0, 0, 0, 0}, call: func(body []byte) error { _, err := DecodeRejoinRaidRequest(body); return err }},
|
|
{name: "waiting", body: []byte{0, 2, 0}, call: func(body []byte) error { _, err := DecodeSetWaitingRequest(body); return err }},
|
|
{name: "entry cost", body: []byte{1, 0}, call: func(body []byte) error { _, err := DecodeEntryCostInfoRequest(body); return err }},
|
|
{name: "set symbol", body: make([]byte, 10), call: func(body []byte) error { _, err := DecodeSetSymbolRequest(body); return err }},
|
|
{name: "manager work", body: make([]byte, 13), call: func(body []byte) error { _, err := DecodeManagerWorkRequest(body); return err }},
|
|
{name: "other channel join", body: make([]byte, 10), call: func(body []byte) error { _, err := DecodeOtherChannelRequestJoinRequest(body); return err }},
|
|
{name: "member state", body: []byte{1, 0}, call: func(body []byte) error { _, err := DecodeMemberChangeStateRequest(body); return err }},
|
|
{name: "move channel fail", body: []byte{1, 2, 0, 0}, call: func(body []byte) error { _, err := DecodeUserMoveChannelFailRequest(body); return err }},
|
|
{name: "simple other channel list", body: []byte{1, 0}, call: func(body []byte) error { _, err := DecodeOtherChannelListRequest(body); return err }},
|
|
{name: "check user", body: []byte{1, 2, 0, 0}, call: func(body []byte) error { _, err := DecodeCheckRaidUserRequest(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)
|
|
}
|
|
})
|
|
}
|
|
|
|
fullList := make([]byte, 14)
|
|
fullList[0] = 0
|
|
if _, err := DecodeOtherChannelListRequest(fullList); err == nil {
|
|
t.Fatalf("DecodeOtherChannelListRequest accepted trailing byte after empty dstr")
|
|
}
|
|
}
|
|
|
|
func TestDecodeSetSymbolRejectsOutOfRangeSymbol(t *testing.T) {
|
|
body := make([]byte, 9)
|
|
body[8] = 3
|
|
if _, err := DecodeSetSymbolRequest(body); err == nil {
|
|
t.Fatalf("DecodeSetSymbolRequest accepted symbol 3")
|
|
}
|
|
}
|
|
|
|
func TestDecodeLeaveStartAndRejoinRaidRequests(t *testing.T) {
|
|
leave, err := DecodeLeaveRaidRequest([]byte{0x34, 0x12})
|
|
if err != nil {
|
|
t.Fatalf("DecodeLeaveRaidRequest error = %v", err)
|
|
}
|
|
if leave.RaidOrMemberKey != 0x1234 {
|
|
t.Fatalf("leave key = 0x%04X, want 0x1234", leave.RaidOrMemberKey)
|
|
}
|
|
if err := DecodeStartRaidRequest(nil); err != nil {
|
|
t.Fatalf("DecodeStartRaidRequest empty error = %v", err)
|
|
}
|
|
if err := DecodeStartRaidRequest([]byte{1}); err == nil {
|
|
t.Fatalf("DecodeStartRaidRequest accepted non-empty body")
|
|
}
|
|
rejoin, err := DecodeRejoinRaidRequest([]byte{0x78, 0x56, 0x34, 0x12})
|
|
if err != nil {
|
|
t.Fatalf("DecodeRejoinRaidRequest error = %v", err)
|
|
}
|
|
if rejoin.RaidKey != 0x12345678 {
|
|
t.Fatalf("rejoin raid key = 0x%08X, want 0x12345678", rejoin.RaidKey)
|
|
}
|
|
}
|
|
|
|
func TestDecodeRaidNeighborRequests(t *testing.T) {
|
|
waiting, err := DecodeSetWaitingRequest([]byte{0, 2})
|
|
if err != nil {
|
|
t.Fatalf("DecodeSetWaitingRequest error = %v", err)
|
|
}
|
|
if waiting.Flag != 0 || waiting.RouteRaidType != 2 {
|
|
t.Fatalf("waiting = %+v, want flag=0 route=2", waiting)
|
|
}
|
|
|
|
entryCost, err := DecodeEntryCostInfoRequest([]byte{1})
|
|
if err != nil {
|
|
t.Fatalf("DecodeEntryCostInfoRequest error = %v", err)
|
|
}
|
|
if entryCost.Enabled != 1 {
|
|
t.Fatalf("entry cost enabled = %d, want 1", entryCost.Enabled)
|
|
}
|
|
|
|
setSymbolBody := make([]byte, 9)
|
|
binary.LittleEndian.PutUint32(setSymbolBody[0:4], 0x11223344)
|
|
binary.LittleEndian.PutUint32(setSymbolBody[4:8], 0x55667788)
|
|
setSymbolBody[8] = 2
|
|
setSymbol, err := DecodeSetSymbolRequest(setSymbolBody)
|
|
if err != nil {
|
|
t.Fatalf("DecodeSetSymbolRequest error = %v", err)
|
|
}
|
|
if setSymbol.SourceValue != 0x11223344 || setSymbol.TargetValue != 0x55667788 || setSymbol.Symbol != 2 {
|
|
t.Fatalf("set symbol = %+v, want source=0x11223344 target=0x55667788 symbol=2", setSymbol)
|
|
}
|
|
|
|
joinBody := []byte{1, 0x34, 0x12, 0x78, 0x56, 0x34, 0x12, 0x02, 0x00}
|
|
join, err := DecodeOtherChannelRequestJoinRequest(joinBody)
|
|
if err != nil {
|
|
t.Fatalf("DecodeOtherChannelRequestJoinRequest error = %v", err)
|
|
}
|
|
if join.Mode != 1 || join.TargetKey != 0x1234 || join.ClientValue != 0x12345678 || join.RouteRaidType != 2 {
|
|
t.Fatalf("join = %+v, want mode=1 target=0x1234 client=0x12345678 route=2", join)
|
|
}
|
|
|
|
state, err := DecodeMemberChangeStateRequest([]byte{3})
|
|
if err != nil {
|
|
t.Fatalf("DecodeMemberChangeStateRequest error = %v", err)
|
|
}
|
|
if state.State != 3 {
|
|
t.Fatalf("state = %d, want 3", state.State)
|
|
}
|
|
|
|
moveFail, err := DecodeUserMoveChannelFailRequest([]byte{4, 0x78, 0x56})
|
|
if err != nil {
|
|
t.Fatalf("DecodeUserMoveChannelFailRequest error = %v", err)
|
|
}
|
|
if moveFail.Mode != 4 || moveFail.TargetKey != 0x5678 {
|
|
t.Fatalf("move fail = %+v, want mode=4 target=0x5678", moveFail)
|
|
}
|
|
|
|
check, err := DecodeCheckRaidUserRequest([]byte{5, 0xbc, 0x9a})
|
|
if err != nil {
|
|
t.Fatalf("DecodeCheckRaidUserRequest error = %v", err)
|
|
}
|
|
if check.Mode != 5 || check.TargetKey != 0x9abc {
|
|
t.Fatalf("check = %+v, want mode=5 target=0x9abc", check)
|
|
}
|
|
}
|
|
|
|
func TestDecodeOtherChannelListRequest(t *testing.T) {
|
|
simple, err := DecodeOtherChannelListRequest([]byte{1})
|
|
if err != nil {
|
|
t.Fatalf("DecodeOtherChannelListRequest simple error = %v", err)
|
|
}
|
|
if simple.Mode != 1 || simple.HasContext || simple.HasNameDstr {
|
|
t.Fatalf("simple list = %+v, want mode=1 without context", simple)
|
|
}
|
|
|
|
body := make([]byte, 13+4)
|
|
body[0] = 0
|
|
copy(body[1:9], []byte{1, 2, 3, 4, 5, 6, 7, 8})
|
|
binary.LittleEndian.PutUint32(body[9:13], 4)
|
|
copy(body[13:], []byte("raid"))
|
|
full, err := DecodeOtherChannelListRequest(body)
|
|
if err != nil {
|
|
t.Fatalf("DecodeOtherChannelListRequest full error = %v", err)
|
|
}
|
|
if full.Mode != 0 || !full.HasContext || !full.HasNameDstr || !bytesEqual(full.NameBytes, []byte("raid")) {
|
|
t.Fatalf("full list = %+v, want mode=0 context name=raid", full)
|
|
}
|
|
if full.Context != [8]byte{1, 2, 3, 4, 5, 6, 7, 8} {
|
|
t.Fatalf("context = % X", full.Context)
|
|
}
|
|
}
|
|
|
|
func TestBuildCheckUserResultBody(t *testing.T) {
|
|
got := BuildCheckUserResultBody(CheckUserResult{
|
|
Header: CheckUserHeader{
|
|
Mode: CheckUserModeRefreshWithHeaderState,
|
|
Field4: 0x11223344,
|
|
Field8: 0x55667788,
|
|
Field12: 0x99,
|
|
Raw13To20: [8]byte{1, 2, 3, 4, 5, 6, 7, 8},
|
|
Field21To24: 0xaabbccdd,
|
|
},
|
|
FirstRows: [][12]byte{
|
|
{0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b},
|
|
},
|
|
SecondRows: [][12]byte{
|
|
{0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b},
|
|
},
|
|
StateRows: [][5]byte{
|
|
{0x30, 0x31, 0x32, 0x33, 0x34},
|
|
},
|
|
})
|
|
want := []byte{
|
|
0x01, 0x00, 0x00, 0x00,
|
|
0x44, 0x33, 0x22, 0x11,
|
|
0x88, 0x77, 0x66, 0x55,
|
|
0x99,
|
|
0x01, 0x02, 0x03, 0x04,
|
|
0x05, 0x06, 0x07, 0x08,
|
|
0xdd, 0xcc, 0xbb, 0xaa,
|
|
0x01, 0x00, 0x00, 0x00,
|
|
0x10, 0x11, 0x12, 0x13,
|
|
0x14, 0x15, 0x16, 0x17,
|
|
0x18, 0x19, 0x1a, 0x1b,
|
|
0x01, 0x00, 0x00, 0x00,
|
|
0x20, 0x21, 0x22, 0x23,
|
|
0x24, 0x25, 0x26, 0x27,
|
|
0x28, 0x29, 0x2a, 0x2b,
|
|
0x01, 0x00, 0x00, 0x00,
|
|
0x30, 0x31, 0x32, 0x33, 0x34,
|
|
}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("check user body = % X, want % X", got, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildRequestMembersResultBody(t *testing.T) {
|
|
got := BuildRequestMembersResultBody(RequestMembersResultEnterPublicRaidState, 0x55)
|
|
want := []byte{0x02, 0x00, 0x00, 0x00, 0x55}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("request members result body = % X, want % X", got, want)
|
|
}
|
|
}
|
|
|
|
func TestRaidCheckUserAndRequestMemberConstants(t *testing.T) {
|
|
if CheckUserModeRefresh != 0 || CheckUserModeRefreshWithHeaderState != 1 {
|
|
t.Fatalf("check user modes = %d/%d, want 0/1", CheckUserModeRefresh, CheckUserModeRefreshWithHeaderState)
|
|
}
|
|
if RequestMembersResultEnterPublicRaidState != 2 || RequestMembersResultSkipLocalError != 3 {
|
|
t.Fatalf("request member results = %d/%d, want 2/3", RequestMembersResultEnterPublicRaidState, RequestMembersResultSkipLocalError)
|
|
}
|
|
if requestMembersUIStatePublicRaid != 8 || requestMembersUIStateLocalFailure != 10 {
|
|
t.Fatalf("request member UI states = %d/%d, want 8/10", requestMembersUIStatePublicRaid, requestMembersUIStateLocalFailure)
|
|
}
|
|
}
|
|
|
|
func TestBuildOtherChannelBodies(t *testing.T) {
|
|
publicObject := RaidObjectMode0{
|
|
RaidKey: 0x00170001,
|
|
TitleBytes: []byte{'R', '1'},
|
|
ObjectField24: 5,
|
|
ObjectField30: 0xff,
|
|
Leader: MemberRecord{CharKey: 1, RawNameBytes: []byte("leader")},
|
|
Members: []MemberRecord{{CharKey: 1, RawNameBytes: []byte("leader")}},
|
|
}
|
|
publicBody, err := BuildPublicRaidListBody(PublicRaidListPage{
|
|
TotalCount: 1,
|
|
Objects: []RaidObjectMode0{publicObject},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("BuildPublicRaidListBody error = %v", err)
|
|
}
|
|
coreObject := publicObject
|
|
coreObject.Members = nil
|
|
mode0Body, err := BuildRaidObjectMode0Body(coreObject)
|
|
if err != nil {
|
|
t.Fatalf("BuildRaidObjectMode0Body core error = %v", err)
|
|
}
|
|
wantPublic := []byte{
|
|
1, 0, 0, 0, // total count
|
|
0, 0, 0, 0, // returned offset
|
|
1, 0, 0, 0, // page row count
|
|
}
|
|
// op591 adds raid-key/mode before the shared object and its own member count
|
|
// plus member records after it. op753 embeds only the shared object plus its
|
|
// member count.
|
|
wantPublic = append(wantPublic, mode0Body[8:len(mode0Body)-1]...)
|
|
wantPublic = append(wantPublic, 1)
|
|
if !reflect.DeepEqual(publicBody, wantPublic) {
|
|
t.Fatalf("public raid list body = % X, want % X", publicBody, wantPublic)
|
|
}
|
|
|
|
emptyPage := BuildOtherChannelListPageBody(OtherChannelListPage{})
|
|
if len(emptyPage) != 28 {
|
|
t.Fatalf("empty other channel list page length = %d, want 28", len(emptyPage))
|
|
}
|
|
if !reflect.DeepEqual(emptyPage[20:], []byte{0, 0, 0, 0, 0, 0, 0, 0}) {
|
|
t.Fatalf("empty other channel list counts = % X, want eight zero bytes", emptyPage[20:])
|
|
}
|
|
|
|
userinfo := BuildOtherChannelUserinfoBody([]uint32{0x11223344, 0x55667788})
|
|
wantUserinfo := []byte{
|
|
0x02, 0x00, 0x00, 0x00,
|
|
0x44, 0x33, 0x22, 0x11,
|
|
0x88, 0x77, 0x66, 0x55,
|
|
}
|
|
if !reflect.DeepEqual(userinfo, wantUserinfo) {
|
|
t.Fatalf("other channel userinfo body = % X, want % X", userinfo, wantUserinfo)
|
|
}
|
|
|
|
join := BuildOtherChannelRequestJoinResultBody(0x01020304, 0xaabbccdd)
|
|
wantJoin := []byte{0x04, 0x03, 0x02, 0x01, 0xdd, 0xcc, 0xbb, 0xaa}
|
|
if !reflect.DeepEqual(join, wantJoin) {
|
|
t.Fatalf("other channel join result body = % X, want % X", join, wantJoin)
|
|
}
|
|
|
|
response := BuildOtherChannelResponseJoinBody(7, 1)
|
|
if !reflect.DeepEqual(response, []byte{7, 1}) {
|
|
t.Fatalf("other channel response join body = % X, want 07 01", response)
|
|
}
|
|
|
|
var header [20]byte
|
|
for i := range header {
|
|
header[i] = byte(i + 1)
|
|
}
|
|
page := BuildOtherChannelListPageBody(OtherChannelListPage{
|
|
Header: header,
|
|
Rows12: [][12]byte{
|
|
{0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b},
|
|
},
|
|
Rows13: [][13]byte{
|
|
{0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c},
|
|
},
|
|
})
|
|
wantPage := []byte{
|
|
0x01, 0x02, 0x03, 0x04,
|
|
0x05, 0x06, 0x07, 0x08,
|
|
0x09, 0x0a, 0x0b, 0x0c,
|
|
0x0d, 0x0e, 0x0f, 0x10,
|
|
0x11, 0x12, 0x13, 0x14,
|
|
0x01, 0x00, 0x00, 0x00,
|
|
0x10, 0x11, 0x12, 0x13,
|
|
0x14, 0x15, 0x16, 0x17,
|
|
0x18, 0x19, 0x1a, 0x1b,
|
|
0x01, 0x00, 0x00, 0x00,
|
|
0x20, 0x21, 0x22, 0x23,
|
|
0x24, 0x25, 0x26, 0x27,
|
|
0x28, 0x29, 0x2a, 0x2b,
|
|
0x2c,
|
|
}
|
|
if !reflect.DeepEqual(page, wantPage) {
|
|
t.Fatalf("other channel list page body = % X, want % X", page, wantPage)
|
|
}
|
|
}
|
|
|
|
func TestBuildMemberRefreshMode3Body(t *testing.T) {
|
|
got, err := BuildMemberRefreshMode3Body(MemberRefresh{
|
|
RaidKey: 0x01020304,
|
|
Members: []MemberRecord{
|
|
{
|
|
CharKey: 1001,
|
|
Field4: 1,
|
|
Name: "A",
|
|
Field40: 2,
|
|
Field44: 3,
|
|
GroupIndex: 4,
|
|
SlotOrder: 5,
|
|
Field48: 0x11223344,
|
|
Field52: 6,
|
|
Field53: 7,
|
|
Field56: 8,
|
|
Field60: 0x55667788,
|
|
Field64: 0x99aa,
|
|
Field66BoolSource: 1,
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("BuildMemberRefreshMode3Body error = %v", err)
|
|
}
|
|
want := []byte{
|
|
0x04, 0x03, 0x02, 0x01,
|
|
0x03, 0x00, 0x00, 0x00,
|
|
0x01,
|
|
0xe9, 0x03,
|
|
0x01,
|
|
0x02, 0x00, 0x00, 0x00,
|
|
0x41, 0x00,
|
|
0x02,
|
|
0x03,
|
|
0x04,
|
|
0x05,
|
|
0x44, 0x33, 0x22, 0x11,
|
|
0x06,
|
|
0x07,
|
|
0x08,
|
|
0x88, 0x77, 0x66, 0x55,
|
|
0xaa, 0x99,
|
|
0x01, 0x00, 0x00, 0x00,
|
|
}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("refresh body = % X, want % X", got, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildMemberRefreshMode3BodyWritesNonZeroEmptyName(t *testing.T) {
|
|
got, err := BuildMemberRefreshMode3Body(MemberRefresh{
|
|
Members: []MemberRecord{{CharKey: 1001}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("BuildMemberRefreshMode3Body error = %v", err)
|
|
}
|
|
nameLenOffset := 8 + 1 + 2 + 1
|
|
if length := binary.LittleEndian.Uint32(got[nameLenOffset : nameLenOffset+4]); length != 2 {
|
|
t.Fatalf("empty name length = %d, want 2", length)
|
|
}
|
|
if !reflect.DeepEqual(got[nameLenOffset+4:nameLenOffset+6], []byte{0, 0}) {
|
|
t.Fatalf("empty name bytes = % X, want 00 00", got[nameLenOffset+4:nameLenOffset+6])
|
|
}
|
|
}
|
|
|
|
func TestBuildMemberRefreshMode3BodyUsesRawNameBytes(t *testing.T) {
|
|
got, err := BuildMemberRefreshMode3Body(MemberRefresh{
|
|
RaidKey: 0x01020304,
|
|
Members: []MemberRecord{{
|
|
CharKey: 1001,
|
|
Name: "七千万",
|
|
RawNameBytes: []byte{0xc6, 0xdf, 0xc7, 0xa7, 0xcd, 0xf2},
|
|
RawNameUTF16LE: []byte{0x03, 0x4e, 0x43, 0x53, 0x07, 0x4e},
|
|
}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("BuildMemberRefreshMode3Body error = %v", err)
|
|
}
|
|
nameLenOffset := 8 + 1 + 2 + 1
|
|
if length := binary.LittleEndian.Uint32(got[nameLenOffset : nameLenOffset+4]); length != 6 {
|
|
t.Fatalf("raw name length = %d, want 6", length)
|
|
}
|
|
if gotName := got[nameLenOffset+4 : nameLenOffset+10]; !reflect.DeepEqual(gotName, []byte{0xc6, 0xdf, 0xc7, 0xa7, 0xcd, 0xf2}) {
|
|
t.Fatalf("raw name bytes = % X, want C6 DF C7 A7 CD F2", gotName)
|
|
}
|
|
}
|
|
|
|
func TestBuildMemberRefreshMode3BodyRejectsTwentyFirstMember(t *testing.T) {
|
|
members := make([]MemberRecord, MaxAttackPartyMembers+1)
|
|
if _, err := BuildMemberRefreshMode3Body(MemberRefresh{Members: members}); err == nil {
|
|
t.Fatalf("BuildMemberRefreshMode3Body accepted %d members", len(members))
|
|
}
|
|
}
|
|
|
|
func TestBuildMemberRefreshMode3BodyAllowsTwentyMembers(t *testing.T) {
|
|
members := make([]MemberRecord, MaxAttackPartyMembers)
|
|
for i := range members {
|
|
members[i].CharKey = uint16(1000 + i)
|
|
}
|
|
got, err := BuildMemberRefreshMode3Body(MemberRefresh{RaidKey: 0x20010001, Members: members})
|
|
if err != nil {
|
|
t.Fatalf("BuildMemberRefreshMode3Body error = %v", err)
|
|
}
|
|
if got[8] != MaxAttackPartyMembers {
|
|
t.Fatalf("member count = %d, want %d", got[8], MaxAttackPartyMembers)
|
|
}
|
|
}
|
|
|
|
func TestRaidHandlerParsesManagerWorkWithoutAck(t *testing.T) {
|
|
body := make([]byte, 12)
|
|
binary.LittleEndian.PutUint32(body[4:8], 1001)
|
|
binary.LittleEndian.PutUint32(body[8:12], 4)
|
|
|
|
got, err := NewHandler().Handle(contextForTest(), alignedcmd.Request{
|
|
Opcode: uint16(dnfenum.CmdPacketRaidManagerWork),
|
|
Body: body,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Handle raid manager work error = %v", err)
|
|
}
|
|
if !got.Handled || got.ResponseAllowed || len(got.UpperResponses) != 0 {
|
|
t.Fatalf("raid manager handled=%v responseAllowed=%v responses=%d", got.Handled, got.ResponseAllowed, len(got.UpperResponses))
|
|
}
|
|
if !strings.Contains(got.Reason, "0x24F mode=3") {
|
|
t.Fatalf("reason = %q, want 0x24F mode=3 hint", got.Reason)
|
|
}
|
|
}
|
|
|
|
func TestRaidHandlerParsesSetSymbolWithoutAck(t *testing.T) {
|
|
body := make([]byte, 9)
|
|
binary.LittleEndian.PutUint32(body[0:4], 0x11223344)
|
|
binary.LittleEndian.PutUint32(body[4:8], 0x55667788)
|
|
body[8] = 1
|
|
|
|
got, err := NewHandler().Handle(contextForTest(), alignedcmd.Request{
|
|
Opcode: uint16(dnfenum.CmdPacketRaidSetSymbol),
|
|
Body: body,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Handle raid set symbol error = %v", err)
|
|
}
|
|
if !got.Handled || got.ResponseAllowed || len(got.UpperResponses) != 0 {
|
|
t.Fatalf("raid set symbol handled=%v responseAllowed=%v responses=%d", got.Handled, got.ResponseAllowed, len(got.UpperResponses))
|
|
}
|
|
if !strings.Contains(got.Reason, "RaidSetSymbol") {
|
|
t.Fatalf("reason = %q, want RaidSetSymbol hint", got.Reason)
|
|
}
|
|
}
|
|
|
|
func contextForTest() context.Context { return context.Background() }
|
|
|
|
func buildRaidInfoBodyForTest(route byte, name []byte, tail byte) []byte {
|
|
body := make([]byte, 1+4+len(name)+1)
|
|
body[0] = route
|
|
binary.LittleEndian.PutUint32(body[1:5], uint32(len(name)))
|
|
copy(body[5:], name)
|
|
body[len(body)-1] = tail
|
|
return body
|
|
}
|
|
|
|
func bytesEqual(left []byte, right []byte) bool {
|
|
if len(left) != len(right) {
|
|
return false
|
|
}
|
|
for i := range left {
|
|
if left[i] != right[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|