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

432 lines
17 KiB
Go

package dnfbridge
import (
"context"
"encoding/binary"
"os"
"reflect"
"strings"
"testing"
"time"
"longheng.io/server/internal/modules/dnf/worldmap"
platformpvf "longheng.io/server/internal/platform/pvf"
)
func TestCurrentDungeonMonsterPlanUsesPVFDimensionMembershipAsOrdinaryPool(t *testing.T) {
service, runtime, _ := prepareCurrentDungeonDropTest(t, 1, 3227)
source := service.dungeonMonsterTable.source.(bridgePVFSource)
source[dungeonCardDropRulePath] = currentDungeonGoldDropTestRuleText()
source[dungeonCardGoldReferencePath] = "[gold drop ref table]\n20 100 0\n"
source[currentDungeonDimensionGatePath] = "[dimensiondungeon list]\n700 25 0\n[/dimensiondungeon list]\n"
source[currentDungeonDimensionInfoPath] = `[dimension dungeon info]
[/dimension dungeon info]
[dungeon difficulty condition info]
1 70 2 80 3 85
[/dungeon difficulty condition info]
[drop item Info]
0 0 3228
0 1 3229
[/drop item Info]
`
source[dungeonDropStackableList] = "3227 `material/test_drop.stk`\n3228 `material/test_dimension_drop.stk`\n3229 `material/test_dimension_drop_2.stk`\n"
source["stackable/material/test_dimension_drop.stk"] = "[name]\n`Test Dimension Drop`\n[stackable type]\n`[material]`\n[stack limit]\n999\n[attach type]\n`[free]`\n"
source["stackable/material/test_dimension_drop_2.stk"] = "[name]\n`Test Dimension Drop 2`\n[stackable type]\n`[material]`\n[stack limit]\n999\n[attach type]\n`[free]`\n"
currentDungeonMonsterDropRulesByCatalog.Delete(service.dungeonMonsterTable)
currentDungeonGoldReferencesByCatalog.Delete(service.dungeonMonsterTable)
currentDungeonDimensionDropsByMonsterCatalog.Delete(service.dungeonMonsterTable)
monster := runtime.Room.Snapshot().Monsters[0]
wires, err := service.planCurrentDungeonMonsterDrops(runtime, monster, currentSceneActorObjectKey(99))
if err != nil {
t.Fatal(err)
}
dimensionRows := 0
for _, wire := range wires {
itemID := binary.LittleEndian.Uint32(wire.Item.data[0x02:0x06])
if itemID == 3228 || itemID == 3229 {
dimensionRows++
}
}
if dimensionRows > 1 {
t.Fatalf("one ordinary-pool attempt emitted %d dimension rows; materials were appended directly: wires=%d", dimensionRows, len(wires))
}
}
func TestCurrentDungeonMonsterPlanKeepsDimensionFragmentsForSpecialRanks(t *testing.T) {
for _, rank := range []string{"[champion]", "[super champion]", "[boss]"} {
t.Run(rank, func(t *testing.T) {
service, runtime, _ := prepareCurrentDungeonDropTest(t, 1, 3227)
source := service.dungeonMonsterTable.source.(bridgePVFSource)
source[dungeonCardDropRulePath] = currentDungeonGoldDropTestRuleText()
source[dungeonCardGoldReferencePath] = "[gold drop ref table]\n20 100 0\n"
source[currentDungeonDimensionGatePath] = "[dimensiondungeon list]\n700 25 0\n[/dimensiondungeon list]\n"
source[currentDungeonDimensionInfoPath] = `[dimension dungeon info]
[/dimension dungeon info]
[dungeon difficulty condition info]
1 70 2 80 3 85
[/dungeon difficulty condition info]
[drop item Info]
0 0 3228
0 1 3229
[/drop item Info]
`
source[dungeonDropStackableList] = "3227 `material/test_drop.stk`\n3228 `material/test_dimension_drop.stk`\n3229 `material/test_dimension_drop_2.stk`\n"
source["stackable/material/test_dimension_drop.stk"] = "[name]\n`Test Dimension Drop`\n[stackable type]\n`[material]`\n[stack limit]\n999\n[attach type]\n`[free]`\n"
source["stackable/material/test_dimension_drop_2.stk"] = "[name]\n`Test Dimension Drop 2`\n[stackable type]\n`[material]`\n[stack limit]\n999\n[attach type]\n`[free]`\n"
currentDungeonMonsterDropRulesByCatalog.Delete(service.dungeonMonsterTable)
currentDungeonGoldReferencesByCatalog.Delete(service.dungeonMonsterTable)
currentDungeonDimensionDropsByMonsterCatalog.Delete(service.dungeonMonsterTable)
runtime.Room.mu.Lock()
runtime.Room.monsters[0].Spawn.Rank = rank
runtime.Room.mu.Unlock()
monster := runtime.Room.Snapshot().Monsters[0]
wires, err := service.planCurrentDungeonMonsterDrops(runtime, monster, currentSceneActorObjectKey(99))
if err != nil {
t.Fatal(err)
}
dimensionRows := 0
for _, wire := range wires {
itemID := binary.LittleEndian.Uint32(wire.Item.data[0x02:0x06])
if itemID == 3228 || itemID == 3229 {
dimensionRows++
}
}
if dimensionRows != 2 {
t.Fatalf("special rank %s emitted %d dimension rows, want both PVF tier/group rows: wires=%d", rank, dimensionRows, len(wires))
}
})
}
}
func TestCurrentDungeonMonsterPlanMapsEquipmentRollToCareerChroniclePool(t *testing.T) {
service, runtime, _ := prepareCurrentDungeonDropTest(t, 1, 3227)
source := service.dungeonMonsterTable.source.(bridgePVFSource)
rules := currentDungeonGoldDropTestRuleText()
rules = strings.Replace(rules, "1 200 10000 0 0 0 0", "1 200 0 0 10000 0 0", 1)
rules = strings.Replace(rules, "[item drop ref table]\n1 0 3", "[item drop ref table]\n20 0 3", 1)
source[dungeonCardDropRulePath] = rules
source[dungeonCardGoldReferencePath] = "[gold drop ref table]\n20 100 0\n"
source[currentDungeonDimensionGatePath] = "[dimensiondungeon list]\n700 25 0\n[/dimensiondungeon list]\n"
source[currentDungeonDimensionInfoPath] = `[dimension dungeon info]
[/dimension dungeon info]
[dungeon difficulty condition info]
1 70 2 80 3 85
[/dungeon difficulty condition info]
[drop item Info]
0 0 3228
[/drop item Info]
`
source[currentDungeonDimensionDropListPath] = `[chronicle grow type]
0 1
[normal chronicle list]
9101 9102
[/normal chronicle list]
[set chronicle list]
9201
[/set chronicle list]
[/chronicle grow type]
`
source[currentDungeonDimensionDropExcludePath] = "[exclude list]\n9102\n[/exclude list]\n"
source[currentDungeonClearRewardRulePath] = "[chronicle set reward rate]\n0 50 0\n[/chronicle set reward rate]\n"
source[dungeonDropStackableList] = "3227 `material/test_drop.stk`\n3228 `material/test_dimension_drop.stk`\n"
source["stackable/material/test_dimension_drop.stk"] = "[name]\n`Test Dimension Drop`\n[stackable type]\n`[material]`\n[stack limit]\n999\n[attach type]\n`[free]`\n"
source[dungeonDropEquipmentList] = "9101 `chronicle/test_normal.equ`\n9102 `chronicle/test_excluded.equ`\n9201 `chronicle/test_set.equ`\n"
for _, itemPath := range []string{"equipment/chronicle/test_normal.equ", "equipment/chronicle/test_excluded.equ", "equipment/chronicle/test_set.equ"} {
source[itemPath] = "[name]\n`Test Chronicle`\n[rarity]\n5\n[minimum level]\n20\n[durability]\n50\n[attach type]\n`[trade]`\n[equipment type]\n`[weapon]`\n"
}
currentDungeonMonsterDropRulesByCatalog.Delete(service.dungeonMonsterTable)
currentDungeonGoldReferencesByCatalog.Delete(service.dungeonMonsterTable)
currentDungeonDimensionDropsByMonsterCatalog.Delete(service.dungeonMonsterTable)
loadedRules, err := currentDungeonMonsterDropRules(service.dungeonMonsterTable)
if err != nil {
t.Fatal(err)
}
if row, found := loadedRules.Probability(20); !found || row.Rates[2] != 10000 {
t.Fatalf("equipment probability row=%+v found=%t", row, found)
}
if row, found := loadedRules.ItemReference(20); !found {
t.Fatalf("equipment item-reference row for level 20 is missing")
} else if row.ValueA != 0 || row.ValueB != 3 {
t.Fatalf("equipment item-reference row=%+v", row)
}
dimensionCatalog, err := currentDungeonDimensionDrops(service.dungeonMonsterTable)
if err != nil {
t.Fatal(err)
}
if pool := dimensionCatalog.ChronicleByCareer[currentDungeonDimensionChronicleKey{Job: 0, GrowType: 1}]; !reflect.DeepEqual(pool.Normal, []uint32{9101}) || !reflect.DeepEqual(pool.Set, []uint32{9201}) {
t.Fatalf("career chronicle pool=%+v", pool)
}
runtime.Character.Job = "0"
runtime.Character.Stats["grow_type"] = 1
runtime.Room.mu.Lock()
runtime.Room.monsters[0].Spawn.RandomDropCount = 1
runtime.Room.monsters[0].Spawn.Rank = "[boss]"
runtime.Room.mu.Unlock()
monster := runtime.Room.Snapshot().Monsters[0]
if monster.Spawn.RandomDropCount != 1 || monster.Spawn.Rank != "[boss]" {
t.Fatalf("monster random_drop_count=%d rank=%q", monster.Spawn.RandomDropCount, monster.Spawn.Rank)
}
wires, err := service.planCurrentDungeonMonsterDrops(runtime, monster, currentSceneActorObjectKey(99))
if err != nil {
t.Fatal(err)
}
counts := make(map[uint32]int)
for _, wire := range wires {
counts[binary.LittleEndian.Uint32(wire.Item.data[0x02:0x06])]++
}
if counts[9101] != 1 || counts[9102] != 0 || counts[9201] != 0 {
t.Fatalf("career chronicle equipment rows=%v", counts)
}
dimensionCatalog.ChronicleRates[0] = currentDungeonDimensionChronicleRate{RewardPercent: 100, SetPercent: 100}
itemCatalog, err := service.dungeonMonsterTable.DropCatalog()
if err != nil {
t.Fatal(err)
}
card, err := currentDungeonRandomDimensionCardRewardBundle(
newCurrentDungeonDropLCG(1),
currentDungeonGoldReference{Level: 20, Base: 100},
currentDungeonCardGoldTerms{Difficulty: 1, PartyMembers: 1, PartyRate: 1, WeightedKills: 40},
itemCatalog,
dimensionCatalog,
700,
20,
0,
1,
time.Unix(0, 0).UTC(),
)
if err != nil {
t.Fatal(err)
}
if len(card.Items) != 1 || card.Items[0].ItemID != 9201 || card.Items[0].Stackable {
t.Fatalf("career chronicle clear-card reward=%+v", card)
}
}
func TestCurrentDungeonDimensionDropsAreDerivedFromPVF(t *testing.T) {
source := bridgePVFSource{
currentDungeonDimensionGatePath: `[dimensiondungeon list]
62 25 0 65 25 0 67 25 0
[/dimensiondungeon list]
`,
currentDungeonDimensionInfoPath: `[dimension dungeon info]
0 7165 1 7166 2 7167
[/dimension dungeon info]
[dungeon difficulty condition info]
1 70 2 80 3 85
[/dungeon difficulty condition info]
[drop item Info]
0 0 3311
1 0 3311
1 1 10303569
2 0 3311
2 1 10303569
[/drop item Info]
`,
}
catalog, err := loadCurrentDungeonDimensionDrops(source)
if err != nil {
t.Fatal(err)
}
tier, pool, matched, err := currentDungeonDimensionDropPool(catalog, 62, 90)
if err != nil {
t.Fatal(err)
}
want := []dungeonMonsterDropPoolEntry{{ItemID: 3311, Weight: 100}, {ItemID: 10303569, Weight: 100}}
if !matched || tier != 2 || !reflect.DeepEqual(pool, want) {
t.Fatalf("legacy dimension pool matched=%t tier=%d pool=%v want=%v", matched, tier, pool, want)
}
tier, pool, matched, err = currentDungeonDimensionDropPool(catalog, 7166, 1)
if err != nil {
t.Fatal(err)
}
if !matched || tier != 1 || !reflect.DeepEqual(pool, want) {
t.Fatalf("exact dimension pool matched=%t tier=%d pool=%v", matched, tier, pool)
}
_, pool, matched, err = currentDungeonDimensionDropPool(catalog, 66, 90)
if err != nil || matched || len(pool) != 0 {
t.Fatalf("PVF-nonmember dungeon matched=%t pool=%v err=%v", matched, pool, err)
}
}
func TestCurrentDungeonDimensionChronicleDropsUseCareerPoolsRatesAndExcludeList(t *testing.T) {
source := bridgePVFSource{
currentDungeonDimensionGatePath: `[dimensiondungeon list]
62 25 0
[/dimensiondungeon list]
`,
currentDungeonDimensionInfoPath: `[dimension dungeon info]
0 7165 1 7166 2 7167
[/dimension dungeon info]
[dungeon difficulty condition info]
1 70 2 80 3 85
[/dungeon difficulty condition info]
[drop item Info]
0 0 3311 1 0 3311 1 1 10303569 2 0 3311 2 1 10303569
[/drop item Info]
`,
currentDungeonDimensionDropListPath: `[chronicle grow type]
0 1
[normal chronicle list]
125012 125013
[/normal chronicle list]
[set chronicle list]
120229 120230
[/set chronicle list]
[/chronicle grow type]
[chronicle grow type]
0 2
[normal chronicle list]
105000
[/normal chronicle list]
[set chronicle list]
100332
[/set chronicle list]
[/chronicle grow type]
`,
currentDungeonDimensionDropExcludePath: `[exclude list]
125013 120230
[/exclude list]
`,
currentDungeonClearRewardRulePath: `[chronicle set reward rate]
0 50 0 1 70 5 2 95 100
[/chronicle set reward rate]
`,
}
catalog, err := loadCurrentDungeonDimensionDrops(source)
if err != nil {
t.Fatal(err)
}
pool := catalog.ChronicleByCareer[currentDungeonDimensionChronicleKey{Job: 0, GrowType: 1}]
if !reflect.DeepEqual(pool.Normal, []uint32{125012}) || !reflect.DeepEqual(pool.Set, []uint32{120229}) {
t.Fatalf("filtered career pool=%+v", pool)
}
if got := catalog.ChronicleRates[2]; got != (currentDungeonDimensionChronicleRate{RewardPercent: 95, SetPercent: 100}) {
t.Fatalf("tier-2 rate=%+v", got)
}
rng := newCurrentDungeonDropLCG(1)
tier, itemID, group, matched, selected, err := currentDungeonDimensionChronicleItem(catalog, 7167, 90, 0, 1, rng, false)
if err != nil || !matched || !selected || tier != 2 || itemID != 120229 || group != "set" {
t.Fatalf("chronicle selection tier=%d item=%d group=%q matched=%t selected=%t err=%v", tier, itemID, group, matched, selected, err)
}
_, _, _, matched, selected, err = currentDungeonDimensionChronicleItem(catalog, 7167, 90, 0, 3, rng, false)
if err != nil || !matched || selected {
t.Fatalf("unknown career matched=%t selected=%t err=%v", matched, selected, err)
}
}
func TestCurrentDungeonDimensionChroniclePVFShapeFailsClosed(t *testing.T) {
base := bridgePVFSource{
currentDungeonDimensionGatePath: "[dimensiondungeon list]\n62 25 0\n[/dimensiondungeon list]\n",
currentDungeonDimensionInfoPath: `[dimension dungeon info]
[/dimension dungeon info]
[dungeon difficulty condition info]
1 70
[/dungeon difficulty condition info]
[drop item Info]
0 0 3311
[/drop item Info]
`,
currentDungeonDimensionDropListPath: `[chronicle grow type]
0 1
[normal chronicle list]
125012
[/normal chronicle list]
[set chronicle list]
120229
[/set chronicle list]
[/chronicle grow type]
`,
currentDungeonDimensionDropExcludePath: "[exclude list]\n[/exclude list]\n",
currentDungeonClearRewardRulePath: "[chronicle set reward rate]\n0 101 0\n[/chronicle set reward rate]\n",
}
if _, err := loadCurrentDungeonDimensionDrops(base); err == nil {
t.Fatal("out-of-range chronicle reward rate was accepted")
}
delete(base, currentDungeonDimensionDropExcludePath)
base[currentDungeonClearRewardRulePath] = "[chronicle set reward rate]\n0 50 0\n[/chronicle set reward rate]\n"
if _, err := loadCurrentDungeonDimensionDrops(base); err == nil {
t.Fatal("chronicle drop list without its exclusion table was accepted")
}
}
func TestCurrentDungeonDimensionDropPVFShapeFailsClosed(t *testing.T) {
_, err := loadCurrentDungeonDimensionDrops(bridgePVFSource{
currentDungeonDimensionGatePath: "[dimensiondungeon list]\n62 25\n[/dimensiondungeon list]\n",
})
if err == nil {
t.Fatal("malformed dimension-dungeon tuple was accepted")
}
catalog := &currentDungeonDimensionDropCatalog{
LegacyDungeons: map[int64]struct{}{62: {}},
ItemsByTier: map[int][]currentDungeonDimensionDropEntry{},
}
if _, _, _, err := currentDungeonDimensionDropPool(catalog, 62, 90); err == nil {
t.Fatal("dimension member without an item tier was accepted")
}
}
func TestRealScriptPVFDimensionDungeonDropsResolve(t *testing.T) {
pvfPath := os.Getenv("DNF_WORLDMAP_REAL_PVF_SMOKE")
if pvfPath == "" {
t.Skip("set DNF_WORLDMAP_REAL_PVF_SMOKE to audit real dimension-dungeon drops")
}
archive, err := platformpvf.Open(pvfPath)
if err != nil {
t.Fatal(err)
}
if _, err := worldmap.LoadSource(context.Background(), archive, worldmap.Options{}); err != nil {
t.Fatal(err)
}
monsterCatalog, err := newPVFDungeonMonsterCatalog(archive)
if err != nil {
t.Fatal(err)
}
dimensionCatalog, err := currentDungeonDimensionDrops(monsterCatalog)
if err != nil {
t.Fatal(err)
}
itemCatalog, err := monsterCatalog.DropCatalog()
if err != nil {
t.Fatal(err)
}
if len(dimensionCatalog.ChronicleByCareer) == 0 || len(dimensionCatalog.ChronicleRates) != 3 {
t.Fatalf("chronicle career pools=%d rates=%v", len(dimensionCatalog.ChronicleByCareer), dimensionCatalog.ChronicleRates)
}
for key, pool := range dimensionCatalog.ChronicleByCareer {
for group, entries := range map[string][]uint32{"normal": pool.Normal, "set": pool.Set} {
for _, itemID := range entries {
definition, resolveErr := itemCatalog.ResolveItem(itemID)
if resolveErr != nil {
t.Fatalf("chronicle job=%d grow=%d group=%s item=%d: %v", key.Job, key.GrowType, group, itemID, resolveErr)
}
if definition.Kind != dungeonDropItemEquipment || definition.Rarity != 5 {
t.Fatalf("chronicle job=%d grow=%d group=%s item=%d kind=%s rarity=%d", key.Job, key.GrowType, group, itemID, definition.Kind, definition.Rarity)
}
}
}
}
for _, dungeonID := range []int64{62, 63, 64, 65, 67} {
tier, pool, matched, resolveErr := currentDungeonDimensionDropPool(dimensionCatalog, dungeonID, 90)
if resolveErr != nil || !matched || len(pool) == 0 {
t.Fatalf("dungeon=%d matched=%t tier=%d pool=%v err=%v", dungeonID, matched, tier, pool, resolveErr)
}
for _, entry := range pool {
if _, resolveErr := itemCatalog.ResolveItem(entry.ItemID); resolveErr != nil {
t.Fatalf("dungeon=%d tier=%d item=%d: %v", dungeonID, tier, entry.ItemID, resolveErr)
}
}
}
if _, _, matched, err := currentDungeonDimensionDropPool(dimensionCatalog, 66, 90); err != nil || matched {
t.Fatalf("current PVF intentionally omits dungeon 66 from dimension membership: matched=%t err=%v", matched, err)
}
}