DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,965 @@
|
||||
// 本文件集中解析背包类 C2S 请求体。
|
||||
// 字节顺序按最新 EXE/C# 对齐证据整理;解析器只产生请求计划,不直接修改资产或生成成功响应。
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protowire"
|
||||
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
listTypeMain byte = 0
|
||||
listTypeAvatar byte = 1
|
||||
listTypePersonalCargo byte = 2
|
||||
listTypeEquipment byte = 3
|
||||
listTypePet byte = 7
|
||||
listTypeActorWornAlt byte = 17
|
||||
listTypeAccountCargo byte = 12
|
||||
listTypeGuildMedal byte = 38
|
||||
maxDeleteEntries = 100
|
||||
)
|
||||
|
||||
// DeleteRequest 描述删除物品请求;Extended=true 时 body 带批量删除 entry。
|
||||
type DeleteRequest struct {
|
||||
Extended bool
|
||||
StateTrailer bool
|
||||
ListType byte
|
||||
SlotIndex int16
|
||||
Count int16
|
||||
Entries []DeleteEntry
|
||||
}
|
||||
|
||||
// DeleteEntry 是扩展删除格式里的 12 字节 entry。
|
||||
type DeleteEntry struct {
|
||||
OpType int16
|
||||
SlotIndex int16
|
||||
ItemID int32
|
||||
DeleteCount int32
|
||||
}
|
||||
|
||||
// DeleteOrSellRequest 描述删除/出售共用的短格式请求。
|
||||
type DeleteOrSellRequest struct {
|
||||
HasListType bool
|
||||
ListType byte
|
||||
SlotIndex int16
|
||||
Count int16
|
||||
}
|
||||
|
||||
// MoveItemspaceRequest 描述物品移动/拆分请求。
|
||||
type MoveItemspaceRequest struct {
|
||||
SourceListType byte
|
||||
SourceSlotIndex int16
|
||||
SourceInstanceValue int32
|
||||
MoveCount int32
|
||||
DestinationListType byte
|
||||
DestinationSlotIndex int16
|
||||
DestinationInstanceValue int32
|
||||
DestinationStack int32
|
||||
ActorIndex int32
|
||||
TrailingState0 byte
|
||||
TrailingState1 byte
|
||||
}
|
||||
|
||||
// SortItemRequest 描述整理背包请求。
|
||||
type SortItemRequest struct {
|
||||
ListType byte
|
||||
Category byte
|
||||
Condition byte
|
||||
}
|
||||
|
||||
// BuyItemRequest 描述 NPC 购买请求。
|
||||
type BuyItemRequest struct {
|
||||
ItemTemplateID int32
|
||||
Count int32
|
||||
}
|
||||
|
||||
// RepairEquipmentRequest 描述装备维修请求。
|
||||
type RepairEquipmentRequest struct {
|
||||
InvenType byte
|
||||
SlotIndex int16
|
||||
RepairItemSlot int16
|
||||
AutoRepair bool
|
||||
QuickRepair bool
|
||||
}
|
||||
|
||||
// DisjointItemRequest 描述物品分解请求。
|
||||
type DisjointItemRequest struct {
|
||||
TargetSlotIndex int16
|
||||
ItemSpace byte
|
||||
DisjointItemSlotIndex int16
|
||||
ContextValue int32
|
||||
}
|
||||
|
||||
// UseStackableRequest 描述使用消耗品请求。
|
||||
type UseStackableRequest struct {
|
||||
SlotIndex int16
|
||||
ListType byte
|
||||
InstanceValue int32
|
||||
ItemCode int32
|
||||
Reserved uint32
|
||||
}
|
||||
|
||||
// EventServerLevelUpRequest is the current NoPack op418 request. The client
|
||||
// sends only the durable inventory location; item identity is resolved from
|
||||
// the repository and revalidated inside the atomic level-up settlement.
|
||||
type EventServerLevelUpRequest struct {
|
||||
SlotIndex int16
|
||||
ListType byte
|
||||
}
|
||||
|
||||
// IncreaseStatusRequest is the current NoPack op30
|
||||
// (ENUM_CMDPACKET_INCREASE_STATUS) request the client sends when an SP/TP
|
||||
// skill book or a character experience book is double-clicked. Its body is a
|
||||
// bare main-inventory slot: no list owner, no instance value and no item
|
||||
// template, so identity is resolved from the durable row and revalidated
|
||||
// inside the atomic settlement, exactly like op418.
|
||||
type IncreaseStatusRequest struct {
|
||||
SlotIndex int16
|
||||
}
|
||||
|
||||
// UseSharedEffectItemRequest describes the current class-1/type251 request.
|
||||
// Current-client evidence shows a compact six-byte body: item template u32,
|
||||
// source slot byte, then the inventory list byte.
|
||||
type UseSharedEffectItemRequest struct {
|
||||
SlotIndex int16
|
||||
ItemTemplateID int32
|
||||
ListType byte
|
||||
}
|
||||
|
||||
type UseStackableActionRequest struct {
|
||||
SourceSlotIndex int16
|
||||
ListType byte
|
||||
Reserved0 uint32
|
||||
ActionIndex uint32
|
||||
Reserved1 uint32
|
||||
Reserved2 uint32
|
||||
}
|
||||
|
||||
type SelectDamageFontRequest struct {
|
||||
FontIndex uint16
|
||||
}
|
||||
|
||||
// ContainerUpgradeRequest is the current-EXE op430/op408 body: one i16 slot.
|
||||
//
|
||||
// 现场 2026-10-03(conn game-000124,测试x = char 11)抓到的就是 2 字节:
|
||||
//
|
||||
// op430 ENUM_CMDPACKET_UPGRADE_INVENTORY body=`4200`(券在背包槽 66)
|
||||
// op408 ENUM_CMDPACKET_UPGRADE_CARGO body=`4400`(券在背包槽 68)
|
||||
//
|
||||
// 官方服务端(df_game_r(1) 带符号)同名分发器读的也是 `PacketBuf::get_short`,之后
|
||||
// 用 INVEN_TYPE=1 去取槽位物品,本字段口径与它一致。客户端只发背包(list 0)里的券。
|
||||
type ContainerUpgradeRequest struct {
|
||||
SlotIndex int16
|
||||
ListType byte
|
||||
}
|
||||
|
||||
// UpgradeItemRequest 描述强化/增幅请求。
|
||||
type UpgradeItemRequest struct {
|
||||
Mode string
|
||||
RawMode uint16
|
||||
// TargetListType is the high byte of the op50 mode word: 0 for a backpack
|
||||
// row, 3 for a worn equipment slot.
|
||||
TargetListType byte
|
||||
TargetSlotIndex int16
|
||||
TargetItemTemplateID int32
|
||||
MaterialSlotIndex int16
|
||||
OptionalTicketSlotIndex int16
|
||||
TargetItemName string
|
||||
// HighUpgrade is op50 sub mode 2: the 高级装备强化机, fed with 高级炉岩炭
|
||||
// instead of the ordinary 无色小晶块. The mode is otherwise a reinforce.
|
||||
HighUpgrade bool
|
||||
}
|
||||
|
||||
// ChronicleGrowthRequest is the fixed current-EXE class1/op271 body.
|
||||
// TargetTaggedValue is independent from authoritative raw+0x58 and is kept
|
||||
// only for protocol completeness.
|
||||
type ChronicleGrowthRequest struct {
|
||||
TicketSlotIndex int16
|
||||
TicketItemCode int32
|
||||
TargetSlotIndex int16
|
||||
TargetItemCode int32
|
||||
TargetTaggedValue byte
|
||||
MaterialSlotIndex int16
|
||||
MaterialItemCode int32
|
||||
}
|
||||
|
||||
// SeparateUpgradeRequest is the current NoPack class1/op438 weapon-forging
|
||||
// request. The item name is a u32 byte-length DSTR, followed by one independent
|
||||
// trailing byte.
|
||||
type SeparateUpgradeRequest struct {
|
||||
TargetListType byte
|
||||
TargetSlotIndex int16
|
||||
TargetItemTemplateID int32
|
||||
MaterialSlotIndex int16
|
||||
TargetItemName string
|
||||
TrailingFlag byte
|
||||
}
|
||||
|
||||
// ChronicleRefineRequest is the exact current NoPack class1/op370 body.
|
||||
type ChronicleRefineRequest struct {
|
||||
MaterialSlotIndex int16
|
||||
MaterialItemTemplateID int32
|
||||
TargetListType byte
|
||||
TargetSlotIndex int16
|
||||
TargetItemTemplateID int32
|
||||
OptionNo byte
|
||||
}
|
||||
|
||||
// EnchantByBeadRequest 描述宝珠附魔请求。
|
||||
type EnchantByBeadRequest struct {
|
||||
BeadListType byte
|
||||
BeadSlotIndex int16
|
||||
TargetListType byte
|
||||
TargetSlotIndex int16
|
||||
}
|
||||
|
||||
// MonsterCardUpgradeRequest is the exact current NoPack op603 body observed
|
||||
// from the Abello card-upgrade window.
|
||||
type MonsterCardUpgradeRequest struct {
|
||||
ListType byte
|
||||
TargetSlot int16
|
||||
MaterialCount int16
|
||||
MaterialSlot int16
|
||||
}
|
||||
|
||||
// DecodeDeleteRequest 解析删除物品请求,兼容 C# 里 simple 与 extended 两种格式。
|
||||
func DecodeDeleteRequest(body []byte) (DeleteRequest, error) {
|
||||
// Skill-material consumption appends a one-byte state marker (0x01)
|
||||
// after the declared delete protobuf. Keep this exception local to op18.
|
||||
if payload, ok := unwrapCurrentDeleteStateTrailer(body); ok {
|
||||
parsed, err := decodeCurrentDeleteRequest(payload)
|
||||
if err != nil {
|
||||
return DeleteRequest{}, err
|
||||
}
|
||||
parsed.StateTrailer = true
|
||||
return parsed, nil
|
||||
}
|
||||
if payload, current, err := unwrapCurrentProtobufBody(body); current {
|
||||
if err != nil {
|
||||
return DeleteRequest{}, err
|
||||
}
|
||||
return decodeCurrentDeleteRequest(payload)
|
||||
}
|
||||
if len(body) < 4 {
|
||||
return DeleteRequest{}, shortBodyError(len(body), 4)
|
||||
}
|
||||
if len(body) >= 15 && body[1] >= 1 && body[1] <= maxDeleteEntries {
|
||||
count := int(body[1])
|
||||
want := 2 + count*12
|
||||
if len(body) < want {
|
||||
return DeleteRequest{}, shortBodyError(len(body), want)
|
||||
}
|
||||
entries := make([]DeleteEntry, 0, count)
|
||||
for i := 0; i < count; i++ {
|
||||
offset := 2 + i*12
|
||||
entries = append(entries, DeleteEntry{
|
||||
OpType: readI16(body, offset),
|
||||
SlotIndex: readI16(body, offset+2),
|
||||
ItemID: readI32(body, offset+4),
|
||||
DeleteCount: readI32(body, offset+8),
|
||||
})
|
||||
}
|
||||
return DeleteRequest{Extended: true, ListType: body[0], Entries: entries}, nil
|
||||
}
|
||||
|
||||
simple, err := DecodeDeleteOrSellRequest(body)
|
||||
if err != nil {
|
||||
return DeleteRequest{}, err
|
||||
}
|
||||
return DeleteRequest{
|
||||
ListType: simple.ListType,
|
||||
SlotIndex: simple.SlotIndex,
|
||||
Count: simple.Count,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DecodeDeleteOrSellRequest 解析删除/出售短格式;首字节是已知 list type 时使用带 list 格式。
|
||||
func unwrapCurrentDeleteStateTrailer(body []byte) ([]byte, bool) {
|
||||
if len(body) < 5 {
|
||||
return nil, false
|
||||
}
|
||||
declared := uint64(binary.LittleEndian.Uint32(body[:4]))
|
||||
available := uint64(len(body) - 4)
|
||||
if declared+1 != available || body[len(body)-1] != 1 || declared > uint64(len(body)-5) {
|
||||
return nil, false
|
||||
}
|
||||
return body[4 : 4+int(declared)], true
|
||||
}
|
||||
|
||||
func DecodeDeleteOrSellRequest(body []byte) (DeleteOrSellRequest, error) {
|
||||
if len(body) < 4 {
|
||||
return DeleteOrSellRequest{}, shortBodyError(len(body), 4)
|
||||
}
|
||||
if len(body) >= 5 && isKnownListType(body[0]) {
|
||||
return DeleteOrSellRequest{
|
||||
HasListType: true,
|
||||
ListType: body[0],
|
||||
SlotIndex: readI16(body, 1),
|
||||
Count: readI16(body, 3),
|
||||
}, nil
|
||||
}
|
||||
return DeleteOrSellRequest{
|
||||
ListType: listTypeMain,
|
||||
SlotIndex: readI16(body, 0),
|
||||
Count: readI16(body, 2),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DecodeMoveItemspaceRequest decodes the 28-byte layout emitted by the current
|
||||
// NoPack.exe senders sub_2326C90, sub_232EA00, and sub_1B33B40.
|
||||
func DecodeMoveItemspaceRequest(body []byte) (MoveItemspaceRequest, error) {
|
||||
const currentMoveItemspaceBodySize = 28
|
||||
if len(body) != currentMoveItemspaceBodySize {
|
||||
return MoveItemspaceRequest{}, fmt.Errorf("move itemspace body length %d, want %d", len(body), currentMoveItemspaceBodySize)
|
||||
}
|
||||
return MoveItemspaceRequest{
|
||||
SourceListType: body[0],
|
||||
SourceSlotIndex: readI16(body, 1),
|
||||
SourceInstanceValue: readI32(body, 3),
|
||||
MoveCount: readI32(body, 7),
|
||||
DestinationListType: body[11],
|
||||
DestinationSlotIndex: readI16(body, 12),
|
||||
DestinationInstanceValue: readI32(body, 14),
|
||||
DestinationStack: readI32(body, 18),
|
||||
ActorIndex: readI32(body, 22),
|
||||
TrailingState0: body[26],
|
||||
TrailingState1: body[27],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DecodeSortItemRequest 解析整理背包请求。
|
||||
func DecodeSortItemRequest(body []byte) (SortItemRequest, error) {
|
||||
if payload, current, err := unwrapCurrentProtobufBody(body); current {
|
||||
if err != nil {
|
||||
return SortItemRequest{}, err
|
||||
}
|
||||
return decodeCurrentSortItemRequest(payload)
|
||||
}
|
||||
if len(body) < 2 {
|
||||
return SortItemRequest{}, shortBodyError(len(body), 2)
|
||||
}
|
||||
req := SortItemRequest{ListType: body[0], Category: body[1]}
|
||||
if len(body) > 2 {
|
||||
req.Condition = body[2]
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// unwrapCurrentProtobufBody recognizes the current u32-length-prefixed body.
|
||||
// One zero byte after the protobuf is accepted because current delete senders
|
||||
// append that terminator. Legacy S4A12 bodies are left to the old decoders.
|
||||
func unwrapCurrentProtobufBody(body []byte) ([]byte, bool, error) {
|
||||
if len(body) < 4 {
|
||||
return nil, false, nil
|
||||
}
|
||||
declared := uint64(binary.LittleEndian.Uint32(body[:4]))
|
||||
available := uint64(len(body) - 4)
|
||||
if declared == available {
|
||||
return body[4:], true, nil
|
||||
}
|
||||
if declared+1 == available {
|
||||
if body[len(body)-1] != 0 {
|
||||
return nil, true, fmt.Errorf("protobuf body has non-zero terminator 0x%02X", body[len(body)-1])
|
||||
}
|
||||
return body[4 : len(body)-1], true, nil
|
||||
}
|
||||
// Current inventory protobufs are small and therefore have a zero upper
|
||||
// length prefix. Treat a mismatch as truncation instead of unsafe fallback.
|
||||
if body[1] == 0 && body[2] == 0 && body[3] == 0 {
|
||||
return nil, true, fmt.Errorf("protobuf body length %d, want %d or %d with zero terminator", available, declared, declared+1)
|
||||
}
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
func decodeCurrentSortItemRequest(payload []byte) (SortItemRequest, error) {
|
||||
var req SortItemRequest
|
||||
var seen uint8
|
||||
for len(payload) > 0 {
|
||||
number, wireType, n := protowire.ConsumeTag(payload)
|
||||
if n < 0 {
|
||||
return SortItemRequest{}, fmt.Errorf("sort protobuf tag: %w", protowire.ParseError(n))
|
||||
}
|
||||
payload = payload[n:]
|
||||
if wireType != protowire.VarintType || number < 2 || number > 4 {
|
||||
return SortItemRequest{}, fmt.Errorf("sort protobuf field %d has unsupported wire type %d", number, wireType)
|
||||
}
|
||||
value, n := protowire.ConsumeVarint(payload)
|
||||
if n < 0 {
|
||||
return SortItemRequest{}, fmt.Errorf("sort protobuf field %d: %w", number, protowire.ParseError(n))
|
||||
}
|
||||
payload = payload[n:]
|
||||
if value > math.MaxUint8 {
|
||||
return SortItemRequest{}, fmt.Errorf("sort protobuf field %d value %d overflows byte", number, value)
|
||||
}
|
||||
bit := uint8(1 << (number - 2))
|
||||
if seen&bit != 0 {
|
||||
return SortItemRequest{}, fmt.Errorf("sort protobuf field %d is duplicated", number)
|
||||
}
|
||||
seen |= bit
|
||||
switch number {
|
||||
case 2:
|
||||
req.ListType = byte(value)
|
||||
case 3:
|
||||
req.Category = byte(value)
|
||||
case 4:
|
||||
req.Condition = byte(value)
|
||||
}
|
||||
}
|
||||
if seen != 0x07 {
|
||||
return SortItemRequest{}, fmt.Errorf("sort protobuf required fields missing: mask=0x%02X", seen)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeCurrentDeleteRequest(payload []byte) (DeleteRequest, error) {
|
||||
var req DeleteRequest
|
||||
req.Extended = true
|
||||
var seenListType, seenCondition bool
|
||||
for len(payload) > 0 {
|
||||
number, wireType, n := protowire.ConsumeTag(payload)
|
||||
if n < 0 {
|
||||
return DeleteRequest{}, fmt.Errorf("delete protobuf tag: %w", protowire.ParseError(n))
|
||||
}
|
||||
payload = payload[n:]
|
||||
switch number {
|
||||
case 2, 4:
|
||||
if wireType != protowire.VarintType {
|
||||
return DeleteRequest{}, fmt.Errorf("delete protobuf field %d has wire type %d, want varint", number, wireType)
|
||||
}
|
||||
value, consumed := protowire.ConsumeVarint(payload)
|
||||
if consumed < 0 {
|
||||
return DeleteRequest{}, fmt.Errorf("delete protobuf field %d: %w", number, protowire.ParseError(consumed))
|
||||
}
|
||||
payload = payload[consumed:]
|
||||
if number == 2 {
|
||||
if seenListType || value > math.MaxUint8 {
|
||||
return DeleteRequest{}, fmt.Errorf("delete protobuf list type is duplicated or invalid: %d", value)
|
||||
}
|
||||
req.ListType = byte(value)
|
||||
seenListType = true
|
||||
} else {
|
||||
if seenCondition || value != 0 {
|
||||
return DeleteRequest{}, fmt.Errorf("delete protobuf condition is duplicated or non-zero: %d", value)
|
||||
}
|
||||
seenCondition = true
|
||||
}
|
||||
case 3:
|
||||
if wireType != protowire.BytesType {
|
||||
return DeleteRequest{}, fmt.Errorf("delete protobuf field 3 has wire type %d, want bytes", wireType)
|
||||
}
|
||||
if len(req.Entries) >= maxDeleteEntries {
|
||||
return DeleteRequest{}, fmt.Errorf("delete protobuf has more than %d entries", maxDeleteEntries)
|
||||
}
|
||||
nested, consumed := protowire.ConsumeBytes(payload)
|
||||
if consumed < 0 {
|
||||
return DeleteRequest{}, fmt.Errorf("delete protobuf entry: %w", protowire.ParseError(consumed))
|
||||
}
|
||||
payload = payload[consumed:]
|
||||
entry, err := decodeCurrentDeleteEntry(nested)
|
||||
if err != nil {
|
||||
return DeleteRequest{}, err
|
||||
}
|
||||
req.Entries = append(req.Entries, entry)
|
||||
default:
|
||||
return DeleteRequest{}, fmt.Errorf("delete protobuf field %d is unsupported", number)
|
||||
}
|
||||
}
|
||||
if !seenListType || !seenCondition || len(req.Entries) == 0 {
|
||||
return DeleteRequest{}, fmt.Errorf("delete protobuf required fields missing: list=%t entries=%d condition=%t", seenListType, len(req.Entries), seenCondition)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeCurrentDeleteEntry(payload []byte) (DeleteEntry, error) {
|
||||
var entry DeleteEntry
|
||||
var seen uint8
|
||||
for len(payload) > 0 {
|
||||
number, wireType, n := protowire.ConsumeTag(payload)
|
||||
if n < 0 {
|
||||
return DeleteEntry{}, fmt.Errorf("delete entry protobuf tag: %w", protowire.ParseError(n))
|
||||
}
|
||||
payload = payload[n:]
|
||||
if wireType != protowire.VarintType || number < 1 || number > 4 {
|
||||
return DeleteEntry{}, fmt.Errorf("delete entry protobuf field %d has unsupported wire type %d", number, wireType)
|
||||
}
|
||||
value, consumed := protowire.ConsumeVarint(payload)
|
||||
if consumed < 0 {
|
||||
return DeleteEntry{}, fmt.Errorf("delete entry protobuf field %d: %w", number, protowire.ParseError(consumed))
|
||||
}
|
||||
payload = payload[consumed:]
|
||||
bit := uint8(1 << (number - 1))
|
||||
if seen&bit != 0 {
|
||||
return DeleteEntry{}, fmt.Errorf("delete entry protobuf field %d is duplicated", number)
|
||||
}
|
||||
seen |= bit
|
||||
switch number {
|
||||
case 1:
|
||||
if value > math.MaxInt16 {
|
||||
return DeleteEntry{}, fmt.Errorf("delete entry op type %d overflows int16", value)
|
||||
}
|
||||
entry.OpType = int16(value)
|
||||
case 2:
|
||||
if value > math.MaxInt16 {
|
||||
return DeleteEntry{}, fmt.Errorf("delete entry slot %d overflows int16", value)
|
||||
}
|
||||
entry.SlotIndex = int16(value)
|
||||
case 3:
|
||||
if value == 0 || value > math.MaxInt32 {
|
||||
return DeleteEntry{}, fmt.Errorf("delete entry item id %d is invalid", value)
|
||||
}
|
||||
entry.ItemID = int32(value)
|
||||
case 4:
|
||||
if value == 0 || value > math.MaxInt32 {
|
||||
return DeleteEntry{}, fmt.Errorf("delete entry count %d is invalid", value)
|
||||
}
|
||||
entry.DeleteCount = int32(value)
|
||||
}
|
||||
}
|
||||
if seen != 0x0F {
|
||||
return DeleteEntry{}, fmt.Errorf("delete entry protobuf required fields missing: mask=0x%02X", seen)
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
// DecodeBuyItemRequest 解析 NPC 购买请求。
|
||||
func DecodeBuyItemRequest(body []byte) (BuyItemRequest, error) {
|
||||
if len(body) < 4 {
|
||||
return BuyItemRequest{}, shortBodyError(len(body), 4)
|
||||
}
|
||||
req := BuyItemRequest{ItemTemplateID: readI32(body, 0), Count: 1}
|
||||
if len(body) >= 8 {
|
||||
req.Count = readI32(body, 4)
|
||||
if req.Count <= 0 {
|
||||
req.Count = 1
|
||||
}
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// DecodeRepairEquipmentRequest 解析装备维修请求。
|
||||
func DecodeRepairEquipmentRequest(body []byte) (RepairEquipmentRequest, error) {
|
||||
if len(body) < 5 {
|
||||
return RepairEquipmentRequest{}, shortBodyError(len(body), 5)
|
||||
}
|
||||
return RepairEquipmentRequest{
|
||||
InvenType: body[0],
|
||||
SlotIndex: readI16(body, 1),
|
||||
RepairItemSlot: readI16(body, 3),
|
||||
AutoRepair: len(body) >= 6 && body[5] == 1,
|
||||
QuickRepair: len(body) >= 8 && body[7] == 1,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DecodeDisjointItemRequest 解析物品分解请求。
|
||||
func DecodeDisjointItemRequest(body []byte) (DisjointItemRequest, error) {
|
||||
if len(body) < 5 {
|
||||
return DisjointItemRequest{}, shortBodyError(len(body), 5)
|
||||
}
|
||||
req := DisjointItemRequest{
|
||||
TargetSlotIndex: readI16(body, 0),
|
||||
ItemSpace: body[2],
|
||||
DisjointItemSlotIndex: readI16(body, 3),
|
||||
}
|
||||
if len(body) >= 9 {
|
||||
req.ContextValue = readI32(body, 5)
|
||||
}
|
||||
if req.TargetSlotIndex < 0 {
|
||||
return DisjointItemRequest{}, fmt.Errorf("target slot %d invalid", req.TargetSlotIndex)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// DecodeUseStackableRequest 解析使用消耗品请求。
|
||||
func DecodeUseStackableRequest(body []byte) (UseStackableRequest, error) {
|
||||
const currentUseStackableBodySize = 15
|
||||
if len(body) != currentUseStackableBodySize {
|
||||
return UseStackableRequest{}, fmt.Errorf("use stackable body length %d, want %d", len(body), currentUseStackableBodySize)
|
||||
}
|
||||
req := UseStackableRequest{
|
||||
SlotIndex: readI16(body, 0),
|
||||
ListType: body[2],
|
||||
InstanceValue: readI32(body, 3),
|
||||
ItemCode: readI32(body, 7),
|
||||
Reserved: readU32(body, 11),
|
||||
}
|
||||
if req.SlotIndex < 0 {
|
||||
return UseStackableRequest{}, fmt.Errorf("use stackable slot %d invalid", req.SlotIndex)
|
||||
}
|
||||
if req.InstanceValue == 0 || req.ItemCode <= 0 {
|
||||
return UseStackableRequest{}, fmt.Errorf("use stackable identity invalid: instance=0x%08X item=%d", uint32(req.InstanceValue), req.ItemCode)
|
||||
}
|
||||
if req.Reserved != 0 {
|
||||
return UseStackableRequest{}, fmt.Errorf("use stackable reserved value 0x%08X, want 0", req.Reserved)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// DecodeEventServerLevelUpRequest parses the exact current NoPack op418 body:
|
||||
// u16 slot followed by u8 inventory list.
|
||||
func DecodeEventServerLevelUpRequest(body []byte) (EventServerLevelUpRequest, error) {
|
||||
const bodySize = 3
|
||||
if len(body) != bodySize {
|
||||
return EventServerLevelUpRequest{}, fmt.Errorf("event server level-up body length %d, want %d", len(body), bodySize)
|
||||
}
|
||||
req := EventServerLevelUpRequest{
|
||||
SlotIndex: readI16(body, 0),
|
||||
ListType: body[2],
|
||||
}
|
||||
if req.SlotIndex < 0 {
|
||||
return EventServerLevelUpRequest{}, fmt.Errorf("event server level-up slot %d invalid", req.SlotIndex)
|
||||
}
|
||||
if req.ListType != listTypeMain {
|
||||
return EventServerLevelUpRequest{}, fmt.Errorf("event server level-up list %d, want main inventory", req.ListType)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// DecodeIncreaseStatusRequest parses the exact current NoPack op30 body: one
|
||||
// u16 main-inventory slot. Two live captures carried the double-clicked books'
|
||||
// own slots (0x42 and 0x43) and nothing else.
|
||||
func DecodeIncreaseStatusRequest(body []byte) (IncreaseStatusRequest, error) {
|
||||
const bodySize = 2
|
||||
if len(body) != bodySize {
|
||||
return IncreaseStatusRequest{}, fmt.Errorf("increase status body length %d, want %d", len(body), bodySize)
|
||||
}
|
||||
req := IncreaseStatusRequest{SlotIndex: readI16(body, 0)}
|
||||
if req.SlotIndex < 0 {
|
||||
return IncreaseStatusRequest{}, fmt.Errorf("increase status slot %d invalid", req.SlotIndex)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// DecodeUseSharedEffectItemRequest parses the exact current type251 body.
|
||||
func DecodeUseSharedEffectItemRequest(body []byte) (UseSharedEffectItemRequest, error) {
|
||||
const bodySize = 6
|
||||
if len(body) != bodySize {
|
||||
return UseSharedEffectItemRequest{}, fmt.Errorf("use shared effect item body length %d, want %d", len(body), bodySize)
|
||||
}
|
||||
req := UseSharedEffectItemRequest{
|
||||
ItemTemplateID: readI32(body, 0),
|
||||
SlotIndex: int16(body[4]),
|
||||
ListType: body[5],
|
||||
}
|
||||
if req.ItemTemplateID <= 0 {
|
||||
return UseSharedEffectItemRequest{}, fmt.Errorf("use shared effect item template invalid: 0x%08X", uint32(req.ItemTemplateID))
|
||||
}
|
||||
if err := checkUseStackableListType(req.ListType); err != nil {
|
||||
return UseSharedEffectItemRequest{}, err
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func DecodeUseStackableActionRequest(body []byte) (UseStackableActionRequest, error) {
|
||||
const bodySize = 19
|
||||
if len(body) != bodySize {
|
||||
return UseStackableActionRequest{}, fmt.Errorf("use stackable action body length %d, want %d", len(body), bodySize)
|
||||
}
|
||||
req := UseStackableActionRequest{
|
||||
SourceSlotIndex: readI16(body, 0),
|
||||
ListType: body[2],
|
||||
Reserved0: readU32(body, 3),
|
||||
ActionIndex: readU32(body, 7),
|
||||
Reserved1: readU32(body, 11),
|
||||
Reserved2: readU32(body, 15),
|
||||
}
|
||||
if req.SourceSlotIndex < 0 {
|
||||
return UseStackableActionRequest{}, fmt.Errorf("use stackable action slot %d invalid", req.SourceSlotIndex)
|
||||
}
|
||||
if req.ListType != listTypeMain {
|
||||
return UseStackableActionRequest{}, fmt.Errorf("use stackable action list %d, want %d", req.ListType, listTypeMain)
|
||||
}
|
||||
if req.ActionIndex != dungeonAdmissionTicketActionIndex && req.ActionIndex != damageFontActionIndex &&
|
||||
req.ActionIndex != questCompletionTicketActionIndex && req.ActionIndex != fatiguePotionActionIndex &&
|
||||
req.ActionIndex != rentalPointActionIndex && req.ActionIndex != accountCargoUpgradeActionIndex {
|
||||
return UseStackableActionRequest{}, fmt.Errorf("use stackable action index %d unsupported", req.ActionIndex)
|
||||
}
|
||||
if req.Reserved0 != 0 || req.Reserved1 != 0 || req.Reserved2 != 0 {
|
||||
return UseStackableActionRequest{}, fmt.Errorf("use stackable action reserved values (%d,%d,%d), want all zero", req.Reserved0, req.Reserved1, req.Reserved2)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func DecodeSelectDamageFontRequest(body []byte) (SelectDamageFontRequest, error) {
|
||||
if len(body) != 2 {
|
||||
return SelectDamageFontRequest{}, fmt.Errorf("select damage font body length %d, want 2", len(body))
|
||||
}
|
||||
return SelectDamageFontRequest{FontIndex: readU16(body, 0)}, nil
|
||||
}
|
||||
|
||||
// DecodeContainerUpgradeRequest 解析 op430/op408 的扩容券请求:只有一个 i16 槽位。
|
||||
//
|
||||
// 严格 2 字节:现场 11 次 op430 + 1 次 op408 全是这个形状,多一个字节就说明这不是
|
||||
// 我们证过的那条路,宁可拒绝也不猜。槽位必须落在背包主表(list 0)且不是账号共享
|
||||
// 固定槽(354..365),因为券只可能躺在普通背包格里。
|
||||
func DecodeContainerUpgradeRequest(body []byte) (ContainerUpgradeRequest, error) {
|
||||
const bodySize = 2
|
||||
if len(body) != bodySize {
|
||||
return ContainerUpgradeRequest{}, fmt.Errorf("container upgrade body length %d, want %d", len(body), bodySize)
|
||||
}
|
||||
req := ContainerUpgradeRequest{
|
||||
SlotIndex: readI16(body, 0),
|
||||
ListType: listTypeMain,
|
||||
}
|
||||
if req.SlotIndex < 0 {
|
||||
return ContainerUpgradeRequest{}, fmt.Errorf("container upgrade slot %d invalid", req.SlotIndex)
|
||||
}
|
||||
if dnfrepo.IsAccountSharedInventorySlot(req.ListType, req.SlotIndex) {
|
||||
return ContainerUpgradeRequest{}, fmt.Errorf("container upgrade slot %d is an account-shared fixed slot", req.SlotIndex)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// DecodeUpgradeItemRequest 解析强化/增幅请求。
|
||||
func DecodeUpgradeItemRequest(body []byte) (UpgradeItemRequest, error) {
|
||||
if len(body) < 16 {
|
||||
return UpgradeItemRequest{}, shortBodyError(len(body), 16)
|
||||
}
|
||||
// The current client packs the two-byte mode word as target list (high
|
||||
// byte) + sub mode (low byte): 0x0000 list-0 reinforce, 0x0001 list-0
|
||||
// amplify, 0x0300 worn list-3 reinforce, 0x0301 worn list-3 amplify.
|
||||
// Reading it as one flat u16 and refusing anything above 1 rejected every
|
||||
// upgrade on a worn item at decode with "raw mode 768 invalid".
|
||||
rawMode := readU16(body, 0)
|
||||
listType := byte(rawMode >> 8)
|
||||
subMode := byte(rawMode & 0xFF)
|
||||
// 公会徽章 (list 38) is a reinforce-only path. Upstream refuses list 38 with
|
||||
// an amplify sub mode and confines it to the medal page slots 0..48: the
|
||||
// native flag_upgrade_table is keyed by flag level, so there is no row for
|
||||
// an amplify and nothing outside the page to address.
|
||||
targetSlot := readI16(body, 2)
|
||||
guildMedal := listType == listTypeGuildMedal && subMode == 0 && targetSlot >= 0 && targetSlot <= 48
|
||||
if listType != listTypeMain && listType != listTypeEquipment && !guildMedal {
|
||||
return UpgradeItemRequest{}, fmt.Errorf("raw mode 0x%04X invalid: target list %d unsupported", rawMode, listType)
|
||||
}
|
||||
// Sub mode 2 is the 高级装备强化机: the client names the 高级炉岩炭 stack in
|
||||
// the material slot instead of the 无色小晶块 the ordinary machine uses.
|
||||
// Live capture 2026-09-11 13:35:37, mode 0x0002, material slot 126 holding
|
||||
// item 2681644 (高级炉岩炭). It was rejected outright until then, so the
|
||||
// machine had never worked on this server.
|
||||
if subMode > 2 {
|
||||
return UpgradeItemRequest{}, fmt.Errorf("raw mode 0x%04X invalid: sub mode %d unsupported", rawMode, subMode)
|
||||
}
|
||||
nameLength := readI32(body, 12)
|
||||
if nameLength < 0 {
|
||||
return UpgradeItemRequest{}, fmt.Errorf("name length %d invalid", nameLength)
|
||||
}
|
||||
end := 16 + int(nameLength)
|
||||
if len(body) < end {
|
||||
return UpgradeItemRequest{}, shortBodyError(len(body), end)
|
||||
}
|
||||
mode := "reinforce"
|
||||
if subMode == 1 {
|
||||
mode = "amplify"
|
||||
}
|
||||
return UpgradeItemRequest{
|
||||
Mode: mode,
|
||||
HighUpgrade: subMode == 2,
|
||||
RawMode: rawMode,
|
||||
TargetListType: listType,
|
||||
TargetSlotIndex: targetSlot,
|
||||
TargetItemTemplateID: readI32(body, 4),
|
||||
MaterialSlotIndex: readI16(body, 8),
|
||||
OptionalTicketSlotIndex: readI16(body, 10),
|
||||
TargetItemName: string(body[16:end]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DecodeSeparateUpgradeRequest decodes the exact current-client op438 body:
|
||||
// u8 list, i16 target slot, i32 item id, i16 material slot, DSTR name, u8 tail.
|
||||
func DecodeChronicleGrowthRequest(body []byte) (ChronicleGrowthRequest, error) {
|
||||
if len(body) != 19 {
|
||||
return ChronicleGrowthRequest{}, fmt.Errorf("chronicle growth body length %d, want 19", len(body))
|
||||
}
|
||||
req := ChronicleGrowthRequest{
|
||||
TicketSlotIndex: readI16(body, 0),
|
||||
TicketItemCode: readI32(body, 2),
|
||||
TargetSlotIndex: readI16(body, 6),
|
||||
TargetItemCode: readI32(body, 8),
|
||||
TargetTaggedValue: body[12],
|
||||
MaterialSlotIndex: readI16(body, 13),
|
||||
MaterialItemCode: readI32(body, 15),
|
||||
}
|
||||
if req.TicketSlotIndex < 0 || req.TargetSlotIndex < 0 ||
|
||||
req.TicketItemCode <= 0 || req.TargetItemCode <= 0 {
|
||||
return ChronicleGrowthRequest{}, fmt.Errorf("chronicle growth request contains invalid slot/id")
|
||||
}
|
||||
// Equipment-emancipation books own a multi-material recipe, so the current
|
||||
// client cannot name one material stack in this fixed body. It sends the
|
||||
// native (-1, 0xffff) auto-material sentinel instead. Ordinary Chronicle
|
||||
// growth still names one concrete material stack and item code.
|
||||
autoMaterials := req.MaterialSlotIndex == -1 && req.MaterialItemCode == 0xffff
|
||||
explicitMaterial := req.MaterialSlotIndex >= 0 && req.MaterialItemCode > 0
|
||||
if !autoMaterials && !explicitMaterial {
|
||||
return ChronicleGrowthRequest{}, fmt.Errorf("chronicle growth request contains invalid material slot/id")
|
||||
}
|
||||
if req.TicketSlotIndex == req.TargetSlotIndex ||
|
||||
(req.MaterialSlotIndex >= 0 && (req.MaterialSlotIndex == req.TargetSlotIndex || req.MaterialSlotIndex == req.TicketSlotIndex)) {
|
||||
return ChronicleGrowthRequest{}, fmt.Errorf("chronicle growth slots overlap")
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func DecodeSeparateUpgradeRequest(body []byte) (SeparateUpgradeRequest, error) {
|
||||
const fixedSizeBeforeName = 13
|
||||
const minimumBodySize = fixedSizeBeforeName + 1
|
||||
if len(body) < minimumBodySize {
|
||||
return SeparateUpgradeRequest{}, shortBodyError(len(body), minimumBodySize)
|
||||
}
|
||||
nameLength := binary.LittleEndian.Uint32(body[9:13])
|
||||
if uint64(nameLength) > uint64(len(body)-minimumBodySize) {
|
||||
return SeparateUpgradeRequest{}, fmt.Errorf("separate upgrade name length %d exceeds body %d", nameLength, len(body))
|
||||
}
|
||||
nameEnd := fixedSizeBeforeName + int(nameLength)
|
||||
if nameEnd+1 != len(body) {
|
||||
return SeparateUpgradeRequest{}, fmt.Errorf("separate upgrade body length %d, want %d from DSTR", len(body), nameEnd+1)
|
||||
}
|
||||
return SeparateUpgradeRequest{
|
||||
TargetListType: body[0],
|
||||
TargetSlotIndex: readI16(body, 1),
|
||||
TargetItemTemplateID: readI32(body, 3),
|
||||
MaterialSlotIndex: readI16(body, 7),
|
||||
TargetItemName: string(body[fixedSizeBeforeName:nameEnd]),
|
||||
TrailingFlag: body[nameEnd],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DecodeChronicleRefineRequest decodes the fixed 14-byte body emitted by
|
||||
// SendChronicleRefineRequest_Op370 in the current client.
|
||||
func DecodeChronicleRefineRequest(body []byte) (ChronicleRefineRequest, error) {
|
||||
const bodySize = 14
|
||||
if len(body) != bodySize {
|
||||
return ChronicleRefineRequest{}, fmt.Errorf("chronicle refine body length %d, want %d", len(body), bodySize)
|
||||
}
|
||||
return ChronicleRefineRequest{
|
||||
MaterialSlotIndex: readI16(body, 0),
|
||||
MaterialItemTemplateID: readI32(body, 2),
|
||||
TargetListType: body[6],
|
||||
TargetSlotIndex: readI16(body, 7),
|
||||
TargetItemTemplateID: readI32(body, 9),
|
||||
OptionNo: body[13],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DecodeEnchantByBeadRequest 解析宝珠附魔请求。
|
||||
func DecodeEnchantByBeadRequest(body []byte) (EnchantByBeadRequest, error) {
|
||||
if len(body) < 6 {
|
||||
return EnchantByBeadRequest{}, shortBodyError(len(body), 6)
|
||||
}
|
||||
return EnchantByBeadRequest{
|
||||
BeadListType: body[0],
|
||||
BeadSlotIndex: readI16(body, 1),
|
||||
TargetListType: body[3],
|
||||
TargetSlotIndex: readI16(body, 4),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DecodeMonsterCardUpgradeRequest decodes the fixed seven-byte op603 body:
|
||||
// u8 inventory list, i16 target slot, i16 material count, i16 material slot.
|
||||
func DecodeMonsterCardUpgradeRequest(body []byte) (MonsterCardUpgradeRequest, error) {
|
||||
if len(body) != 7 {
|
||||
return MonsterCardUpgradeRequest{}, fmt.Errorf("monster card upgrade body=%d want=7", len(body))
|
||||
}
|
||||
return MonsterCardUpgradeRequest{
|
||||
ListType: body[0],
|
||||
TargetSlot: readI16(body, 1),
|
||||
MaterialCount: readI16(body, 3),
|
||||
MaterialSlot: readI16(body, 5),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r DeleteRequest) String() string {
|
||||
if r.Extended {
|
||||
return fmt.Sprintf("extended list=%d entries=%d", r.ListType, len(r.Entries))
|
||||
}
|
||||
return fmt.Sprintf("list=%d slot=%d count=%d", r.ListType, r.SlotIndex, r.Count)
|
||||
}
|
||||
|
||||
func (r DeleteOrSellRequest) String() string {
|
||||
return fmt.Sprintf("hasList=%t list=%d slot=%d count=%d", r.HasListType, r.ListType, r.SlotIndex, r.Count)
|
||||
}
|
||||
|
||||
func (r MoveItemspaceRequest) String() string {
|
||||
return fmt.Sprintf("src=(%d,%d,0x%08X) count=%d dst=(%d,%d,0x%08X) dstStack=%d actor=%d tail=(%d,%d)", r.SourceListType, r.SourceSlotIndex, uint32(r.SourceInstanceValue), r.MoveCount, r.DestinationListType, r.DestinationSlotIndex, uint32(r.DestinationInstanceValue), r.DestinationStack, r.ActorIndex, r.TrailingState0, r.TrailingState1)
|
||||
}
|
||||
|
||||
func (r SortItemRequest) String() string {
|
||||
return fmt.Sprintf("list=%d category=%d condition=%d", r.ListType, r.Category, r.Condition)
|
||||
}
|
||||
|
||||
func (r BuyItemRequest) String() string {
|
||||
return fmt.Sprintf("item=%d count=%d", r.ItemTemplateID, r.Count)
|
||||
}
|
||||
|
||||
func (r RepairEquipmentRequest) String() string {
|
||||
return fmt.Sprintf("inven=%d slot=%d repairSlot=%d auto=%t quick=%t", r.InvenType, r.SlotIndex, r.RepairItemSlot, r.AutoRepair, r.QuickRepair)
|
||||
}
|
||||
|
||||
func (r DisjointItemRequest) String() string {
|
||||
return fmt.Sprintf("targetSlot=%d itemSpace=%d disjointSlot=%d ctx=0x%08X", r.TargetSlotIndex, r.ItemSpace, r.DisjointItemSlotIndex, uint32(r.ContextValue))
|
||||
}
|
||||
|
||||
func (r UseStackableRequest) String() string {
|
||||
return fmt.Sprintf("slot=%d list=%d instance=0x%08X item=0x%08X reserved=0x%08X", r.SlotIndex, r.ListType, uint32(r.InstanceValue), uint32(r.ItemCode), r.Reserved)
|
||||
}
|
||||
|
||||
func (r EventServerLevelUpRequest) String() string {
|
||||
return fmt.Sprintf("slot=%d list=%d", r.SlotIndex, r.ListType)
|
||||
}
|
||||
|
||||
func (r UseSharedEffectItemRequest) String() string {
|
||||
return fmt.Sprintf("slot=%d list=%d item=0x%08X", r.SlotIndex, r.ListType, uint32(r.ItemTemplateID))
|
||||
}
|
||||
|
||||
func (r UseStackableActionRequest) String() string {
|
||||
return fmt.Sprintf("slot=%d list=%d action=%d reserved=(%d,%d,%d)", r.SourceSlotIndex, r.ListType, r.ActionIndex, r.Reserved0, r.Reserved1, r.Reserved2)
|
||||
}
|
||||
|
||||
func (r SelectDamageFontRequest) String() string {
|
||||
return fmt.Sprintf("font=%d", r.FontIndex)
|
||||
}
|
||||
|
||||
func (r UpgradeItemRequest) String() string {
|
||||
return fmt.Sprintf("mode=%s target=(%d,%d) material=%d ticket=%d name=%q", r.Mode, r.TargetSlotIndex, r.TargetItemTemplateID, r.MaterialSlotIndex, r.OptionalTicketSlotIndex, r.TargetItemName)
|
||||
}
|
||||
|
||||
func (r SeparateUpgradeRequest) String() string {
|
||||
return fmt.Sprintf("target=(%d,%d,%d) material=%d name=%q tail=%d", r.TargetListType, r.TargetSlotIndex, r.TargetItemTemplateID, r.MaterialSlotIndex, r.TargetItemName, r.TrailingFlag)
|
||||
}
|
||||
|
||||
func (r EnchantByBeadRequest) String() string {
|
||||
return fmt.Sprintf("bead=(%d,%d) target=(%d,%d)", r.BeadListType, r.BeadSlotIndex, r.TargetListType, r.TargetSlotIndex)
|
||||
}
|
||||
|
||||
func isKnownListType(v byte) bool {
|
||||
switch v {
|
||||
case listTypeMain, listTypeAvatar, listTypePersonalCargo, listTypeEquipment, listTypePet, listTypeAccountCargo, listTypeGuildMedal:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func shortBodyError(got int, want int) error {
|
||||
return fmt.Errorf("body too short: got %d want >= %d", got, want)
|
||||
}
|
||||
|
||||
func readI16(body []byte, offset int) int16 {
|
||||
return int16(binary.LittleEndian.Uint16(body[offset:]))
|
||||
}
|
||||
|
||||
func readU16(body []byte, offset int) uint16 {
|
||||
return binary.LittleEndian.Uint16(body[offset:])
|
||||
}
|
||||
|
||||
func readI32(body []byte, offset int) int32 {
|
||||
return int32(binary.LittleEndian.Uint32(body[offset:]))
|
||||
}
|
||||
|
||||
func readU32(body []byte, offset int) uint32 {
|
||||
return binary.LittleEndian.Uint32(body[offset:])
|
||||
}
|
||||
Reference in new issue
Block a user