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,501 @@
|
||||
package titlebook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
ListType byte = 100
|
||||
SlotBase int16 = 1000
|
||||
MaxPerCategory = 200
|
||||
|
||||
// TitleEquipmentSlot 是当前客户端 list-3(穿戴栏)里的称号槽位号。
|
||||
//
|
||||
// 依据 2026-09-18 玩家实测:从背包正常穿戴称号时客户端发的是
|
||||
// op19 MOVE_ITEMSPACE「源 list0:11 → 目标 list3:13」(packet_log 15:36:07),
|
||||
// 服务端按该槽位落库后小退重登依然在。称号簿的「右键使用」客户端只会本地穿上
|
||||
// (同一次操作里没有再发任何装备请求),所以服务端必须自己落到这个槽位。
|
||||
TitleEquipmentSlot int16 = 13
|
||||
)
|
||||
|
||||
var (
|
||||
ErrOwnerUnavailable = errors.New("title book owner unavailable")
|
||||
ErrCharacterRequired = errors.New("selected character id required")
|
||||
ErrInventoryNotFound = errors.New("title book inventory not found")
|
||||
ErrSourceNotFound = errors.New("title book source item not found")
|
||||
ErrCategoryInvalid = errors.New("title book category or index is invalid")
|
||||
ErrCategoryFull = errors.New("title book category is full")
|
||||
ErrTargetFull = errors.New("title book target inventory is full")
|
||||
ErrEquipmentNotFound = errors.New("title book character equipment not found")
|
||||
)
|
||||
|
||||
// EquipResult 是「从称号簿直接使用(装备)」的结果。
|
||||
// 称号按官方语义从簿里移出(见 Equip 注释),所以调用方要刷新称号簿页。
|
||||
type EquipResult struct {
|
||||
CharacterID string
|
||||
ItemSpace int32
|
||||
ItemID int32
|
||||
Category int32
|
||||
BookIndex int32
|
||||
EquipmentSlot int16
|
||||
EquipmentEntry dnfrepo.EquipmentEntry
|
||||
// Replaced 为真表示原来穿着另一个称号,它被放回背包 ReturnSlot。
|
||||
Replaced bool
|
||||
ReturnSlot int16
|
||||
ReturnItem dnfrepo.ItemStack
|
||||
}
|
||||
|
||||
type PutCommand struct {
|
||||
SelectedCharacterID uint16
|
||||
ItemSpace int32
|
||||
SourceSlot int16
|
||||
ItemID int32
|
||||
Category int32
|
||||
BookIndex int32
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type PutResult struct {
|
||||
CharacterID string
|
||||
ItemSpace int32
|
||||
SourceList byte
|
||||
SourceSlot int16
|
||||
ItemID int32
|
||||
Category int32
|
||||
BookIndex int32
|
||||
TargetSlot int16
|
||||
}
|
||||
|
||||
type GetCommand struct {
|
||||
SelectedCharacterID uint16
|
||||
ItemSpace int32
|
||||
SourceSlot int16
|
||||
// PreferredSlot 是客户端在 op413 里指定的背包落点:客户端会按回包里的槽位把物品
|
||||
// 画在那个格子上,所以能放下就必须用这个槽(放不下再退回第一个空格)。
|
||||
PreferredSlot int16
|
||||
ItemID int32
|
||||
Category int32
|
||||
BookIndex int32
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type GetResult struct {
|
||||
CharacterID string
|
||||
ItemSpace int32
|
||||
SourceSlot int16
|
||||
ItemID int32
|
||||
Category int32
|
||||
BookIndex int32
|
||||
TargetSlot int16
|
||||
TargetStack dnfrepo.ItemStack
|
||||
}
|
||||
|
||||
type Owner struct {
|
||||
settlements dnfrepo.CharacterSettlementUnitOfWork
|
||||
}
|
||||
|
||||
func NewOwner(repos dnfrepo.Group) (*Owner, error) {
|
||||
if repos.Inventory == nil || repos.CharacterSettlement == nil {
|
||||
return nil, ErrOwnerUnavailable
|
||||
}
|
||||
return &Owner{settlements: repos.CharacterSettlement}, nil
|
||||
}
|
||||
|
||||
func (o *Owner) Put(ctx context.Context, cmd PutCommand) (PutResult, error) {
|
||||
if o == nil || o.settlements == nil {
|
||||
return PutResult{}, ErrOwnerUnavailable
|
||||
}
|
||||
if err := validateBookLocation(cmd.Category, cmd.BookIndex); err != nil {
|
||||
return PutResult{}, err
|
||||
}
|
||||
characterID, err := selectedCharacterKey(cmd.SelectedCharacterID)
|
||||
if err != nil {
|
||||
return PutResult{}, err
|
||||
}
|
||||
var result PutResult
|
||||
err = o.settlements.WithinCharacterSettlement(ctx, characterID, func(tx dnfrepo.Group) error {
|
||||
inventory, loadErr := loadInventory(ctx, tx.Inventory, characterID)
|
||||
if loadErr != nil {
|
||||
return loadErr
|
||||
}
|
||||
sourceList := byte(cmd.ItemSpace)
|
||||
sourceKey := inventoryKey(sourceList, cmd.SourceSlot)
|
||||
sourceStack, found := inventory.Slots[sourceKey]
|
||||
if !found || sourceStack.ItemID != int64(cmd.ItemID) || sourceStack.ItemID <= 0 {
|
||||
return fmt.Errorf(
|
||||
"%w: list=%d slot=%d item=%d",
|
||||
ErrSourceNotFound,
|
||||
sourceList,
|
||||
cmd.SourceSlot,
|
||||
cmd.ItemID,
|
||||
)
|
||||
}
|
||||
targetSlot, bookIndex, found := firstBookSlot(inventory.Slots, cmd.Category, cmd.BookIndex)
|
||||
if !found {
|
||||
return fmt.Errorf("%w: category=%d", ErrCategoryFull, cmd.Category)
|
||||
}
|
||||
|
||||
delete(inventory.Slots, sourceKey)
|
||||
titleStack := sourceStack
|
||||
titleStack.Count = 1
|
||||
if titleStack.Extra == nil {
|
||||
titleStack.Extra = make(map[string]string, 4)
|
||||
}
|
||||
titleStack.Extra["title_book_category"] = strconv.FormatInt(int64(cmd.Category), 10)
|
||||
titleStack.Extra["title_book_index"] = strconv.FormatInt(int64(bookIndex), 10)
|
||||
titleStack.Extra["source"] = "title_book_put"
|
||||
inventory.Slots[inventoryKey(ListType, targetSlot)] = titleStack
|
||||
inventory.UpdatedAt = mutationTime(cmd.UpdatedAt)
|
||||
if saveErr := dnfrepo.SaveInventoryFields(ctx, tx.Inventory, inventory, dnfrepo.InventoryFieldSlots); saveErr != nil {
|
||||
return saveErr
|
||||
}
|
||||
result = PutResult{
|
||||
CharacterID: characterID,
|
||||
ItemSpace: cmd.ItemSpace,
|
||||
SourceList: sourceList,
|
||||
SourceSlot: cmd.SourceSlot,
|
||||
ItemID: cmd.ItemID,
|
||||
Category: cmd.Category,
|
||||
BookIndex: bookIndex,
|
||||
TargetSlot: targetSlot,
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (o *Owner) Get(ctx context.Context, cmd GetCommand) (GetResult, error) {
|
||||
if o == nil || o.settlements == nil {
|
||||
return GetResult{}, ErrOwnerUnavailable
|
||||
}
|
||||
if err := validateBookLocation(cmd.Category, cmd.BookIndex); err != nil {
|
||||
return GetResult{}, err
|
||||
}
|
||||
characterID, err := selectedCharacterKey(cmd.SelectedCharacterID)
|
||||
if err != nil {
|
||||
return GetResult{}, err
|
||||
}
|
||||
var result GetResult
|
||||
err = o.settlements.WithinCharacterSettlement(ctx, characterID, func(tx dnfrepo.Group) error {
|
||||
inventory, loadErr := loadInventory(ctx, tx.Inventory, characterID)
|
||||
if loadErr != nil {
|
||||
return loadErr
|
||||
}
|
||||
sourceKey := BookSlotKey(cmd.Category, cmd.BookIndex)
|
||||
titleStack, found := inventory.Slots[sourceKey]
|
||||
if !found || titleStack.ItemID <= 0 ||
|
||||
(cmd.ItemID > 0 && titleStack.ItemID != int64(cmd.ItemID)) {
|
||||
return fmt.Errorf(
|
||||
"%w: category=%d index=%d item=%d",
|
||||
ErrSourceNotFound,
|
||||
cmd.Category,
|
||||
cmd.BookIndex,
|
||||
cmd.ItemID,
|
||||
)
|
||||
}
|
||||
targetSlot, found := preferredMainInventorySlot(inventory.Slots, cmd.PreferredSlot)
|
||||
if !found {
|
||||
return ErrTargetFull
|
||||
}
|
||||
|
||||
delete(inventory.Slots, sourceKey)
|
||||
targetStack := titleStack
|
||||
targetStack.Count = 1
|
||||
if targetStack.Extra == nil {
|
||||
targetStack.Extra = make(map[string]string, 2)
|
||||
}
|
||||
targetStack.Extra["source"] = "title_book_get"
|
||||
inventory.Slots[inventoryKey(dnfrepo.MainInventoryListType, targetSlot)] = targetStack
|
||||
inventory.UpdatedAt = mutationTime(cmd.UpdatedAt)
|
||||
if saveErr := dnfrepo.SaveInventoryFields(ctx, tx.Inventory, inventory, dnfrepo.InventoryFieldSlots); saveErr != nil {
|
||||
return saveErr
|
||||
}
|
||||
result = GetResult{
|
||||
CharacterID: characterID,
|
||||
ItemSpace: cmd.ItemSpace,
|
||||
SourceSlot: cmd.SourceSlot,
|
||||
ItemID: int32(titleStack.ItemID),
|
||||
Category: cmd.Category,
|
||||
BookIndex: cmd.BookIndex,
|
||||
TargetSlot: targetSlot,
|
||||
TargetStack: targetStack,
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
func BookSlotKey(category, index int32) string {
|
||||
return inventoryKey(ListType, int16(category)*SlotBase+int16(index))
|
||||
}
|
||||
|
||||
// Equip 实现「称号簿右键直接使用」:把簿里登记的称号**穿到身上**并落库,
|
||||
// 而不是像 Get 那样塞回背包。
|
||||
//
|
||||
// 背景(2026-09-18 玩家报障):客户端在 op413 之后会自己把称号穿上,但服务端
|
||||
// 只把物品放回背包,于是出现「背包一个、穿戴栏一个」,而穿戴栏那个因为服务端
|
||||
// 从未落库,小退重登就消失。这里改为:直接写 list-3 称号槽(槽位号见
|
||||
// TitleEquipmentSlot),登录时随装备一起回放,所以重登后仍在。
|
||||
//
|
||||
// 称号簿条目按官方语义**移动**(穿到身上就不再留在簿里),不是复制。依据官方
|
||||
// 参考服务端 df_game_r(1):CTitleBook::getTitle(0x086425C4) 在目标物品空间是
|
||||
// 装备(space=3)时依次调用 CInventory::tryInsertItemIntoEquipment(0x8501AFA)
|
||||
// → CTitleBook::_deleteTitle(0x8641D50) → CInventory::insertItemIntoEquipment
|
||||
// (0x8502344),也就是「先穿上、再把条目从簿里删掉」,最后只调
|
||||
// CUser::send_equip(0x865DD14)——而 send_equip 对槽位 13(称号)直接 return
|
||||
// (它只广播槽位 0..0xB 与 0x16 的外观变化)。所以客户端是凭 413 回包里的
|
||||
// (space=3, slot=13) 自己完成本地穿戴的,服务端不需要也不应该再补发物品列表;
|
||||
// 条目留在簿里只会让玩家同时看到「簿里一个、身上一个」。
|
||||
//
|
||||
// 若身上已经穿着**另一个**称号,则把它放回背包第一个空位(不这么做就会凭空
|
||||
// 丢东西),并把这件事报给调用方用于刷新。
|
||||
func (o *Owner) Equip(ctx context.Context, cmd GetCommand) (EquipResult, error) {
|
||||
if o == nil || o.settlements == nil {
|
||||
return EquipResult{}, ErrOwnerUnavailable
|
||||
}
|
||||
if err := validateBookLocation(cmd.Category, cmd.BookIndex); err != nil {
|
||||
return EquipResult{}, err
|
||||
}
|
||||
characterID, err := selectedCharacterKey(cmd.SelectedCharacterID)
|
||||
if err != nil {
|
||||
return EquipResult{}, err
|
||||
}
|
||||
var result EquipResult
|
||||
err = o.settlements.WithinCharacterSettlement(ctx, characterID, func(tx dnfrepo.Group) error {
|
||||
inventory, loadErr := loadInventory(ctx, tx.Inventory, characterID)
|
||||
if loadErr != nil {
|
||||
return loadErr
|
||||
}
|
||||
sourceKey := BookSlotKey(cmd.Category, cmd.BookIndex)
|
||||
titleStack, found := inventory.Slots[sourceKey]
|
||||
if !found || titleStack.ItemID <= 0 ||
|
||||
(cmd.ItemID > 0 && titleStack.ItemID != int64(cmd.ItemID)) {
|
||||
return fmt.Errorf(
|
||||
"%w: category=%d index=%d item=%d",
|
||||
ErrSourceNotFound,
|
||||
cmd.Category,
|
||||
cmd.BookIndex,
|
||||
cmd.ItemID,
|
||||
)
|
||||
}
|
||||
equipment, equipFound, loadErr := tx.Equipment.Load(ctx, characterID)
|
||||
if loadErr != nil {
|
||||
return loadErr
|
||||
}
|
||||
if !equipFound {
|
||||
equipment = dnfrepo.EquipmentRecord{CharacterID: characterID}
|
||||
}
|
||||
if equipment.Entries == nil {
|
||||
equipment.Entries = make(map[string]dnfrepo.EquipmentEntry, 1)
|
||||
}
|
||||
slotKey := strconv.FormatInt(int64(TitleEquipmentSlot), 10)
|
||||
|
||||
// 移动语义:称号离开称号簿,同一件东西不会同时存在于簿里和身上。
|
||||
delete(inventory.Slots, sourceKey)
|
||||
|
||||
previous, occupied := equipment.Entries[slotKey]
|
||||
if occupied && previous.ItemID != 0 && previous.ItemID != titleStack.ItemID {
|
||||
returnSlot, slotFound := firstMainInventorySlot(inventory.Slots)
|
||||
if !slotFound {
|
||||
return ErrTargetFull
|
||||
}
|
||||
returned := dnfrepo.ItemStack{
|
||||
ItemID: previous.ItemID,
|
||||
Count: 1,
|
||||
Bind: previous.Bind,
|
||||
ExpireAt: previous.ExpireAt,
|
||||
RawEntry: rewriteEquipmentRowSlot(previous.RawEntry, returnSlot),
|
||||
Extra: cloneTitleBookExtra(previous.Extra),
|
||||
}
|
||||
if returned.Extra == nil {
|
||||
returned.Extra = make(map[string]string, 2)
|
||||
}
|
||||
delete(returned.Extra, "equipment_slot")
|
||||
delete(returned.Extra, "equipped_slot")
|
||||
returned.Extra["source"] = "title_book_equip_replaced"
|
||||
inventory.Slots[inventoryKey(dnfrepo.MainInventoryListType, returnSlot)] = returned
|
||||
result.Replaced = true
|
||||
result.ReturnSlot = returnSlot
|
||||
result.ReturnItem = returned
|
||||
}
|
||||
|
||||
// 【2026-09-28 修复:礼白666 型卡死】achievement.grantTitle 把称号写进称号簿时
|
||||
// **不写 RawEntry**。这里如果原样把空 raw 搬进装备行,装备行的 RawEntry 就是空的,
|
||||
// 而 equip 的 unequip() 开头就有一道 `len(source.RawEntry) == 0 →
|
||||
// ErrMoveRawEntryMissing`,于是这个称号从此摘不下来(玩家报障:称号展示栏卡了
|
||||
// 一个成就称号)。簿里没有行数据时,按称号的构造规则补一行 43 字节的。
|
||||
raw := rewriteEquipmentRowSlot(titleStack.RawEntry, TitleEquipmentSlot)
|
||||
// 2026-09-28 线上回退:补行本身没被证伪,但它和上面那三行一起上线时出现了回归,
|
||||
// 先关掉保持行为与改动前一致。
|
||||
if false && len(raw) == 0 && titleStack.ItemID > 0 && titleStack.ItemID <= int64(^uint32(0)) {
|
||||
raw = EquipmentRawEntry(titleStack.ItemID, TitleEquipmentSlot)
|
||||
}
|
||||
extra := cloneTitleBookExtra(titleStack.Extra)
|
||||
if extra == nil {
|
||||
extra = make(map[string]string, 8)
|
||||
}
|
||||
extra["equipment_slot"] = slotKey
|
||||
extra["equipped_slot"] = slotKey
|
||||
extra["item_kind"] = "equipment"
|
||||
extra["source"] = "title_book_equip"
|
||||
extra["title_book_category"] = strconv.FormatInt(int64(cmd.Category), 10)
|
||||
extra["title_book_index"] = strconv.FormatInt(int64(cmd.BookIndex), 10)
|
||||
entry := dnfrepo.EquipmentEntry{
|
||||
SlotIndex: TitleEquipmentSlot,
|
||||
ItemID: titleStack.ItemID,
|
||||
Bind: titleStack.Bind,
|
||||
ExpireAt: titleStack.ExpireAt,
|
||||
RawEntry: raw,
|
||||
Extra: extra,
|
||||
}
|
||||
equipment.Entries[slotKey] = entry
|
||||
equipment.UpdatedAt = mutationTime(cmd.UpdatedAt)
|
||||
|
||||
// 称号簿条目被删除(或旧称号被换回背包)都属于背包变更,必须落库。
|
||||
inventory.UpdatedAt = mutationTime(cmd.UpdatedAt)
|
||||
if saveErr := dnfrepo.SaveInventoryFields(ctx, tx.Inventory, inventory, dnfrepo.InventoryFieldSlots); saveErr != nil {
|
||||
return saveErr
|
||||
}
|
||||
if saveErr := dnfrepo.SaveEquipmentFields(ctx, tx.Equipment, equipment, dnfrepo.EquipmentFieldEntries); saveErr != nil {
|
||||
return saveErr
|
||||
}
|
||||
result.CharacterID = characterID
|
||||
result.ItemSpace = cmd.ItemSpace
|
||||
result.ItemID = int32(titleStack.ItemID)
|
||||
result.Category = cmd.Category
|
||||
result.BookIndex = cmd.BookIndex
|
||||
result.EquipmentSlot = TitleEquipmentSlot
|
||||
result.EquipmentEntry = entry
|
||||
return nil
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
// rewriteEquipmentRowSlot 把一行 0x77 物品行的槽位字段改成目标槽,并返回副本。
|
||||
// 称号在背包里存的是 list-0 行,穿到 list-3 后行首的槽位号必须跟着变,客户端才
|
||||
// 会把它画在穿戴栏的称号格上(实测落库行首是 0D00 = 13)。
|
||||
func rewriteEquipmentRowSlot(raw []byte, slot int16) []byte {
|
||||
out := append([]byte(nil), raw...)
|
||||
if len(out) >= 2 {
|
||||
out[0] = byte(slot)
|
||||
out[1] = byte(uint16(slot) >> 8)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// EquipmentRawEntry 按当前客户端的 43 字节装备行构造规则造一行称号装备数据:
|
||||
// 字节 0 是角色装备槽,+1 是 PVF 物品号,+5 是构造器要求的 create 标量 1,其余为 0。
|
||||
//
|
||||
// 用途:achievement.grantTitle 入簿时不写 RawEntry,穿到装备栏时若不补行,装备行
|
||||
// 就会是空的,equip 的 unequip() 会以 ErrMoveRawEntryMissing 把它锁死(玩家报障
|
||||
// "称号摘不下来")。equip 包的 buildCurrentTitleEquipmentRawEntry 现在直接转调这里,
|
||||
// 两份实现不会再漂移。
|
||||
func EquipmentRawEntry(itemID int64, slot int16) []byte {
|
||||
raw := make([]byte, 43)
|
||||
raw[0] = byte(slot)
|
||||
binary.LittleEndian.PutUint32(raw[1:5], uint32(itemID))
|
||||
binary.LittleEndian.PutUint32(raw[5:9], 1)
|
||||
return raw
|
||||
}
|
||||
|
||||
func cloneTitleBookExtra(extra map[string]string) map[string]string {
|
||||
if len(extra) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]string, len(extra)+4)
|
||||
for key, value := range extra {
|
||||
out[key] = value
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func firstBookSlot(items map[string]dnfrepo.ItemStack, category, preferred int32) (int16, int32, bool) {
|
||||
if preferred >= 0 && preferred < MaxPerCategory {
|
||||
slot := int16(category)*SlotBase + int16(preferred)
|
||||
if _, occupied := items[inventoryKey(ListType, slot)]; !occupied {
|
||||
return slot, preferred, true
|
||||
}
|
||||
}
|
||||
for index := int32(0); index < MaxPerCategory; index++ {
|
||||
slot := int16(category)*SlotBase + int16(index)
|
||||
if _, occupied := items[inventoryKey(ListType, slot)]; !occupied {
|
||||
return slot, index, true
|
||||
}
|
||||
}
|
||||
return -1, -1, false
|
||||
}
|
||||
|
||||
func firstMainInventorySlot(items map[string]dnfrepo.ItemStack) (int16, bool) {
|
||||
for slot := int16(9); slot <= 64; slot++ {
|
||||
if _, occupied := items[inventoryKey(dnfrepo.MainInventoryListType, slot)]; !occupied {
|
||||
return slot, true
|
||||
}
|
||||
}
|
||||
return -1, false
|
||||
}
|
||||
|
||||
// preferredMainInventorySlot 优先用客户端指定的落点,占用时退回第一个空格。
|
||||
//
|
||||
// 依据 2026-09-18 玩家报障:称号从簿里「拿出来」时客户端会发
|
||||
// op413 (space=0, slot=<期望格子>),并且**按回包里的槽位**把物品画在背包那个格子上。
|
||||
// 旧实现忽略这个槽位、ack 还回错了空间(把装备槽 13 报给背包请求),于是玩家看到
|
||||
// 「背包里有一个」,但服务端实际把它穿上了,小退重登就变成「自动穿戴」。
|
||||
func preferredMainInventorySlot(items map[string]dnfrepo.ItemStack, preferred int16) (int16, bool) {
|
||||
if preferred >= 9 && preferred <= 64 {
|
||||
if _, occupied := items[inventoryKey(dnfrepo.MainInventoryListType, preferred)]; !occupied {
|
||||
return preferred, true
|
||||
}
|
||||
}
|
||||
return firstMainInventorySlot(items)
|
||||
}
|
||||
|
||||
func inventoryKey(listType byte, slot int16) string {
|
||||
return fmt.Sprintf("%d:%d", listType, slot)
|
||||
}
|
||||
|
||||
func mutationTime(value time.Time) time.Time {
|
||||
if value.IsZero() {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
return value.UTC()
|
||||
}
|
||||
|
||||
func selectedCharacterKey(selectedCharacterID uint16) (string, error) {
|
||||
if selectedCharacterID == 0 {
|
||||
return "", ErrCharacterRequired
|
||||
}
|
||||
return strconv.FormatUint(uint64(selectedCharacterID), 10), nil
|
||||
}
|
||||
|
||||
func loadInventory(ctx context.Context, repository dnfrepo.InventoryRepository, characterID string) (dnfrepo.InventoryRecord, error) {
|
||||
if repository == nil {
|
||||
return dnfrepo.InventoryRecord{}, ErrOwnerUnavailable
|
||||
}
|
||||
inventory, found, err := repository.Load(ctx, characterID)
|
||||
if err != nil {
|
||||
return dnfrepo.InventoryRecord{}, err
|
||||
}
|
||||
if !found || strings.TrimSpace(inventory.CharacterID) != characterID {
|
||||
return dnfrepo.InventoryRecord{}, fmt.Errorf("%w: character=%s", ErrInventoryNotFound, characterID)
|
||||
}
|
||||
inventory = dnfrepo.CloneInventory(inventory)
|
||||
if inventory.Slots == nil {
|
||||
inventory.Slots = make(map[string]dnfrepo.ItemStack)
|
||||
}
|
||||
return inventory, nil
|
||||
}
|
||||
|
||||
func validateBookLocation(category, index int32) error {
|
||||
if category < 0 || category >= 5 || index < 0 || index >= MaxPerCategory {
|
||||
return fmt.Errorf("%w: category=%d index=%d", ErrCategoryInvalid, category, index)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user