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,678 @@
package dungeonadmission
import (
"errors"
"fmt"
"sort"
"strings"
"time"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
)
const (
CrackOfDimensionPath = "etc/crackofdimensionlist.etc"
DimensionDungeonInfoPath = "etc/dungeonetc/dimensiondungeoninfo.etc"
LicenseDungeonInfoPath = "etc/dungeonetc/licensedungeoninfo.etc"
DimensionGatePath = "etc/dimensiongate.etc"
WeeklyDungeonPath = "etc/weeklydungeon.etc"
AccountWeeklyDungeonPath = "etc/accountcharacdungeonincountchn.etc"
TowerOfDespairPath = "etc/towerofdespair.etc"
TowerOfDespairInvitationPath = "stackable/invitation_drapyus2.stk"
SAOTowerOfDespairPath = "etc/sao_towerofdespair.etc"
TowerOfGriefPath = "etc/towerofgrief.etc"
BeastDungeonPath = "etc/beastdungeon/beastdungeon.etc"
AntonRaidPath = "etc/raid/anton.etc"
LukeRaidPath = "etc/raid/luke/luke.etc"
FiendWarRaidPath = "etc/raid/fiendwar/fiendwar.etc"
DungeonListPath = "dungeon/dungeon.lst"
)
// Raid weekly admission policy keys, one pair (account + character) per raid.
//
// These are raid-START limits: like the raid daily keys they carry no
// DungeonIDs, so they are looked up by semantic key and are never applied once
// per internal raid dungeon. dnfbridge charges them at start
// (raid_admission_counter.go) and renders them in the team-channel window
// (raid_daily_availability.go).
const (
AntonWeeklyAccountPolicyKey = "raid.anton.account.weekly"
AntonWeeklyCharacterPolicyKey = "raid.anton.character.weekly"
LukeWeeklyAccountPolicyKey = "raid.luke.account.weekly"
// LukeWeeklyCharacterPolicyKey owns Luke's per-character weekly entry count.
// Kept as a named constant because dnfbridge's per-gate observer
// (raid_luke_weekly_observation.go) still quotes it; it is now charged and
// rendered like every other raid weekly policy.
LukeWeeklyCharacterPolicyKey = "raid.luke.character.weekly"
FiendWarWeeklyAccountPolicyKey = "raid.fiend_war.account.weekly"
FiendWarWeeklyCharacterPolicyKey = "raid.fiend_war.character.weekly"
)
var (
ErrCatalogSourceRequired = errors.New("dungeon admission catalog source is required")
ErrCatalogInvalid = errors.New("dungeon admission catalog is invalid")
)
// Evidence records a structured limit found in the active PVF. A zero Period
// means the available source set does not prove a daily/weekly reset and is
// deliberately not converted into an enforceable Policy. This prevents values
// such as `[recently enter dungeon count] 3` from being guessed as limits; the
// two tower families become daily only because their companion recommendation
// actions carry service-window use counters for those exact entry pools.
type Evidence struct {
Key string
SourcePath string
SourceSection string
Scope Scope
Period Period
Limit uint32
ResetWeekday time.Weekday
DungeonIDs []uint32
Enforced bool
Reason string
RawValues []int64
}
// Catalog is the immutable active-PVF admission-limit view. Policies without
// DungeonIDs (currently raid-start limits) are looked up by semantic key and
// must not be applied once per internal raid dungeon.
type Catalog struct {
policies map[string]Policy
byDungeon map[uint32][]string
evidence []Evidence
towerRoute map[TowerFamily]TowerRoute
}
func LoadCatalog(source dnfpvf.Source) (*Catalog, error) {
if source == nil {
return nil, ErrCatalogSourceRequired
}
documents := make(map[string]*dnfpvf.Document)
for _, sourcePath := range []string{
CrackOfDimensionPath,
DimensionDungeonInfoPath,
LicenseDungeonInfoPath,
DimensionGatePath,
WeeklyDungeonPath,
AccountWeeklyDungeonPath,
TowerOfDespairPath,
TowerOfDespairInvitationPath,
SAOTowerOfDespairPath,
TowerOfGriefPath,
BeastDungeonPath,
AntonRaidPath,
LukeRaidPath,
FiendWarRaidPath,
DungeonListPath,
} {
text, err := source.ReadText(sourcePath)
if err != nil {
return nil, fmt.Errorf("%w: read %s: %v", ErrCatalogInvalid, sourcePath, err)
}
document, err := dnfpvf.Parse(sourcePath, text)
if err != nil {
return nil, fmt.Errorf("%w: parse %s: %v", ErrCatalogInvalid, sourcePath, err)
}
documents[sourcePath] = document
}
catalog := &Catalog{
policies: make(map[string]Policy),
byDungeon: make(map[uint32][]string),
towerRoute: make(map[TowerFamily]TowerRoute),
}
if err := catalog.loadExplicitDaily(documents); err != nil {
return nil, err
}
if err := catalog.loadExplicitWeekly(documents); err != nil {
return nil, err
}
if err := catalog.loadUnprovedPeriodEvidence(documents); err != nil {
return nil, err
}
catalog.sort()
return catalog, nil
}
func (c *Catalog) PoliciesForDungeon(dungeonID uint32) []Policy {
if c == nil || dungeonID == 0 {
return nil
}
keys := c.byDungeon[dungeonID]
out := make([]Policy, 0, len(keys))
for _, key := range keys {
if policy, ok := c.policies[key]; ok {
out = append(out, clonePolicy(policy))
}
}
return out
}
// TowerRoute returns a detached copy of the PVF-authored ordered layer map.
func (c *Catalog) TowerRoute(family TowerFamily) (TowerRoute, bool) {
if c == nil {
return TowerRoute{}, false
}
route, found := c.towerRoute[family]
if !found {
return TowerRoute{}, false
}
route.DungeonIDs = append([]uint32(nil), route.DungeonIDs...)
route.UseAllowItems = append([]TowerUseItem(nil), route.UseAllowItems...)
return route, true
}
func (c *Catalog) Policy(key string) (Policy, bool) {
if c == nil {
return Policy{}, false
}
policy, ok := c.policies[strings.TrimSpace(key)]
return clonePolicy(policy), ok
}
func (c *Catalog) Policies() []Policy {
if c == nil {
return nil
}
keys := make([]string, 0, len(c.policies))
for key := range c.policies {
keys = append(keys, key)
}
sort.Strings(keys)
out := make([]Policy, 0, len(keys))
for _, key := range keys {
out = append(out, clonePolicy(c.policies[key]))
}
return out
}
func (c *Catalog) Evidence() []Evidence {
if c == nil {
return nil
}
out := make([]Evidence, len(c.evidence))
for index, evidence := range c.evidence {
evidence.DungeonIDs = append([]uint32(nil), evidence.DungeonIDs...)
evidence.RawValues = append([]int64(nil), evidence.RawValues...)
out[index] = evidence
}
return out
}
func (c *Catalog) EvidenceForDungeon(dungeonID uint32) []Evidence {
if c == nil || dungeonID == 0 {
return nil
}
out := make([]Evidence, 0)
for _, evidence := range c.evidence {
for _, candidate := range evidence.DungeonIDs {
if candidate == dungeonID {
evidence.DungeonIDs = append([]uint32(nil), evidence.DungeonIDs...)
evidence.RawValues = append([]int64(nil), evidence.RawValues...)
out = append(out, evidence)
break
}
}
}
return out
}
func (c *Catalog) loadExplicitDaily(documents map[string]*dnfpvf.Document) error {
crackID, err := crackOfDimensionDungeonID(documents[DungeonListPath])
if err != nil {
return err
}
if err := c.addSectionPolicy(documents[CrackOfDimensionPath], Policy{
Key: "crack_of_dimension.character.daily", SourcePath: CrackOfDimensionPath, SourceSection: "daily enter count",
Scope: ScopeCharacter, Period: PeriodDaily, DungeonIDs: []uint32{crackID},
}, "daily enter count"); err != nil {
return err
}
dimensionIDs, err := tupleDungeonIDs(documents[DimensionDungeonInfoPath], "dimension dungeon info", 2, 1)
if err != nil {
return err
}
if err := c.addSectionPolicy(documents[DimensionDungeonInfoPath], Policy{
Key: "dimension_dungeon.character.daily", SourcePath: DimensionDungeonInfoPath, SourceSection: "max daily enter count",
Scope: ScopeCharacter, Period: PeriodDaily, DungeonIDs: dimensionIDs,
}, "max daily enter count"); err != nil {
return err
}
licenseIDs, err := tupleDungeonIDs(documents[LicenseDungeonInfoPath], "dungeon license info", 5, 1)
if err != nil {
return err
}
if err := c.addSectionPolicy(documents[LicenseDungeonInfoPath], Policy{
Key: "license_dungeon.character.daily", SourcePath: LicenseDungeonInfoPath, SourceSection: "daily enter count",
Scope: ScopeCharacter, Period: PeriodDaily, DungeonIDs: licenseIDs,
}, "daily enter count"); err != nil {
return err
}
dimensionGateIDs, err := tupleDungeonIDs(documents[DimensionGatePath], "dimensiondungeon list", 3, 0)
if err != nil {
return err
}
if err := c.addSectionPolicy(documents[DimensionGatePath], Policy{
Key: "dimension_gate.character.daily", SourcePath: DimensionGatePath, SourceSection: "daily default enter count",
Scope: ScopeCharacter, Period: PeriodDaily, DungeonIDs: dimensionGateIDs,
}, "daily default enter count"); err != nil {
return err
}
// This nearby value is a selector-history length, not an admission limit.
c.addEvidence(Evidence{
Key: "dimension_gate.recent_history", SourcePath: DimensionGatePath, SourceSection: "recently enter dungeon count",
Limit: positiveSectionValue(documents[DimensionGatePath], "recently enter dungeon count"), DungeonIDs: dimensionGateIDs,
Reason: "selector recent-history count has no limit/reset ownership",
RawValues: documents[DimensionGatePath].Ints("recently enter dungeon count"),
})
// The current PVF's recommendation actions carry daily use counters and add
// the matching tower entry pools. Tower of Despair additionally proves its
// same-day boundary in the required invitation's [explain] text. Together
// these close the period gap left by the two tower ETC files alone while
// preserving their authored character/account caps and exact membership.
if err := validateTowerOfDespairDailyEvidence(documents[TowerOfDespairInvitationPath]); err != nil {
return err
}
for _, tower := range []struct {
path string
family TowerFamily
}{
{path: TowerOfDespairPath, family: TowerFamilyDespair},
{path: TowerOfGriefPath, family: TowerFamilyGrief},
} {
route, towerErr := parseTowerRoute(documents[tower.path], tower.family)
if towerErr != nil {
return towerErr
}
c.towerRoute[tower.family] = route
for _, policy := range []Policy{
{
Key: string(tower.family) + ".character.daily", SourcePath: tower.path, SourceSection: "enterable count",
Scope: ScopeCharacter, Period: PeriodDaily, DungeonIDs: route.DungeonIDs,
},
{
Key: string(tower.family) + ".account.daily", SourcePath: tower.path, SourceSection: "account enterable max count",
Scope: ScopeAccount, Period: PeriodDaily, DungeonIDs: route.DungeonIDs,
},
} {
if err := c.addSectionPolicy(documents[tower.path], policy, policy.SourceSection); err != nil {
return err
}
}
}
for _, raid := range []struct {
key string
path string
}{
{key: "raid.anton.account.daily", path: AntonRaidPath},
{key: "raid.luke.account.daily", path: LukeRaidPath},
{key: "raid.fiend_war.account.daily", path: FiendWarRaidPath},
} {
if err := c.addSectionPolicy(documents[raid.path], Policy{
Key: raid.key, SourcePath: raid.path, SourceSection: "daily account enter limit", Scope: ScopeAccount, Period: PeriodDaily,
}, "daily account enter limit"); err != nil {
return err
}
}
return nil
}
func parseTowerRoute(document *dnfpvf.Document, family TowerFamily) (TowerRoute, error) {
if document == nil || (family != TowerFamilyDespair && family != TowerFamilyGrief) {
return TowerRoute{}, fmt.Errorf("%w: invalid tower route family=%q", ErrCatalogInvalid, family)
}
values := document.Ints("each layer matching dungeon")
if len(values) == 0 || len(values)%3 != 0 || len(values)/3 > int(^uint16(0)) {
return TowerRoute{}, catalogSectionError(document.Path, "each layer matching dungeon", values)
}
route := TowerRoute{Family: family, DungeonIDs: make([]uint32, 0, len(values)/3)}
seen := make(map[uint32]struct{}, len(values)/3)
for offset := 0; offset < len(values); offset += 3 {
floor := offset/3 + 1
dungeonID := values[offset+1]
if values[offset] != int64(floor) || dungeonID <= 0 || dungeonID > int64(^uint32(0)) {
return TowerRoute{}, catalogSectionError(document.Path, "each layer matching dungeon", values)
}
id := uint32(dungeonID)
if _, duplicate := seen[id]; duplicate {
return TowerRoute{}, fmt.Errorf("%w: %s duplicate tower dungeon=%d", ErrCatalogInvalid, document.Path, id)
}
seen[id] = struct{}{}
route.DungeonIDs = append(route.DungeonIDs, id)
}
itemValues := document.Ints("use allow item list in dungeon")
if family == TowerFamilyGrief && (len(itemValues) == 0 || len(itemValues)%2 != 0 || len(itemValues)/2 > int(^uint8(0))) {
return TowerRoute{}, catalogSectionError(document.Path, "use allow item list in dungeon", itemValues)
}
if len(itemValues)%2 != 0 || len(itemValues)/2 > int(^uint8(0)) {
return TowerRoute{}, catalogSectionError(document.Path, "use allow item list in dungeon", itemValues)
}
for offset := 0; offset < len(itemValues); offset += 2 {
itemID, count := itemValues[offset], itemValues[offset+1]
if itemID <= 0 || itemID > int64(^uint32(0)) || count < int64(-1<<31) || count > int64(1<<31-1) {
return TowerRoute{}, catalogSectionError(document.Path, "use allow item list in dungeon", itemValues)
}
route.UseAllowItems = append(route.UseAllowItems, TowerUseItem{ItemID: uint32(itemID), Count: int32(count)})
}
return route, nil
}
func (c *Catalog) loadExplicitWeekly(documents map[string]*dnfpvf.Document) error {
resetValues := documents[WeeklyDungeonPath].Ints("reset day index")
if len(resetValues) != 1 || resetValues[0] < int64(time.Sunday) || resetValues[0] > int64(time.Saturday) {
return catalogSectionError(WeeklyDungeonPath, "reset day index", resetValues)
}
resetDay := time.Weekday(resetValues[0])
maxValues := documents[AccountWeeklyDungeonPath].Ints("dungeon enter max count")
if len(maxValues) == 0 || len(maxValues)%2 != 0 {
return catalogSectionError(AccountWeeklyDungeonPath, "dungeon enter max count", maxValues)
}
weeklyIndex := documents[AccountWeeklyDungeonPath].Ints("weekly dungeon index")
if len(weeklyIndex) == 0 || len(weeklyIndex)%2 != 0 {
return catalogSectionError(AccountWeeklyDungeonPath, "weekly dungeon index", weeklyIndex)
}
indexed := make(map[uint32]struct{})
for offset := 0; offset < len(weeklyIndex); offset += 2 {
if weeklyIndex[offset] < 0 || weeklyIndex[offset] > 6 || weeklyIndex[offset+1] <= 0 || weeklyIndex[offset+1] > int64(^uint32(0)) {
return catalogSectionError(AccountWeeklyDungeonPath, "weekly dungeon index", weeklyIndex)
}
indexed[uint32(weeklyIndex[offset+1])] = struct{}{}
}
for offset := 0; offset < len(maxValues); offset += 2 {
dungeonID, limit := maxValues[offset], maxValues[offset+1]
if dungeonID <= 0 || dungeonID > int64(^uint32(0)) || limit <= 0 || limit > int64(^uint32(0)) {
return catalogSectionError(AccountWeeklyDungeonPath, "dungeon enter max count", maxValues)
}
id := uint32(dungeonID)
if _, ok := indexed[id]; !ok {
return fmt.Errorf("%w: %s dungeon=%d absent from weekly dungeon index", ErrCatalogInvalid, AccountWeeklyDungeonPath, id)
}
c.addEvidence(Evidence{
Key: fmt.Sprintf("account_character_weekly_dungeon.%d.owner_unproved.weekly", id), SourcePath: AccountWeeklyDungeonPath,
SourceSection: "dungeon enter max count", Period: PeriodWeekly, Limit: uint32(limit),
ResetWeekday: resetDay, DungeonIDs: []uint32{id},
Reason: "PVF proves the weekly period, cap, and dungeon membership but does not declare whether the durable counter owner is character or account",
RawValues: []int64{dungeonID, limit},
})
}
c.addEvidence(Evidence{
Key: "weekly_dungeon.default_enter_count", SourcePath: WeeklyDungeonPath, SourceSection: "weekly default enter count",
Period: PeriodWeekly, Limit: positiveSectionValue(documents[WeeklyDungeonPath], "weekly default enter count"), ResetWeekday: resetDay,
Reason: "no dungeon identity or durable owner is declared in this document",
RawValues: documents[WeeklyDungeonPath].Ints("weekly default enter count"),
})
return nil
}
func (c *Catalog) loadUnprovedPeriodEvidence(documents map[string]*dnfpvf.Document) error {
for _, tower := range []struct {
key, path string
scope Scope
section string
idSection string
}{
{key: "sao_tower_of_despair.character", path: SAOTowerOfDespairPath, scope: ScopeCharacter, section: "enterable count", idSection: "each layer matching dungeon"},
} {
ids, err := tupleDungeonIDs(documents[tower.path], tower.idSection, 3, 1)
if err != nil {
return err
}
values := documents[tower.path].Ints(tower.section)
limit, err := onePositive(values)
if err != nil {
return catalogSectionError(tower.path, tower.section, values)
}
c.addEvidence(Evidence{
Key: tower.key, SourcePath: tower.path, SourceSection: tower.section, Scope: tower.scope, Limit: limit, DungeonIDs: ids,
Reason: "PVF proves the cap and dungeon membership but does not declare a daily/weekly reset period",
RawValues: values,
})
}
beastValues := documents[BeastDungeonPath].Ints("limit enter count")
beastLimit, err := onePositive(beastValues)
if err != nil {
return catalogSectionError(BeastDungeonPath, "limit enter count", beastValues)
}
c.addEvidence(Evidence{
Key: "beast_dungeon.unbound", SourcePath: BeastDungeonPath, SourceSection: "limit enter count", Limit: beastLimit,
Reason: "PVF declares a cap but no daily/weekly reset period in the same structured config", RawValues: beastValues,
})
// [WEEKLY ENTER COUNT] is a two-operand row on every raid document, and the
// operand meaning is now READ OFF THE SHIPPED CLIENT rather than inferred:
//
// etc/raid/anton.etc [WEEKLY ENTER COUNT] 30 3
// etc/raid/luke/luke.etc [WEEKLY ENTER COUNT] 20 2
// etc/raid/fiendwar/….etc [WEEKLY ENTER COUNT] 20 2
//
// and the client's two raid windows render exactly those pairs:
//
// window A "选择团队频道" 帐号周入场场数: 30/30 | 20/20 | 20/20
// window B "团队模式入场信息" 角色周入场次数: 3/3 | 2/2 | 2/2
//
// (Window screenshots supplied by the server owner 2026-09-30; the earlier
// reading -- first operand = account-wide ceiling, second = per-character
// share, from a 2017-11 China-service notice -- is thereby confirmed by the
// client itself rather than by the notice alone.)
//
// So each raid gets BOTH scopes as real Policies. Two properties are
// deliberate:
//
// - No DungeonIDs, exactly like the raid daily policies: these are
// raid-START limits looked up by semantic key, and PoliciesForDungeon must
// never apply them once per internal raid dungeon.
// - ResetWeekday is this repository's established weekly boundary
// (Thursday 06:00, the same node etc/weeklydungeon.etc [reset day index]
// feeds). The raid documents declare no reset weekday of their own, so
// this remains an explicit assumption -- but a CHARGED one now, so every
// raid start logs the window it was charged to and the real node can be
// falsified from production traffic.
for _, raid := range []struct {
path string
accountKey string
characterKey string
}{
{path: AntonRaidPath, accountKey: AntonWeeklyAccountPolicyKey, characterKey: AntonWeeklyCharacterPolicyKey},
{path: LukeRaidPath, accountKey: LukeWeeklyAccountPolicyKey, characterKey: LukeWeeklyCharacterPolicyKey},
{path: FiendWarRaidPath, accountKey: FiendWarWeeklyAccountPolicyKey, characterKey: FiendWarWeeklyCharacterPolicyKey},
} {
accountLimit, characterLimit, values, err := raidWeeklyEnterCounts(documents[raid.path])
if err != nil {
return err
}
for _, policy := range []Policy{
{
Key: raid.accountKey, SourcePath: raid.path, SourceSection: "weekly enter count",
Scope: ScopeAccount, Period: PeriodWeekly, Limit: accountLimit, ResetWeekday: time.Thursday,
},
{
Key: raid.characterKey, SourcePath: raid.path, SourceSection: "weekly enter count",
Scope: ScopeCharacter, Period: PeriodWeekly, Limit: characterLimit, ResetWeekday: time.Thursday,
},
} {
if err := c.addPolicy(policy); err != nil {
return err
}
}
c.addEvidence(Evidence{
Key: "raid_weekly_enter_count." + strings.ReplaceAll(raid.path, "/", "."), SourcePath: raid.path,
SourceSection: "weekly enter count", Period: PeriodWeekly, ResetWeekday: time.Thursday,
Limit: accountLimit, RawValues: values, Enforced: true,
})
}
return nil
}
// raidWeeklyEnterCounts reads one raid document's [WEEKLY ENTER COUNT] row and
// returns (account weekly, character weekly).
//
// The shipped fiendwar.etc declares the section TWICE, so Ints() yields
// `20 2 20 2`. Repeating the same pair is harmless and is tolerated; a repeated
// section whose operands DISAGREE is not a document this server will guess
// about, so it fails loud instead of silently taking one of the two readings.
func raidWeeklyEnterCounts(document *dnfpvf.Document) (uint32, uint32, []int64, error) {
if document == nil {
return 0, 0, nil, fmt.Errorf("%w: raid weekly document is missing", ErrCatalogInvalid)
}
values := document.Ints("weekly enter count")
if len(values) < 2 || len(values)%2 != 0 {
return 0, 0, values, catalogSectionError(document.Path, "weekly enter count", values)
}
accountLimit, characterLimit := values[0], values[1]
for offset := 2; offset < len(values); offset += 2 {
if values[offset] != accountLimit || values[offset+1] != characterLimit {
return 0, 0, values, catalogSectionError(document.Path, "weekly enter count", values)
}
}
if accountLimit <= 0 || accountLimit > int64(^uint32(0)) ||
characterLimit <= 0 || characterLimit > int64(^uint32(0)) {
return 0, 0, values, catalogSectionError(document.Path, "weekly enter count", values)
}
return uint32(accountLimit), uint32(characterLimit), values, nil
}
func validateTowerOfDespairDailyEvidence(document *dnfpvf.Document) error {
if document == nil {
return fmt.Errorf("%w: %s document is missing", ErrCatalogInvalid, TowerOfDespairInvitationPath)
}
stackableType, found := document.Text("stackable type")
if !found || !strings.EqualFold(strings.TrimSpace(stackableType), "[material]") {
return fmt.Errorf("%w: %s [stackable type]=%q want [material]", ErrCatalogInvalid, TowerOfDespairInvitationPath, stackableType)
}
explain := strings.Join(document.Texts("explain"), "\n")
if !strings.Contains(explain, "绝望之塔") || !strings.Contains(explain, "当日") {
return fmt.Errorf("%w: %s does not prove same-day Tower of Despair entry semantics", ErrCatalogInvalid, TowerOfDespairInvitationPath)
}
return nil
}
func (c *Catalog) addSectionPolicy(document *dnfpvf.Document, policy Policy, section string) error {
values := document.Ints(section)
limit, err := onePositive(values)
if err != nil {
return catalogSectionError(policy.SourcePath, section, values)
}
policy.Limit = limit
return c.addPolicy(policy)
}
func (c *Catalog) addPolicy(policy Policy) error {
if err := validatePolicy(policy); err != nil {
return fmt.Errorf("%w: %v", ErrCatalogInvalid, err)
}
if _, duplicate := c.policies[policy.Key]; duplicate {
return fmt.Errorf("%w: duplicate policy=%s", ErrCatalogInvalid, policy.Key)
}
seen := make(map[uint32]struct{}, len(policy.DungeonIDs))
for _, dungeonID := range policy.DungeonIDs {
if dungeonID == 0 {
return fmt.Errorf("%w: policy=%s contains dungeon 0", ErrCatalogInvalid, policy.Key)
}
if _, duplicate := seen[dungeonID]; duplicate {
return fmt.Errorf("%w: policy=%s duplicate dungeon=%d", ErrCatalogInvalid, policy.Key, dungeonID)
}
seen[dungeonID] = struct{}{}
}
policy.DungeonIDs = sortedUint32(policy.DungeonIDs)
c.policies[policy.Key] = policy
for _, dungeonID := range policy.DungeonIDs {
c.byDungeon[dungeonID] = append(c.byDungeon[dungeonID], policy.Key)
}
c.addEvidence(Evidence{
Key: policy.Key, SourcePath: policy.SourcePath, SourceSection: policy.SourceSection, Scope: policy.Scope,
Period: policy.Period, Limit: policy.Limit, ResetWeekday: policy.ResetWeekday, DungeonIDs: policy.DungeonIDs, Enforced: true,
})
return nil
}
func (c *Catalog) addEvidence(evidence Evidence) {
evidence.DungeonIDs = sortedUint32(evidence.DungeonIDs)
evidence.RawValues = append([]int64(nil), evidence.RawValues...)
c.evidence = append(c.evidence, evidence)
}
func (c *Catalog) sort() {
for dungeonID := range c.byDungeon {
sort.Strings(c.byDungeon[dungeonID])
}
sort.Slice(c.evidence, func(left, right int) bool {
if c.evidence[left].SourcePath != c.evidence[right].SourcePath {
return c.evidence[left].SourcePath < c.evidence[right].SourcePath
}
if c.evidence[left].SourceSection != c.evidence[right].SourceSection {
return c.evidence[left].SourceSection < c.evidence[right].SourceSection
}
return c.evidence[left].Key < c.evidence[right].Key
})
}
func tupleDungeonIDs(document *dnfpvf.Document, section string, width, dungeonOffset int) ([]uint32, error) {
values := document.Ints(section)
if width <= 0 || dungeonOffset < 0 || dungeonOffset >= width || len(values) == 0 || len(values)%width != 0 {
return nil, catalogSectionError(document.Path, section, values)
}
ids := make([]uint32, 0, len(values)/width)
seen := make(map[uint32]struct{})
for offset := 0; offset < len(values); offset += width {
value := values[offset+dungeonOffset]
if value <= 0 || value > int64(^uint32(0)) {
return nil, catalogSectionError(document.Path, section, values)
}
id := uint32(value)
if _, duplicate := seen[id]; duplicate {
return nil, fmt.Errorf("%w: %s section=%s duplicate dungeon=%d", ErrCatalogInvalid, document.Path, section, id)
}
seen[id] = struct{}{}
ids = append(ids, id)
}
return sortedUint32(ids), nil
}
func crackOfDimensionDungeonID(document *dnfpvf.Document) (uint32, error) {
var found uint32
for _, entry := range dnfpvf.ParseList(document) {
path := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(entry.Path), "\\", "/"))
if path != "crackofdimension/crackofdimension.dgn" {
continue
}
if found != 0 || entry.ID <= 0 || entry.ID > int64(^uint32(0)) {
return 0, fmt.Errorf("%w: ambiguous crack-of-dimension dungeon list row", ErrCatalogInvalid)
}
found = uint32(entry.ID)
}
if found == 0 {
return 0, fmt.Errorf("%w: crack-of-dimension dungeon list row missing", ErrCatalogInvalid)
}
return found, nil
}
func positiveSectionValue(document *dnfpvf.Document, section string) uint32 {
value, _ := onePositive(document.Ints(section))
return value
}
func onePositive(values []int64) (uint32, error) {
if len(values) != 1 || values[0] <= 0 || values[0] > int64(^uint32(0)) {
return 0, ErrCatalogInvalid
}
return uint32(values[0]), nil
}
func catalogSectionError(path, section string, values []int64) error {
return fmt.Errorf("%w: %s section=%s values=%v", ErrCatalogInvalid, path, section, values)
}
func sortedUint32(values []uint32) []uint32 {
out := append([]uint32(nil), values...)
sort.Slice(out, func(left, right int) bool { return out[left] < out[right] })
return out
}
func clonePolicy(policy Policy) Policy {
policy.DungeonIDs = append([]uint32(nil), policy.DungeonIDs...)
return policy
}