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

268 lines
10 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package dnfbridge
import (
"context"
"strings"
"testing"
"longheng.io/server/internal/modules/dnf/alignedcmd"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
dnfmemory "longheng.io/server/internal/modules/dnf/repository/memory"
)
// 服主 2026-10-03 截图口径:未佩戴赫尔德 273960 金币,佩戴后 263002。
// 273960 × (100-4) / 100 = 263001.6 ⇒ 客户端显示 263002(服务端同口径取下整)。
func TestCurrentUpgradeAssistFromText(t *testing.T) {
for _, tc := range []struct {
name string
text string
wantDiscount int64
wantCompound int64
}{
{
name: "hilda the pet",
text: "[name]\n`赫尔德` \n[upgrade cost discount]\n4 \n[increase upgrade compound rate]\n400 \n",
wantDiscount: 4,
wantCompound: 400,
},
{
name: "a belt with only the cost discount",
text: "[name]\n`传承腰带` \n[upgrade cost discount]\n10 \n",
wantDiscount: 10,
},
{
name: "a title with only the rate",
text: "[name]\n`craftsmanship` \n[increase upgrade compound rate]\n200 \n",
wantCompound: 200,
},
{name: "an unrelated item", text: "[name]\n`potion` \n[rarity]\n2 \n"},
} {
t.Run(tc.name, func(t *testing.T) {
discount, compound := currentUpgradeAssistFromText(tc.text)
if discount != tc.wantDiscount || compound != tc.wantCompound {
t.Fatalf("discount=%d compound=%d want %d/%d",
discount, compound, tc.wantDiscount, tc.wantCompound)
}
})
}
}
// 端到端:把 PVF 策略包一层,戴上赫尔德之后
//
// 金币 273960 → 263002(与客户端确认框一致)
// 成功权重 13000 → 13520(+400/10000 = +4%)
//
// 夹具刻意照**线上 PVF 的真实形状**摆:equipment.lst 里记的是相对路径
// `creature/hilda/hilda.equ`,归档里的真名是 `equipment/creature/hilda/hilda.equ`。
// 早先这里两边都手写成带 equipment/ 前缀,于是测试全绿而线上永远取到 0
// (见 upgrade_worn_assist.go 里那段断点注释)——现在夹具复现真实形状,这个用例就是回归锁。
func TestWithCurrentWornUpgradeAssistMatchesTheClientPopup(t *testing.T) {
ctx := context.Background()
repositories := dnfmemory.NewMemoryGroup()
if err := repositories.Equipment.Save(ctx, dnfrepo.EquipmentRecord{
CharacterID: "11",
Entries: map[string]dnfrepo.EquipmentEntry{
// 宠物在穿戴槽 26(inventory.isWornCreatureSlot)。
"26": {SlotIndex: 26, ItemID: 400990169},
},
}); err != nil {
t.Fatal(err)
}
service := &Service{
pvfItemCatalog: &pvfDungeonDropCatalog{
source: dungeonDropCatalogTestSource{
"equipment/creature/hilda/hilda.equ": hildaEquFixtureText,
},
itemRefs: map[uint32]dungeonDropItemReference{
// lst 原文:相对 equipment/ 的写法(线上就是这样)。
400990169: {kind: dungeonDropItemEquipment, path: "creature/hilda/hilda.equ"},
},
},
}
session := &gameSession{connID: "worn-assist-test", selectedCharacterID: 11}
wrapped := service.withCurrentWornUpgradeAssist(
func(string, int, int64, int) (alignedcmd.UpgradePolicyResolution, error) {
return alignedcmd.UpgradePolicyResolution{SuccessWeight: 13000, GoldCost: 273960}, nil
},
session,
repositories,
)
policy, err := wrapped("reinforce", 0, 0x1234, 0)
if err != nil {
t.Fatal(err)
}
if policy.GoldCost != 263002 {
t.Fatalf("gold cost=%d want 263002 (the client popup shows the discounted price)", policy.GoldCost)
}
if policy.SuccessWeight != 13520 {
t.Fatalf("success weight=%d want 13520 (+4%%)", policy.SuccessWeight)
}
// 没戴宠物:策略原样不动。
empty := dnfmemory.NewMemoryGroup()
plain := service.withCurrentWornUpgradeAssist(
func(string, int, int64, int) (alignedcmd.UpgradePolicyResolution, error) {
return alignedcmd.UpgradePolicyResolution{SuccessWeight: 13000, GoldCost: 273960}, nil
},
session,
empty,
)
got, err := plain("reinforce", 0, 0x1234, 0)
if err != nil {
t.Fatal(err)
}
if got.GoldCost != 273960 || got.SuccessWeight != 13000 {
t.Fatalf("without the pet the policy must be untouched: %+v", got)
}
}
// 穿戴中的宠物**不在装备记录里**,而在宠物记录的 EquippedKey 上 —— 现场 2026-10-03
// 第一次只扫了装备记录,结果戴了赫尔德还是按原价扣(服主:"费用还是不对")。
func TestWornUpgradeAssistReadsTheEquippedPetRecord(t *testing.T) {
ctx := context.Background()
repositories := dnfmemory.NewMemoryGroup()
if err := repositories.Pet.Save(ctx, dnfrepo.PetRecord{
CharacterID: "11",
EquippedKey: "pet-a",
Entries: map[string]dnfrepo.PetEntry{
"pet-a": {PetKey: "pet-a", ItemID: 400990169},
},
}); err != nil {
t.Fatal(err)
}
service := &Service{
pvfItemCatalog: &pvfDungeonDropCatalog{
source: dungeonDropCatalogTestSource{
"equipment/creature/hilda/hilda.equ": hildaEquFixtureText,
},
itemRefs: map[uint32]dungeonDropItemReference{
400990169: {kind: dungeonDropItemEquipment, path: "creature/hilda/hilda.equ"},
},
},
}
session := &gameSession{connID: "worn-pet-test", selectedCharacterID: 11}
wrapped := service.withCurrentWornUpgradeAssist(
func(string, int, int64, int) (alignedcmd.UpgradePolicyResolution, error) {
return alignedcmd.UpgradePolicyResolution{SuccessWeight: 13000, GoldCost: 273960}, nil
},
session,
repositories,
)
policy, err := wrapped("reinforce", 0, 0x1234, 0)
if err != nil {
t.Fatal(err)
}
if policy.GoldCost != 263002 || policy.SuccessWeight != 13520 {
t.Fatalf("equipped pet record was ignored: cost=%d weight=%d want 263002/13520",
policy.GoldCost, policy.SuccessWeight)
}
}
// hildaEquFixtureText 是线上 equipment/creature/hilda/hilda.equ 原文的等价夹具
// (字段名、值、行尾多余空格都照抄;服主那只宠物 id 400990076 就是这份)。
const hildaEquFixtureText = "[name]\n`赫尔德` \n[explain]\n`装备后, 强化费用降低4%%且强化成功率增加4%%。` \n" +
"[upgrade cost discount]\n4 \n[increase upgrade compound rate]\n400 \n"
// 断点证据链:宠物的 lst 路径与归档路径必须**分开**报出来,而且两个字段的原始值要能读到。
// 现场 2026-10-03 第二轮就是靠这一条定位的(聚合值 0/0 时看不出断在哪一环)。
func TestWornUpgradeAssistReportsPerItemPathEvidence(t *testing.T) {
ctx := context.Background()
repositories := dnfmemory.NewMemoryGroup()
if err := repositories.Pet.Save(ctx, dnfrepo.PetRecord{
CharacterID: "11",
EquippedKey: "pet-a",
Entries: map[string]dnfrepo.PetEntry{
"pet-a": {PetKey: "pet-a", ItemID: 400990076},
},
}); err != nil {
t.Fatal(err)
}
service := &Service{
pvfItemCatalog: &pvfDungeonDropCatalog{
source: dungeonDropCatalogTestSource{
"equipment/creature/hilda/hilda.equ": hildaEquFixtureText,
},
// itemRefs 故意留空:逼着走"现查 equipment.lst"那条兜底,
// 顺带把 id → lst 原文路径 这一段也钉住。
itemRefs: map[uint32]dungeonDropItemReference{},
},
}
catalog := service.pvfItemCatalog
catalog.upgradeAssistPathIndex = map[uint32]string{
400990076: "creature/hilda/hilda.equ",
}
assist := service.currentWornUpgradeAssist(ctx, repositories, "11")
if !assist.PetEquipped || assist.PetItemID != 400990076 {
t.Fatalf("the worn pet was not seen: equipped=%v item=%d", assist.PetEquipped, assist.PetItemID)
}
pet := assist.petEvidence()
if pet.ListedPath != "creature/hilda/hilda.equ" {
t.Fatalf("listed path=%q want the relative equipment.lst spelling", pet.ListedPath)
}
if pet.ArchivePath != "equipment/creature/hilda/hilda.equ" {
t.Fatalf("archive path=%q want the real archive entry", pet.ArchivePath)
}
if pet.TextBytes == 0 {
t.Fatalf("the PVF text was not read at all: %+v", pet)
}
if pet.Discount != 4 || pet.Compound != 400 {
t.Fatalf("raw field values=%d/%d want 4/400 (err=%q)", pet.Discount, pet.Compound, pet.Err)
}
if pet.Err != "" {
t.Fatalf("unexpected read error: %q", pet.Err)
}
if assist.DiscountPercent != 4 || assist.CompoundRate != 400 {
t.Fatalf("aggregate=%d/%d want 4/400", assist.DiscountPercent, assist.CompoundRate)
}
// 日志字段必须是无空格的一行,否则会被当成多个 key=value。
described := assist.describeItems()
if described == "" || described == "-" {
t.Fatalf("describeItems came back empty: %q", described)
}
if strings.ContainsAny(described, " \t\n") {
t.Fatalf("describeItems must not contain whitespace: %q", described)
}
if !strings.Contains(described, "400990076|creature/hilda/hilda.equ|equipment/creature/hilda/hilda.equ|") {
t.Fatalf("describeItems lost the evidence chain: %q", described)
}
}
// 读不到文本时(归档里没这个路径)证据要写清楚原因,而不是静默 0。
func TestWornUpgradeAssistReportsAMissingDocument(t *testing.T) {
service := &Service{
pvfItemCatalog: &pvfDungeonDropCatalog{
source: dungeonDropCatalogTestSource{},
itemRefs: map[uint32]dungeonDropItemReference{},
},
}
service.pvfItemCatalog.upgradeAssistPathIndex = map[uint32]string{
400990076: "creature/hilda/hilda.equ",
}
item := service.currentPVFUpgradeAssist(service.pvfItemCatalog, 400990076)
if item.Discount != 0 || item.Compound != 0 {
t.Fatalf("a missing document must not produce values: %+v", item)
}
if item.Err == "" {
t.Fatalf("a missing document must record why: %+v", item)
}
if strings.ContainsAny(item.Err, " \t\n") {
t.Fatalf("the error must stay a single log-field token: %q", item.Err)
}
// id 连 lst 里都没有时是另一截断点,要能区分开。
unlisted := service.currentPVFUpgradeAssist(service.pvfItemCatalog, 123456789)
if unlisted.Err != "item-id-not-in-equipment-lst" {
t.Fatalf("unlisted id reported %q", unlisted.Err)
}
}
// currentUpgradeAssistFromText 的负数/垃圾值一律按 0 处理(绝不放大成负折扣)。
func TestCurrentUpgradeAssistFromTextIgnoresJunk(t *testing.T) {
discount, compound := currentUpgradeAssistFromText(
"[upgrade cost discount]\nabc \n[increase upgrade compound rate]\n \n")
if discount != 0 || compound != 0 {
t.Fatalf("junk text produced discount=%d compound=%d", discount, compound)
}
}