Files
90CNServer/server/internal/modules/dnf/dungeonadmission/catalog_test.go
T
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

213 lines
10 KiB
Go

package dungeonadmission
import (
"fmt"
"strings"
"testing"
"time"
)
type catalogTestSource map[string]string
func (s catalogTestSource) ReadText(path string) (string, error) {
for candidate, text := range s {
if strings.EqualFold(candidate, path) {
return text, nil
}
}
return "", fmt.Errorf("missing %s", path)
}
func TestCatalogBindsOnlyPVFProvedPeriodsAndMembership(t *testing.T) {
catalog, err := LoadCatalog(syntheticCatalogSource())
if err != nil {
t.Fatal(err)
}
assertDungeonPolicy(t, catalog, 8000, "crack_of_dimension.character.daily", 10, PeriodDaily, 0)
assertDungeonPolicy(t, catalog, 7165, "dimension_dungeon.character.daily", 10, PeriodDaily, 0)
assertDungeonPolicy(t, catalog, 5006, "license_dungeon.character.daily", 2, PeriodDaily, 0)
assertDungeonPolicy(t, catalog, 62, "dimension_gate.character.daily", 5, PeriodDaily, 0)
assertDungeonPolicy(t, catalog, 5115, "tower_of_grief.character.daily", 2, PeriodDaily, 0)
assertDungeonPolicy(t, catalog, 5115, "tower_of_grief.account.daily", 1, PeriodDaily, 0)
for dungeonID, limit := range map[uint32]uint32{4906: 2, 4907: 1} {
if policies := catalog.PoliciesForDungeon(dungeonID); len(policies) != 0 {
t.Fatalf("weekly owner was guessed dungeon=%d policies=%+v", dungeonID, policies)
}
evidence := catalog.EvidenceForDungeon(dungeonID)
if len(evidence) != 1 || evidence[0].Enforced || evidence[0].Scope != 0 || evidence[0].Period != PeriodWeekly ||
evidence[0].Limit != limit || evidence[0].ResetWeekday != time.Thursday || !strings.Contains(evidence[0].Reason, "does not declare") {
t.Fatalf("weekly evidence dungeon=%d rows=%+v", dungeonID, evidence)
}
}
assertDungeonPolicy(t, catalog, 11008, "tower_of_despair.character.daily", 3, PeriodDaily, 0)
assertDungeonPolicy(t, catalog, 11008, "tower_of_despair.account.daily", 3, PeriodDaily, 0)
evidence := catalog.EvidenceForDungeon(11008)
if len(evidence) != 2 {
t.Fatalf("tower evidence=%+v want character/account", evidence)
}
for _, row := range evidence {
if !row.Enforced || row.Period != PeriodDaily || row.Limit != 3 {
t.Fatalf("tower evidence incorrectly enforceable=%+v", row)
}
}
if history := findEvidence(catalog, "dimension_gate.recent_history"); history.Enforced || history.Period != 0 || history.Limit != 3 {
t.Fatalf("recent-history count became a limit=%+v", history)
}
for _, key := range []string{"raid.anton.account.daily", "raid.luke.account.daily", "raid.fiend_war.account.daily"} {
policy, found := catalog.Policy(key)
if !found || policy.Scope != ScopeAccount || policy.Period != PeriodDaily || policy.Limit != 10 || len(policy.DungeonIDs) != 0 {
t.Fatalf("raid-start policy %s found=%t policy=%+v", key, found, policy)
}
}
// Every raid's [WEEKLY ENTER COUNT] row is now BOTH scopes, not evidence:
// the shipped documents declare `30 3` / `20 2` / `20 2` and the client's two
// team-mode windows render exactly those as 帐号周入场场数 (first operand) and
// 角色周入场次数 (second operand), so the operand meaning is read off the
// client rather than inferred. All six carry no dungeon ids for the same
// reason the daily policies do not: these are raid-START limits looked up by
// semantic key, so PoliciesForDungeon must never apply them per gate.
for _, weekly := range []struct {
key string
path string
scope Scope
limit uint32
}{
{AntonWeeklyAccountPolicyKey, AntonRaidPath, ScopeAccount, 30},
{AntonWeeklyCharacterPolicyKey, AntonRaidPath, ScopeCharacter, 3},
{LukeWeeklyAccountPolicyKey, LukeRaidPath, ScopeAccount, 20},
{LukeWeeklyCharacterPolicyKey, LukeRaidPath, ScopeCharacter, 2},
{FiendWarWeeklyAccountPolicyKey, FiendWarRaidPath, ScopeAccount, 20},
{FiendWarWeeklyCharacterPolicyKey, FiendWarRaidPath, ScopeCharacter, 2},
} {
policy, found := catalog.Policy(weekly.key)
if !found || policy.Scope != weekly.scope || policy.Period != PeriodWeekly ||
policy.Limit != weekly.limit || policy.ResetWeekday != time.Thursday ||
len(policy.DungeonIDs) != 0 || policy.SourcePath != weekly.path ||
policy.SourceSection != "weekly enter count" {
t.Fatalf("raid weekly policy %s found=%t policy=%+v", weekly.key, found, policy)
}
}
// The shipped fiendwar.etc repeats the [WEEKLY ENTER COUNT] section verbatim.
// A repeated-but-identical pair is tolerated; the pair that lands in the
// catalog is the first one and both scopes must agree with it.
if repeated, found := catalog.Policy(FiendWarWeeklyAccountPolicyKey); !found || repeated.Limit != 20 {
t.Fatalf("duplicated fiendwar weekly section produced %+v found=%t", repeated, found)
}
if beast := findEvidence(catalog, "beast_dungeon.unbound"); beast.Enforced || beast.Period != 0 || beast.Limit != 2 {
t.Fatalf("beast period was guessed=%+v", beast)
}
// Luke's [WEEKLY ENTER COUNT] `20 2` additionally gets a real Policy, because its
// per-character share is corroborated for this client's version window instead of
// merely guessed. It owns the counter and the window, and it carries NO dungeon
// ids on purpose: PoliciesForDungeon must keep returning nothing for Luke's gates
// so that nothing starts refusing until the reset node is confirmed.
weekly, found := catalog.Policy(LukeWeeklyCharacterPolicyKey)
if !found || weekly.Scope != ScopeCharacter || weekly.Period != PeriodWeekly || weekly.Limit != 2 ||
weekly.ResetWeekday != time.Thursday || len(weekly.DungeonIDs) != 0 ||
weekly.SourcePath != LukeRaidPath || weekly.SourceSection != "weekly enter count" {
t.Fatalf("luke weekly policy found=%t policy=%+v", found, weekly)
}
for _, dungeonID := range []uint32{5054, 5057, 5061, 5323} {
if policies := catalog.PoliciesForDungeon(dungeonID); len(policies) != 0 {
t.Fatalf("luke weekly policy became enforceable dungeon=%d policies=%+v", dungeonID, policies)
}
}
}
func TestCatalogRejectsWeeklyDungeonMissingFromDayIndex(t *testing.T) {
source := syntheticCatalogSource()
source[AccountWeeklyDungeonPath] = "[dungeon enter max count]\n4906 2 4907 1\n[/dungeon enter max count]\n" +
"[weekly dungeon index]\n0 4906\n[/weekly dungeon index]\n"
if _, err := LoadCatalog(source); err == nil || !strings.Contains(err.Error(), "4907 absent") {
t.Fatalf("weekly mismatch err=%v", err)
}
}
func TestCatalogPoliciesAreImmutableCopies(t *testing.T) {
catalog, err := LoadCatalog(syntheticCatalogSource())
if err != nil {
t.Fatal(err)
}
policies := catalog.PoliciesForDungeon(7165)
policies[0].DungeonIDs[0] = 99
again := catalog.PoliciesForDungeon(7165)
if len(again) != 1 || again[0].DungeonIDs[0] != 7165 {
t.Fatalf("catalog mutated through copy=%+v", again)
}
}
func TestCatalogKeepsOrderedTowerLayersAndEntryPairs(t *testing.T) {
catalog, err := LoadCatalog(syntheticCatalogSource())
if err != nil {
t.Fatal(err)
}
despair, found := catalog.TowerRoute(TowerFamilyDespair)
if !found || len(despair.DungeonIDs) != 2 || despair.DungeonIDs[0] != 11008 || despair.DungeonIDs[1] != 11009 || len(despair.UseAllowItems) != 0 {
t.Fatalf("despair route=%+v found=%t", despair, found)
}
grief, found := catalog.TowerRoute(TowerFamilyGrief)
if !found || len(grief.DungeonIDs) != 1 || grief.DungeonIDs[0] != 5115 || len(grief.UseAllowItems) != 2 ||
grief.UseAllowItems[0] != (TowerUseItem{ItemID: 10160849, Count: 10}) ||
grief.UseAllowItems[1] != (TowerUseItem{ItemID: 10163185, Count: -1}) {
t.Fatalf("grief route=%+v found=%t", grief, found)
}
despair.DungeonIDs[0] = 1
again, _ := catalog.TowerRoute(TowerFamilyDespair)
if again.DungeonIDs[0] != 11008 {
t.Fatalf("tower route leaked mutable slice=%+v", again)
}
}
func assertDungeonPolicy(t *testing.T, catalog *Catalog, dungeonID uint32, key string, limit uint32, period Period, weekday time.Weekday) {
t.Helper()
policies := catalog.PoliciesForDungeon(dungeonID)
for _, policy := range policies {
if policy.Key == key {
if policy.Limit != limit || policy.Period != period || policy.ResetWeekday != weekday {
t.Fatalf("dungeon=%d policy=%+v", dungeonID, policy)
}
return
}
}
t.Fatalf("dungeon=%d policies=%+v missing=%s", dungeonID, policies, key)
}
func findEvidence(catalog *Catalog, key string) Evidence {
for _, evidence := range catalog.Evidence() {
if evidence.Key == key {
return evidence
}
}
return Evidence{}
}
func syntheticCatalogSource() catalogTestSource {
return catalogTestSource{
DungeonListPath: "8000 `Crackofdimension/Crackofdimension.dgn`\n",
CrackOfDimensionPath: "[daily enter count]\n10\n[/daily enter count]\n",
DimensionDungeonInfoPath: "[dimension dungeon info]\n0 7165 1 7166 2 7167\n[/dimension dungeon info]\n" +
"[max daily enter count]\n10\n",
LicenseDungeonInfoPath: "[dungeon license info]\n1 5006 1 10 2 1 5007 2 7 4\n[/dungeon license info]\n" +
"[daily enter count]\n2\n",
DimensionGatePath: "[daily default enter count]\n5\n[dimensiondungeon list]\n62 25 0 64 25 0\n[/dimensiondungeon list]\n" +
"[recently enter dungeon count]\n3\n",
WeeklyDungeonPath: "[reset day index]\n4\n[weekly default enter count]\n6\n",
AccountWeeklyDungeonPath: "[dungeon enter max count]\n4906 2 4907 1\n[/dungeon enter max count]\n" +
"[weekly dungeon index]\n0 4907 1 4906\n[/weekly dungeon index]\n",
TowerOfDespairPath: "[enterable count]\n3\n[account enterable max count]\n3\n" +
"[each layer matching dungeon]\n1 11008 -1 2 11009 -1\n[/each layer matching dungeon]\n",
TowerOfDespairInvitationPath: "[name]\n`绝望之邀请函`\n[explain]\n`通关成功后当日无法再继续挑战绝望之塔。`\n[stackable type]\n`[material]` 0\n",
SAOTowerOfDespairPath: "[enterable count]\n3\n[each layer matching dungeon]\n1 3600 -1\n[/each layer matching dungeon]\n",
TowerOfGriefPath: "[enterable count]\n2\n[account enterable max count]\n1\n" +
"[each layer matching dungeon]\n1 5115 -1\n[/each layer matching dungeon]\n" +
"[use allow item list in dungeon]\n10160849 10 10163185 -1\n[/use allow item list in dungeon]\n",
BeastDungeonPath: "[limit enter count]\n2\n",
AntonRaidPath: "[daily account enter limit]\n10\n[weekly enter count]\n30 3\n",
LukeRaidPath: "[daily account enter limit]\n10\n[weekly enter count]\n20 2\n",
FiendWarRaidPath: "[daily account enter limit]\n10\n[weekly enter count]\n20 2\n",
}
}