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,220 @@
package guild
import (
"errors"
"fmt"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
)
// 公会勋章合成 / 守护珠合成的配方表。
//
// 来源 etc/guilddungeoncompound.etc,两段:
//
// [compound info] -> 守护珠 [flag gem]
// 0 4 2000 1 4 1500 2 4 750 3 2 0
// [guild flag compound info] -> 公会勋章 [flag]
// 0 4 2500 1 4 2000 2 4 1000 3 4 500 6 2 0
//
// 每三个一组:**稀有度、材料件数上限、每件材料的成功率(1/10000)**。稀有度就是
// 物品自己的 [rarity](无该字段记 0),两条阶梯分别是:
//
// 勋章 0 -> 1 -> 2 -> 3 -> 6(6 是"完美的X图腾勋章",封顶)
// 守护珠 0 -> 1 -> 2 -> 3(封顶)
//
// 成功率为 0 的那一行是**封顶行**,不是"永远失败":它是阶梯的最后一档,没有
// 更高一档可以产出,所以直接判不可合成。
//
// # 件数与成功率的关系(2026-09-15 按玩家实测界面修正)
//
// 第二列是**上限**不是定值:客户端允许放 2..4 件,**放得越多成功率越高**。
// 所以第三列是**每件**材料的贡献,总成功率 = 件数 × 第三列,封顶 100%:
//
// 勋章 稀有度 0 2件 50% 3件 75% 4件 100%
// 稀有度 1 2件 40% 3件 60% 4件 80%
// 稀有度 2 2件 20% 3件 30% 4件 40%
// 稀有度 3 2件 10% 3件 15% 4件 20%
// 守护珠 稀有度 0 2件 40% 3件 60% 4件 80%
// 稀有度 1 2件 30% 3件 45% 4件 60%
// 稀有度 2 2件 15% 3件 22.5% 4件 30%
//
// 支持这个读法的一个信号:勋章最低档 2500 × 4 恰好等于 10000,也就是满件数必成,
// 是个不像巧合的整数。
//
// 最初的实现把第二列当成"必须正好这么多件",于是玩家放 2 件被一句
//「无法进行合成。」挡掉 —— 那是这一版修正的起因。
//
// 注意这张表**不在客户端里**:DNF.exe 的任何节里都搜不到 "guilddungeoncompound"
// 或 "guild flag compound" 这两个串。所以客户端不校验件数,玩家往格子里放几个就
// 发几个(实测勋章那次只放了 2 个就发了 count=2),规则全部由服务端裁决。
//
// 第三列的语义(成功率 1/10000)是推断而非反汇编证据:客户端不读这张表,服务端
// 原本也没有实现,所以没有任何一端能直接证明。运营 2026-09-15 确认按成功率实现,
// 且失败照常消耗材料。要改语义只需改这里。
var (
ErrCompoundCatalogUnavailable = errors.New("guild compound catalog is unavailable")
ErrCompoundCatalogInvalid = errors.New("guild compound catalog is malformed")
)
const (
compoundPath = "etc/guilddungeoncompound.etc"
compoundGemKey = "compound info"
compoundFlagKey = "guild flag compound info"
compoundRateDenominat = 10000
)
// CompoundKind 区分两条阶梯。
type CompoundKind byte
const (
CompoundGem CompoundKind = iota
CompoundFlag
)
func (k CompoundKind) String() string {
if k == CompoundFlag {
return "flag"
}
return "gem"
}
// CompoundMinMaterialCount 是任何一档的最少件数。客户端的合成界面允许放 2..4 件,
// 配方表只写了上限,下限只能从界面行为来。
const CompoundMinMaterialCount uint32 = 2
// CompoundRecipe 是一档的配方。
type CompoundRecipe struct {
// Rarity 是被消耗的材料档位。
Rarity uint32
// MaxMaterialCount 是这一档最多能放几件材料。
MaxMaterialCount uint32
// RatePerMaterial 是**每件**材料贡献的万分率成功率。0 表示封顶、不可再合成。
RatePerMaterial uint32
}
// Compoundable 报告这一档能不能继续往上合。
func (r CompoundRecipe) Compoundable() bool {
return r.RatePerMaterial > 0 && r.MaxMaterialCount >= CompoundMinMaterialCount
}
// AcceptsMaterialCount 报告这一档收不收这么多件。
func (r CompoundRecipe) AcceptsMaterialCount(count uint32) bool {
return r.Compoundable() && count >= CompoundMinMaterialCount && count <= r.MaxMaterialCount
}
// SuccessRateFor 是放这么多件时的万分率成功率,封顶 100%。
func (r CompoundRecipe) SuccessRateFor(count uint32) uint32 {
if !r.AcceptsMaterialCount(count) {
return 0
}
rate := r.RatePerMaterial * count
if rate > CompoundRateDenominator {
return CompoundRateDenominator
}
return rate
}
// CompoundLadder 是一条完整的阶梯,按稀有度升序。
type CompoundLadder struct {
rows []CompoundRecipe
}
// Recipe 返回该稀有度的配方。
func (l CompoundLadder) Recipe(rarity uint32) (CompoundRecipe, bool) {
for _, row := range l.rows {
if row.Rarity == rarity {
return row, true
}
}
return CompoundRecipe{}, false
}
// NextRarity 返回这一档合成出来的稀有度,也就是阶梯里紧挨着的下一档。
// 勋章的 3 -> 6 这一跳正是靠"表里的下一行"得到的,不是 rarity+1。
func (l CompoundLadder) NextRarity(rarity uint32) (uint32, bool) {
for index, row := range l.rows {
if row.Rarity != rarity {
continue
}
if !row.Compoundable() || index+1 >= len(l.rows) {
return 0, false
}
return l.rows[index+1].Rarity, true
}
return 0, false
}
// Rarities lists the ladder in order, lowest first.
func (l CompoundLadder) Rarities() []uint32 {
out := make([]uint32, 0, len(l.rows))
for _, row := range l.rows {
out = append(out, row.Rarity)
}
return out
}
// CompoundCatalog holds both ladders.
type CompoundCatalog struct {
gem CompoundLadder
flag CompoundLadder
}
// Ladder picks the ladder for a kind.
func (c CompoundCatalog) Ladder(kind CompoundKind) CompoundLadder {
if kind == CompoundFlag {
return c.flag
}
return c.gem
}
// LoadCompoundCatalog parses etc/guilddungeoncompound.etc.
func LoadCompoundCatalog(source dnfpvf.Source) (CompoundCatalog, error) {
if source == nil {
return CompoundCatalog{}, ErrCompoundCatalogUnavailable
}
text, err := source.ReadText(compoundPath)
if err != nil {
return CompoundCatalog{}, fmt.Errorf("read %s: %w", compoundPath, err)
}
document, err := dnfpvf.Parse(compoundPath, text)
if err != nil {
return CompoundCatalog{}, fmt.Errorf("parse %s: %w", compoundPath, err)
}
gem, err := parseCompoundLadder(document.Ints(compoundGemKey), compoundGemKey)
if err != nil {
return CompoundCatalog{}, err
}
flag, err := parseCompoundLadder(document.Ints(compoundFlagKey), compoundFlagKey)
if err != nil {
return CompoundCatalog{}, err
}
return CompoundCatalog{gem: gem, flag: flag}, nil
}
func parseCompoundLadder(values []int64, key string) (CompoundLadder, error) {
if len(values) < 3 || len(values)%3 != 0 {
return CompoundLadder{}, fmt.Errorf("%w: %s has %d values", ErrCompoundCatalogInvalid, key, len(values))
}
rows := make([]CompoundRecipe, 0, len(values)/3)
for index := 0; index+2 < len(values); index += 3 {
rarity, count, rate := values[index], values[index+1], values[index+2]
if rarity < 0 || count < 0 || rate < 0 ||
rarity > int64(^uint32(0)) || count > int64(^uint32(0)) || rate > compoundRateDenominat {
return CompoundLadder{}, fmt.Errorf("%w: %s row %d = %d %d %d",
ErrCompoundCatalogInvalid, key, index/3, rarity, count, rate)
}
if index > 0 && uint32(rarity) <= rows[len(rows)-1].Rarity {
return CompoundLadder{}, fmt.Errorf("%w: %s rarities are not ascending at row %d",
ErrCompoundCatalogInvalid, key, index/3)
}
rows = append(rows, CompoundRecipe{
Rarity: uint32(rarity),
MaxMaterialCount: uint32(count),
RatePerMaterial: uint32(rate),
})
}
return CompoundLadder{rows: rows}, nil
}
// CompoundRateDenominator 是成功率的分母。
const CompoundRateDenominator = compoundRateDenominat