DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,323 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user