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

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