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,264 @@
|
||||
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]
|
||||
`,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user