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,120 @@
package inventory
import (
"fmt"
"math"
"strconv"
"longheng.io/server/internal/modules/dnf/alignedcmd"
repo "longheng.io/server/internal/modules/dnf/repository"
)
// PlaceStackableReward stages a PVF-resolved reward without committing it. A
// full inventory leaves the input untouched, including partially filled stacks.
// The caller owns the transaction and its durable reward receipt.
func PlaceStackableReward(record repo.InventoryRecord, item alignedcmd.MagicBoxRewardItem, count int64) (repo.InventoryRecord, error) {
if count <= 0 || count > math.MaxInt32 || item.StackLimit < 0 || item.StackLimit > math.MaxInt32 || item.SlotEnd > 32766 {
return repo.InventoryRecord{}, fmt.Errorf("invalid stackable reward contract")
}
// An omitted PVF stack limit means unlimited in the existing grant owner;
// retain that behavior with an explicit bound for the native count field.
if item.StackLimit == 0 {
item.StackLimit = math.MaxInt32
}
staged := repo.CloneInventory(record)
if staged.Slots == nil {
staged.Slots = map[string]repo.ItemStack{}
}
if _, err := placeStackableRewardRows(staged.Slots, item, count); err != nil {
return repo.InventoryRecord{}, err
}
return staged, nil
}
// placeStackableRewardRows 按**行上限**放置可叠加奖励:先把范围内已有的同类行补满
// (每行上限 = PVF [stack limit]),再用范围内的空格开新行,最后一行允许不满。
//
// 刻意不复用 owner.go 的 grantRandomRewardItem:本树那个版本把 [stack limit] 同时当作
// "该道具在**整个容器**里的总量上限"(ContainerItemSpace),那是既有的、影响所有随机奖励
// 路径的运营决策,不属于本次决斗场移植范围。决斗场总战绩奖励走参考实现的按行语义,
// 因此这里自带一份放置逻辑,两者互不影响。
func placeStackableRewardRows(items map[string]repo.ItemStack, reward alignedcmd.MagicBoxRewardItem, count int64) ([]int16, error) {
if count <= 0 {
return nil, nil
}
if reward.ItemID <= 0 || reward.Kind != "stackable" || reward.TargetListType != listTypeMain || reward.SlotStart < 0 || reward.SlotEnd < reward.SlotStart {
return nil, fmt.Errorf("%w: item=%d kind=%q target_list=%d slots=%d..%d", ErrRandomRewardContractRequired, reward.ItemID, reward.Kind, reward.TargetListType, reward.SlotStart, reward.SlotEnd)
}
remaining := count
changed := make([]int16, 0, 2)
markChanged := func(slot int16) {
for _, previous := range changed {
if previous == slot {
return
}
}
changed = append(changed, slot)
}
// 第一遍:补进范围内已有的同类行。
for slot := reward.SlotStart; slot <= reward.SlotEnd && remaining > 0; slot++ {
key := slotKey(listTypeMain, slot)
stack, found := items[key]
if !found || stack.ItemID != reward.ItemID || stack.Count <= 0 || reward.StackLimit == 1 || !randomRewardExpirationMatches(stack, reward.ExpireAt) {
continue
}
capacity := remaining
if reward.StackLimit > 0 {
capacity = reward.StackLimit - stack.Count
}
if capacity <= 0 {
continue
}
add := remaining
if add > capacity {
add = capacity
}
stack = cloneStack(stack)
stack.Count += add
updateStackRawAmount(&stack)
items[key] = stack
remaining -= add
markChanged(slot)
}
// 第二遍:占用范围内的空格开新行,每行最多 StackLimit。
for slot := reward.SlotStart; slot <= reward.SlotEnd && remaining > 0; slot++ {
key := slotKey(listTypeMain, slot)
if _, occupied := items[key]; occupied {
continue
}
insert := remaining
if reward.StackLimit > 0 && insert > reward.StackLimit {
insert = reward.StackLimit
}
stack := repo.ItemStack{
ItemID: reward.ItemID,
Count: insert,
ExpireAt: reward.ExpireAt,
Extra: map[string]string{
"item_kind": "stackable",
"pvf_path": reward.PVFPath,
},
}
if !reward.ExpireAt.IsZero() && reward.ExpireAt.Unix() > 0 {
expire := strconv.FormatInt(reward.ExpireAt.Unix(), 10)
stack.Extra["expire_time"] = expire
stack.Extra["expire_unix"] = expire
}
if reward.UsablePeriodDays > 0 {
stack.Extra["usable_period_days"] = strconv.FormatInt(reward.UsablePeriodDays, 10)
stack.Extra["expiration_source"] = "runtime_pvf_usable_period_grant"
}
updateStackRawAmount(&stack)
items[key] = stack
remaining -= insert
markChanged(slot)
}
if remaining > 0 {
return nil, fmt.Errorf("%w: item=%d remaining=%d", ErrRandomRewardInventoryFull, reward.ItemID, remaining)
}
return changed, nil
}