DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

@@ -0,0 +1,323 @@
package dnfbridge
import (
"errors"
"testing"
"longheng.io/server/internal/modules/dnf/worldmap"
)
func TestPassiveObjectDropPlanUsesDungeonWideDefinitionForEveryMaze(t *testing.T) {
service, runtime, _ := prepareCurrentDungeonDropTest(t, 0, 3227)
runtime.Dungeon.Mazes = append(runtime.Dungeon.Mazes, worldmap.Maze{Index: 1})
runtime.Dungeon.Metadata.SpecialPassiveObjectItems = []worldmap.SpecialPassiveObjectItem{{
Index: 0, ItemID: 3227, DropRate: 10000,
}}
scene, ok := runtime.Session.Scene()
if !ok {
t.Fatal("missing current scene")
}
scene.SpecialPassiveObjects = []worldmap.SpecialPassiveObject{{
Spawns: []worldmap.SpecialObjectSpawn{{Kind: "[item]", Code: 0, Level: 1, Params: [3]int64{-1, -1, -1}}},
}}
for _, mazeIndex := range []int{0, 1} {
runtime.MazeIndex = mazeIndex
plan, err := service.planCurrentDungeonPassiveObjectDrops(
&gameSession{selectedCharacterID: 99}, runtime, scene, runtime.NextObjectKey, 7,
)
if err != nil {
t.Fatalf("maze=%d: %v", mazeIndex, err)
}
if plan.ConfiguredRowCount != 1 || len(plan.Drops) != 1 {
t.Fatalf("maze=%d rows=%d drops=%d", mazeIndex, plan.ConfiguredRowCount, len(plan.Drops))
}
}
}
func TestPassiveObjectDropPlanCommitsOnlyAfterStartMap(t *testing.T) {
service, runtime, _ := prepareCurrentDungeonDropTest(t, 0, 3227)
runtime.Dungeon.Mazes[runtime.MazeIndex].SpecialPassiveObjectItems = []worldmap.SpecialPassiveObjectItem{
{Index: 0, ItemID: 3227, DropRate: 10000},
{Index: 1, ItemID: 3227, DropRate: 10000},
}
scene, ok := runtime.Session.Scene()
if !ok {
t.Fatal("missing current scene")
}
scene.SpecialPassiveObjects = []worldmap.SpecialPassiveObject{
{Spawns: []worldmap.SpecialObjectSpawn{{Kind: "[item]", Code: 0, Level: 1, Params: [3]int64{-1, -1, -1}}}},
{Spawns: []worldmap.SpecialObjectSpawn{{Kind: "[item]", Code: 1, Level: 1, Params: [3]int64{-1, -1, -1}}}},
}
projectionScene := scene
runtime.Room.specialObjectCount = len(scene.SpecialPassiveObjects)
visit, err := runtime.currentDungeonRoomVisit(scene)
if err != nil {
t.Fatal(err)
}
startObjectKey, startRNG := runtime.NextObjectKey, visit.DropRNG
plan, err := service.planCurrentDungeonPassiveObjectDrops(
&gameSession{selectedCharacterID: 99}, runtime, scene, startObjectKey, startRNG,
)
if err != nil {
t.Fatal(err)
}
if len(plan.ExtraEntries) != 2 || len(plan.Drops) != 2 {
t.Fatalf("plan entries=%d drops=%d", len(plan.ExtraEntries), len(plan.Drops))
}
if runtime.NextObjectKey != startObjectKey || runtime.DropOwner != nil || visit.DropRNG != startRNG || visit.PassiveObjectDropsPlanned {
t.Fatalf("preflight mutated runtime: next=%d owner=%p rng=%08x planned=%t", runtime.NextObjectKey, runtime.DropOwner, visit.DropRNG, visit.PassiveObjectDropsPlanned)
}
packets, err := buildCurrentDungeonEntryPacketsWithPassiveDrops(runtime, projectionScene, plan.ExtraEntries)
if err != nil {
t.Fatal(err)
}
if len(packets.StartMap) == 0 {
t.Fatal("start-map projection is empty")
}
actorCount := int(packets.StartMap[18])
extraCountOffset := 19 + actorCount*21
if extraCountOffset >= len(packets.StartMap) || packets.StartMap[extraCountOffset] != 2 {
t.Fatalf("start-map extra count offset=%d body=%x", extraCountOffset, packets.StartMap)
}
if err := commitCurrentDungeonPassiveObjectDrops(runtime, scene, plan); err != nil {
t.Fatal(err)
}
visit, err = runtime.currentDungeonRoomVisit(scene)
if err != nil {
t.Fatal(err)
}
if runtime.NextObjectKey != plan.NextObjectKey || runtime.DropOwner == nil ||
visit.DropRNG != plan.NextDropRNG || !visit.PassiveObjectDropsPlanned || len(visit.PassiveObjectExtraEntries) != 2 {
t.Fatalf("committed runtime mismatch: next=%d owner=%p visit=%+v", runtime.NextObjectKey, runtime.DropOwner, visit)
}
ownerKey := currentSceneActorObjectKey(99)
for _, drop := range plan.Drops {
owned, ownErr := runtime.DropOwner.owned(drop.ObjectKey, runtimeDungeonRoomKeyFromScene(scene), ownerKey)
if ownErr != nil || owned != drop {
t.Fatalf("pickup ownership object=%d owned=%p want=%p err=%v", drop.ObjectKey, owned, drop, ownErr)
}
}
if err := commitCurrentDungeonPassiveObjectDrops(runtime, scene, plan); !errors.Is(err, errDungeonMoveRuntimeOwnerMismatch) {
t.Fatalf("duplicate commit error=%v", err)
}
}
func TestPassiveObjectDropRateUsesTenThousandDenominator(t *testing.T) {
service, runtime, _ := prepareCurrentDungeonDropTest(t, 0, 3227)
runtime.Dungeon.Mazes[runtime.MazeIndex].SpecialPassiveObjectItems = []worldmap.SpecialPassiveObjectItem{{
Index: 1, ItemID: 3227, DropRate: 9400,
}}
scene, _ := runtime.Session.Scene()
scene.SpecialPassiveObjects = []worldmap.SpecialPassiveObject{{
Spawns: []worldmap.SpecialObjectSpawn{{Kind: "[item]", Code: 1, Level: 1, Params: [3]int64{-1, -1, -1}}},
}}
var selectedSeed, rejectedSeed uint32
for seed := uint32(1); seed < 100000 && (selectedSeed == 0 || rejectedSeed == 0); seed++ {
roll := newCurrentDungeonDropLCG(seed).Next(currentDungeonDropDenominator)
if roll < 9400 && selectedSeed == 0 {
selectedSeed = seed
}
if roll >= 9400 && rejectedSeed == 0 {
rejectedSeed = seed
}
}
if selectedSeed == 0 || rejectedSeed == 0 {
t.Fatal("failed to find deterministic rate fixtures")
}
for _, test := range []struct {
seed uint32
want int
}{{selectedSeed, 1}, {rejectedSeed, 0}} {
plan, err := service.planCurrentDungeonPassiveObjectDrops(
&gameSession{selectedCharacterID: 99}, runtime, scene, runtime.NextObjectKey, test.seed,
)
if err != nil {
t.Fatal(err)
}
if len(plan.ExtraEntries) != test.want {
t.Fatalf("seed=%d roll=%d selected=%d want=%d", test.seed, newCurrentDungeonDropLCG(test.seed).Next(10000), len(plan.ExtraEntries), test.want)
}
}
}
func TestPassiveObjectDropPlanRollsOncePerSpecialObjectInstance(t *testing.T) {
service, runtime, _ := prepareCurrentDungeonDropTest(t, 0, 3227)
runtime.Dungeon.Mazes[runtime.MazeIndex].SpecialPassiveObjectItems = []worldmap.SpecialPassiveObjectItem{{
Index: 0, ItemID: 3227, DropRate: 1500,
}}
scene, _ := runtime.Session.Scene()
for index := 0; index < 4; index++ {
scene.SpecialPassiveObjects = append(scene.SpecialPassiveObjects, worldmap.SpecialPassiveObject{
PassiveObject: worldmap.PassiveObject{ObjectID: int64(16111 + index%2)},
Spawns: []worldmap.SpecialObjectSpawn{{Kind: "[item]", Code: 0, Level: 1, Params: [3]int64{-1, -1, -1}}},
})
}
seed := uint32(1)
wantSelected := 0
wantRNG := newCurrentDungeonDropLCG(seed)
for index := 0; index < 4; index++ {
if wantRNG.Next(currentDungeonDropDenominator) < 1500 {
wantSelected++
}
}
plan, err := service.planCurrentDungeonPassiveObjectDrops(
&gameSession{selectedCharacterID: 99}, runtime, scene, runtime.NextObjectKey, seed,
)
if err != nil {
t.Fatal(err)
}
if len(plan.ExtraEntries) != wantSelected || len(plan.Drops) != wantSelected {
t.Fatalf("selected=%d drops=%d want=%d", len(plan.ExtraEntries), len(plan.Drops), wantSelected)
}
if plan.NextDropRNG != wantRNG.Seed() {
t.Fatalf("next RNG=%08x want=%08x", plan.NextDropRNG, wantRNG.Seed())
}
}
func TestPassiveObjectDropPlanUsesOwningSpecialObjectIndexAcrossRewardGroups(t *testing.T) {
service, runtime, _ := prepareCurrentDungeonDropTest(t, 0, 3227)
runtime.Dungeon.Mazes[runtime.MazeIndex].SpecialPassiveObjectItems = []worldmap.SpecialPassiveObjectItem{
{Index: 0, ItemID: 3227, DropRate: 10000},
{Index: 1, ItemID: 3227, DropRate: 10000},
{Index: 2, ItemID: 3227, DropRate: 10000},
}
scene, _ := runtime.Session.Scene()
scene.SpecialPassiveObjects = []worldmap.SpecialPassiveObject{{
PassiveObject: worldmap.PassiveObject{ObjectID: 37224},
Spawns: []worldmap.SpecialObjectSpawn{
{Kind: "[item]", Code: 0, Level: 1, Params: [3]int64{-1, -1, -1}},
{Kind: "[item]", Code: 1, Level: 1, Params: [3]int64{-1, -1, -1}},
{Kind: "[item]", Code: 2, Level: 1, Params: [3]int64{-1, -1, -1}},
},
}}
plan, err := service.planCurrentDungeonPassiveObjectDrops(
&gameSession{selectedCharacterID: 99}, runtime, scene, runtime.NextObjectKey, 7,
)
if err != nil {
t.Fatal(err)
}
if len(plan.ExtraEntries) != 3 {
t.Fatalf("entries=%d want=3", len(plan.ExtraEntries))
}
for index, entry := range plan.ExtraEntries {
if entry.PassiveObjectIndex != 0 {
t.Fatalf("entry[%d] passive object index=%d want=0", index, entry.PassiveObjectIndex)
}
}
}
func TestPassiveObjectDropPlanKeepsDistinctSpecialObjectOwners(t *testing.T) {
service, runtime, _ := prepareCurrentDungeonDropTest(t, 0, 3227)
runtime.Dungeon.Mazes[runtime.MazeIndex].SpecialPassiveObjectItems = []worldmap.SpecialPassiveObjectItem{{
Index: 0, ItemID: 3227, DropRate: 10000,
}}
scene, _ := runtime.Session.Scene()
for index := 0; index < 3; index++ {
scene.SpecialPassiveObjects = append(scene.SpecialPassiveObjects, worldmap.SpecialPassiveObject{
PassiveObject: worldmap.PassiveObject{ObjectID: int64(37224 + index)},
Spawns: []worldmap.SpecialObjectSpawn{{Kind: "[item]", Code: 0, Level: 1, Params: [3]int64{-1, -1, -1}}},
})
}
plan, err := service.planCurrentDungeonPassiveObjectDrops(
&gameSession{selectedCharacterID: 99}, runtime, scene, runtime.NextObjectKey, 7,
)
if err != nil {
t.Fatal(err)
}
if len(plan.ExtraEntries) != 3 {
t.Fatalf("entries=%d want=3", len(plan.ExtraEntries))
}
for index, entry := range plan.ExtraEntries {
if entry.PassiveObjectIndex != byte(index) {
t.Fatalf("entry[%d] passive object index=%d want=%d", index, entry.PassiveObjectIndex, index)
}
}
}
func TestPassiveObjectDropPlanCountsOrdinaryQuestItemObjectsPerInstance(t *testing.T) {
service, runtime, _ := prepareCurrentDungeonDropTest(t, 0, 3227)
source := service.dungeonMonsterTable.source.(bridgePVFSource)
source[currentDungeonPassiveObjectList] = "16111 `actionobject/test/magtonium1.obj`\n16112 `actionobject/test/magtonium2.obj`\n"
source["passiveobject/actionobject/test/magtonium1.obj"] = "[reward type]\n`[quest item]`\n"
source["passiveobject/actionobject/test/magtonium2.obj"] = "[reward type]\n`[quest item]`\n"
runtime.Dungeon.Mazes[runtime.MazeIndex].SpecialPassiveObjectItems = []worldmap.SpecialPassiveObjectItem{{
Index: 1, ItemID: 3227, DropRate: 10000,
}}
scene, _ := runtime.Session.Scene()
for index := 0; index < 3; index++ {
scene.PassiveObjects = append(scene.PassiveObjects, worldmap.PassiveObject{ObjectID: 16111})
}
for index := 0; index < 4; index++ {
scene.PassiveObjects = append(scene.PassiveObjects, worldmap.PassiveObject{ObjectID: 16112})
}
plan, err := service.planCurrentDungeonPassiveObjectDrops(
&gameSession{selectedCharacterID: 99}, runtime, scene, runtime.NextObjectKey, 7,
)
if err != nil {
t.Fatal(err)
}
if len(plan.ExtraEntries) != 7 || len(plan.Drops) != 7 {
t.Fatalf("entries=%d drops=%d want=7", len(plan.ExtraEntries), len(plan.Drops))
}
for index, entry := range plan.ExtraEntries {
if entry.PassiveObjectIndex != 1 {
t.Fatalf("entry[%d] passive object index=%d want legacy group selector 1", index, entry.PassiveObjectIndex)
}
}
}
func TestPassiveObjectDropPlanSkipsMissingOrdinaryObjectDefinition(t *testing.T) {
service, runtime, _ := prepareCurrentDungeonDropTest(t, 0, 3227)
source := service.dungeonMonsterTable.source.(bridgePVFSource)
source[currentDungeonPassiveObjectList] = "72228 `ActionObject/Map/MetroCenter/TimePlaza/building_broken_c.obj`\n"
runtime.Dungeon.Mazes[runtime.MazeIndex].SpecialPassiveObjectItems = []worldmap.SpecialPassiveObjectItem{{
Index: 1, ItemID: 3227, DropRate: 10000,
}}
scene, _ := runtime.Session.Scene()
scene.SpecialPassiveObjects = []worldmap.SpecialPassiveObject{{
Spawns: []worldmap.SpecialObjectSpawn{{Kind: "[item]", Code: 1, Level: 1, Params: [3]int64{-1, -1, -1}}},
}}
scene.PassiveObjects = []worldmap.PassiveObject{{ObjectID: 72228}}
plan, err := service.planCurrentDungeonPassiveObjectDrops(
&gameSession{selectedCharacterID: 99}, runtime, scene, runtime.NextObjectKey, 7,
)
if err != nil {
t.Fatal(err)
}
if len(plan.ExtraEntries) != 1 || len(plan.Drops) != 1 {
t.Fatalf("entries=%d drops=%d want=1", len(plan.ExtraEntries), len(plan.Drops))
}
group, known, err := service.dungeonMonsterTable.passiveObjectRewardGroup(72228)
if err != nil || known || group != 0 {
t.Fatalf("missing passive object group=%d known=%t err=%v", group, known, err)
}
}
func TestPassiveObjectDropPlanDoesNotRollMissingGroup(t *testing.T) {
service, runtime, _ := prepareCurrentDungeonDropTest(t, 0, 3227)
runtime.Dungeon.Mazes[runtime.MazeIndex].SpecialPassiveObjectItems = []worldmap.SpecialPassiveObjectItem{{
Index: 1, ItemID: 3227, DropRate: 10000,
}}
scene, _ := runtime.Session.Scene()
scene.SpecialPassiveObjects = []worldmap.SpecialPassiveObject{{
Spawns: []worldmap.SpecialObjectSpawn{{Kind: "[item]", Code: 0, Level: 1, Params: [3]int64{-1, -1, -1}}},
}}
seed := uint32(0x12345678)
plan, err := service.planCurrentDungeonPassiveObjectDrops(
&gameSession{selectedCharacterID: 99}, runtime, scene, runtime.NextObjectKey, seed,
)
if err != nil {
t.Fatal(err)
}
// The reference consumes the attempt's roll even when the selected group
// has no rows; only the drop is absent.
wantRNG := newCurrentDungeonDropLCG(seed)
wantRNG.Next(currentDungeonDropDenominator)
if len(plan.ExtraEntries) != 0 || plan.NextDropRNG != wantRNG.Seed() {
t.Fatalf("plan=%+v want no drop and exactly one consumed roll", plan)
}
}