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,223 @@
package dnfbridge
import (
"testing"
"longheng.io/server/internal/modules/dnf/raid"
)
// The reward gate reads the phase's own [MINIMAL DUNGEON CLEAR COUNT]
// (runtimeRaidMemberClearedGateLocked -> runtimeRaidPhaseMinimalClearRequirement).
//
// The official reward condition is 「1阶段:无条件 / 2阶段:通关2个以上的地下城」,
// which is exactly the pair Luke declares, so the card window must withhold for a
// member whose 地下城通关 column has not reached the phase's requirement.
//
// lukeClearRequirementConfig is that shape: phase one 0, phase two 2, and the
// single-mode phase -- whose PVF block omits the section entirely -- left at the
// shared parser's default of 1 (anton_pvf.go:503), NOT inheriting team phase one's 0.
// All three values are pinned against the shipped PVF by
// raid.TestRealScriptPVFLukePhaseMechanismTables.
func lukeClearRequirementConfig() raid.AntonConfig {
return raid.AntonConfig{
Phases: []raid.AntonPhase{
{Name: "进入源生宫殿", MinimalDungeonClearCount: 0},
{Name: "进入卓越圣殿", MinimalDungeonClearCount: 2},
},
SingleMode: &raid.AntonSingleModeConfig{
Name: "进入源生宫殿",
Phase: raid.AntonPhase{Name: "进入源生宫殿", MinimalDungeonClearCount: 1},
},
}
}
// antonClearRequirementConfig is the shipped etc/raid/anton.etc shape: both team
// phases declare 1 (anton_pvf_test.go:544/:565, read from the real PVF) and the
// single phase is defaulted to 1.
func antonClearRequirementConfig() raid.AntonConfig {
return raid.AntonConfig{
Phases: []raid.AntonPhase{
{Name: "阻截安徒恩", MinimalDungeonClearCount: 1},
{Name: "灭杀安徒恩", MinimalDungeonClearCount: 1},
},
SingleMode: &raid.AntonSingleModeConfig{
Phase: raid.AntonPhase{Name: "阻截安徒恩", MinimalDungeonClearCount: 1},
},
}
}
func TestRuntimeRaidMemberClearedGateLockedUsesThePhaseClearRequirement(t *testing.T) {
t.Parallel()
lukeConfig := lukeClearRequirementConfig()
antonConfig := antonClearRequirementConfig()
luke := &raid.LukeConfig{Raid: &lukeConfig}
lukeState := func(phase byte) runtimeRaidState {
return runtimeRaidState{
RaidMode: runtimeRaidOwnerLuke,
LukeConfig: luke,
Lifecycle: runtimeRaidLifecycleState{Phase: phase},
}
}
antonState := func(phase byte) runtimeRaidState {
return runtimeRaidState{
RaidMode: runtimeRaidOwnerAnton,
AntonConfig: &antonConfig,
Lifecycle: runtimeRaidLifecycleState{Phase: phase},
}
}
lukeSingle := lukeState(runtimeRaidDungeonPhaseOne)
lukeSingle.TailFlag = 1
antonSingle := antonState(runtimeRaidDungeonPhaseOne)
antonSingle.TailFlag = 1
tests := []struct {
name string
state runtimeRaidState
characterID uint16
cleared uint32
want bool
}{
{"luke phase two withholds at zero clears", lukeState(runtimeRaidDungeonPhaseTwo), 19, 0, false},
{"luke phase two withholds at one clear", lukeState(runtimeRaidDungeonPhaseTwo), 19, 1, false},
{"luke phase two pays at two clears", lukeState(runtimeRaidDungeonPhaseTwo), 19, 2, true},
{"luke phase two pays above the requirement", lukeState(runtimeRaidDungeonPhaseTwo), 19, 3, true},
{"luke phase one is unconditional", lukeState(runtimeRaidDungeonPhaseOne), 19, 0, true},
{"luke phase one pays a clearing member too", lukeState(runtimeRaidDungeonPhaseOne), 19, 3, true},
{"luke single mode withholds at zero clears", lukeSingle, 19, 0, false},
{"luke single mode pays at one clear", lukeSingle, 19, 1, true},
{"anton phase one withholds at zero clears", antonState(runtimeRaidDungeonPhaseOne), 19, 0, false},
{"anton phase one pays at one clear", antonState(runtimeRaidDungeonPhaseOne), 19, 1, true},
{"anton phase two withholds at zero clears", antonState(runtimeRaidDungeonPhaseTwo), 19, 0, false},
{"anton phase two pays at one clear", antonState(runtimeRaidDungeonPhaseTwo), 19, 1, true},
{"anton single mode withholds at zero clears", antonSingle, 19, 0, false},
{"anton single mode pays at one clear", antonSingle, 19, 1, true},
{"character zero is never eligible", lukeState(runtimeRaidDungeonPhaseTwo), 0, 9, false},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
test.state.Lifecycle.PhaseClearCounts = map[uint16]uint32{19: test.cleared}
if got := runtimeRaidMemberClearedGateLocked(test.state, test.characterID); got != test.want {
t.Fatalf("cleared=%d eligible=%t want %t", test.cleared, got, test.want)
}
})
}
}
// TestRuntimeRaidClearedGateKeepsTheLegacyRuleWithoutACatalog covers the states a
// hand-built fixture or an unavailable config produces. runtimeRaidPhaseMinimalClearRequirement
// reports them as "no declaration", and a missing declaration must not be read as a
// declared 0 -- otherwise every member of a catalog-less raid would be handed a card.
func TestRuntimeRaidClearedGateKeepsTheLegacyRuleWithoutACatalog(t *testing.T) {
t.Parallel()
states := map[string]runtimeRaidState{
"no config": {Lifecycle: runtimeRaidLifecycleState{Phase: runtimeRaidDungeonPhaseOne}},
"phase zero": {AntonConfig: &raid.AntonConfig{}, Lifecycle: runtimeRaidLifecycleState{Phase: 0}},
"phase past the catalog": {
AntonConfig: &raid.AntonConfig{Phases: []raid.AntonPhase{{MinimalDungeonClearCount: 1}}},
Lifecycle: runtimeRaidLifecycleState{Phase: runtimeRaidDungeonPhaseTwo},
},
"single mode without a single catalog": {
TailFlag: 1,
AntonConfig: &raid.AntonConfig{Phases: []raid.AntonPhase{{MinimalDungeonClearCount: 1}}},
Lifecycle: runtimeRaidLifecycleState{Phase: runtimeRaidDungeonPhaseOne},
},
}
for name, state := range states {
state := state
t.Run(name, func(t *testing.T) {
state.Lifecycle.PhaseClearCounts = map[uint16]uint32{19: 0}
if runtimeRaidMemberClearedGateLocked(state, 19) {
t.Fatal("a missing declaration was read as an unconditional phase")
}
state.Lifecycle.PhaseClearCounts[19] = 1
if !runtimeRaidMemberClearedGateLocked(state, 19) {
t.Fatal("the pre-PVF rule no longer pays a member that cleared a gate")
}
})
}
}
// TestRuntimeRaidAntonClearRequirementIsBitForBitUnchanged is the A/B that makes the
// Luke fix safe. Every Anton phase declares [MINIMAL DUNGEON CLEAR COUNT] 1:
//
// - team phases cannot declare anything else, because antonParsePhaseWithPartyMax
// demands a positive value (anton_pvf.go:586);
// - the single phase's PVF block omits the section entirely and is defaulted to 1
// (anton_pvf.go:503).
//
// The clear counter is a uint32, so `count >= 1` and the pre-PVF `count > 0` are the
// same predicate. This sweep proves the new requirement cannot diverge for any count
// a phase can register, including the counts the display cap can hold.
func TestRuntimeRaidAntonClearRequirementIsBitForBitUnchanged(t *testing.T) {
t.Parallel()
config := antonClearRequirementConfig()
for _, phase := range []byte{runtimeRaidDungeonPhaseOne, runtimeRaidDungeonPhaseTwo} {
for tail := byte(0); tail <= 1; tail++ {
state := runtimeRaidState{
RaidMode: runtimeRaidOwnerAnton,
TailFlag: tail,
AntonConfig: &config,
Lifecycle: runtimeRaidLifecycleState{Phase: phase},
}
for cleared := uint32(0); cleared <= 8; cleared++ {
state.Lifecycle.PhaseClearCounts = map[uint16]uint32{19: cleared}
want := cleared > 0
if got := runtimeRaidMemberClearedGateLocked(state, 19); got != want {
t.Fatalf("phase=%d tail=%d cleared=%d eligible=%t want %t",
phase, tail, cleared, got, want)
}
}
}
}
}
// TestRuntimeRaidLukePhaseTwoRewardNeedsTwoRegisteredClears drives the requirement
// through the real registration path instead of a hand-written counter, and pins the
// two ends of the official rule: Luke phase two pays only once the 地下城通关 column
// reaches 2, while Luke phase one is unconditional.
func TestRuntimeRaidLukePhaseTwoRewardNeedsTwoRegisteredClears(t *testing.T) {
t.Parallel()
lukeConfig := lukeClearRequirementConfig()
member := runtimeRaidMemberState{CharID: 19, Name: "leader", GroupIndex: 1, SlotOrder: 0}
phaseTwo := &runtimeRaidState{
RaidMode: runtimeRaidOwnerLuke,
LukeConfig: &raid.LukeConfig{Raid: &lukeConfig},
Members: []runtimeRaidMemberState{member},
FrozenMembers: []runtimeRaidMemberState{member},
Lifecycle: runtimeRaidLifecycleState{Phase: runtimeRaidDungeonPhaseTwo},
}
runtimeRaidNotePhaseClearLocked(phaseTwo, 19, runtimeRaidDungeonPhaseTwo)
if counts := phaseTwo.Lifecycle.PhaseClearCounts[19]; counts != 1 {
t.Fatalf("registered clears=%d want 1", counts)
}
if runtimeRaidMemberClearedGateLocked(*phaseTwo, 19) {
t.Fatal("one registered clear already paid the card")
}
runtimeRaidNotePhaseClearLocked(phaseTwo, 19, runtimeRaidDungeonPhaseTwo)
if counts := phaseTwo.Lifecycle.PhaseClearCounts[19]; counts != 2 {
t.Fatalf("registered clears=%d want 2 (capped at the phase requirement)", counts)
}
if !runtimeRaidMemberClearedGateLocked(*phaseTwo, 19) {
t.Fatal("two registered clears did not pay the card")
}
// Phase one declares 0, which is the official 「1阶段:无条件」: the roster is
// paid without registering a single clear.
phaseOne := &runtimeRaidState{
RaidMode: runtimeRaidOwnerLuke,
LukeConfig: &raid.LukeConfig{Raid: &lukeConfig},
Members: []runtimeRaidMemberState{member},
FrozenMembers: []runtimeRaidMemberState{member},
Lifecycle: runtimeRaidLifecycleState{Phase: runtimeRaidDungeonPhaseOne},
}
if !runtimeRaidMemberClearedGateLocked(*phaseOne, 19) {
t.Fatal("Luke phase one is unconditional and must pay a member with no clear")
}
}