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,130 @@
// 本文件提供"一个容器内同一道具 id 的总量不得超过 PVF [stack limit]"这条规则的共享助手。
// 规则口径(项目 owner 定稿):PVF [stack limit] N 既是单行上限,也是该容器能容纳的该道具
// 总量上限。因此一个容器里同一道具 id 只能存在一行:容器里已经有该道具的行时,入包必须
// 并进那一行,不能再开第二行。[stack limit] 1 表示该容器只能有一件;没有声明
// [stack limit] 的道具(装备等 StackLimit <= 0)不受限。
//
// 容器 = 线协议 list 类型对应的持久化 map:list 0 角色背包(InventoryRecord.Slots)、
// list 2 个人仓库(InventoryRecord.Warehouse)、list 12 账号金库
// (AccountInventoryRecord.Slots),以及 list 1/7/38 和账号共享晶石/灵魂行
// (键形如 "0:354".."0:365")等同类 map。行键统一为 "<listType>:<slot>"。
//
// 本文件只做统计和计算,不修改任何容器内容,也不做任何 IO。
package repository
import (
"math"
"strconv"
"strings"
)
// ContainerUncappedSpace 是"PVF 没有声明 [stack limit]"时本包返回的剩余空间哨兵。
// 与 runtime PVF 的既有口径一致(见 alignedcmd.StackMergeResolution):省略
// [stack limit] 的可叠加道具按 MaxInt32 处理,不做总量限制。
const ContainerUncappedSpace int64 = math.MaxInt32
// InventorySlotKey 拼出 <listType>:<slot> 形式的行键。
func InventorySlotKey(listType byte, slot int16) string {
return strconv.Itoa(int(listType)) + ":" + strconv.FormatInt(int64(slot), 10)
}
// ParseInventorySlotKey 解析 <listType>:<slot> 形式的行键。ok=false 表示键不是
// 该形式(listType 超出 0..255 或 slot 不是整数)。
func ParseInventorySlotKey(key string) (byte, int16, bool) {
listRaw, slotRaw, ok := strings.Cut(key, ":")
if !ok {
return 0, 0, false
}
listValue, err := strconv.ParseInt(strings.TrimSpace(listRaw), 10, 16)
if err != nil || listValue < 0 || listValue > 255 {
return 0, 0, false
}
slotValue, err := strconv.ParseInt(strings.TrimSpace(slotRaw), 10, 16)
if err != nil {
return 0, 0, false
}
return byte(listValue), int16(slotValue), true
}
// ContainerItemTotal 统计某个容器里同一道具 id 的总量。
// 口径是"这道具在这个容器里一共有多少件":整理锁、绑定、期限不同的行都算在一起,
// 只要行键的 listType 相同且 Count > 0。
func ContainerItemTotal(items map[string]ItemStack, listType byte, itemID int64) int64 {
if itemID <= 0 || len(items) == 0 {
return 0
}
total := int64(0)
for key, stack := range items {
keyListType, _, ok := ParseInventorySlotKey(key)
if !ok || keyListType != listType || stack.ItemID != itemID || stack.Count <= 0 {
continue
}
total += stack.Count
}
return total
}
// ContainerItemSpace 返回该容器还能再接收多少件该道具:
//
// stackLimit > 0 -> max(0, stackLimit - ContainerItemTotal(...))
// stackLimit <= 0 -> ContainerUncappedSpace(PVF 未声明上限,不受限)
//
// 调用方必须用返回的额度封顶本次入包量,并且只能把额度用在这一道具 id 已有的那一行
// (容器里没有该道具的行时才允许占用空格)。
func ContainerItemSpace(items map[string]ItemStack, listType byte, itemID int64, stackLimit int64) int64 {
if itemID <= 0 {
return 0
}
if stackLimit <= 0 {
return ContainerUncappedSpace
}
space := stackLimit - ContainerItemTotal(items, listType, itemID)
if space < 0 {
return 0
}
return space
}
// ContainerItemRow 返回该容器里该道具 id 现存的槽位最小的那一行。
// ok=false 表示容器里没有这道具的行(此时并且只有此时才允许占用空格)。
// 多个同 id 行(历史数据)时返回槽位最小的那一行,保证结果确定。
func ContainerItemRow(items map[string]ItemStack, listType byte, itemID int64) (string, ItemStack, bool) {
if itemID <= 0 || len(items) == 0 {
return "", ItemStack{}, false
}
bestKey := ""
bestSlot := int16(0)
var bestStack ItemStack
for key, stack := range items {
keyListType, slot, ok := ParseInventorySlotKey(key)
if !ok || keyListType != listType || stack.ItemID != itemID || stack.Count <= 0 {
continue
}
if bestKey != "" && slot >= bestSlot {
continue
}
bestKey, bestSlot, bestStack = key, slot, stack
}
if bestKey == "" {
return "", ItemStack{}, false
}
return bestKey, bestStack, true
}
// CanOpenContainerItemRow 报告该容器是否还能为 itemID 新开一行(占用空格):
//
// stackLimit <= 0 -> true(PVF 未声明上限,沿用旧行为,不受本规则约束)
// stackLimit > 0 -> 仅当容器里还没有该道具的行,并且总量还没到上限
//
// 入包路径的"占用空格"分支必须先过这一关:容器里已经有该道具的行时只能并进那一行,
// 并不下就走该路径既有的溢出/拒绝处理,绝不能开第二行。
func CanOpenContainerItemRow(items map[string]ItemStack, listType byte, itemID int64, stackLimit int64) bool {
if stackLimit <= 0 || itemID <= 0 {
return true
}
if ContainerItemSpace(items, listType, itemID, stackLimit) <= 0 {
return false
}
_, _, exists := ContainerItemRow(items, listType, itemID)
return !exists
}