服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
214 lines
7.9 KiB
Go
214 lines
7.9 KiB
Go
package dnfbridge
|
||
|
||
import (
|
||
"fmt"
|
||
"math"
|
||
"sort"
|
||
"strconv"
|
||
|
||
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||
)
|
||
|
||
// grantCurrentBoosterRewardByOwner routes the fixed crystal/soul identities
|
||
// into the account-shared list-0 warehouse before falling back to the normal
|
||
// reward placement. The account and character writes are committed by the
|
||
// same BoosterAssets transaction as source consumption and Epic Book credit.
|
||
func grantCurrentBoosterRewardByOwner(
|
||
inventory *dnfrepo.InventoryRecord,
|
||
account *dnfrepo.AccountInventoryRecord,
|
||
source dnfpvf.Source,
|
||
reward currentBoosterReward,
|
||
mainSlotCount uint16,
|
||
) ([]string, bool, error) {
|
||
if inventory == nil || account == nil || reward.Definition.ItemID == 0 || reward.Count == 0 {
|
||
return nil, false, errCurrentBoosterPVFInvalid
|
||
}
|
||
remaining := int64(reward.Count)
|
||
accountChanged := false
|
||
// 统一入口(account_shared_grant.go)。礼盒自己还要在账号行上补 last_grant_source
|
||
// 溯源标记 —— 那一步对"合并进已有行"也生效,所以保留在入口之后。
|
||
shared := grantCurrentAccountSharedFirst(account, reward.Definition, reward.Count, "booster_item")
|
||
if shared.Changed {
|
||
accountChanged = true
|
||
key := dnfrepo.AccountSharedInventorySlotKey(shared.Slot)
|
||
stack := account.Slots[key]
|
||
if stack.Extra == nil {
|
||
stack.Extra = make(map[string]string, 6)
|
||
}
|
||
stack.Extra["source"] = "booster_item"
|
||
stack.Extra["last_grant_source"] = "booster_item"
|
||
account.Slots[key] = stack
|
||
}
|
||
remaining = int64(shared.Remainder)
|
||
if remaining == 0 {
|
||
return nil, accountChanged, nil
|
||
}
|
||
if remaining > math.MaxUint32 {
|
||
return nil, false, errCurrentBoosterPVFInvalid
|
||
}
|
||
overflow := reward
|
||
overflow.Count = uint32(remaining)
|
||
keys, err := grantCurrentBoosterReward(inventory, source, overflow, mainSlotCount)
|
||
return keys, accountChanged, err
|
||
}
|
||
func grantCurrentBoosterReward(
|
||
inventory *dnfrepo.InventoryRecord,
|
||
source dnfpvf.Source,
|
||
reward currentBoosterReward,
|
||
mainSlotCount uint16,
|
||
) ([]string, error) {
|
||
if inventory == nil || reward.Definition.ItemID == 0 || reward.Count == 0 {
|
||
return nil, errCurrentBoosterPVFInvalid
|
||
}
|
||
keys := make([]string, 0, 1)
|
||
switch reward.Placement {
|
||
case currentBoosterRewardAvatar:
|
||
for index := uint32(0); index < reward.Count; index++ {
|
||
slot, err := grantCurrentCeraShopAvatar(
|
||
inventory,
|
||
source,
|
||
reward.Definition,
|
||
currentCeraShopProduct{ItemID: reward.Definition.ItemID, Count: 1, Section: "avatar"},
|
||
reward.Option,
|
||
)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
keys = append(keys, currentCeraShopInventorySlotKey(1, slot))
|
||
}
|
||
case currentBoosterRewardPetBody:
|
||
for index := uint32(0); index < reward.Count; index++ {
|
||
slot, err := grantCurrentCeraShopPet(inventory, reward.Definition)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
keys = append(keys, currentCeraShopInventorySlotKey(currentPetInventoryListType, slot))
|
||
}
|
||
case currentBoosterRewardPetArtifact:
|
||
for index := uint32(0); index < reward.Count; index++ {
|
||
slot, err := grantCurrentBoosterPetArtifact(inventory, reward)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
keys = append(keys, currentCeraShopInventorySlotKey(currentPetInventoryListType, slot))
|
||
}
|
||
case currentBoosterRewardPetConsumable:
|
||
slots, err := grantCurrentCeraShopPetConsumable(inventory, reward.Definition, reward.Count)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for _, slot := range slots {
|
||
keys = append(keys, currentCeraShopInventorySlotKey(currentPetInventoryListType, slot))
|
||
}
|
||
default:
|
||
// 走 grantCurrentNPCShopProduct 而不是 grantCurrentCeraShopProduct:
|
||
// 前者会按 PVF 的 [flag]/[flag gem] 把公会勋章和守护珠路由到 list 38,
|
||
// 后者只会往 list 0 塞。公会地下城奖励礼盒(10156320)的 [booster info]
|
||
// 整张表都是 100380021..100380050 的勋章和 9000x..9012x 的守护珠,全部
|
||
// 属于 list 38 —— 用 list 0 的放置逻辑必然失败,客户端把失败码 4 显示成
|
||
// 「物品栏空间不足」。
|
||
//
|
||
// 调用方的 changedLists 是从 key 里反解出来的 listType,所以这里只要把
|
||
// 正确的 list 写进 key,后续 op14 刷新就自动跟着走。
|
||
//
|
||
// 【合并说明 2026-09-18,来自服务端仓库 87f7e8c】我们这一侧给授予链补了
|
||
// mainSlotCount(可用槽位/开行上限)参数,这里必须继续往下传:该函数在非
|
||
// 勋章分支仍会转调 grantCurrentCeraShopProduct(..., mainSlotCount),所以
|
||
// 两边改动都生效 —— 勋章/守护珠进 list 38,普通奖励仍受槽位上限约束。
|
||
listType, slots, err := grantCurrentNPCShopProduct(inventory, reward.Definition, reward.Count, mainSlotCount)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for _, slot := range slots {
|
||
keys = append(keys, currentCeraShopInventorySlotKey(listType, slot))
|
||
}
|
||
}
|
||
for _, key := range keys {
|
||
stack := inventory.Slots[key]
|
||
if stack.Extra == nil {
|
||
stack.Extra = make(map[string]string, 4)
|
||
}
|
||
if reward.Seal {
|
||
stack.Extra["seal_flag"] = "1"
|
||
}
|
||
// [booster equipment upgrade] 指定的强化等级:只作用于**主背包里的装备**。
|
||
// 四个键必须一起写,客户端读的是 RawEntry[0x0A](本树其它强化路径同样口径,
|
||
// 见 gm_admin.go),所以写完 Extra 还要重建 RawEntry,否则客户端看到的仍是 +0。
|
||
if reward.UpgradeLevel > 0 && reward.Definition.Kind == dungeonDropItemEquipment &&
|
||
reward.Placement == currentBoosterRewardMain {
|
||
level := strconv.Itoa(int(reward.UpgradeLevel))
|
||
stack.Extra["ext_data0"] = level
|
||
stack.Extra["packed_flag_byte"] = level
|
||
stack.Extra["reinforce"] = level
|
||
stack.Extra["upgrade_level"] = level
|
||
listType, slot, ok := parseSceneInventorySlotKey(key)
|
||
if !ok {
|
||
return nil, errCurrentBoosterPVFInvalid
|
||
}
|
||
entry := currentItemListEntryFromStack(listType, slot, stack)
|
||
stack.RawEntry = append([]byte(nil), entry.data[:]...)
|
||
}
|
||
inventory.Slots[key] = stack
|
||
}
|
||
return keys, nil
|
||
}
|
||
|
||
func grantCurrentBoosterPetArtifact(
|
||
inventory *dnfrepo.InventoryRecord,
|
||
reward currentBoosterReward,
|
||
) (int16, error) {
|
||
if inventory == nil || reward.Definition.Kind != dungeonDropItemEquipment || reward.Placement != currentBoosterRewardPetArtifact {
|
||
return 0, errCurrentBoosterPVFInvalid
|
||
}
|
||
if inventory.Slots == nil {
|
||
inventory.Slots = make(map[string]dnfrepo.ItemStack)
|
||
}
|
||
for slot := currentBoosterPetArtifactStart; slot <= currentBoosterPetArtifactEnd; slot++ {
|
||
key := currentCeraShopInventorySlotKey(currentPetInventoryListType, slot)
|
||
if _, occupied := inventory.Slots[key]; occupied {
|
||
continue
|
||
}
|
||
extra := map[string]string{
|
||
"source": "booster_item",
|
||
"item_kind": "equipment",
|
||
"pvf_path": reward.Definition.PVFPath,
|
||
"equipment_type": reward.Definition.EquipmentType,
|
||
"amount_or_count": "0",
|
||
}
|
||
if reward.Seal {
|
||
extra["seal_flag"] = "1"
|
||
}
|
||
stack := dnfrepo.ItemStack{ItemID: int64(reward.Definition.ItemID), Count: 1, Extra: extra}
|
||
if !reward.Definition.ExpirationDate.IsZero() {
|
||
stack, _ = applyCurrentPVFItemExpiration(stack, reward.Definition)
|
||
}
|
||
entry := currentItemListEntryFromStack(currentPetInventoryListType, slot, stack)
|
||
stack.RawEntry = append([]byte(nil), entry.data[:]...)
|
||
inventory.Slots[key] = stack
|
||
return slot, nil
|
||
}
|
||
return 0, fmt.Errorf("%w: pet artifact item=%d range=%d..%d", errDungeonPickupInventoryFull, reward.Definition.ItemID, currentBoosterPetArtifactStart, currentBoosterPetArtifactEnd)
|
||
}
|
||
|
||
func findCurrentBoosterMaterial(
|
||
items map[string]dnfrepo.ItemStack,
|
||
itemID int64,
|
||
count int64,
|
||
sourceKey string,
|
||
) string {
|
||
keys := make([]string, 0, len(items))
|
||
for key, stack := range items {
|
||
listType, _, ok := parseSceneInventorySlotKey(key)
|
||
if !ok || listType != dnfrepo.MainInventoryListType || key == sourceKey || stack.ItemID != itemID || stack.Count < count {
|
||
continue
|
||
}
|
||
keys = append(keys, key)
|
||
}
|
||
sort.Strings(keys)
|
||
if len(keys) == 0 {
|
||
return ""
|
||
}
|
||
return keys[0]
|
||
}
|