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

214 lines
7.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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]
}