Files
90CNServer/server/internal/services/dnfbridge/dungeon_drop_area_material_test.go
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

207 lines
7.7 KiB
Go

package dnfbridge
import (
"context"
"encoding/binary"
"os"
"strings"
"testing"
"longheng.io/server/internal/modules/dnf/worldmap"
platformpvf "longheng.io/server/internal/platform/pvf"
)
func TestCurrentDungeonAreaMaterialMapsOnlySelectableMetroCenterRange(t *testing.T) {
for dungeonID := int64(310); dungeonID <= 315; dungeonID++ {
entry, matched := currentDungeonAreaMaterialPoolEntry(dungeonID)
if !matched || entry.ItemID != currentDungeonBirthBudItemID || entry.Weight != currentDungeonAreaMaterialWeight {
t.Fatalf("dungeon=%d matched=%t entry=%+v", dungeonID, matched, entry)
}
}
for _, dungeonID := range []int64{0, 144, 309, 316, 317, 5097, 7155} {
if entry, matched := currentDungeonAreaMaterialPoolEntry(dungeonID); matched {
t.Fatalf("dungeon=%d unexpectedly mapped to %+v", dungeonID, entry)
}
}
}
func TestCurrentDungeonAreaMaterialUsesSharedExplicitPoolRoll(t *testing.T) {
monsterEntry := dungeonMonsterDropPoolEntry{ItemID: 9001, Weight: 10}
areaEntry, matched := currentDungeonAreaMaterialPoolEntry(310)
if !matched {
t.Fatal("time plaza area material is not mapped")
}
pool := []dungeonMonsterDropPoolEntry{monsterEntry, areaEntry}
var seed uint32
selected := dungeonMonsterDropPoolEntry{}
for candidate := uint32(1); candidate < 100000; candidate++ {
rng := newCurrentDungeonDropLCG(candidate)
if currentDungeonExplicitPoolRate <= rng.Next(currentDungeonDropDenominator) {
continue
}
entry, ok := selectDungeonMonsterDrop(pool, uint64(rng.NextUint32()))
if ok && entry.ItemID == currentDungeonBirthBudItemID {
seed = candidate
selected = entry
break
}
}
if seed == 0 || selected != areaEntry {
t.Fatalf("shared explicit-pool path did not select birth bud: seed=%d selected=%+v", seed, selected)
}
// The regional item is not rolled independently. A failed shared outer
// roll must suppress the entire weighted pool, including the area entry.
failedOuterRollFound := false
for candidate := uint32(1); candidate < 100000; candidate++ {
rng := newCurrentDungeonDropLCG(candidate)
if currentDungeonExplicitPoolRate <= rng.Next(currentDungeonDropDenominator) {
failedOuterRollFound = true
break
}
}
if !failedOuterRollFound {
t.Fatal("could not exercise failed shared explicit-pool outer roll")
}
}
func TestCurrentDungeonMonsterPlanIntegratesBirthBudAreaMaterialBoundaries(t *testing.T) {
const sharedExplicitPoolSeed uint32 = 39
tests := []struct {
name string
dungeonID int64
practice bool
wantBirthBud bool
wantAnyDrops bool
wantUnchanged bool
}{
{name: "time plaza", dungeonID: 310, wantBirthBud: true, wantAnyDrops: true},
{name: "particular hell excluded", dungeonID: 316, wantAnyDrops: true},
{name: "practice suppresses the regional pool", dungeonID: 310, practice: true, wantUnchanged: true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
service, runtime, _ := prepareCurrentDungeonDropTest(t, 1, 3227)
source, ok := service.dungeonMonsterTable.source.(bridgePVFSource)
if !ok {
t.Fatalf("test source type=%T", service.dungeonMonsterTable.source)
}
// Keep the ordinary PVF formula valid but outside the synthetic
// monster's level. This isolates the explicit monster/template pool,
// whose fixed_drop_count=1 is the source used by area materials.
rules := strings.Replace(
currentDungeonGoldDropTestRuleText(),
"1 200 10000 0 0 0 0",
"1 1 0 0 0 0 0",
1,
)
source[dungeonCardDropRulePath] = rules
source[dungeonDropStackableList] = "3227 `material/test_drop.stk`\n" +
"10161610 `material/test_birth_bud.stk`\n"
source["stackable/material/test_birth_bud.stk"] = "[name]\n`Birth Bud`\n" +
"[stackable type]\n`[material]`\n[stack limit]\n999\n[attach type]\n`[free]`\n"
currentDungeonMonsterDropRulesByCatalog.Delete(service.dungeonMonsterTable)
runtime.Dungeon.ID = test.dungeonID
if test.practice {
runtime.ProtocolMode = currentDungeonProtocolModePractice
runtime.Request.RuntimeState = byte(currentDungeonProtocolModePractice)
}
scene, found := runtime.Session.Scene()
if !found {
t.Fatal("missing dungeon scene")
}
visit, err := runtime.currentDungeonRoomVisit(scene)
if err != nil {
t.Fatal(err)
}
visit.DropRNG = sharedExplicitPoolSeed
startNextObjectKey := runtime.NextObjectKey
startDropOwner := runtime.DropOwner
monster := runtime.Room.Snapshot().Monsters[0]
wires, err := service.planCurrentDungeonMonsterDrops(runtime, monster, currentSceneActorObjectKey(99))
if err != nil {
t.Fatal(err)
}
birthBudCount := 0
for _, wire := range wires {
if binary.LittleEndian.Uint32(wire.Item.data[0x02:0x06]) == currentDungeonBirthBudItemID {
birthBudCount++
}
}
if (birthBudCount != 0) != test.wantBirthBud {
t.Fatalf("dungeon=%d practice=%t birth_bud_count=%d wires=%d", test.dungeonID, test.practice, birthBudCount, len(wires))
}
if (len(wires) != 0) != test.wantAnyDrops {
t.Fatalf("dungeon=%d practice=%t wires=%d want_any=%t", test.dungeonID, test.practice, len(wires), test.wantAnyDrops)
}
if test.wantUnchanged && (visit.DropRNG != sharedExplicitPoolSeed || runtime.NextObjectKey != startNextObjectKey || runtime.DropOwner != startDropOwner) {
t.Fatalf("practice mutated drop state: rng=%08x next=%d owner=%p", visit.DropRNG, runtime.NextObjectKey, runtime.DropOwner)
}
})
}
}
func TestRealPVFMetroCenterBirthBudAreaMaterial(t *testing.T) {
pvfPath := os.Getenv("DNF_WORLDMAP_REAL_PVF_SMOKE")
if pvfPath == "" {
t.Skip("set DNF_WORLDMAP_REAL_PVF_SMOKE to verify Metro Center area material")
}
archive, err := platformpvf.Open(pvfPath)
if err != nil {
t.Fatal(err)
}
dropCatalog, err := newPVFDungeonDropCatalog(archive)
if err != nil {
t.Fatal(err)
}
birthBud, err := dropCatalog.ResolveItem(currentDungeonBirthBudItemID)
if err != nil {
t.Fatal(err)
}
if birthBud.Kind != dungeonDropItemStackable || birthBud.Name != "诞生之芽" ||
normalizeDungeonDropStackableType(birthBud.StackableType) != "[material]" || birthBud.CreationRate != 0 {
t.Fatalf("birth bud metadata=%+v", birthBud)
}
table, err := worldmap.LoadSource(context.Background(), archive, worldmap.Options{})
if err != nil {
t.Fatal(err)
}
expectedPaths := map[int64]string{
310: "dungeon/MetroCenter/Time_Plaza/Time_Plaza.dgn",
311: "dungeon/MetroCenter/Canyon_of_Beast/Canyon_of_Beast.dgn",
312: "dungeon/MetroCenter/Hideout_of_Fear/Hideout_of_Fear.dgn",
313: "dungeon/MetroCenter/Gust_Zone/Gust_Zone.dgn",
314: "dungeon/MetroCenter/Forest_of_Redwitch/Forest_of_Redwitch.dgn",
315: "dungeon/MetroCenter/Special/Special.dgn",
}
for dungeonID, expectedPath := range expectedPaths {
dungeon, found := table.FindDungeon(dungeonID)
if !found {
t.Fatalf("real PVF dungeon %d is missing", dungeonID)
}
gotPath := strings.ReplaceAll(dungeon.Path, "\\", "/")
if !strings.EqualFold(gotPath, expectedPath) {
t.Fatalf("dungeon=%d path=%q want=%q", dungeonID, gotPath, expectedPath)
}
entry, matched := currentDungeonAreaMaterialPoolEntry(dungeonID)
if !matched || entry.ItemID != currentDungeonBirthBudItemID || entry.Weight != currentDungeonAreaMaterialWeight {
t.Fatalf("dungeon=%d area entry matched=%t entry=%+v", dungeonID, matched, entry)
}
}
for _, excludedDungeonID := range []int64{316, 317} {
if _, found := table.FindDungeon(excludedDungeonID); !found {
t.Fatalf("real PVF excluded boundary dungeon %d is missing", excludedDungeonID)
}
if entry, matched := currentDungeonAreaMaterialPoolEntry(excludedDungeonID); matched {
t.Fatalf("excluded dungeon=%d unexpectedly mapped to %+v", excludedDungeonID, entry)
}
}
t.Logf("birth bud area material: item=%d creation_rate=%d dungeons=310..315 weight=%d",
birthBud.ItemID, birthBud.CreationRate, currentDungeonAreaMaterialWeight)
}