DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

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

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

No files matched your search

@@ -0,0 +1,214 @@
package pet
import (
"errors"
"fmt"
"os"
"strings"
"testing"
platformpvf "longheng.io/server/internal/platform/pvf"
)
type petCatalogTestSource map[string]string
func (s petCatalogTestSource) ReadText(path string) (string, error) {
if text, found := s[path]; found {
return text, nil
}
return "", fmt.Errorf("missing %s", path)
}
func TestPVFCatalogResolvesHatchAndCreatureExperience(t *testing.T) {
source := petCatalogTestSource{
petEquipmentListPath: "63006 `creature/egg_faras.equ`\n63000 `creature/faras.equ`\n",
"equipment/creature/egg_faras.equ": "[equipment type] `[creature]`\n[output index] 63000\n",
"equipment/creature/faras.equ": "[name] `Faras`\n[equipment type] `[creature]`\n[minimum level] 3\n",
petCreatureExperiencePath: petCatalogTestExperienceText(),
}
catalog, err := NewPVFCatalog(source)
if err != nil {
t.Fatal(err)
}
var resolver PetHatchResolver = catalog
definition, err := resolver.ResolveHatch(63006)
if err != nil {
t.Fatal(err)
}
if definition != (PetHatchDefinition{
EggItemID: 63006,
HatchedItemID: 63000,
EggPVFPath: "equipment/creature/egg_faras.equ",
HatchedPVFPath: "equipment/creature/faras.equ",
MinimumLevel: 3,
}) {
t.Fatalf("definition=%+v", definition)
}
creature, err := catalog.ResolveCreature(63000)
if err != nil {
t.Fatal(err)
}
if creature.Name != "Faras" || creature.PVFPath != "equipment/creature/faras.equ" {
t.Fatalf("creature=%+v", creature)
}
table := catalog.ExperienceTable()
if got := table.LevelForExperience(0); got != 1 {
t.Fatalf("level(exp=0)=%d", got)
}
if got := table.LevelForExperience(2); got != 2 {
t.Fatalf("level(exp=2)=%d", got)
}
if got := table.LevelForExperience(4); got != 2 {
t.Fatalf("level(exp=4)=%d", got)
}
if got := table.LevelForExperience(5); got != 3 {
t.Fatalf("level(exp=5)=%d", got)
}
if got := table.LevelForExperience(1 << 60); got != MaxCreatureLevel {
t.Fatalf("level(max exp)=%d", got)
}
values := table.Thresholds()
values[0] = 999999
if catalog.ExperienceTable().Thresholds()[0] != 2 {
t.Fatal("experience thresholds leaked mutable catalog state")
}
}
func TestPVFCatalogDefaultsMissingCreatureMinimumLevelToOne(t *testing.T) {
source := petCatalogTestSource{
petEquipmentListPath: "10 `egg.equ`\n11 `pet.equ`\n",
"equipment/egg.equ": "[equipment type] `[creature]`\n[output index] 11\n",
"equipment/pet.equ": "[equipment type] `[creature]`\n",
petCreatureExperiencePath: petCatalogTestExperienceText(),
}
catalog, err := NewPVFCatalog(source)
if err != nil {
t.Fatal(err)
}
definition, err := catalog.ResolveHatch(10)
if err != nil {
t.Fatal(err)
}
if definition.MinimumLevel != 1 {
t.Fatalf("minimum level=%d", definition.MinimumLevel)
}
}
func TestPVFCatalogRejectsUnprovedHatchMappings(t *testing.T) {
base := petCatalogTestSource{
petEquipmentListPath: "10 `egg.equ`\n11 `pet.equ`\n",
"equipment/egg.equ": "[equipment type] `[creature]`\n[output index] 11\n",
"equipment/pet.equ": "[equipment type] `[creature]`\n",
petCreatureExperiencePath: petCatalogTestExperienceText(),
}
tests := []struct {
name string
mutate func(petCatalogTestSource)
want error
}{
{
name: "source is not creature",
mutate: func(source petCatalogTestSource) {
source["equipment/egg.equ"] = "[equipment type] `[weapon]`\n[output index] 11\n"
},
want: ErrPetPVFNotCreature,
},
{
name: "missing output",
mutate: func(source petCatalogTestSource) {
source["equipment/egg.equ"] = "[equipment type] `[creature]`\n"
},
want: ErrPetPVFHatchOutputInvalid,
},
{
name: "same output",
mutate: func(source petCatalogTestSource) {
source["equipment/egg.equ"] = "[equipment type] `[creature]`\n[output index] 10\n"
},
want: ErrPetPVFHatchOutputInvalid,
},
{
name: "output absent from equipment list",
mutate: func(source petCatalogTestSource) {
source["equipment/egg.equ"] = "[equipment type] `[creature]`\n[output index] 12\n"
},
want: ErrPetPVFEquipmentUnresolved,
},
{
name: "output is not creature",
mutate: func(source petCatalogTestSource) {
source["equipment/pet.equ"] = "[equipment type] `[artifact red]`\n"
},
want: ErrPetPVFNotCreature,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
source := make(petCatalogTestSource, len(base))
for key, value := range base {
source[key] = value
}
test.mutate(source)
catalog, err := NewPVFCatalog(source)
if err != nil {
t.Fatal(err)
}
_, err = catalog.ResolveHatch(10)
if !errors.Is(err, test.want) {
t.Fatalf("error=%v want=%v", err, test.want)
}
})
}
}
func TestPVFCatalogRejectsMalformedExperienceTable(t *testing.T) {
base := petCatalogTestSource{
petEquipmentListPath: "10 `egg.equ`\n",
petCreatureExperiencePath: petCatalogTestExperienceText(),
}
for _, text := range []string{
"2 5 9",
strings.Replace(petCatalogTestExperienceText(), " 5 ", " 2 ", 1),
} {
source := make(petCatalogTestSource, len(base))
for key, value := range base {
source[key] = value
}
source[petCreatureExperiencePath] = text
if _, err := NewPVFCatalog(source); !errors.Is(err, ErrPetPVFExperienceTableShape) {
t.Fatalf("error=%v", err)
}
}
}
func TestRealScriptPVFPetCatalogHatchAndExperience(t *testing.T) {
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
if pvfPath == "" {
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to verify the runtime pet catalog")
}
archive, err := platformpvf.LoadArchive(platformpvf.Options{Path: pvfPath})
if err != nil {
t.Fatal(err)
}
catalog, err := NewPVFCatalog(archive)
if err != nil {
t.Fatal(err)
}
definition, err := catalog.ResolveHatch(63006)
if err != nil {
t.Fatal(err)
}
if definition.HatchedItemID != 63000 || definition.MinimumLevel != 1 {
t.Fatalf("real hatch definition=%+v", definition)
}
thresholds := catalog.ExperienceTable().Thresholds()
if len(thresholds) != MaxCreatureLevel || thresholds[0] != 2 || thresholds[len(thresholds)-1] != 3564 {
t.Fatalf("real experience thresholds count=%d first=%d last=%d", len(thresholds), thresholds[0], thresholds[len(thresholds)-1])
}
}
func petCatalogTestExperienceText() string {
return "2 5 9 15 23 33 44 57 71 88 106 126 148 171 196 223 252 283 315 350 " +
"388 429 472 517 565 615 668 725 785 849 918 993 1073 1158 1245 1335 1430 1532 1642 1759 " +
"1879 2011 2151 2301 2464 2639 2829 3049 3294 5579\n"
}