服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
481 lines
17 KiB
Go
481 lines
17 KiB
Go
package dnfbridge
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
dnfdungeon "longheng.io/server/internal/modules/dnf/dungeon"
|
||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||
dnfsocketemblem "longheng.io/server/internal/modules/dnf/socketemblem"
|
||
)
|
||
|
||
// 本文件实现 GM 发放时的 **附魔宝珠** 与 **时装/装备徽章**。
|
||
//
|
||
// 为什么不能只写 Extra
|
||
// 徽章不是普通的 Extra 键值:它走 socketemblem 的
|
||
// data[0]=已开孔数 + data[1..]=各孔徽章 ID 的向量,而且镶嵌时
|
||
// currentApplyEquipmentEmblems 会**从背包消耗**对应徽章行。
|
||
// 所以发放带徽章的装备必须复用那套投影逻辑,不能自己拼 key。
|
||
//
|
||
// 附魔宝珠相对简单:权威写法是 inventory.setEnchantFields ——
|
||
// Extra["value_a"] / ["enchant_card_id"] = 宝珠 item_id
|
||
// Extra["byte_12"] / ["enchant_upgrade_count"] = 升级次数
|
||
// 其中 value_a 会被 currentItemListEntryFromStack 写进 raw[0x0E:0x12]。
|
||
|
||
const (
|
||
gmEmblemListMain byte = currentSocketListMain
|
||
gmEmblemListAvatar byte = currentSocketListAvatar
|
||
gmEmblemMaxSockets = 4
|
||
)
|
||
|
||
// GMEmblemRequest 是请求里的一枚徽章。
|
||
//
|
||
// SocketIndex 省略(<0)时按出现顺序自动分配 0,1,2,…;
|
||
// EmblemSlot 省略(<0)时按 EmblemItemID 在主背包里找第一行并消耗 1 个。
|
||
type GMEmblemRequest struct {
|
||
SocketIndex *int `json:"socket_index,omitempty"`
|
||
EmblemItemID uint32 `json:"emblem_item_id"`
|
||
EmblemSlot *int `json:"emblem_slot,omitempty"`
|
||
}
|
||
|
||
// GMItemWithExtrasRequest 对应 POST /local/dnf-gm/item/grant-with-emblems。
|
||
type GMItemWithExtrasRequest struct {
|
||
CharacterID string `json:"character_id"`
|
||
ItemID uint32 `json:"item_id"`
|
||
// 附魔宝珠:直接落到目标装备上(不消耗背包里的宝珠行)。
|
||
EnchantBeadItemID uint32 `json:"enchant_bead_item_id,omitempty"`
|
||
EnchantUpgrade *int `json:"enchant_upgrade_count,omitempty"`
|
||
// 徽章
|
||
Emblems []GMEmblemRequest `json:"emblems,omitempty"`
|
||
// 已开孔数;0 表示按徽章数量自动开孔(上限由装备部位决定)。
|
||
OpenSockets *int `json:"open_sockets,omitempty"`
|
||
}
|
||
|
||
// GMItemWithExtrasResult 描述带附魔/徽章发放的结果。
|
||
type GMItemWithExtrasResult struct {
|
||
CharacterID string `json:"character_id"`
|
||
ItemID uint32 `json:"item_id"`
|
||
ListType byte `json:"list_type"`
|
||
Slot int `json:"slot"`
|
||
EntryKey string `json:"entry_key"`
|
||
ItemName string `json:"item_name,omitempty"`
|
||
IsAvatar bool `json:"is_avatar"`
|
||
EnchantCardID int64 `json:"enchant_card_id,omitempty"`
|
||
OpenSockets int `json:"open_sockets"`
|
||
Sockets []int64 `json:"sockets,omitempty"`
|
||
ConsumedEmblems []string `json:"consumed_emblems,omitempty"`
|
||
Extra map[string]string `json:"extra,omitempty"`
|
||
}
|
||
|
||
// GMGrantItemWithEmblems 发放一件带附魔宝珠与徽章的装备 / 时装。
|
||
//
|
||
// 流程(同一事务内):
|
||
// 1. 构造道具行,写入附魔字段(Extra + 由 RawEntry 序列化承载 0x0E 宝珠 ID)
|
||
// 2. 放入背包(沿用副本拾取那套容器/槽位规则)
|
||
// 3. 若给了徽章:先把徽章行放进背包(以便被消耗),再按 socketemblem 的投影
|
||
// 逻辑开孔并镶嵌,最后写回目标行的 socket 向量
|
||
func (s *Service) GMGrantItemWithEmblems(
|
||
ctx context.Context,
|
||
request GMItemWithExtrasRequest,
|
||
) (GMItemWithExtrasResult, error) {
|
||
result := GMItemWithExtrasResult{}
|
||
if s == nil {
|
||
return result, ErrGMItemGrantUnavailable
|
||
}
|
||
if ctx == nil {
|
||
ctx = context.Background()
|
||
}
|
||
characterID := strings.TrimSpace(request.CharacterID)
|
||
if characterID == "" {
|
||
return result, fmt.Errorf("%w: character_id is required", ErrGMRequestInvalid)
|
||
}
|
||
if request.ItemID == 0 {
|
||
return result, fmt.Errorf("%w: item_id is required", ErrGMRequestInvalid)
|
||
}
|
||
result.CharacterID = characterID
|
||
result.ItemID = request.ItemID
|
||
|
||
catalog, err := s.currentPVFItemCatalog()
|
||
if err != nil {
|
||
return result, fmt.Errorf("%w: %v", ErrGMItemGrantUnavailable, err)
|
||
}
|
||
definition, err := catalog.ResolveItem(request.ItemID)
|
||
if err != nil {
|
||
return result, fmt.Errorf("%w: item=%d resolved=false", ErrGMItemNotFound, request.ItemID)
|
||
}
|
||
result.ItemName = definition.Name
|
||
|
||
// 目标必须是装备/时装:附魔与徽章只对这两类有意义。
|
||
rule, ruleOK := currentEquipmentPlacementRuleForPVFType(definition.EquipmentType)
|
||
isAvatar := ruleOK && rule.class == currentEquipmentPlacementClassAvatar
|
||
if definition.Kind != dungeonDropItemEquipment {
|
||
return result, fmt.Errorf("%w: item=%d is not equipment; enchant/emblems require equipment",
|
||
ErrGMRequestInvalid, request.ItemID)
|
||
}
|
||
result.IsAvatar = isAvatar
|
||
|
||
if request.OpenSockets != nil && (*request.OpenSockets < 0 || *request.OpenSockets > gmEmblemMaxSockets) {
|
||
return result, fmt.Errorf("%w: open_sockets must be 0..%d", ErrGMRequestInvalid, gmEmblemMaxSockets)
|
||
}
|
||
|
||
// 附魔宝珠校验:必须是已解析的道具。
|
||
var enchantCardID int64
|
||
if request.EnchantBeadItemID != 0 {
|
||
if _, beadErr := catalog.ResolveItem(request.EnchantBeadItemID); beadErr != nil {
|
||
return result, fmt.Errorf("%w: enchant bead item=%d resolved=false",
|
||
ErrGMItemNotFound, request.EnchantBeadItemID)
|
||
}
|
||
enchantCardID = int64(request.EnchantBeadItemID)
|
||
}
|
||
upgradeCount := 0
|
||
if request.EnchantUpgrade != nil {
|
||
if *request.EnchantUpgrade < 0 || *request.EnchantUpgrade > 255 {
|
||
return result, fmt.Errorf("%w: enchant_upgrade_count must be 0..255", ErrGMRequestInvalid)
|
||
}
|
||
upgradeCount = *request.EnchantUpgrade
|
||
}
|
||
|
||
stack, buildErr := s.buildGMGrantItemStack(ctx, catalog, definition, 1, gmGrantItemOptions{})
|
||
if buildErr != nil {
|
||
return result, buildErr
|
||
}
|
||
if enchantCardID != 0 {
|
||
// 与 inventory.setEnchantFields 写同样的键集合。
|
||
stack.Extra["value_a"] = fmt.Sprintf("%d", enchantCardID)
|
||
stack.Extra["enchant_card_id"] = fmt.Sprintf("%d", enchantCardID)
|
||
stack.Extra["byte_12"] = fmt.Sprintf("%d", upgradeCount)
|
||
stack.Extra["enchant_upgrade_count"] = fmt.Sprintf("%d", upgradeCount)
|
||
result.EnchantCardID = enchantCardID
|
||
}
|
||
|
||
// 徽章参数先规范化,任何非法输入都在动库之前拒绝。
|
||
emblems, autoSocketCount, normErr := gmNormalizeEmblems(request.Emblems)
|
||
if normErr != nil {
|
||
return result, normErr
|
||
}
|
||
openSockets := autoSocketCount
|
||
if request.OpenSockets != nil && *request.OpenSockets > openSockets {
|
||
openSockets = *request.OpenSockets
|
||
}
|
||
|
||
placement := currentDungeonPickupPlacement(definition, 1)
|
||
placement.BuildNew = func(int16) (dnfrepo.ItemStack, error) {
|
||
return gmCloneStack(stack), nil
|
||
}
|
||
placement.NormalizeExisting = nil
|
||
|
||
repositories, available := s.repositoryGroup()
|
||
if !available || repositories.CharacterItems == nil {
|
||
return result, ErrGMRepositoryUnavailable
|
||
}
|
||
owner, err := dnfdungeon.NewOwner(repositories)
|
||
if err != nil {
|
||
return result, fmt.Errorf("%w: %v", ErrGMItemGrantUnavailable, err)
|
||
}
|
||
grant, err := owner.GrantPickupItem(ctx, dnfdungeon.PickupItemCommand{
|
||
CharacterID: characterID,
|
||
Placement: placement,
|
||
UpdatedAt: time.Now().UTC(),
|
||
})
|
||
if err != nil {
|
||
return result, err
|
||
}
|
||
result.ListType = placement.ListType
|
||
result.Slot = int(grant.Slot)
|
||
result.EntryKey = fmt.Sprintf("%d:%d", placement.ListType, grant.Slot)
|
||
result.OpenSockets = openSockets
|
||
|
||
// 没有徽章时也要把附魔序列化进原始行 —— **必须放在发放之后**。
|
||
// 构建阶段的 raw 会被落库过程冲掉(实测:只在 BuildNew 里刷新时
|
||
// raw[0x0E:0x12] 仍是 0),而徽章路径之所以正确,正是因为它是在目标行
|
||
// 已经落位之后才调用 currentRefreshStackRawEntry。
|
||
if len(emblems) == 0 {
|
||
if err := s.gmRefreshGrantedItemRaw(ctx, repositories, grant.Slot, placement.ListType, characterID); err != nil {
|
||
return result, err
|
||
}
|
||
if inventory, found, loadErr := repositories.Inventory.Load(ctx, characterID); loadErr == nil && found {
|
||
if final, present := inventory.Slots[result.EntryKey]; present {
|
||
result.Extra = final.Extra
|
||
}
|
||
}
|
||
return result, nil
|
||
}
|
||
return s.gmAttachEmblemsToGrantedItem(ctx, repositories, catalog, rule, request.ItemID,
|
||
placement.ListType, int16(grant.Slot), openSockets, emblems, result)
|
||
}
|
||
|
||
// gmRefreshGrantedItemRaw 在目标行已经落位之后,用 Extra 重新序列化它的原始行。
|
||
//
|
||
// 顺序很关键:必须"先落库、后刷新",否则刷新出来的 raw 会被落库路径覆盖回旧值。
|
||
func (s *Service) gmRefreshGrantedItemRaw(
|
||
ctx context.Context,
|
||
repositories dnfrepo.Group,
|
||
slot uint16,
|
||
listType byte,
|
||
characterID string,
|
||
) error {
|
||
if repositories.CharacterItems == nil {
|
||
return ErrGMRepositoryUnavailable
|
||
}
|
||
key := fmt.Sprintf("%d:%d", listType, slot)
|
||
return repositories.CharacterItems.WithinCharacterItems(ctx, characterID, func(
|
||
inventories dnfrepo.InventoryRepository,
|
||
_ dnfrepo.EquipmentRepository,
|
||
) error {
|
||
record, found, loadErr := inventories.Load(ctx, characterID)
|
||
if loadErr != nil {
|
||
return loadErr
|
||
}
|
||
if !found {
|
||
return fmt.Errorf("%w: character=%s", ErrGMInventoryNotFound, characterID)
|
||
}
|
||
stack, present := record.Slots[key]
|
||
if !present {
|
||
return fmt.Errorf("%w: entry_key=%s", ErrGMItemRowNotFound, key)
|
||
}
|
||
record = dnfrepo.CloneInventory(record)
|
||
currentRefreshStackRawEntry(&stack, listType, int16(slot))
|
||
record.Slots[key] = stack
|
||
record.UpdatedAt = time.Now().UTC()
|
||
return dnfrepo.SaveInventoryFields(ctx, inventories, record, dnfrepo.InventoryFieldSlots)
|
||
})
|
||
}
|
||
|
||
// gmNormalizeEmblems 校验并补全徽章参数,返回"至少需要开几孔"。
|
||
func gmNormalizeEmblems(values []GMEmblemRequest) ([]currentEmblemApplyRequest, int, error) {
|
||
if len(values) == 0 {
|
||
return nil, 0, nil
|
||
}
|
||
if len(values) > gmEmblemMaxSockets {
|
||
return nil, 0, fmt.Errorf("%w: at most %d emblems", ErrGMRequestInvalid, gmEmblemMaxSockets)
|
||
}
|
||
out := make([]currentEmblemApplyRequest, 0, len(values))
|
||
seenSocket := map[int]bool{}
|
||
nextIndex := 0
|
||
for _, value := range values {
|
||
if value.EmblemItemID == 0 {
|
||
return nil, 0, fmt.Errorf("%w: emblem_item_id is required", ErrGMRequestInvalid)
|
||
}
|
||
index := nextIndex
|
||
if value.SocketIndex != nil {
|
||
index = *value.SocketIndex
|
||
}
|
||
if index < 0 || index >= gmEmblemMaxSockets {
|
||
return nil, 0, fmt.Errorf("%w: socket_index must be 0..%d", ErrGMRequestInvalid, gmEmblemMaxSockets-1)
|
||
}
|
||
if seenSocket[index] {
|
||
return nil, 0, fmt.Errorf("%w: socket_index %d repeated", ErrGMRequestInvalid, index)
|
||
}
|
||
seenSocket[index] = true
|
||
if index >= nextIndex {
|
||
nextIndex = index + 1
|
||
}
|
||
slot := int16(-1)
|
||
if value.EmblemSlot != nil {
|
||
if *value.EmblemSlot < 0 {
|
||
return nil, 0, fmt.Errorf("%w: emblem_slot must not be negative", ErrGMRequestInvalid)
|
||
}
|
||
slot = int16(*value.EmblemSlot)
|
||
}
|
||
out = append(out, currentEmblemApplyRequest{
|
||
EmblemSlot: slot,
|
||
EmblemItemID: int64(value.EmblemItemID),
|
||
SocketIndex: byte(index),
|
||
})
|
||
}
|
||
return out, nextIndex, nil
|
||
}
|
||
|
||
// gmAttachEmblemsToGrantedItem 在同一事务里给刚放好的装备开孔并镶嵌徽章。
|
||
//
|
||
// 徽章行会被临时放进同一个背包记录里,交给 currentApplyEquipmentEmblems 消耗 ——
|
||
// 那条路径是游戏内镶嵌用的同一份实现,因此不会写出客户端读不了的向量。
|
||
func (s *Service) gmAttachEmblemsToGrantedItem(
|
||
ctx context.Context,
|
||
repositories dnfrepo.Group,
|
||
catalog *pvfDungeonDropCatalog,
|
||
rule currentEquipmentPlacementRule,
|
||
targetItemID uint32,
|
||
listType byte,
|
||
slot int16,
|
||
openSockets int,
|
||
emblems []currentEmblemApplyRequest,
|
||
result GMItemWithExtrasResult,
|
||
) (GMItemWithExtrasResult, error) {
|
||
owner, err := dnfsocketemblem.NewOwner(repositories)
|
||
if err != nil {
|
||
return result, fmt.Errorf("%w: socket emblem owner: %v", ErrGMItemGrantUnavailable, err)
|
||
}
|
||
targetKey := fmt.Sprintf("%d:%d", listType, slot)
|
||
|
||
attach := func(assets *dnfsocketemblem.Assets) (dnfsocketemblem.Changes, error) {
|
||
inventory := assets.Inventory
|
||
if inventory == nil {
|
||
return dnfsocketemblem.Changes{}, errCurrentSocketInventoryMissing
|
||
}
|
||
target, ok := inventory.Slots[targetKey]
|
||
if !ok || target.ItemID != int64(targetItemID) {
|
||
return dnfsocketemblem.Changes{}, fmt.Errorf("%w: target %s", errCurrentSocketTargetMissing, targetKey)
|
||
}
|
||
|
||
// 1) 把徽章行临时放进背包(原地消耗,不再清理;
|
||
// 镶嵌成功后它们就"消耗掉了",与游戏内一致)。
|
||
resolved, materialiseErr := gmMaterialiseEmblems(inventory, emblems)
|
||
if materialiseErr != nil {
|
||
return dnfsocketemblem.Changes{}, materialiseErr
|
||
}
|
||
|
||
// 2) 打开所需孔位。
|
||
isAvatar := rule.class == currentEquipmentPlacementClassAvatar
|
||
if isAvatar {
|
||
data := currentAvatarSocketData(target.Extra)
|
||
want := openSockets
|
||
if want <= 0 {
|
||
want = len(resolved)
|
||
}
|
||
for i := 0; i < want && i < currentAvatarSocketCount; i++ {
|
||
if data[i] == 0 && i >= int(data[0]) {
|
||
// 空孔记 0xFF 占位,交给应用阶段写入真实徽章 ID。
|
||
data[i] = 0xFF
|
||
}
|
||
}
|
||
data[0] = byte(want)
|
||
if applyErr := currentApplyAvatarEmblems(catalog, inventory, &data, resolved); applyErr != nil {
|
||
return dnfsocketemblem.Changes{}, applyErr
|
||
}
|
||
currentApplyAvatarSocketDataToStack(&target, listType, slot, data)
|
||
} else {
|
||
dataBytes := currentEquipmentEmblemData(target.Extra, target.RawEntry)
|
||
maxOpen := currentEquipmentSocketOpenCount(rule)
|
||
want := openSockets
|
||
if want <= 0 {
|
||
want = len(resolved)
|
||
}
|
||
if want > maxOpen {
|
||
want = maxOpen
|
||
}
|
||
currentEnsureEquipmentEmblemSocketsOpen(&dataBytes, maxOpen, want)
|
||
if applyErr := currentApplyEquipmentEmblems(catalog, rule, inventory, &dataBytes, resolved); applyErr != nil {
|
||
return dnfsocketemblem.Changes{}, applyErr
|
||
}
|
||
currentApplyEquipmentEmblemDataToStack(&target, listType, slot, dataBytes, currentEquipmentJewelSocketType(rule))
|
||
}
|
||
inventory.Slots[targetKey] = target
|
||
return dnfsocketemblem.Changes{Inventory: true}, nil
|
||
}
|
||
|
||
if err := owner.AttachEquipmentEmblems(ctx, dnfsocketemblem.Command{
|
||
CharacterID: result.CharacterID,
|
||
Project: attach,
|
||
}); err != nil {
|
||
return result, err
|
||
}
|
||
|
||
// 回读最终行,把实际写入的附魔/徽章信息返回给工具端。
|
||
inventory, found, loadErr := repositories.Inventory.Load(ctx, result.CharacterID)
|
||
if loadErr == nil && found {
|
||
if final, present := inventory.Slots[targetKey]; present {
|
||
result.Extra = final.Extra
|
||
if text := strings.TrimSpace(final.Extra["enchant_card_id"]); text != "" {
|
||
var parsed int64
|
||
if _, scanErr := fmt.Sscanf(text, "%d", &parsed); scanErr == nil {
|
||
result.EnchantCardID = parsed
|
||
}
|
||
}
|
||
if isAvatarRule(rule) {
|
||
data := currentAvatarSocketData(final.Extra)
|
||
result.OpenSockets = int(data[0])
|
||
for i := 1; i < len(data) && i <= result.OpenSockets; i++ {
|
||
if data[i] != 0 && data[i] != 0xFF {
|
||
result.Sockets = append(result.Sockets, int64(data[i]))
|
||
}
|
||
}
|
||
} else {
|
||
data := currentEquipmentEmblemData(final.Extra, final.RawEntry)
|
||
result.OpenSockets = int(data[0])
|
||
for i := 1; i < len(data) && i <= result.OpenSockets; i++ {
|
||
if data[i] != 0 && data[i] != 0xFF {
|
||
result.Sockets = append(result.Sockets, int64(data[i]))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
func isAvatarRule(rule currentEquipmentPlacementRule) bool {
|
||
return rule.class == currentEquipmentPlacementClassAvatar
|
||
}
|
||
|
||
// gmMaterialiseEmblems 把请求里的徽章落到背包行,返回带真实槽位的申请列表。
|
||
//
|
||
// 已经给了 EmblemSlot 就直接用;否则找同 item_id 的第一行消耗 1 个。
|
||
// 找不到时**临时插入**一行(工具端通常还没把徽章发给角色)。
|
||
func gmMaterialiseEmblems(
|
||
inventory *dnfrepo.InventoryRecord,
|
||
emblems []currentEmblemApplyRequest,
|
||
) ([]currentEmblemApplyRequest, error) {
|
||
if inventory.Slots == nil {
|
||
inventory.Slots = map[string]dnfrepo.ItemStack{}
|
||
}
|
||
out := make([]currentEmblemApplyRequest, 0, len(emblems))
|
||
// 临时槽从主背包末段往前找,避免占用正常装备/消耗品段。
|
||
nextTempSlot := int16(350)
|
||
for _, emblem := range emblems {
|
||
slot := emblem.EmblemSlot
|
||
if slot >= 0 {
|
||
if existing, ok := inventory.Slots[fmt.Sprintf("%d:%d", gmEmblemListMain, slot)]; ok &&
|
||
existing.ItemID == emblem.EmblemItemID {
|
||
emblem.EmblemSlot = slot
|
||
out = append(out, emblem)
|
||
continue
|
||
}
|
||
}
|
||
// 找同 id 的现有行。
|
||
foundSlot := int16(-1)
|
||
for key, candidate := range inventory.Slots {
|
||
var keyList byte
|
||
var keySlot int16
|
||
if _, scanErr := fmt.Sscanf(key, "%d:%d", &keyList, &keySlot); scanErr != nil {
|
||
continue
|
||
}
|
||
if keyList != gmEmblemListMain || candidate.ItemID != emblem.EmblemItemID || candidate.Count <= 0 {
|
||
continue
|
||
}
|
||
if foundSlot < 0 || keySlot < foundSlot {
|
||
foundSlot = keySlot
|
||
}
|
||
}
|
||
if foundSlot < 0 {
|
||
// 临时插入一行,让镶嵌阶段能"消耗"掉它。
|
||
for {
|
||
key := fmt.Sprintf("%d:%d", gmEmblemListMain, nextTempSlot)
|
||
if _, taken := inventory.Slots[key]; !taken {
|
||
inventory.Slots[key] = dnfrepo.ItemStack{
|
||
ItemID: emblem.EmblemItemID,
|
||
Count: 1,
|
||
Extra: map[string]string{
|
||
"source": "gm_tool_emblem_materialise",
|
||
"item_kind": "stackable",
|
||
},
|
||
}
|
||
foundSlot = nextTempSlot
|
||
nextTempSlot--
|
||
break
|
||
}
|
||
nextTempSlot--
|
||
if nextTempSlot < 300 {
|
||
return nil, fmt.Errorf("%w: no temporary slot for emblem %d",
|
||
ErrGMRequestInvalid, emblem.EmblemItemID)
|
||
}
|
||
}
|
||
}
|
||
emblem.EmblemSlot = foundSlot
|
||
out = append(out, emblem)
|
||
}
|
||
return out, nil
|
||
}
|