DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,199 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user