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,212 @@
|
||||
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",
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user