Files
90CNServer/server/internal/modules/dnf/progression/table.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

200 lines
6.3 KiB
Go

package progression
import (
"context"
"errors"
"fmt"
"math"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
)
const (
ExperienceTablePath = "character/ExpTable.tbl"
SkillPointTablePath = "Etc/spTable.etc"
QuestParameterPath = "n_quest/questParameter.etc"
// MaximumCharacterLevel is the absolute playable level ceiling for the
// current DNF90 compatibility unit. Runtime PVF tables intentionally retain
// later rows, so their length must never be treated as authorization to
// persist level 91 or above.
MaximumCharacterLevel = 90
// CharacterLevelCap 是 Server-90 对同一个上限使用的名字。
// 支援兵(companion)模块是从 Server-90 移植过来的,引用的是这个名字;
// 这里做成别名,既不用改移植进来的代码,也保证两边永远同值。
CharacterLevelCap = MaximumCharacterLevel
)
var (
ErrTableEmpty = errors.New("dnf progression PVF table is empty")
ErrTableMalformed = errors.New("dnf progression PVF table is malformed")
ErrLevelOutOfRange = errors.New("dnf progression level is outside the loaded PVF table")
ErrExperienceOutOfRange = errors.New("dnf progression experience is outside the current EXE u32 range")
ErrSkillPointLedger = errors.New("dnf progression skill-point ledger is invalid")
ErrQuestDifficulty = errors.New("dnf progression quest difficulty is absent from PVF")
ErrQuestExperienceLookup = errors.New("dnf progression quest experience level is absent from PVF")
)
// Tables is an immutable snapshot of the current runtime PVF progression data.
type Tables struct {
experienceThresholds []uint32
spByLevel map[int]int
tpByLevel map[int]int
quest questExperienceRules
}
type Snapshot struct {
ExperienceThresholds int
SkillPointLevels int
TechniquePointLevels int
QuestExperienceLevels int
QuestGoldLevels int
QuestDifficulties int
}
// Load reads only the three typed progression files. No C# constants or packet
// captures are used as data fallbacks.
func Load(ctx context.Context, source dnfpvf.Source) (*Tables, error) {
if source == nil {
return nil, dnfpvf.ErrSourceRequired
}
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return nil, err
}
experience, err := LoadExperienceTable(ctx, source)
if err != nil {
return nil, err
}
if err := ctx.Err(); err != nil {
return nil, err
}
spDoc, err := readDocument(source, SkillPointTablePath)
if err != nil {
return nil, err
}
spByLevel, tpByLevel, err := parseSkillPointTable(spDoc)
if err != nil {
return nil, err
}
if err := ctx.Err(); err != nil {
return nil, err
}
questDoc, err := readDocument(source, QuestParameterPath)
if err != nil {
return nil, err
}
quest, err := parseQuestExperienceRules(questDoc)
if err != nil {
return nil, err
}
return &Tables{
experienceThresholds: append([]uint32(nil), experience.thresholds...),
spByLevel: spByLevel,
tpByLevel: tpByLevel,
quest: quest,
}, nil
}
func (t *Tables) Snapshot() Snapshot {
if t == nil {
return Snapshot{}
}
return Snapshot{
ExperienceThresholds: len(t.experienceThresholds),
SkillPointLevels: len(t.spByLevel),
TechniquePointLevels: len(t.tpByLevel),
QuestExperienceLevels: len(t.quest.baseByLevel),
QuestGoldLevels: len(t.quest.goldByLevel),
QuestDifficulties: len(t.quest.difficultyWeight),
}
}
func readDocument(source dnfpvf.Source, path string) (*dnfpvf.Document, error) {
text, err := source.ReadText(path)
if err != nil {
return nil, fmt.Errorf("read dnf progression PVF %q: %w", path, err)
}
doc, err := dnfpvf.Parse(path, text)
if err != nil {
return nil, fmt.Errorf("parse dnf progression PVF %q: %w", path, err)
}
return doc, nil
}
func parseExperienceThresholds(doc *dnfpvf.Document) ([]uint32, error) {
if doc == nil {
return nil, fmt.Errorf("%w: %s", ErrTableEmpty, ExperienceTablePath)
}
values := make([]uint32, 0, len(doc.Tokens))
for _, token := range doc.Tokens {
if token.Kind != dnfpvf.TokenInt {
continue
}
if token.Int < 0 || token.Int > math.MaxUint32 {
return nil, fmt.Errorf("%w: %s value=%d", ErrExperienceOutOfRange, ExperienceTablePath, token.Int)
}
value := uint32(token.Int)
// The runtime table uses repeated MaxInt32 sentinels above its playable
// range. Equal adjacent sentinels are valid; a decrease is not.
if len(values) > 0 && value < values[len(values)-1] {
return nil, fmt.Errorf("%w: %s threshold[%d]=%d previous=%d", ErrTableMalformed, ExperienceTablePath, len(values), value, values[len(values)-1])
}
values = append(values, value)
}
if len(values) == 0 {
return nil, fmt.Errorf("%w: %s", ErrTableEmpty, ExperienceTablePath)
}
return values, nil
}
func parseSkillPointTable(doc *dnfpvf.Document) (map[int]int, map[int]int, error) {
if doc == nil {
return nil, nil, fmt.Errorf("%w: %s", ErrTableEmpty, SkillPointTablePath)
}
sp, err := parseSkillPointSection(doc, "sp table")
if err != nil {
return nil, nil, err
}
tp, err := parseSkillPointSection(doc, "tp table")
if err != nil {
return nil, nil, err
}
return sp, tp, nil
}
func parseSkillPointSection(doc *dnfpvf.Document, section string) (map[int]int, error) {
values := doc.Ints(section)
if len(values) == 0 {
return nil, fmt.Errorf("%w: %s [%s]", ErrTableEmpty, SkillPointTablePath, section)
}
if len(values)%2 != 0 {
return nil, fmt.Errorf("%w: %s [%s] integers=%d", ErrTableMalformed, SkillPointTablePath, section, len(values))
}
table := make(map[int]int, len(values)/2)
for index := 0; index < len(values); index += 2 {
levelValue, spValue := values[index], values[index+1]
if levelValue <= 0 || levelValue > math.MaxInt || spValue < 0 || spValue > math.MaxInt {
return nil, fmt.Errorf("%w: %s [%s] level=%d points=%d", ErrTableMalformed, SkillPointTablePath, section, levelValue, spValue)
}
level := int(levelValue)
if _, duplicate := table[level]; duplicate {
return nil, fmt.Errorf("%w: %s [%s] duplicate level=%d", ErrTableMalformed, SkillPointTablePath, section, level)
}
table[level] = int(spValue)
}
return table, nil
}
func sectionTokens(doc *dnfpvf.Document, name string) []dnfpvf.Token {
if doc == nil {
return nil
}
tokens, ok := doc.Section(name)
if !ok {
return nil
}
return tokens
}