Files
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

265 lines
9.3 KiB
Go

package progression
import (
"errors"
"os"
"reflect"
"strings"
"testing"
platformpvf "longheng.io/server/internal/platform/pvf"
)
func TestMonsterExperienceSourcesPreservePVFValuesWithoutInventingAwardFormula(t *testing.T) {
sources, err := LoadMonsterExperienceSources(monsterExperienceTestSource())
if err != nil {
t.Fatal(err)
}
if snapshot := sources.Snapshot(); snapshot != (MonsterExperienceSourceSnapshot{
MonsterLevels: 3, MonsterGlobalRates: 1, PartyRates: 4,
StarterPartyRates: 4, DifficultyRates: 5, DungeonIncreasingRates: 2,
PenaltyRows: 37, ClearGlobalRates: 1, ClearRankRates: 5,
}) {
t.Fatalf("snapshot=%+v", snapshot)
}
if value, found := sources.MonsterTableValue(2); !found || value != 60 {
t.Fatalf("level 2=%d,%t", value, found)
}
if _, found := sources.MonsterTableValue(4); found {
t.Fatal("out-of-table monster level fabricated")
}
if value, found := sources.DungeonIncreasingRate(7146); !found || value != 2.3 {
t.Fatalf("dungeon 7146=%v,%t", value, found)
}
if _, found := sources.DungeonIncreasingRate(3); found {
t.Fatal("dungeon 3 rate fabricated")
}
modifiers := sources.RawModifiers()
if !reflect.DeepEqual(modifiers.MonsterGlobalRates, []float64{1}) ||
modifiers.PartyRates != ([4]float64{1, 2, 3, 4}) ||
modifiers.StarterPartyRates != ([4]float64{1, 2, 3, 4}) ||
modifiers.DifficultyRates != ([5]float64{1.3, 2, 2.5, 3, 4}) ||
!reflect.DeepEqual(modifiers.ClearGlobalRates, []float64{1}) ||
modifiers.ClearRankRates != ([5]float64{0.05, 0.1, 0.12, 0.15, 0.2}) {
t.Fatalf("raw modifiers=%+v", modifiers)
}
if rate, found := sources.PenaltyRateBySourceKey(-16); !found || rate != 0.3 {
t.Fatalf("penalty -16=%v,%t", rate, found)
}
if rate, found := sources.PenaltyRateBySourceKey(5); !found || rate != 1 {
t.Fatalf("penalty 5=%v,%t", rate, found)
}
if _, found := sources.PenaltyRateBySourceKey(21); found {
t.Fatal("penalty key outside PVF table fabricated")
}
plan, err := sources.PlanSource(1, 1, 0, 7146)
if err != nil {
t.Fatal(err)
}
if plan.MonsterTableValue != 30 || plan.PartyRate != 1 || plan.StarterPartyRate != 1 ||
plan.DifficultyRate != 1.3 || !plan.DungeonRateFound || plan.DungeonIncreasingRate != 2.3 {
t.Fatalf("plan source values=%+v", plan)
}
if plan.AwardReady || !reflect.DeepEqual(plan.Blockers, []MonsterExperienceBlocker{
MonsterExperienceCombinationUnproved,
MonsterExperiencePenaltyDirectionUnproved,
MonsterExperienceActorTypeUnproved,
}) {
t.Fatalf("unproved award was enabled: %+v", plan)
}
missingDungeon, err := sources.PlanSource(1, 1, 0, 3)
if err != nil {
t.Fatal(err)
}
if missingDungeon.DungeonRateFound || missingDungeon.DungeonIncreasingRate != 0 || missingDungeon.AwardReady {
t.Fatalf("missing dungeon modifier got a fallback: %+v", missingDungeon)
}
for _, request := range []struct {
level int
party int
difficulty int
}{
{level: 0, party: 1, difficulty: 0},
{level: 1, party: 0, difficulty: 0},
{level: 1, party: 1, difficulty: 5},
} {
if _, err := sources.PlanSource(request.level, request.party, request.difficulty, 3); !errors.Is(err, ErrMonsterExperienceLookup) {
t.Fatalf("request=%+v error=%v", request, err)
}
}
}
func TestMonsterExperienceSourcesRejectMalformedOrPartialPVF(t *testing.T) {
if _, err := LoadMonsterExperienceSources(nil); !errors.Is(err, ErrMonsterExperienceSourceRequired) {
t.Fatalf("nil source error=%v", err)
}
tests := []struct {
name string
mutate func(progressionMemorySource)
}{
{
name: "odd monster table",
mutate: func(source progressionMemorySource) {
source[MonsterExperienceTablePath] = "1 30 2"
},
},
{
name: "noncontiguous monster level",
mutate: func(source progressionMemorySource) {
source[MonsterExperienceTablePath] = "1 30 3 72"
},
},
{
name: "nonnumeric monster value",
mutate: func(source progressionMemorySource) {
source[MonsterExperienceTablePath] = "1 30 2 missing"
},
},
{
name: "missing global section",
mutate: func(source progressionMemorySource) {
source[ServerParameterPath] = strings.Replace(source[ServerParameterPath], "[monster exp bonusrate]\n1.00\n", "", 1)
},
},
{
name: "short party vector",
mutate: func(source progressionMemorySource) {
source[ServerParameterPath] = strings.Replace(source[ServerParameterPath], "[party user number exp bonusrate]\n1 2 3 4", "[party user number exp bonusrate]\n1 2 3", 1)
},
},
{
name: "short difficulty vector",
mutate: func(source progressionMemorySource) {
source[ServerParameterPath] = strings.Replace(source[ServerParameterPath], "1.30 2.00 2.50 3.00 4.00", "1.30 2.00", 1)
},
},
{
name: "missing clear rate",
mutate: func(source progressionMemorySource) {
source[ServerParameterPath] = strings.Replace(source[ServerParameterPath], "[clear exp bonusrate]\n1.00\n", "", 1)
},
},
{
name: "short clear rank vector",
mutate: func(source progressionMemorySource) {
source[ServerParameterPath] = strings.Replace(source[ServerParameterPath], "0.05 0.10 0.12 0.15 0.20", "0.05 0.10", 1)
},
},
{
name: "duplicate dungeon modifier",
mutate: func(source progressionMemorySource) {
source[ServerParameterPath] = strings.Replace(source[ServerParameterPath], "7146 2.30 4907 22.00", "7146 2.30 7146 22.00", 1)
},
},
{
name: "nonnumeric dungeon rate",
mutate: func(source progressionMemorySource) {
source[ServerParameterPath] = strings.Replace(source[ServerParameterPath], "7146 2.30", "7146 unknown", 1)
},
},
{
name: "short penalty table",
mutate: func(source progressionMemorySource) {
source[WorldMapExperiencePenaltyPath] = "[penalty table info]\n-16 0.30\n[/penalty table info]\n"
},
},
{
name: "noncontiguous penalty key",
mutate: func(source progressionMemorySource) {
source[WorldMapExperiencePenaltyPath] = strings.Replace(source[WorldMapExperiencePenaltyPath], "-15 0.50", "-14 0.50", 1)
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
source := monsterExperienceTestSource()
test.mutate(source)
_, err := LoadMonsterExperienceSources(source)
if !errors.Is(err, ErrMonsterExperienceSectionShape) && !errors.Is(err, ErrMonsterExperienceSectionMissing) {
t.Fatalf("error=%v", err)
}
})
}
}
func TestRealScriptPVFMonsterExperienceSourcesPreserveExactRuntimeRows(t *testing.T) {
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
if pvfPath == "" {
pvfPath = os.Getenv("DNF_WORLDMAP_REAL_PVF_SMOKE")
}
if pvfPath == "" {
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to verify real monster experience PVF sources")
}
archive, err := platformpvf.Open(pvfPath)
if err != nil {
t.Fatal(err)
}
sources, err := LoadMonsterExperienceSources(archive)
if err != nil {
t.Fatal(err)
}
if snapshot := sources.Snapshot(); snapshot != (MonsterExperienceSourceSnapshot{
MonsterLevels: 300, MonsterGlobalRates: 1, PartyRates: 4,
StarterPartyRates: 4, DifficultyRates: 5, DungeonIncreasingRates: 11,
PenaltyRows: 37, ClearGlobalRates: 1, ClearRankRates: 5,
}) {
t.Fatalf("real snapshot=%+v", snapshot)
}
for level, want := range map[int]uint32{1: 30, 2: 60, 3: 72, 100: 9517, 300: 64906} {
if got, found := sources.MonsterTableValue(level); !found || got != want {
t.Fatalf("real monster table level=%d value=%d,%t want=%d", level, got, found, want)
}
}
modifiers := sources.RawModifiers()
if !reflect.DeepEqual(modifiers.MonsterGlobalRates, []float64{1}) ||
modifiers.PartyRates != ([4]float64{1, 2, 3, 4}) ||
modifiers.StarterPartyRates != ([4]float64{1, 2, 3, 4}) ||
modifiers.DifficultyRates != ([5]float64{1.3, 2, 2.5, 3, 4}) ||
!reflect.DeepEqual(modifiers.ClearGlobalRates, []float64{1}) ||
modifiers.ClearRankRates != ([5]float64{0.05, 0.1, 0.12, 0.15, 0.2}) {
t.Fatalf("real raw modifiers=%+v", modifiers)
}
for key, want := range map[int]float64{-16: 0.3, -7: 0.8, -3: 1, 5: 1, 6: 0.9, 20: 0.3} {
if got, found := sources.PenaltyRateBySourceKey(key); !found || got != want {
t.Fatalf("real penalty key=%d value=%v,%t want=%v", key, got, found, want)
}
}
for dungeonID, want := range map[int64]float64{7146: 2.3, 7155: 2.9, 4907: 22} {
if got, found := sources.DungeonIncreasingRate(dungeonID); !found || got != want {
t.Fatalf("real dungeon=%d value=%v,%t want=%v", dungeonID, got, found, want)
}
}
if plan, err := sources.PlanSource(1, 1, 0, 3); err != nil || plan.AwardReady || plan.DungeonRateFound {
t.Fatalf("real dungeon 3 source plan=%+v err=%v", plan, err)
}
}
func monsterExperienceTestSource() progressionMemorySource {
return progressionMemorySource{
MonsterExperienceTablePath: "1 30 2 60 3 72\n",
ServerParameterPath: `[monster exp bonusrate]
1.00
[party user number exp bonusrate]
1 2 3 4
[party user number exp bonusrate starter server]
1 2 3 4
[dungeon difficulty exp bonusrate]
1.30 2.00 2.50 3.00 4.00
[clear exp bonusrate]
1.00
[clear rank exp bonusrate]
0.05 0.10 0.12 0.15 0.20
[experience increasing point]
7146 2.30 4907 22.00
[/experience increasing point]
`,
WorldMapExperiencePenaltyPath: `[penalty table info]
-16 0.30 -15 0.50 -14 0.50 -13 0.50 -12 0.70 -11 0.70 -10 0.70 -9 0.80 -8 0.80 -7 0.80 -6 0.90 -5 0.90 -4 0.90 -3 1.00 -2 1.00 -1 1.00 0 1.00 1 1.00 2 1.00 3 1.00 4 1.00 5 1.00 6 0.90 7 0.90 8 0.90 9 0.80 10 0.80 11 0.80 12 0.70 13 0.70 14 0.70 15 0.50 16 0.50 17 0.50 18 0.30 19 0.30 20 0.30
[/penalty table info]
`,
}
}