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

324 lines
9.0 KiB
Go

package guild
import "testing"
// The layout file nests [dungeon] blocks with their own subsections, and the
// section names span several whitespace-separated fields, so the scanner is the
// risky part. This fixture is a trimmed copy of the real
// etc/guild/guilddungeon/0_2Way6Dungeon.etc.
const dungeonCatalogFixture = `
[weekly reward]
[final dungeon]
8508
[/weekly reward]
[dungeon]
[dungeon index]
8500
[gauge limit]
10000
[gauge on clear]
150
[conquest gauge ratio]
50
[color]
` + "`black`" + `
[type]
` + "`normal`" + `
[reward]
[gauge ratio]
99
[item]
10154439 3
[contribute]
20
[/gauge ratio]
[/reward]
[/dungeon]
[dungeon]
[dungeon index]
8504
[gauge limit]
10000
[gauge on clear]
75
[conquest gauge ratio]
50
[color]
` + "`red`" + `
[type]
` + "`normal`" + `
[required dungeon conquest]
8500
[/required dungeon conquest]
[/dungeon]
[dungeon]
[dungeon index]
8503
[gauge limit]
10000
[gauge on clear]
75
[conquest gauge ratio]
50
[color]
` + "`blue`" + `
[type]
` + "`normal`" + `
[/dungeon]
[dungeon]
[dungeon index]
8505
[gauge limit]
10000
[gauge on clear]
50
[conquest gauge ratio]
50
[color]
` + "`red`" + `
[type]
` + "`hard`" + `
[/dungeon]
[dungeon]
[dungeon index]
8508
[gauge limit]
10000
[gauge on clear]
30
[conquest gauge ratio]
100
[color]
` + "`red`" + `
[type]
` + "`final`" + `
[required dungeon conquest]
8503 8505
[/required dungeon conquest]
[/dungeon]
`
func TestParseDungeonCatalogReadsStagesAndPrerequisites(t *testing.T) {
catalog, err := parseDungeonCatalog(dungeonCatalogFixture)
if err != nil {
t.Fatal(err)
}
if catalog.FinalDungeon != 8508 {
t.Fatalf("final dungeon=%d want 8508", catalog.FinalDungeon)
}
if len(catalog.Stages) != 5 {
t.Fatalf("stages=%d want 5", len(catalog.Stages))
}
first, ok := catalog.Stage(8500)
if !ok {
t.Fatal("8500 missing")
}
if first.GaugeLimit != 10000 || first.GaugeOnClear != 150 || first.ConquestGaugeRatio != 50 {
t.Fatalf("8500=%+v", first)
}
if first.Color != "black" || first.Type != "normal" {
t.Fatalf("8500 color=%q type=%q", first.Color, first.Type)
}
// The reward block's own numbers (99, 10154439, 3, 20) must not leak into
// the stage: they sit inside [reward] and belong to a different scope.
if len(first.RequiredConquest) != 0 {
t.Fatalf("8500 must have no prerequisite, got %v", first.RequiredConquest)
}
// 50% of 10000.
if got := first.ConquestGauge(); got != 5000 {
t.Fatalf("8500 conquest gauge=%d want 5000", got)
}
second, _ := catalog.Stage(8504)
if len(second.RequiredConquest) != 1 || second.RequiredConquest[0] != 8500 {
t.Fatalf("8504 prerequisites=%v want [8500]", second.RequiredConquest)
}
final, _ := catalog.Stage(8508)
if len(final.RequiredConquest) != 2 || final.RequiredConquest[0] != 8503 || final.RequiredConquest[1] != 8505 {
t.Fatalf("8508 prerequisites=%v want [8503 8505]", final.RequiredConquest)
}
if got := final.ConquestGauge(); got != 10000 {
t.Fatalf("8508 conquest gauge=%d want 10000 (100%%)", got)
}
}
// The unlock rule is what the client's own gate mirrors: a stage opens only
// once every prerequisite has reached its conquest threshold.
func TestDungeonCatalogUnlockFollowsConquest(t *testing.T) {
catalog, err := parseDungeonCatalog(dungeonCatalogFixture)
if err != nil {
t.Fatal(err)
}
gauges := map[uint32]uint32{}
// No prerequisites: always open, which is why an empty guild still sees 伤城.
if !catalog.Unlocked(gauges, 8500) {
t.Fatal("8500 must be open with no gauges")
}
if catalog.Unlocked(gauges, 8504) {
t.Fatal("8504 must be shut before 8500 is conquered")
}
// One clear is not a conquest: 150 of the 5000 needed.
gauges[8500] = 150
if catalog.Unlocked(gauges, 8504) {
t.Fatal("8504 opened after a single clear of 8500")
}
gauges[8500] = 4999
if catalog.Unlocked(gauges, 8504) {
t.Fatal("8504 opened one point short of the threshold")
}
gauges[8500] = 5000
if !catalog.Unlocked(gauges, 8504) {
t.Fatal("8504 still shut at exactly the threshold")
}
// The final stage needs both prerequisites, and at 100%.
gauges[8503] = 10000
if catalog.Unlocked(gauges, 8508) {
t.Fatal("8508 opened with only one prerequisite conquered")
}
// 8505 conquers at 50% of 10000, so one point short must still block.
gauges[8505] = 4999
if catalog.Unlocked(gauges, 8508) {
t.Fatal("8508 opened while 8505 was below its own threshold")
}
gauges[8505] = 5000
if !catalog.Unlocked(gauges, 8508) {
t.Fatal("8508 shut with both prerequisites conquered")
}
}
func TestAddDungeonGaugeClampsToTheStageLimit(t *testing.T) {
registry := NewRegistry()
registry.Guilds[1] = &Guild{ID: 1}
// The pair is (before, after): the caller needs both to tell which reward
// thresholds this clear actually crossed.
if previous, gauge, err := registry.AddDungeonGauge(1, 8500, 150, 10000); err != nil || previous != 0 || gauge != 150 {
t.Fatalf("first clear %d->%d err=%v want 0->150", previous, gauge, err)
}
if previous, gauge, err := registry.AddDungeonGauge(1, 8500, 150, 10000); err != nil || previous != 150 || gauge != 300 {
t.Fatalf("second clear %d->%d err=%v want 150->300", previous, gauge, err)
}
if _, gauge, err := registry.AddDungeonGauge(1, 8500, 100_000, 10000); err != nil || gauge != 10000 {
t.Fatalf("clamped gauge=%d err=%v want 10000", gauge, err)
}
if _, _, err := registry.AddDungeonGauge(99, 8500, 1, 10000); err == nil {
t.Fatal("an unknown guild must not accumulate a gauge")
}
}
// Rewards are paid on crossing a [gauge ratio] line, once. A repeat clear past
// the line must not pay again, and a clear that jumps several lines at once
// pays each of them.
func TestRewardsCrossedPaysEachThresholdOnce(t *testing.T) {
stage := DungeonStage{
GaugeLimit: 10000,
GaugeOnClear: 150,
Rewards: []DungeonReward{
{GaugeRatio: 99, ItemID: 10154439, ItemCount: 3, Contribute: 20},
{GaugeRatio: 100, ItemID: 10154439, ItemCount: 3, Contribute: 20},
},
}
if got := stage.RewardsCrossed(0, 150); len(got) != 0 {
t.Fatalf("an early clear paid %d rewards", len(got))
}
// 99% of 10000 is 9900.
if got := stage.RewardsCrossed(9800, 9900); len(got) != 1 || got[0].GaugeRatio != 99 {
t.Fatalf("crossing 99%% paid %+v", got)
}
// Already past it: nothing more until 100%.
if got := stage.RewardsCrossed(9900, 9950); len(got) != 0 {
t.Fatalf("staying above 99%% paid again: %+v", got)
}
if got := stage.RewardsCrossed(9950, 10000); len(got) != 1 || got[0].GaugeRatio != 100 {
t.Fatalf("crossing 100%% paid %+v", got)
}
// One clear that jumps both lines pays both.
if got := stage.RewardsCrossed(0, 10000); len(got) != 2 {
t.Fatalf("a full jump paid %d rewards, want 2", len(got))
}
}
// The claim marker is guild-wide and survives within the week, so a second
// member cannot collect the same threshold.
func TestClaimDungeonRewardIsOncePerGuildPerThreshold(t *testing.T) {
registry := NewRegistry()
registry.Guilds[1] = &Guild{ID: 1}
claimed, err := registry.ClaimDungeonReward(1, 8500, 99)
if err != nil || !claimed {
t.Fatalf("first claim=%t err=%v", claimed, err)
}
claimed, err = registry.ClaimDungeonReward(1, 8500, 99)
if err != nil || claimed {
t.Fatalf("second claim=%t err=%v, must not pay twice", claimed, err)
}
// A different threshold, and a different stage, are separate claims.
if claimed, _ := registry.ClaimDungeonReward(1, 8500, 100); !claimed {
t.Fatal("the 100%% threshold was blocked by the 99%% claim")
}
if claimed, _ := registry.ClaimDungeonReward(1, 8504, 99); !claimed {
t.Fatal("another stage was blocked by the first stage's claim")
}
}
// A new week wipes gauges and claims so the cycle restarts; the same week is a
// no-op, which is what keeps a clear from resetting the bar it just raised.
func TestResetDungeonWeekClearsProgressOnlyOnANewWeek(t *testing.T) {
registry := NewRegistry()
registry.Guilds[1] = &Guild{ID: 1}
if _, _, err := registry.AddDungeonGauge(1, 8500, 9000, 10000); err != nil {
t.Fatal(err)
}
if _, err := registry.ClaimDungeonReward(1, 8500, 99); err != nil {
t.Fatal(err)
}
// First settle of a fresh guild stamps the week without wiping anything.
if reset, err := registry.ResetDungeonWeek(1, 100); err != nil || !reset {
t.Fatalf("first settle reset=%t err=%v", reset, err)
}
if len(registry.DungeonGauges(1)) != 0 {
t.Fatal("the first settle should have cleared the pre-existing progress")
}
if _, _, err := registry.AddDungeonGauge(1, 8500, 600, 10000); err != nil {
t.Fatal(err)
}
// Same week: progress must survive.
if reset, err := registry.ResetDungeonWeek(1, 100); err != nil || reset {
t.Fatalf("same-week reset=%t err=%v", reset, err)
}
if registry.DungeonGauges(1)[8500] != 600 {
t.Fatalf("same-week settle wiped the gauge: %v", registry.DungeonGauges(1))
}
// Next week: wiped, claims too.
if reset, err := registry.ResetDungeonWeek(1, 101); err != nil || !reset {
t.Fatalf("new-week reset=%t err=%v", reset, err)
}
if len(registry.DungeonGauges(1)) != 0 {
t.Fatalf("new week kept gauges: %v", registry.DungeonGauges(1))
}
if claimed, _ := registry.ClaimDungeonReward(1, 8500, 99); !claimed {
t.Fatal("new week kept last week's reward claim")
}
}