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

165 lines
4.6 KiB
Go

package dnfbridge
import (
"context"
"errors"
"os"
"testing"
"longheng.io/server/internal/modules/dnf/alignedcmd"
platformpvf "longheng.io/server/internal/platform/pvf"
)
type guardianShieldPVFTestSource map[string]string
func (source guardianShieldPVFTestSource) ReadText(path string) (string, error) {
text, found := source[path]
if !found {
return "", os.ErrNotExist
}
return text, nil
}
func TestParseCurrentGuardianShieldCatalogPreservesRepeatedShieldSections(t *testing.T) {
catalog, err := parseCurrentGuardianShieldCatalog(`
[tab]
` + "`quest shield tab grid`" + `
[shield]
[item index]
1001
[get condition]
quest
[grow type]
1
[clear quest index]
12723
[/shield]
[shield]
[item index]
2001
[get condition]
quest
[grow type]
2
[clear quest index]
3046
[/shield]
[shield]
[item index]
3001
[get condition]
level
[grow type]
0
[get shield level]
20
[/shield]
[/shield]
[/tab]
`)
if err != nil {
t.Fatal(err)
}
if _, found := catalog[1][1001]; !found {
t.Fatalf("grow1 catalog=%v", catalog[1])
}
if _, found := catalog[2][2001]; !found {
t.Fatalf("grow2 catalog=%v", catalog[2])
}
if catalog[1][1001].ClearQuestID != 12723 || catalog[2][2001].ClearQuestID != 3046 {
t.Fatalf("catalog clear quests=%+v", catalog)
}
if entry := catalog[0][3001]; entry.UnlockKind != alignedcmd.GuardianShieldUnlockLevel || entry.MinimumLevel != 20 {
t.Fatalf("shared level catalog=%+v", catalog[0])
}
}
func TestParseCurrentGuardianShieldCatalogRejectsIncompleteOpenedEntry(t *testing.T) {
_, err := parseCurrentGuardianShieldCatalog(`
[shield]
[item index]
4001
[/shield]
`)
if err == nil {
t.Fatal("incomplete opened shield entry was accepted")
}
}
func TestCurrentGuardianShieldResolverRequiresCatalogTypeJobGroupAndPath(t *testing.T) {
const (
goodPath = "equipment/character/knight/weapon/shield/good.equ"
levelPath = "equipment/character/knight/weapon/shield/level.equ"
)
shieldText := "[equipment type] `[support weapon]` 21\n[usable job] `[knight]`\n[/usable job]\n[item group name] `shield`\n"
resolver := currentGuardianShieldPVFResolver{
source: guardianShieldPVFTestSource{
goodPath: shieldText,
levelPath: shieldText,
},
equipmentPaths: map[int64]string{
1001: "character/knight/weapon/shield/good.equ",
3001: "character/knight/weapon/shield/level.equ",
},
catalog: map[byte]map[uint32]currentGuardianShieldCatalogEntry{
0: {3001: {UnlockKind: alignedcmd.GuardianShieldUnlockLevel, MinimumLevel: 20}},
1: {1001: {UnlockKind: alignedcmd.GuardianShieldUnlockQuest, ClearQuestID: 12723}},
2: {2001: {UnlockKind: alignedcmd.GuardianShieldUnlockQuest, ClearQuestID: 3046}},
},
}
resolution, err := resolver.Resolve(context.Background(), 1001, 0x21)
if err != nil {
t.Fatal(err)
}
if resolution.ItemID != 1001 || resolution.PVFPath != goodPath ||
resolution.EquipmentType != "[support weapon]" ||
resolution.UnlockKind != alignedcmd.GuardianShieldUnlockQuest || resolution.ClearQuestID != 12723 {
t.Fatalf("resolution=%+v", resolution)
}
if _, err := resolver.Resolve(context.Background(), 1001, 2); !errors.Is(err, errGuardianShieldNotInCatalog) {
t.Fatalf("cross-grow error=%v", err)
}
for _, growType := range []byte{1, 2, 0x21, 0x32} {
levelResolution, err := resolver.Resolve(context.Background(), 3001, growType)
if err != nil {
t.Fatalf("shared level shield grow=%d: %v", growType, err)
}
if levelResolution.UnlockKind != alignedcmd.GuardianShieldUnlockLevel || levelResolution.MinimumLevel != 20 {
t.Fatalf("shared level resolution grow=%d: %+v", growType, levelResolution)
}
}
resolver.source = guardianShieldPVFTestSource{
goodPath: "[equipment type] `[support weapon]` 21\n[usable job] `[swordman]`\n[/usable job]\n[item group name] `shield`\n",
}
if _, err := resolver.Resolve(context.Background(), 1001, 1); !errors.Is(err, errGuardianShieldItemInvalid) {
t.Fatalf("wrong usable job error=%v", err)
}
}
func TestCurrentGuardianShieldCatalogRealPVFHas25Rows(t *testing.T) {
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
if pvfPath == "" {
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to run real shieldwindownewdata smoke")
}
archive, err := platformpvf.LoadArchive(platformpvf.Options{Path: pvfPath})
if err != nil {
t.Fatal(err)
}
text, err := archive.ReadText(currentGuardianShieldCatalogPath)
if err != nil {
t.Fatal(err)
}
catalog, err := parseCurrentGuardianShieldCatalog(text)
if err != nil {
t.Fatal(err)
}
count := 0
for _, entries := range catalog {
count += len(entries)
}
if count != 25 {
t.Fatalf("shieldwindownewdata rows=%d, want 25 (level=%d grow1=%d grow2=%d)", count, len(catalog[0]), len(catalog[1]), len(catalog[2]))
}
}