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

192 lines
7.1 KiB
Go

package dnfbridge
import (
"bytes"
"testing"
"longheng.io/server/internal/modules/dnf/dnfenum"
"longheng.io/server/internal/modules/dnf/dungeoncmd"
"longheng.io/server/internal/modules/dnf/repository"
"longheng.io/server/internal/modules/dnf/worldmap"
)
func newCurrentFruzNarkeTestRuntime(t *testing.T) (*Service, *gameSession, *runtimeDungeonState, worldmap.DungeonRoomScene, uint16) {
t.Helper()
scene := worldmap.DungeonRoomScene{
Coordinate: worldmap.RoomCoordinate{X: 2, Y: 1},
Map: worldmap.ResolvedMap{Map: worldmap.Map{ID: 76503}},
PassiveObjects: []worldmap.PassiveObject{
{ObjectID: currentFruzStationObstacleSmall},
{ObjectID: currentFruzStationObstacleLarge},
{ObjectID: currentFruzStationObstacleLarge},
{ObjectID: currentFruzStationObstacleLarge},
{ObjectID: currentFruzStationObstacleLarge},
{ObjectID: currentFruzStationObstacleLarge},
},
}
room := &runtimeDungeonRoom{coordinate: scene.Coordinate, mapID: scene.Map.Map.ID}
narkeKey := uint16(405)
narkeRoom := &runtimeDungeonRoom{
coordinate: worldmap.RoomCoordinate{X: 0, Y: 1},
mapID: 76500,
extendedActors: []runtimeDungeonExtendedActor{{
Kind: runtimeDungeonActorAICharacter,
ObjectKey: uint32(narkeKey),
Packet: currentDungeonStartMapActor{
ObjectKey: narkeKey,
Code: uint32(currentFruzStationNarkeActorID),
Type: 5,
},
AICharacter: &worldmap.AICharacter{
Code: currentFruzStationNarkeActorID,
Faction: "[character]",
AIType: "[normal]",
},
State: runtimeDungeonMonsterAnnounced,
}},
}
runtime := &runtimeDungeonState{
Request: dungeoncmd.SelectDungeonRequest{DungeonID: uint32(currentFruzStationDungeonID)},
Dungeon: worldmap.Dungeon{
ID: currentFruzStationDungeonID,
Mazes: []worldmap.Maze{{
QuestConnection: []int64{0, currentFruzStationNarkeQuestID, -1},
}},
},
MazeIndex: 0,
Character: repository.CharacterRecord{CharacterID: "19", AccountID: "acc"},
Session: &worldmap.DungeonSession{},
Room: room,
Seed: 7,
Rooms: map[runtimeDungeonRoomKey]*runtimeDungeonRoomVisit{
runtimeDungeonRoomKeyFromScene(scene): {
Scene: scene,
Room: room,
Seed: 7,
},
{X: 0, Y: 1, MapID: 76500}: {
Room: narkeRoom,
Seed: 3,
},
},
}
conn := &bufferConn{}
session := &gameSession{
conn: conn, connID: "fruz-narke", selectedCharacterID: 19,
dungeon: dungeonSessionState{runtime: runtime},
}
return &Service{options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain}}, session, runtime, scene, narkeKey
}
func TestCurrentFruzStationNarkeObstacleDeathAcknowledgesEveryMapInstance(t *testing.T) {
service, session, runtime, scene, narkeKey := newCurrentFruzNarkeTestRuntime(t)
request := dungeoncmd.DieMonsterRequest{
Layout: dungeoncmd.DieMonsterRequestLayoutVariableCombat,
RuntimeObjectKey: uint32(currentFruzStationObstacleLarge),
OwnerObjectKey: narkeKey,
CombatEntryCount: 0,
}
roomBefore := runtime.Room.Snapshot()
for attempt := 0; attempt < 6; attempt++ {
handled, err := service.handleCurrentFruzStationNarkeObstacleDeathLocked(
session, runtime, scene, request,
)
if err != nil || !handled {
t.Fatalf("attempt %d handled=%t err=%v", attempt+1, handled, err)
}
}
visit := runtime.Rooms[runtimeDungeonRoomKeyFromScene(scene)]
if got := visit.QuestPassiveObjectDeaths[currentFruzStationObstacleLarge]; got != 5 {
t.Fatalf("retired obstacle count=%d want=5", got)
}
wantBody, err := buildCurrentDungeonDeathNotificationBody(
uint32(currentFruzStationObstacleLarge), currentDungeonDeathResponseAICharacter,
)
if err != nil {
t.Fatal(err)
}
remaining := session.conn.(*bufferConn).write.Bytes()
for packetIndex := 0; packetIndex < 6; packetIndex++ {
packet, rest := splitGameServerUpperPacket(t, remaining)
if packet.Header.MsgID != uint16(dnfenum.CmdPacketNotifyDieMonster) ||
packet.Header.Classification != 0 || !bytes.Equal(packet.Body, wantBody) {
t.Fatalf("packet %d header=%+v body=%x want=%x", packetIndex+1, packet.Header, packet.Body, wantBody)
}
remaining = rest
}
if len(remaining) != 0 {
t.Fatalf("trailing packets=%x", remaining)
}
roomAfter := runtime.Room.Snapshot()
if len(roomAfter.Monsters) != len(roomBefore.Monsters) ||
len(roomAfter.ExtendedActors) != len(roomBefore.ExtendedActors) {
t.Fatalf("obstacle receipt mutated room actors: before=%+v after=%+v", roomBefore, roomAfter)
}
}
func TestCurrentFruzStationNarkeObstacleDeathAcceptsSentinelOwner(t *testing.T) {
service, session, runtime, scene, _ := newCurrentFruzNarkeTestRuntime(t)
handled, err := service.handleCurrentFruzStationNarkeObstacleDeathLocked(
session,
runtime,
scene,
dungeoncmd.DieMonsterRequest{
Layout: dungeoncmd.DieMonsterRequestLayoutVariableCombat,
RuntimeObjectKey: uint32(currentFruzStationObstacleSmall),
OwnerObjectKey: ^uint16(0),
},
)
if err != nil || !handled {
t.Fatalf("sentinel owner handled=%t err=%v", handled, err)
}
}
func TestCurrentFruzStationNarkeObstacleDeathRejectsForeignShapes(t *testing.T) {
tests := []struct {
name string
mutate func(*runtimeDungeonState, *worldmap.DungeonRoomScene, *dungeoncmd.DieMonsterRequest)
}{
{name: "wrong dungeon", mutate: func(runtime *runtimeDungeonState, _ *worldmap.DungeonRoomScene, _ *dungeoncmd.DieMonsterRequest) {
runtime.Dungeon.ID++
}},
{name: "wrong quest", mutate: func(runtime *runtimeDungeonState, _ *worldmap.DungeonRoomScene, _ *dungeoncmd.DieMonsterRequest) {
runtime.Dungeon.Mazes[0].QuestConnection[1]++
}},
{name: "wrong map", mutate: func(_ *runtimeDungeonState, scene *worldmap.DungeonRoomScene, _ *dungeoncmd.DieMonsterRequest) {
scene.Map.Map.ID = 76505
}},
{name: "foreign target", mutate: func(_ *runtimeDungeonState, _ *worldmap.DungeonRoomScene, request *dungeoncmd.DieMonsterRequest) {
request.RuntimeObjectKey = 16111
}},
{name: "foreign owner", mutate: func(_ *runtimeDungeonState, _ *worldmap.DungeonRoomScene, request *dungeoncmd.DieMonsterRequest) {
request.OwnerObjectKey = 499
}},
{name: "fixed layout", mutate: func(_ *runtimeDungeonState, _ *worldmap.DungeonRoomScene, request *dungeoncmd.DieMonsterRequest) {
request.Layout = dungeoncmd.DieMonsterRequestLayoutFixed62
}},
{name: "combat rows", mutate: func(_ *runtimeDungeonState, _ *worldmap.DungeonRoomScene, request *dungeoncmd.DieMonsterRequest) {
request.CombatEntryCount = 1
}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
service, session, runtime, scene, narkeKey := newCurrentFruzNarkeTestRuntime(t)
request := dungeoncmd.DieMonsterRequest{
Layout: dungeoncmd.DieMonsterRequestLayoutVariableCombat,
RuntimeObjectKey: uint32(currentFruzStationObstacleSmall),
OwnerObjectKey: narkeKey,
}
test.mutate(runtime, &scene, &request)
handled, err := service.handleCurrentFruzStationNarkeObstacleDeathLocked(session, runtime, scene, request)
if err != nil || handled {
t.Fatalf("handled=%t err=%v", handled, err)
}
if session.conn.(*bufferConn).write.Len() != 0 {
t.Fatalf("rejected shape emitted packets=%x", session.conn.(*bufferConn).write.Bytes())
}
})
}
}