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

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

221 lines
7.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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