服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
383 lines
12 KiB
Go
383 lines
12 KiB
Go
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"strings"
|
|
"testing"
|
|
|
|
"longheng.io/server/internal/modules/dnf/alignedcmd"
|
|
"longheng.io/server/internal/modules/dnf/dnfenum"
|
|
)
|
|
|
|
func TestHandlerRecognizesDirectInventoryCommand(t *testing.T) {
|
|
handler := NewHandler()
|
|
got, err := handler.Handle(context.Background(), alignedcmd.Request{
|
|
Opcode: uint16(dnfenum.CmdPacketDisjointItem),
|
|
Body: []byte{0x02, 0x00, 0x00, 0x04, 0x00},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Handle error = %v", err)
|
|
}
|
|
if !got.Handled {
|
|
t.Fatalf("disjoint item should be handled by inventory module")
|
|
}
|
|
if got.ResponseAllowed {
|
|
t.Fatalf("inventory module must not allow response before writer evidence is complete")
|
|
}
|
|
if got.Operation != "disjoint_item" {
|
|
t.Fatalf("operation = %q, want disjoint_item", got.Operation)
|
|
}
|
|
if got.Reason == "" {
|
|
t.Fatalf("pending reason is empty")
|
|
}
|
|
}
|
|
|
|
func TestHandlerRejectsNonInventoryOpcode(t *testing.T) {
|
|
handler := NewHandler()
|
|
got, err := handler.Handle(context.Background(), alignedcmd.Request{Opcode: uint16(dnfenum.CmdPacketMailboxOpen)})
|
|
if err != nil {
|
|
t.Fatalf("Handle error = %v", err)
|
|
}
|
|
if got.Handled {
|
|
t.Fatalf("mailbox opcode must not be handled by inventory module")
|
|
}
|
|
}
|
|
|
|
func TestDecodeUseSharedEffectItemRequestCurrentBody(t *testing.T) {
|
|
got, err := DecodeUseSharedEffectItemRequest([]byte{0x59, 0xE4, 0x34, 0x1D, 0x5D, 0x00})
|
|
if err != nil {
|
|
t.Fatalf("DecodeUseSharedEffectItemRequest error = %v", err)
|
|
}
|
|
if got.SlotIndex != 93 || got.ListType != listTypeMain || uint32(got.ItemTemplateID) != 0x1D34E459 {
|
|
t.Fatalf("decoded type251 request = %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestDecodeUseSharedEffectItemRequestRejectsWrongLengthAndList(t *testing.T) {
|
|
if _, err := DecodeUseSharedEffectItemRequest([]byte{89, 0xE4, 0x34, 0x1D, 0x5D}); err == nil {
|
|
t.Fatal("type251 truncated body unexpectedly accepted")
|
|
}
|
|
if _, err := DecodeUseSharedEffectItemRequest([]byte{89, 0xE4, 0x34, 0x1D, 0x5D, listTypePet}); err == nil {
|
|
t.Fatal("type251 non-main list unexpectedly accepted")
|
|
}
|
|
}
|
|
|
|
func TestDecodeDeleteRequestExtended(t *testing.T) {
|
|
body := []byte{
|
|
0x00, 0x01,
|
|
0x02, 0x00,
|
|
0x05, 0x00,
|
|
0x44, 0x33, 0x22, 0x11,
|
|
0x03, 0x00, 0x00, 0x00,
|
|
0x00,
|
|
}
|
|
got, err := DecodeDeleteRequest(body)
|
|
if err != nil {
|
|
t.Fatalf("DecodeDeleteRequest error = %v", err)
|
|
}
|
|
if !got.Extended || got.StateTrailer || got.ListType != listTypeMain || len(got.Entries) != 1 {
|
|
t.Fatalf("decoded extended header = %+v", got)
|
|
}
|
|
entry := got.Entries[0]
|
|
if entry.OpType != 2 || entry.SlotIndex != 5 || entry.ItemID != 0x11223344 || entry.DeleteCount != 3 {
|
|
t.Fatalf("decoded entry = %+v", entry)
|
|
}
|
|
}
|
|
|
|
func TestDecodeCurrentSortItemRequestFromPacketLog(t *testing.T) {
|
|
body := []byte{0x06, 0x00, 0x00, 0x00, 0x10, 0x00, 0x18, 0x02, 0x20, 0x00}
|
|
got, err := DecodeSortItemRequest(body)
|
|
if err != nil {
|
|
t.Fatalf("DecodeSortItemRequest error = %v", err)
|
|
}
|
|
if got.ListType != listTypeMain || got.Category != 2 || got.Condition != 0 {
|
|
t.Fatalf("decoded sort request = %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestDecodeCurrentDeleteRequestFromPacketLog(t *testing.T) {
|
|
body := []byte{
|
|
0x11, 0x00, 0x00, 0x00,
|
|
0x10, 0x00,
|
|
0x1A, 0x0B,
|
|
0x08, 0x01,
|
|
0x10, 0x09,
|
|
0x18, 0x8B, 0x8A, 0xA8, 0x37,
|
|
0x20, 0x01,
|
|
0x20, 0x00,
|
|
0x00,
|
|
}
|
|
got, err := DecodeDeleteRequest(body)
|
|
if err != nil {
|
|
t.Fatalf("DecodeDeleteRequest error = %v", err)
|
|
}
|
|
if !got.Extended || got.StateTrailer || got.ListType != listTypeMain || len(got.Entries) != 1 {
|
|
t.Fatalf("decoded delete request = %+v", got)
|
|
}
|
|
entry := got.Entries[0]
|
|
if entry.OpType != 1 || entry.SlotIndex != 9 || entry.ItemID != 116000011 || entry.DeleteCount != 1 {
|
|
t.Fatalf("decoded delete entry = %+v", entry)
|
|
}
|
|
}
|
|
|
|
func TestDecodeCurrentDeleteRequestWithSkillStateTrailer(t *testing.T) {
|
|
body := []byte{
|
|
0x10, 0x00, 0x00, 0x00,
|
|
0x10, 0x00,
|
|
0x1A, 0x0A,
|
|
0x08, 0x02,
|
|
0x10, 0xE6, 0x02,
|
|
0x18, 0xDD, 0x17,
|
|
0x20, 0x01,
|
|
0x20, 0x00,
|
|
0x01,
|
|
}
|
|
got, err := DecodeDeleteRequest(body)
|
|
if err != nil {
|
|
t.Fatalf("DecodeDeleteRequest error = %v", err)
|
|
}
|
|
if !got.Extended || !got.StateTrailer || got.ListType != listTypeMain || len(got.Entries) != 1 {
|
|
t.Fatalf("decoded delete request = %+v", got)
|
|
}
|
|
entry := got.Entries[0]
|
|
if entry.OpType != 2 || entry.SlotIndex != 358 || entry.ItemID != 3037 || entry.DeleteCount != 1 {
|
|
t.Fatalf("decoded skill material delete entry = %+v", entry)
|
|
}
|
|
}
|
|
|
|
func TestCurrentInventoryProtobufRequestsRejectMalformedBodies(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
decode func([]byte) error
|
|
body []byte
|
|
}{
|
|
{
|
|
name: "sort truncated envelope",
|
|
decode: func(body []byte) error {
|
|
_, err := DecodeSortItemRequest(body)
|
|
return err
|
|
},
|
|
body: []byte{0x06, 0, 0, 0, 0x10, 0},
|
|
},
|
|
{
|
|
name: "sort wrong wire type",
|
|
decode: func(body []byte) error {
|
|
_, err := DecodeSortItemRequest(body)
|
|
return err
|
|
},
|
|
body: []byte{0x06, 0, 0, 0, 0x12, 0, 0x18, 2, 0x20, 0},
|
|
},
|
|
{
|
|
name: "sort byte overflow",
|
|
decode: func(body []byte) error {
|
|
_, err := DecodeSortItemRequest(body)
|
|
return err
|
|
},
|
|
body: []byte{0x07, 0, 0, 0, 0x10, 0x80, 0x02, 0x18, 2, 0x20, 0},
|
|
},
|
|
{
|
|
name: "delete non-zero terminator",
|
|
decode: func(body []byte) error {
|
|
_, err := DecodeDeleteRequest(body)
|
|
return err
|
|
},
|
|
body: []byte{0x11, 0, 0, 0, 0x10, 0, 0x1A, 0x0B, 0x08, 1, 0x10, 9, 0x18, 0x8B, 0x8A, 0xA8, 0x37, 0x20, 1, 0x20, 0, 2},
|
|
},
|
|
{
|
|
name: "delete missing item id",
|
|
decode: func(body []byte) error {
|
|
_, err := DecodeDeleteRequest(body)
|
|
return err
|
|
},
|
|
body: []byte{0x0C, 0, 0, 0, 0x10, 0, 0x1A, 5, 0x08, 1, 0x10, 9, 0x20, 1, 0x20, 0},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if err := test.decode(test.body); err == nil {
|
|
t.Fatal("malformed current protobuf body unexpectedly accepted")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecodeCurrentDeleteRequestRejectsMoreThan100Entries(t *testing.T) {
|
|
payload := []byte{0x10, listTypeMain}
|
|
entry := []byte{0x1A, 0x08, 0x08, 1, 0x10, 1, 0x18, 1, 0x20, 1}
|
|
for range maxDeleteEntries + 1 {
|
|
payload = append(payload, entry...)
|
|
}
|
|
payload = append(payload, 0x20, 0)
|
|
body := make([]byte, 4, 4+len(payload))
|
|
binary.LittleEndian.PutUint32(body, uint32(len(payload)))
|
|
body = append(body, payload...)
|
|
|
|
if _, err := DecodeDeleteRequest(body); err == nil || !strings.Contains(err.Error(), "more than 100 entries") {
|
|
t.Fatalf("DecodeDeleteRequest error = %v, want entry limit rejection", err)
|
|
}
|
|
}
|
|
|
|
func TestDecodeDeleteOrSellRequestFormats(t *testing.T) {
|
|
withList, err := DecodeDeleteOrSellRequest([]byte{listTypePet, 0x10, 0x00, 0x02, 0x00})
|
|
if err != nil {
|
|
t.Fatalf("DecodeDeleteOrSellRequest with list error = %v", err)
|
|
}
|
|
if !withList.HasListType || withList.ListType != listTypePet || withList.SlotIndex != 16 || withList.Count != 2 {
|
|
t.Fatalf("with list = %+v", withList)
|
|
}
|
|
|
|
legacy, err := DecodeDeleteOrSellRequest([]byte{0x10, 0x00, 0x03, 0x00})
|
|
if err != nil {
|
|
t.Fatalf("DecodeDeleteOrSellRequest legacy error = %v", err)
|
|
}
|
|
if legacy.HasListType || legacy.ListType != listTypeMain || legacy.SlotIndex != 16 || legacy.Count != 3 {
|
|
t.Fatalf("legacy = %+v", legacy)
|
|
}
|
|
}
|
|
|
|
func TestDecodeMoveItemspaceRequest(t *testing.T) {
|
|
body := []byte{
|
|
listTypeMain,
|
|
0x02, 0x00,
|
|
0x44, 0x33, 0x22, 0x11,
|
|
0x05, 0x00, 0x00, 0x00,
|
|
listTypePersonalCargo,
|
|
0x09, 0x00,
|
|
0x88, 0x77, 0x66, 0x55,
|
|
0x03, 0x00, 0x00, 0x00,
|
|
0xFF, 0xFF, 0xFF, 0xFF,
|
|
0x00, 0x00,
|
|
}
|
|
got, err := DecodeMoveItemspaceRequest(body)
|
|
if err != nil {
|
|
t.Fatalf("DecodeMoveItemspaceRequest error = %v", err)
|
|
}
|
|
if got.SourceListType != listTypeMain || got.SourceSlotIndex != 2 || got.SourceInstanceValue != 0x11223344 || got.MoveCount != 5 {
|
|
t.Fatalf("source = %+v", got)
|
|
}
|
|
if got.DestinationListType != listTypePersonalCargo || got.DestinationSlotIndex != 9 || got.DestinationInstanceValue != 0x55667788 || got.DestinationStack != 3 {
|
|
t.Fatalf("destination = %+v", got)
|
|
}
|
|
if got.ActorIndex != -1 || got.TrailingState0 != 0 || got.TrailingState1 != 0 {
|
|
t.Fatalf("current tail = %+v", got)
|
|
}
|
|
if _, err := DecodeMoveItemspaceRequest(body[:22]); err == nil {
|
|
t.Fatal("22-byte legacy/partial move body unexpectedly accepted")
|
|
}
|
|
}
|
|
|
|
func TestDecodeUseStackableRequest(t *testing.T) {
|
|
body := []byte{
|
|
0x04, 0x00,
|
|
listTypePet,
|
|
0x78, 0x56, 0x34, 0x12,
|
|
0x21, 0x43, 0x65, 0x07,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
}
|
|
got, err := DecodeUseStackableRequest(body)
|
|
if err != nil {
|
|
t.Fatalf("DecodeUseStackableRequest error = %v", err)
|
|
}
|
|
if got.SlotIndex != 4 || got.ListType != listTypePet || got.InstanceValue != 0x12345678 || uint32(got.ItemCode) != 0x07654321 || got.Reserved != 0 {
|
|
t.Fatalf("got = %+v", got)
|
|
}
|
|
if _, err := DecodeUseStackableRequest(body[:14]); err == nil {
|
|
t.Fatal("14-byte use-stackable body unexpectedly accepted")
|
|
}
|
|
if _, err := DecodeUseStackableRequest(append(append([]byte(nil), body...), 0)); err == nil {
|
|
t.Fatal("16-byte use-stackable body unexpectedly accepted")
|
|
}
|
|
reserved := append([]byte(nil), body...)
|
|
reserved[11] = 1
|
|
if _, err := DecodeUseStackableRequest(reserved); err == nil {
|
|
t.Fatal("non-zero use-stackable reserved field unexpectedly accepted")
|
|
}
|
|
}
|
|
|
|
func TestDecodeEventServerLevelUpRequest(t *testing.T) {
|
|
got, err := DecodeEventServerLevelUpRequest([]byte{0x41, 0x00, listTypeMain})
|
|
if err != nil {
|
|
t.Fatalf("DecodeEventServerLevelUpRequest error = %v", err)
|
|
}
|
|
if got.SlotIndex != 65 || got.ListType != listTypeMain {
|
|
t.Fatalf("got = %+v", got)
|
|
}
|
|
if _, err := DecodeEventServerLevelUpRequest([]byte{0x41, 0x00}); err == nil {
|
|
t.Fatal("truncated op418 body unexpectedly accepted")
|
|
}
|
|
if _, err := DecodeEventServerLevelUpRequest([]byte{0x41, 0x00, listTypePet}); err == nil {
|
|
t.Fatal("non-main op418 list unexpectedly accepted")
|
|
}
|
|
}
|
|
|
|
func TestDecodeUpgradeAndEnchantRequests(t *testing.T) {
|
|
upgradeBody := []byte{
|
|
0x01, 0x00,
|
|
0x08, 0x00,
|
|
0x44, 0x33, 0x22, 0x11,
|
|
0x0A, 0x00,
|
|
0xFF, 0xFF,
|
|
0x03, 0x00, 0x00, 0x00,
|
|
'a', 'b', 'c',
|
|
}
|
|
upgrade, err := DecodeUpgradeItemRequest(upgradeBody)
|
|
if err != nil {
|
|
t.Fatalf("DecodeUpgradeItemRequest error = %v", err)
|
|
}
|
|
if upgrade.Mode != "amplify" || upgrade.TargetSlotIndex != 8 || upgrade.TargetItemTemplateID != 0x11223344 || upgrade.MaterialSlotIndex != 10 || upgrade.OptionalTicketSlotIndex != -1 || upgrade.TargetItemName != "abc" {
|
|
t.Fatalf("upgrade = %+v", upgrade)
|
|
}
|
|
|
|
enchant, err := DecodeEnchantByBeadRequest([]byte{listTypeMain, 0x03, 0x00, listTypeEquipment, 0x09, 0x00})
|
|
if err != nil {
|
|
t.Fatalf("DecodeEnchantByBeadRequest error = %v", err)
|
|
}
|
|
if enchant.BeadListType != listTypeMain || enchant.BeadSlotIndex != 3 || enchant.TargetListType != listTypeEquipment || enchant.TargetSlotIndex != 9 {
|
|
t.Fatalf("enchant = %+v", enchant)
|
|
}
|
|
}
|
|
|
|
func TestMoveCommandRecordsInventoryOwnerGap(t *testing.T) {
|
|
cmd := NewMoveCommand(alignedcmd.Request{
|
|
AccountID: " acc ",
|
|
SelectedCharacterID: 77,
|
|
}, MoveItemspaceRequest{
|
|
SourceListType: listTypeMain,
|
|
SourceSlotIndex: 2,
|
|
SourceInstanceValue: 0x11223344,
|
|
MoveCount: 5,
|
|
DestinationListType: listTypeEquipment,
|
|
DestinationSlotIndex: 9,
|
|
DestinationInstanceValue: 0x55667788,
|
|
DestinationStack: 3,
|
|
ActorIndex: -1,
|
|
TrailingState0: 4,
|
|
TrailingState1: 5,
|
|
})
|
|
summary := cmd.String()
|
|
for _, want := range []string{`account="acc"`, "char=77", "src=(0,2,0x11223344)", "dst=(3,9,0x55667788)", "actor=-1", "tail=(4,5)", "mutation id", "USERINFO"} {
|
|
if !strings.Contains(summary, want) {
|
|
t.Fatalf("summary %q missing %q", summary, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDisjointCommandRecordsRewardOwnerGap(t *testing.T) {
|
|
cmd := NewDisjointCommand(alignedcmd.Request{
|
|
AccountID: "acc",
|
|
SelectedCharacterID: 88,
|
|
}, DisjointItemRequest{
|
|
TargetSlotIndex: 4,
|
|
ItemSpace: listTypeMain,
|
|
DisjointItemSlotIndex: 9,
|
|
ContextValue: 0x12345678,
|
|
})
|
|
summary := cmd.String()
|
|
for _, want := range []string{"targetSlot=4", "disjointSlot=9", "0x12345678", "disjoint reward owner", "popup"} {
|
|
if !strings.Contains(summary, want) {
|
|
t.Fatalf("summary %q missing %q", summary, want)
|
|
}
|
|
}
|
|
}
|