DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

@@ -0,0 +1,213 @@
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]
}