服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
207 lines
7.7 KiB
Go
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)
|
|
}
|