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,110 @@
package dnfbridge
import (
"testing"
)
// 现场(2026-09-20 09:58:41Z,raid 1507329,两人队打完 220 心脏、攻坚结束):
// 两名成员都站在 19/2 等候室里,客户端反复发 op36 想走到 19/1,服务端每次都回
// game-town-set-user-area-blocked … reason=active_dungeon_runtime_rejects_town_area_request
// 紧接着这个拒绝之前是
// game-dungeon-completed-raid-waiting-presence-deferred … reason=raid_membership_missing
// 也就是:攻坚没了,回城路线里「恢复等候区 presence」这一步拒绝执行,于是按注释
// 「保留 runtime 当重试路径」把**已完成的 run 留在 session 上**;而 town_move.go:83
// 只要看到 session 上还有 dungeon runtime 就拒绝一切 op36 ⇒ 玩家出不去等候室。
//
// 这个测试钉住:攻坚已经不存在时,完成的 run 必须被正常退休(presence 退回频道城镇的
// 普通实例),之后 op36 才可能被受理。
const (
raidWaitingRoomExitTestReturnOp = uint16(72)
raidWaitingRoomExitTestSource = "raid_ended_leave_waiting_room_test"
)
func raidWaitingRoomExitTestTransition() currentDungeonTownTransition {
return currentDungeonTownTransition{
TownID: byte(currentAntonRaidTownID),
AreaID: currentAntonRaidWaitingAreaID,
AreaState: currentAntonRaidAreaState,
}
}
// TestCompletedRaidRunWhoseRaidIsGoneStillLeavesTheWaitingRoom covers the report
// "攻坚结束后退出队伍了 没法离开等候室了".
func TestCompletedRaidRunWhoseRaidIsGoneStillLeavesTheWaitingRoom(t *testing.T) {
fixture := newRuntimeRaidPhaseForceReturnFixture(t)
// The member is inside a phase-one gate of a raid that is about to disappear.
runtime := fixture.enterGateRun(t, 99, 11, antonBlackFogSourceMapID)
session, ok := fixture.service.onlineGameSession(99)
if !ok {
t.Fatal("member has no session")
}
// The raid ends: finished, disbanded, or the member left it. What matters is
// that it no longer owns this run.
fixture.service.raidMu.Lock()
delete(fixture.service.raids, fixture.raidKey)
fixture.service.raidMu.Unlock()
if err := fixture.service.sendCurrentActiveDungeonReturnToTown(
session,
runtime,
raidWaitingRoomExitTestTransition(),
raidWaitingRoomExitTestReturnOp,
raidWaitingRoomExitTestSource,
); err != nil {
t.Fatalf("completed return with the raid gone: %v", err)
}
// The completed run must be retired: an attached run is what refused every
// later area request.
if kept := fixture.sessionRuntime(t, 99); kept != nil {
t.Fatalf("the completed run stayed attached after the raid ended (dungeon %d): "+
"town_move.go refuses every op36 in that state", kept.Dungeon.ID)
}
// And the member must be back in the channel town's ordinary instance, not in a
// raid instance nobody owns any more.
presence, present := fixture.service.onlinePlayers.PlayerForCharacter(99)
if !present {
t.Fatal("no town presence was restored after the raid ended")
}
if presence.TownID != byte(currentAntonRaidTownID) || presence.AreaID != currentAntonRaidWaitingAreaID {
t.Fatalf("presence=%d/%d want %d/%d", presence.TownID, presence.AreaID,
currentAntonRaidTownID, currentAntonRaidWaitingAreaID)
}
if presence.InstanceKey != 0 {
t.Fatalf("presence still points at the dead raid instance %d", presence.InstanceKey)
}
}
// TestCompletedRaidRunKeepsItsWaitingInstanceWhileTheRaidLives is the guard in the
// other direction: as long as the raid owns the run, the member must come back
// INTO the raid's waiting instance (the private scene the squad shares), and the
// run must be retired then too.
func TestCompletedRaidRunKeepsItsWaitingInstanceWhileTheRaidLives(t *testing.T) {
fixture := newRuntimeRaidPhaseForceReturnFixture(t)
runtime := fixture.enterGateRun(t, 99, 11, antonBlackFogSourceMapID)
session, ok := fixture.service.onlineGameSession(99)
if !ok {
t.Fatal("member has no session")
}
if err := fixture.service.sendCurrentActiveDungeonReturnToTown(
session,
runtime,
raidWaitingRoomExitTestTransition(),
raidWaitingRoomExitTestReturnOp,
"raid_alive_waiting_room_return_test",
); err != nil {
t.Fatalf("completed return while the raid lives: %v", err)
}
if kept := fixture.sessionRuntime(t, 99); kept != nil {
t.Fatalf("the completed run stayed attached while the raid lives (dungeon %d)", kept.Dungeon.ID)
}
presence, present := fixture.service.onlinePlayers.PlayerForCharacter(99)
if !present {
t.Fatal("no waiting-area presence was restored")
}
if presence.InstanceKey != fixture.raidKey {
t.Fatalf("presence instance=%d want the live raid instance %d",
presence.InstanceKey, fixture.raidKey)
}
}