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,148 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/alignedcmd"
|
||||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||||
)
|
||||
|
||||
// op30 previously had no inbound route at all: the gameplay module registry did
|
||||
// not own it and it was not an aligned command, so the legacy dispatcher's
|
||||
// default branch logged "dnfbridge unhandled latest game command cmd=1 type=30"
|
||||
// and dropped the request. Both transports must now reach the inventory domain.
|
||||
func TestIncreaseStatusIsRoutedToTheInventoryDomain(t *testing.T) {
|
||||
opcode := uint16(dnfenum.CmdPacketIncreaseStatus)
|
||||
if _, owned := currentGameplayModules.legacyHandlers[opcode]; owned {
|
||||
t.Fatalf("op30 must stay on the aligned inventory route, not a gameplay module")
|
||||
}
|
||||
if _, owned := currentGameplayModules.upperHandlers[opcode]; owned {
|
||||
t.Fatalf("op30 must stay on the aligned inventory route, not a gameplay module")
|
||||
}
|
||||
decision, classified := alignedcmd.Classify(opcode)
|
||||
if !classified || decision.Domain != dnfenum.AlignedDomainInventory ||
|
||||
decision.Support != dnfenum.AlignedSupportDirect || decision.Action != alignedcmd.ActionPendingModule {
|
||||
t.Fatalf("classified=%t decision=%+v", classified, decision)
|
||||
}
|
||||
}
|
||||
|
||||
// op30 carries no item identity, so its owner depends on exactly the same three
|
||||
// PVF-backed inputs op44 uses. Each gate is opcode-keyed and must admit op30.
|
||||
func TestIncreaseStatusOpensEveryPVFBackedOwnerInput(t *testing.T) {
|
||||
if !alignedSkillOwnerRulesRequired(dnfenum.CmdPacketIncreaseStatus) {
|
||||
t.Fatal("op30 SP/TP books need the active-PVF skill catalog and point baseline")
|
||||
}
|
||||
service := &Service{}
|
||||
if _, err := service.alignedStackableUseResolverForCommand(dnfenum.CmdPacketIncreaseStatus); err == nil {
|
||||
t.Fatal("op30 must ask for the stackable-use resolver, so a missing catalog has to be reported")
|
||||
}
|
||||
if _, err := service.alignedDirectExperienceConsumablePlannerForCommand(dnfenum.CmdPacketIncreaseStatus); err == nil {
|
||||
t.Fatal("op30 must ask for the direct-experience planner, so a missing catalog has to be reported")
|
||||
}
|
||||
// An unrelated opcode still receives no resolver and no error.
|
||||
resolver, err := service.alignedStackableUseResolverForCommand(dnfenum.CmdPacketSortItem)
|
||||
if resolver != nil || err != nil {
|
||||
t.Fatalf("unrelated opcode resolver=%v err=%v", resolver != nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncreaseStatusRealPVFResolvesEveryBookTheClientSendsItFor(t *testing.T) {
|
||||
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
||||
if pvfPath == "" {
|
||||
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to verify the runtime op30 book families")
|
||||
}
|
||||
source, err := platformpvf.Open(pvfPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
catalog, err := newPVFDungeonDropCatalog(source)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resolver, err := (&Service{pvfItemCatalog: catalog}).alignedStackableUseResolverForCommand(dnfenum.CmdPacketIncreaseStatus)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for itemID, want := range map[int64]struct {
|
||||
path string
|
||||
kind alignedcmd.SkillPointBookKind
|
||||
delta int64
|
||||
}{
|
||||
1031: {path: "stackable/book_skill1.stk", kind: alignedcmd.SkillPointBookAddSP, delta: 5},
|
||||
1038: {path: "stackable/book_skill2.stk", kind: alignedcmd.SkillPointBookAddSP, delta: 20},
|
||||
1204: {path: "stackable/book_fskill1.stk", kind: alignedcmd.SkillPointBookAddTP, delta: 1},
|
||||
1205: {path: "stackable/book_fskill2.stk", kind: alignedcmd.SkillPointBookAddTP, delta: 5},
|
||||
80003: {path: "stackable/extention/test_book_skill1.stk", kind: alignedcmd.SkillPointBookAddSP, delta: 5},
|
||||
1206: {path: "stackable/book_fskillreset.stk", kind: alignedcmd.SkillPointBookResetTP},
|
||||
1253: {path: "stackable/book_fskillreset_event.stk", kind: alignedcmd.SkillPointBookResetTP},
|
||||
} {
|
||||
resolution, resolveErr := resolver(itemID)
|
||||
if resolveErr != nil || resolution.PVFPath != want.path || resolution.TPBook == nil ||
|
||||
resolution.TPBook.Kind != want.kind || resolution.TPBook.Delta != want.delta ||
|
||||
resolution.TPBook.MinimumLevel <= 0 {
|
||||
t.Fatalf("runtime SP/TP book %d resolution=%+v err=%v", itemID, resolution, resolveErr)
|
||||
}
|
||||
}
|
||||
|
||||
// Every runtime [increase status type] item is [etc], the same stackable
|
||||
// family the client sends op30 for. They must reach op30 through exactly the
|
||||
// planner op44 already owns, with their amounts read from the PVF operands.
|
||||
capsules := 0
|
||||
// The six numbered character-experience books, on the other hand, declare no
|
||||
// effect at all in the active PVF: they are bare [etc] stackables whose only
|
||||
// numeric trace is the localized name. They reach the same planner through
|
||||
// the path-keyed fallback in currentPVFCharacterExperienceBook, audited
|
||||
// against that name, so no stackable/book_exp* path may stay unowned here.
|
||||
undeclaredCharacterBooks := make([]string, 0, 6)
|
||||
ownedCharacterBooks := make([]string, 0, 6)
|
||||
for _, itemID := range catalog.stackableIDs {
|
||||
definition, resolveErr := catalog.ResolveItem(itemID)
|
||||
if resolveErr != nil {
|
||||
t.Fatalf("resolve stackable item=%d: %v", itemID, resolveErr)
|
||||
}
|
||||
resolution, resolveErr := resolver(int64(itemID))
|
||||
if resolveErr != nil {
|
||||
continue
|
||||
}
|
||||
if normalizeDirectExperienceType(definition.DirectExperienceType) != "" {
|
||||
if normalizeDungeonDropStackableType(definition.StackableType) != "[etc]" {
|
||||
t.Fatalf("increase-status item %d path=%s is %q, not [etc]", itemID, definition.PVFPath, definition.StackableType)
|
||||
}
|
||||
if resolution.DirectExperience == nil ||
|
||||
normalizeDirectExperienceType(resolution.DirectExperience.EffectType) == "" ||
|
||||
len(resolution.DirectExperience.EffectValues) == 0 {
|
||||
t.Fatalf("increase-status item %d path=%s resolution=%+v", itemID, definition.PVFPath, resolution)
|
||||
}
|
||||
capsules++
|
||||
}
|
||||
normalized := strings.ToLower(strings.ReplaceAll(definition.PVFPath, "\\", "/"))
|
||||
if !strings.HasPrefix(normalized, "stackable/book_exp") || strings.Contains(normalized, "guild") {
|
||||
continue
|
||||
}
|
||||
if resolution.DirectExperience == nil || resolution.TPBook != nil ||
|
||||
normalizeDirectExperienceType(resolution.DirectExperience.EffectType) != "[expup]" ||
|
||||
len(resolution.DirectExperience.EffectValues) != 1 ||
|
||||
resolution.DirectExperience.EffectValues[0] <= 0 {
|
||||
undeclaredCharacterBooks = append(undeclaredCharacterBooks, definition.PVFPath)
|
||||
continue
|
||||
}
|
||||
ownedCharacterBooks = append(ownedCharacterBooks, definition.PVFPath)
|
||||
}
|
||||
if capsules < 20 {
|
||||
t.Fatalf("runtime [increase status type] items reachable through op30: %d", capsules)
|
||||
}
|
||||
sort.Strings(undeclaredCharacterBooks)
|
||||
if len(undeclaredCharacterBooks) != 0 {
|
||||
t.Fatalf("op30 character-experience books still without a grant (%d): %v",
|
||||
len(undeclaredCharacterBooks), undeclaredCharacterBooks)
|
||||
}
|
||||
sort.Strings(ownedCharacterBooks)
|
||||
if len(ownedCharacterBooks) != len(currentCharacterExperienceBookAmounts) {
|
||||
t.Fatalf("op30 owns %d character-experience books, want %d: %v",
|
||||
len(ownedCharacterBooks), len(currentCharacterExperienceBookAmounts), ownedCharacterBooks)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user