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,142 @@
package dnfbridge
import (
"sort"
"strings"
"sync"
dnfguild "longheng.io/server/internal/modules/dnf/guild"
)
// 公会勋章 / 守护珠的合成候选表。
//
// 合成的产物是"下一档里的随机一件",所以需要按稀有度分组的完整候选集。这里不写
// 死 ID 段:itemRefs / stackableIDs / equipmentIDs 是从 PVF 的 .lst 全量枚举出来
// 的,逐个 ResolveItem 之后按 [flag] / [flag gem] 归类,和 list-38 的归位规则
// (currentMailboxPVFPlacement)用的是同一个判据。
//
// 结果排序后缓存:同一档里选哪一件是随机的,但候选顺序必须稳定,否则测试和日志
// 都没法复现。
type currentGuildCompoundCandidates struct {
once sync.Once
err error
byKey map[currentGuildCompoundKey][]uint32
}
type currentGuildCompoundKey struct {
Kind dnfguild.CompoundKind
Rarity uint32
}
func (s *Service) currentGuildCompoundCandidates(
kind dnfguild.CompoundKind,
rarity uint32,
) ([]uint32, error) {
catalog, err := s.currentPVFItemCatalog()
if err != nil {
return nil, err
}
s.guildCompoundCandidates.once.Do(func() {
s.guildCompoundCandidates.byKey, s.guildCompoundCandidates.err =
buildCurrentGuildCompoundCandidates(catalog)
})
if s.guildCompoundCandidates.err != nil {
return nil, s.guildCompoundCandidates.err
}
return s.guildCompoundCandidates.byKey[currentGuildCompoundKey{Kind: kind, Rarity: rarity}], nil
}
func buildCurrentGuildCompoundCandidates(
catalog *pvfDungeonDropCatalog,
) (map[currentGuildCompoundKey][]uint32, error) {
out := make(map[currentGuildCompoundKey][]uint32, 16)
add := func(kind dnfguild.CompoundKind, itemID uint32, rarity int64) {
if rarity < 0 {
return
}
key := currentGuildCompoundKey{Kind: kind, Rarity: uint32(rarity)}
out[key] = append(out[key], itemID)
}
for _, itemID := range catalog.stackableIDs {
if !currentGuildCompoundPathMayHold(catalog, itemID) {
continue
}
definition, err := catalog.ResolveItem(itemID)
if err != nil || definition.Kind != dungeonDropItemStackable {
continue
}
if !strings.HasPrefix(normalizeDungeonDropStackableType(definition.StackableType), "[flag gem]") {
continue
}
add(dnfguild.CompoundGem, itemID, definition.Rarity)
}
for _, itemID := range catalog.equipmentIDs {
if !currentGuildCompoundPathMayHold(catalog, itemID) {
continue
}
definition, err := catalog.ResolveItem(itemID)
if err != nil || definition.Kind != dungeonDropItemEquipment {
continue
}
if normalizeEquipmentPlacementPVFType(definition.EquipmentType) != "[flag]" {
continue
}
add(dnfguild.CompoundFlag, itemID, definition.Rarity)
}
for key := range out {
sort.Slice(out[key], func(i, j int) bool { return out[key][i] < out[key][j] })
}
return out, nil
}
// currentGuildCompoundPathMayHold 是建索引时的**廉价预筛**。
//
// 没有它,建一次索引要对 PVF 里**每一件**可堆叠物与装备跑一次 ResolveItem(各自
// 都要解析一份文档)—— 实测第一次合成因此卡了 **8.66 秒**,玩家看到的就是"合成
// 失败会卡住一段时间"。
//
// 预筛只看已经加载好的引用路径,不读任何文档:守护珠在 stackable/flaggem/ 下,
// 勋章在 equipment/character/common/flag/ 下,两者的路径里都带 "flag"。判据故意
// 放宽成子串匹配而不是精确前缀 —— 宁可多解析几件,也不要因为路径换了目录而悄悄
// 丢掉候选。真正的归类判据仍然是 PVF 的 [flag] / [flag gem],没有放松。
func currentGuildCompoundPathMayHold(catalog *pvfDungeonDropCatalog, itemID uint32) bool {
reference, found := catalog.itemRefs[itemID]
if !found {
return false
}
return strings.Contains(strings.ToLower(reference.path), "flag")
}
// currentGuildCompoundGemFamilySuccessor 是同属性守护珠的下一档 ID。
//
// 守护珠的 ID 是极其规整的 900<属性2位><档位1位>:90000..90003 是物理防御力的
// 0..3 档,90010..90013 是魔法防御力的 0..3 档,一直到 90120..90123,13 个属性
// 各 4 档。所以"同属性升一档"就是 ID + 1。
//
// 只有在四件材料是**同一个 ID** 时才走这条路(守护珠是可堆叠的,正常操作就是从
// 同一摞里拖四个出来,实测请求也确实是四个槽位都指向同一格)。材料属性不一致时
// 没有"同属性"可言,退回随机。
//
// 这条规则是设计判断而非反汇编证据:这张配方表不在客户端里,服务端此前也没有实现,
// 两端都不存在可以证明它的代码。挑它是因为属性对玩家有意义,随机掉属性会让合成
// 变成纯赌博。勋章没有这种对应关系(稀有度 0 的"古老/破旧/耀眼"和稀有度 1 的
// "简陋/破旧/寒酸"不是同一组词),所以勋章只能随机。
func currentGuildCompoundGemFamilySuccessor(materials []uint32, nextRarity uint32, candidates []uint32) (uint32, bool) {
if len(materials) == 0 {
return 0, false
}
first := materials[0]
for _, material := range materials[1:] {
if material != first {
return 0, false
}
}
successor := first + 1
for _, candidate := range candidates {
if candidate == successor {
return successor, true
}
}
_ = nextRarity
return 0, false
}