服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
1045 lines
39 KiB
Go
1045 lines
39 KiB
Go
package raid
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
|
|
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
|
)
|
|
|
|
const (
|
|
// AntonPVFPath is the only archive member read by this catalog.
|
|
AntonPVFPath = "etc/raid/anton.etc"
|
|
|
|
antonOrdinaryPhaseCount = 2
|
|
antonMovieEndValueCount = 4
|
|
antonSingleRaidPhaseName = "single raid phase"
|
|
)
|
|
|
|
var (
|
|
ErrAntonPVFSourceRequired = errors.New("anton raid PVF source is required")
|
|
ErrAntonPVFDocumentInvalid = errors.New("anton raid PVF document is invalid")
|
|
ErrAntonPVFSectionMissing = errors.New("anton raid PVF section is missing")
|
|
ErrAntonPVFSectionShape = errors.New("anton raid PVF section shape is invalid")
|
|
ErrAntonPVFValueInvalid = errors.New("anton raid PVF value is invalid")
|
|
ErrAntonPVFDuplicateDungeon = errors.New("anton raid PVF dungeon is duplicated")
|
|
)
|
|
|
|
// AntonDungeonState is one native [INIT STATE] value.
|
|
type AntonDungeonState string
|
|
|
|
const (
|
|
AntonDungeonOpen AntonDungeonState = "open"
|
|
AntonDungeonHide AntonDungeonState = "hide"
|
|
AntonDungeonClosed AntonDungeonState = "close"
|
|
|
|
// AntonDungeonCleared is the runtime-only "this gate has been finished"
|
|
// state. etc/raid/anton.etc never spells it -- the parser below still accepts
|
|
// only the three PVF words -- but the client draws it differently from a
|
|
// merely hidden gate, and the 86JP reference server sets it (its value 3) the
|
|
// moment a gate's clear count reaches its requirement
|
|
// (RaidHandler.Progression.cs:240 for the legs, ClearBlackFogSourceAsync for
|
|
// the regenerated fog).
|
|
AntonDungeonCleared AntonDungeonState = "cleared"
|
|
)
|
|
|
|
// AntonEntryCost is the item/count pair inside the global [ENTRY COST] block.
|
|
type AntonEntryCost struct {
|
|
ItemID uint32
|
|
Count uint32
|
|
}
|
|
|
|
// AntonStateReward preserves one [STATE REWARD] row in source order. ItemID is
|
|
// the reward-definition item, not an instruction to grant that item directly.
|
|
type AntonStateReward struct {
|
|
Kind string
|
|
Rank int
|
|
Weight int
|
|
DefinitionItemID uint32
|
|
Flag byte
|
|
}
|
|
|
|
// AntonRankCondition is one [DEAD COUNT] row inside a phase's
|
|
// [CONDITION FOR RANK]: the inclusive team death-count range that earns Rank.
|
|
// The rows are kept in source order and raw. The current catalogs leave a
|
|
// one-death gap between adjacent rows (0-1, 2-5, 6-9, 10-14), so collapsing
|
|
// them into thresholds would invent a rule for the gap.
|
|
type AntonRankCondition struct {
|
|
MinimumDeaths int
|
|
MaximumDeaths int
|
|
Rank int
|
|
}
|
|
|
|
// AntonDungeonInfo preserves one ordinary-phase [DUNGEON INFO] block.
|
|
type AntonDungeonInfo struct {
|
|
Index uint32
|
|
InitState AntonDungeonState
|
|
HP int
|
|
PartyMax int
|
|
// LockoutType is the native [LOCKOUT TYPE] pair. Only the Luke catalog
|
|
// carries the section (its phase-two gates use 4 on the light route, 5 on
|
|
// the dark one and 0 on the two shared rooms); etc/raid/anton.etc omits it
|
|
// and leaves the pair at zero. What the value gates is not proven by the
|
|
// PVF alone, so both numbers stay raw.
|
|
LockoutType [2]int
|
|
// DestroyObjects lists the object-group markers declared under this gate's
|
|
// [DESTROY OBJECT INFO], in source order and with the native section
|
|
// spelling. The section has no closing tag, so its entries run to the end
|
|
// of the [DUNGEON INFO] block. Only the Luke phase-one sanctums declare it
|
|
// ("birth sanctum object" and its two siblings).
|
|
DestroyObjects []string
|
|
}
|
|
|
|
// AntonPhase preserves one phase block and its ordered rewards and dungeons.
|
|
// The single-mode phase uses the same shape even though its PVF block omits
|
|
// the ordinary minimum-clear-count section.
|
|
type AntonPhase struct {
|
|
Name string
|
|
MinimalDungeonClearCount int
|
|
Rewards []AntonStateReward
|
|
Dungeons []AntonDungeonInfo
|
|
// RankConditions is the phase's [CONDITION FOR RANK] table. Every phase of
|
|
// every current raid catalog declares one, including the single-mode phase.
|
|
RankConditions []AntonRankCondition
|
|
// CoinLimit is the optional [PHASE COIN LIMIT] revive-coin cap. Zero means
|
|
// the phase declares no cap, which is every Anton phase and Luke's first.
|
|
CoinLimit int
|
|
}
|
|
|
|
// AntonRaidModeKind identifies a mode advertised by the native Anton PVF.
|
|
type AntonRaidModeKind string
|
|
|
|
const (
|
|
AntonRaidModeTeam AntonRaidModeKind = "team"
|
|
AntonRaidModeSingle AntonRaidModeKind = "single"
|
|
)
|
|
|
|
// AntonRaidModeChoice is the small, read-only projection needed by a mode
|
|
// selection UI. The complete phase data remains available on AntonConfig.
|
|
type AntonRaidModeChoice struct {
|
|
Kind AntonRaidModeKind
|
|
PhaseNames []string
|
|
StartMinimumMembers int
|
|
TimeLimitSeconds []int
|
|
}
|
|
|
|
// AntonSingleModeConfig is the complete phase data declared by the native
|
|
// [SINGLE RAID PHASE]. Trigger expressions which are client/server script
|
|
// details remain opaque; the dungeon graph is advanced by the runtime owner.
|
|
type AntonSingleModeConfig struct {
|
|
Name string
|
|
TimeLimitSeconds int
|
|
Phase AntonPhase
|
|
}
|
|
|
|
// AntonConfig is the strictly validated, read-only subset of the current
|
|
// etc/raid/anton.etc needed by both ordinary and single-mode raid runtimes.
|
|
type AntonConfig struct {
|
|
PhaseMax int
|
|
MemberMax int
|
|
StartMinimumMembers int
|
|
SingleStartMinimumMembers int
|
|
StartDelaySeconds int
|
|
// DisconnMemberWaitTime is the global [DISCONN MEMBER WAIT TIME]: how long a
|
|
// dropped member's attack-party seat is held before the member is removed.
|
|
// Both shipped raids declare 300 (etc/raid/anton.etc and
|
|
// etc/raid/luke/luke.etc). Zero means the document does not declare it, and
|
|
// the runtime then keeps its own 300-second default.
|
|
DisconnMemberWaitTime int
|
|
PhaseTimeoutSeconds []int
|
|
PhaseBreakSeconds int
|
|
MovieEndSeconds []int
|
|
EntryCost AntonEntryCost
|
|
// WeeklyEnterCount preserves the two global [WEEKLY ENTER COUNT] values in
|
|
// source order (Luke 20 2, Anton 30 3). The client draws a weekly counter of
|
|
// its own (dstr 75388 "周卢克 %d/%d"), but which of the two values is the
|
|
// roster limit and which the per-character share is not proven here, so both
|
|
// stay raw and no admission policy is derived from them yet.
|
|
WeeklyEnterCount [2]int
|
|
Phases []AntonPhase
|
|
SingleMode *AntonSingleModeConfig
|
|
}
|
|
|
|
// ModeChoices returns the native team/single choices in PVF order. It returns
|
|
// fresh slices so callers can use the projection for UI rendering without
|
|
// mutating the cached configuration. No lifecycle state is created or changed.
|
|
func (config *AntonConfig) ModeChoices() []AntonRaidModeChoice {
|
|
if config == nil {
|
|
return nil
|
|
}
|
|
choices := make([]AntonRaidModeChoice, 0, 2)
|
|
if len(config.Phases) > 0 {
|
|
choices = append(choices, AntonRaidModeChoice{
|
|
Kind: AntonRaidModeTeam,
|
|
PhaseNames: antonPhaseNames(config.Phases),
|
|
StartMinimumMembers: config.StartMinimumMembers,
|
|
TimeLimitSeconds: append([]int(nil), config.PhaseTimeoutSeconds...),
|
|
})
|
|
}
|
|
if config.SingleMode != nil {
|
|
choices = append(choices, AntonRaidModeChoice{
|
|
Kind: AntonRaidModeSingle,
|
|
PhaseNames: []string{config.SingleMode.Name},
|
|
StartMinimumMembers: config.SingleStartMinimumMembers,
|
|
TimeLimitSeconds: []int{config.SingleMode.TimeLimitSeconds},
|
|
})
|
|
}
|
|
return choices
|
|
}
|
|
|
|
func antonPhaseNames(phases []AntonPhase) []string {
|
|
names := make([]string, 0, len(phases))
|
|
for _, phase := range phases {
|
|
names = append(names, phase.Name)
|
|
}
|
|
return names
|
|
}
|
|
|
|
// LoadAntonConfig reads exactly etc/raid/anton.etc and parses no other archive
|
|
// member. This makes the source boundary usable by both the runtime archive and
|
|
// small in-memory unit-test sources.
|
|
func LoadAntonConfig(source dnfpvf.Source) (*AntonConfig, error) {
|
|
if source == nil {
|
|
return nil, ErrAntonPVFSourceRequired
|
|
}
|
|
text, err := source.ReadText(AntonPVFPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read anton raid PVF %q: %w", AntonPVFPath, err)
|
|
}
|
|
document, err := dnfpvf.Parse(AntonPVFPath, text)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse anton raid PVF %q: %w", AntonPVFPath, err)
|
|
}
|
|
return ParseAntonConfig(document)
|
|
}
|
|
|
|
// ParseAntonConfig converts one already-tokenized target document. It rejects
|
|
// a document for any other path so callers cannot accidentally feed a nearby
|
|
// raid file into the Anton catalog.
|
|
func ParseAntonConfig(document *dnfpvf.Document) (*AntonConfig, error) {
|
|
if document == nil || antonPathKey(document.Path) != antonPathKey(AntonPVFPath) {
|
|
path := ""
|
|
if document != nil {
|
|
path = document.Path
|
|
}
|
|
return nil, fmt.Errorf("%w: path=%q want=%q", ErrAntonPVFDocumentInvalid, path, AntonPVFPath)
|
|
}
|
|
|
|
phaseBlocks, err := antonFindBlocks(document, "phase", 0, len(document.Sections))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(phaseBlocks) == 0 {
|
|
return nil, antonMissing("phase", "ordinary phases")
|
|
}
|
|
globalEnd := phaseBlocks[0].open
|
|
singleBlocks, err := antonFindBlocks(document, antonSingleRaidPhaseName, 0, len(document.Sections))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(singleBlocks) > 1 {
|
|
return nil, antonShape(antonSingleRaidPhaseName, fmt.Sprintf("blocks=%d want<=1", len(singleBlocks)))
|
|
}
|
|
|
|
config := &AntonConfig{}
|
|
if config.PhaseMax, err = antonReadPositiveIntSection(document, "phase max", 0, globalEnd); err != nil {
|
|
return nil, err
|
|
}
|
|
if config.PhaseMax != antonOrdinaryPhaseCount {
|
|
return nil, antonInvalid("phase max", config.PhaseMax, fmt.Sprintf("want=%d", antonOrdinaryPhaseCount))
|
|
}
|
|
if len(phaseBlocks) != config.PhaseMax {
|
|
return nil, fmt.Errorf("%w: path=%s field=phase blocks got=%d want=%d", ErrAntonPVFSectionShape, AntonPVFPath, len(phaseBlocks), config.PhaseMax)
|
|
}
|
|
if config.MemberMax, err = antonReadPositiveIntSection(document, "raid member max", 0, globalEnd); err != nil {
|
|
return nil, err
|
|
}
|
|
if config.MemberMax > MaxAttackPartyMembers {
|
|
return nil, antonInvalid("raid member max", config.MemberMax, fmt.Sprintf("want<=%d", MaxAttackPartyMembers))
|
|
}
|
|
if config.StartMinimumMembers, err = antonReadPositiveIntSection(document, "raid start minimum member", 0, globalEnd); err != nil {
|
|
return nil, err
|
|
}
|
|
// MINIUM is the spelling in the current native PVF and is deliberately
|
|
// matched exactly instead of accepting an invented alias. Team-only PVFs
|
|
// may omit both this field and [SINGLE RAID PHASE]. Once a single block is
|
|
// declared, however, its minimum-member field is required.
|
|
singleMinimumIndex, hasSingleMinimum, findErr := antonOptionalUniqueSection(document, "single raid start minium member", 0, globalEnd)
|
|
if findErr != nil {
|
|
return nil, findErr
|
|
}
|
|
if hasSingleMinimum {
|
|
if config.SingleStartMinimumMembers, err = antonPositiveIntAt(document, singleMinimumIndex, "single raid start minium member"); err != nil {
|
|
return nil, err
|
|
}
|
|
} else if len(singleBlocks) == 1 {
|
|
return nil, antonMissing("single raid start minium member", "single raid phase")
|
|
}
|
|
if config.StartMinimumMembers > config.MemberMax {
|
|
return nil, antonInvalid("raid start minimum member", config.StartMinimumMembers, "want<=raid member max")
|
|
}
|
|
if config.SingleStartMinimumMembers > config.MemberMax {
|
|
return nil, antonInvalid("single raid start minium member", config.SingleStartMinimumMembers, "want<=raid member max")
|
|
}
|
|
if config.StartDelaySeconds, err = antonReadPositiveIntSection(document, "start delay time", 0, globalEnd); err != nil {
|
|
return nil, err
|
|
}
|
|
// [DISCONN MEMBER WAIT TIME] is read optionally: both shipped raids declare
|
|
// 300, but a document that omits the section must still parse, and the
|
|
// runtime keeps its own default for the zero.
|
|
disconnWaitIndex, hasDisconnWait, err := antonOptionalUniqueSection(document, "disconn member wait time", 0, globalEnd)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if hasDisconnWait {
|
|
if config.DisconnMemberWaitTime, err = antonPositiveIntAt(document, disconnWaitIndex, "disconn member wait time"); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if config.PhaseTimeoutSeconds, err = antonReadPositiveIntListSection(document, "phase time over", 0, globalEnd, config.PhaseMax); err != nil {
|
|
return nil, err
|
|
}
|
|
if config.PhaseBreakSeconds, err = antonReadPositiveIntSection(document, "phase break time", 0, globalEnd); err != nil {
|
|
return nil, err
|
|
}
|
|
if config.MovieEndSeconds, err = antonReadPositiveIntListSection(document, "movie end time", 0, globalEnd, antonMovieEndValueCount); err != nil {
|
|
return nil, err
|
|
}
|
|
if config.EntryCost, err = antonParseEntryCost(document, 0, globalEnd); err != nil {
|
|
return nil, err
|
|
}
|
|
if config.WeeklyEnterCount, err = antonParseWeeklyEnterCount(document, 0, globalEnd); err != nil {
|
|
return nil, err
|
|
}
|
|
seenDungeons := make(map[uint32]int)
|
|
config.Phases = make([]AntonPhase, 0, len(phaseBlocks))
|
|
for index, block := range phaseBlocks {
|
|
phase, parseErr := antonParsePhase(document, block, index, config.MemberMax, seenDungeons)
|
|
if parseErr != nil {
|
|
return nil, parseErr
|
|
}
|
|
config.Phases = append(config.Phases, phase)
|
|
}
|
|
|
|
// The native single-mode declaration follows the ordinary phases. Its
|
|
// trigger expressions remain opaque, while the phase name, rewards and
|
|
// dungeon definitions are consumed by the single-mode runtime owner.
|
|
if len(singleBlocks) == 1 {
|
|
singleBlock := singleBlocks[0]
|
|
timeLimitSeconds, readErr := antonReadPositiveIntSection(document, "single raid time over", 0, singleBlock.open)
|
|
if readErr != nil {
|
|
return nil, readErr
|
|
}
|
|
phaseName, parseErr := antonParseSingleModeName(document, singleBlock)
|
|
if parseErr != nil {
|
|
return nil, parseErr
|
|
}
|
|
phase, phaseErr := antonParseSinglePhase(document, singleBlock, config.MemberMax, seenDungeons)
|
|
if phaseErr != nil {
|
|
return nil, phaseErr
|
|
}
|
|
config.SingleMode = &AntonSingleModeConfig{Name: phaseName, TimeLimitSeconds: timeLimitSeconds, Phase: phase}
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
type antonSectionBlock struct {
|
|
open int
|
|
close int
|
|
}
|
|
|
|
func antonParseEntryCost(document *dnfpvf.Document, start, end int) (AntonEntryCost, error) {
|
|
blocks, err := antonFindBlocks(document, "entry cost", start, end)
|
|
if err != nil {
|
|
return AntonEntryCost{}, err
|
|
}
|
|
if len(blocks) == 0 {
|
|
return AntonEntryCost{}, antonMissing("entry cost", "global")
|
|
}
|
|
if len(blocks) != 1 {
|
|
return AntonEntryCost{}, antonShape("entry cost", fmt.Sprintf("blocks=%d want=1", len(blocks)))
|
|
}
|
|
block := blocks[0]
|
|
if block.close != block.open+1 {
|
|
return AntonEntryCost{}, antonShape("entry cost", "unexpected nested sections")
|
|
}
|
|
if err := antonRequireEmptySection(document, block.close); err != nil {
|
|
return AntonEntryCost{}, err
|
|
}
|
|
tokens, err := antonSectionTokens(document, block.open)
|
|
if err != nil {
|
|
return AntonEntryCost{}, err
|
|
}
|
|
if len(tokens) != 2 {
|
|
return AntonEntryCost{}, antonShape("entry cost", fmt.Sprintf("values=%d want=2", len(tokens)))
|
|
}
|
|
itemID, err := antonPositiveUint32Token(tokens[0], "entry cost item id")
|
|
if err != nil {
|
|
return AntonEntryCost{}, err
|
|
}
|
|
count, err := antonPositiveUint32Token(tokens[1], "entry cost count")
|
|
if err != nil {
|
|
return AntonEntryCost{}, err
|
|
}
|
|
return AntonEntryCost{ItemID: itemID, Count: count}, nil
|
|
}
|
|
|
|
// antonParseWeeklyEnterCount reads the global [WEEKLY ENTER COUNT] pair.
|
|
// etc/raid/fiendwar/fiendwar.etc declares the same section twice at global
|
|
// scope; that catalog is not parsed by this grammar, and a duplicate stays a
|
|
// shape error here rather than a silently preferred first row.
|
|
func antonParseWeeklyEnterCount(document *dnfpvf.Document, start, end int) ([2]int, error) {
|
|
var values [2]int
|
|
sectionIndex, err := antonUniqueSection(document, "weekly enter count", start, end)
|
|
if err != nil {
|
|
return values, err
|
|
}
|
|
tokens, err := antonSectionTokens(document, sectionIndex)
|
|
if err != nil {
|
|
return values, err
|
|
}
|
|
if len(tokens) != len(values) {
|
|
return values, antonShape("weekly enter count", fmt.Sprintf("values=%d want=%d", len(tokens), len(values)))
|
|
}
|
|
for index, token := range tokens {
|
|
value, valueErr := antonPositiveIntToken(token, "weekly enter count")
|
|
if valueErr != nil {
|
|
return [2]int{}, valueErr
|
|
}
|
|
values[index] = value
|
|
}
|
|
return values, nil
|
|
}
|
|
|
|
// antonParseRankConditions reads one phase's [CONDITION FOR RANK] block into
|
|
// its ordered [DEAD COUNT] rows. The block admits no other section.
|
|
func antonParseRankConditions(document *dnfpvf.Document, start, end int, context string) ([]AntonRankCondition, error) {
|
|
blocks, err := antonFindBlocks(document, "condition for rank", start, end)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(blocks) == 0 {
|
|
return nil, antonMissing("condition for rank", context)
|
|
}
|
|
if len(blocks) != 1 {
|
|
return nil, antonShape("condition for rank", fmt.Sprintf("%s blocks=%d want=1", context, len(blocks)))
|
|
}
|
|
block := blocks[0]
|
|
if err := antonRequireEmptySection(document, block.open); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := antonRequireEmptySection(document, block.close); err != nil {
|
|
return nil, err
|
|
}
|
|
rows := make([]AntonRankCondition, 0, block.close-block.open-1)
|
|
for index := block.open + 1; index < block.close; index++ {
|
|
if antonSectionName(document.Sections[index].Name) != "dead count" {
|
|
return nil, antonShape("condition for rank",
|
|
fmt.Sprintf("%s unexpected_section=%q", context, document.Sections[index].Name))
|
|
}
|
|
tokens, tokenErr := antonSectionTokens(document, index)
|
|
if tokenErr != nil {
|
|
return nil, tokenErr
|
|
}
|
|
if len(tokens) != 3 {
|
|
return nil, antonShape("dead count", fmt.Sprintf("%s values=%d want=3", context, len(tokens)))
|
|
}
|
|
var values [3]int
|
|
for position, token := range tokens {
|
|
value, valueErr := antonIntToken(token, "dead count")
|
|
if valueErr != nil {
|
|
return nil, valueErr
|
|
}
|
|
if value < 0 {
|
|
return nil, antonInvalid("dead count", value, fmt.Sprintf("%s want_nonnegative", context))
|
|
}
|
|
values[position] = value
|
|
}
|
|
if values[1] < values[0] {
|
|
return nil, antonInvalid("dead count", values[1], fmt.Sprintf("%s want>=minimum(%d)", context, values[0]))
|
|
}
|
|
rows = append(rows, AntonRankCondition{MinimumDeaths: values[0], MaximumDeaths: values[1], Rank: values[2]})
|
|
}
|
|
if len(rows) == 0 {
|
|
return nil, antonMissing("dead count", context)
|
|
}
|
|
return rows, nil
|
|
}
|
|
|
|
func antonParsePhase(document *dnfpvf.Document, block antonSectionBlock, phaseIndex, memberMax int, seenDungeons map[uint32]int) (AntonPhase, error) {
|
|
// The native Anton PVF uses PARTY MAX=0 for maps whose party size is
|
|
// governed by the attack-party rules rather than a dungeon-local cap.
|
|
// Preserve that value for ordinary and single maps alike.
|
|
return antonParsePhaseWithPartyMax(document, block, phaseIndex, memberMax, true, seenDungeons)
|
|
}
|
|
|
|
func antonParseSinglePhase(document *dnfpvf.Document, block antonSectionBlock, memberMax int, seenDungeons map[uint32]int) (AntonPhase, error) {
|
|
if err := antonRequireEmptySection(document, block.open); err != nil {
|
|
return AntonPhase{}, err
|
|
}
|
|
if err := antonRequireEmptySection(document, block.close); err != nil {
|
|
return AntonPhase{}, err
|
|
}
|
|
start, end := block.open+1, block.close
|
|
nameIndex, err := antonUniqueSection(document, "phase name", start, end)
|
|
if err != nil {
|
|
return AntonPhase{}, err
|
|
}
|
|
nameTokens, err := antonSectionTokens(document, nameIndex)
|
|
if err != nil {
|
|
return AntonPhase{}, err
|
|
}
|
|
if len(nameTokens) != 1 || nameTokens[0].Kind != dnfpvf.TokenString || strings.TrimSpace(nameTokens[0].Value) == "" {
|
|
return AntonPhase{}, antonShape("phase name", "single phase want=one nonempty string")
|
|
}
|
|
rewardBlocks, err := antonFindBlocks(document, "clear reward item", start, end)
|
|
if err != nil {
|
|
return AntonPhase{}, err
|
|
}
|
|
if len(rewardBlocks) != 1 {
|
|
return AntonPhase{}, antonShape("clear reward item", fmt.Sprintf("single phase blocks=%d want=1", len(rewardBlocks)))
|
|
}
|
|
rewards, err := antonParseRewards(document, rewardBlocks[0], 2)
|
|
if err != nil {
|
|
return AntonPhase{}, err
|
|
}
|
|
rankConditions, err := antonParseRankConditions(document, start, end, "single phase")
|
|
if err != nil {
|
|
return AntonPhase{}, err
|
|
}
|
|
dungeonBlocks, err := antonFindBlocks(document, "dungeon info", start, end)
|
|
if err != nil {
|
|
return AntonPhase{}, err
|
|
}
|
|
if len(dungeonBlocks) == 0 {
|
|
return AntonPhase{}, antonMissing("dungeon info", "single phase")
|
|
}
|
|
phase := AntonPhase{
|
|
Name: nameTokens[0].Value,
|
|
MinimalDungeonClearCount: 1,
|
|
Rewards: rewards,
|
|
Dungeons: make([]AntonDungeonInfo, 0, len(dungeonBlocks)),
|
|
RankConditions: rankConditions,
|
|
}
|
|
for _, dungeonBlock := range dungeonBlocks {
|
|
dungeon, parseErr := antonParseDungeonWithPartyMax(document, dungeonBlock, 2, memberMax, true)
|
|
if parseErr != nil {
|
|
return AntonPhase{}, parseErr
|
|
}
|
|
if firstPhase, exists := seenDungeons[dungeon.Index]; exists {
|
|
return AntonPhase{}, fmt.Errorf("%w: path=%s dungeon=%d first_phase=%d duplicate_phase=single", ErrAntonPVFDuplicateDungeon, AntonPVFPath, dungeon.Index, firstPhase+1)
|
|
}
|
|
seenDungeons[dungeon.Index] = 2
|
|
phase.Dungeons = append(phase.Dungeons, dungeon)
|
|
}
|
|
return phase, nil
|
|
}
|
|
|
|
func antonParsePhaseWithPartyMax(document *dnfpvf.Document, block antonSectionBlock, phaseIndex, memberMax int, allowZeroPartyMax bool, seenDungeons map[uint32]int) (AntonPhase, error) {
|
|
if err := antonRequireEmptySection(document, block.open); err != nil {
|
|
return AntonPhase{}, err
|
|
}
|
|
if err := antonRequireEmptySection(document, block.close); err != nil {
|
|
return AntonPhase{}, err
|
|
}
|
|
start, end := block.open+1, block.close
|
|
|
|
nameIndex, err := antonUniqueSection(document, "phase name", start, end)
|
|
if err != nil {
|
|
return AntonPhase{}, err
|
|
}
|
|
nameTokens, err := antonSectionTokens(document, nameIndex)
|
|
if err != nil {
|
|
return AntonPhase{}, err
|
|
}
|
|
if len(nameTokens) != 1 || nameTokens[0].Kind != dnfpvf.TokenString || strings.TrimSpace(nameTokens[0].Value) == "" {
|
|
return AntonPhase{}, antonShape("phase name", fmt.Sprintf("phase=%d want=one nonempty string", phaseIndex+1))
|
|
}
|
|
phase := AntonPhase{Name: nameTokens[0].Value}
|
|
|
|
rewardBlocks, err := antonFindBlocks(document, "clear reward item", start, end)
|
|
if err != nil {
|
|
return AntonPhase{}, err
|
|
}
|
|
if len(rewardBlocks) == 0 {
|
|
return AntonPhase{}, antonMissing("clear reward item", fmt.Sprintf("phase=%d", phaseIndex+1))
|
|
}
|
|
if len(rewardBlocks) != 1 {
|
|
return AntonPhase{}, antonShape("clear reward item", fmt.Sprintf("phase=%d blocks=%d want=1", phaseIndex+1, len(rewardBlocks)))
|
|
}
|
|
if nameIndex >= rewardBlocks[0].open {
|
|
return AntonPhase{}, antonShape("phase", fmt.Sprintf("phase=%d phase name must precede rewards", phaseIndex+1))
|
|
}
|
|
phase.Rewards, err = antonParseRewards(document, rewardBlocks[0], phaseIndex)
|
|
if err != nil {
|
|
return AntonPhase{}, err
|
|
}
|
|
|
|
phase.RankConditions, err = antonParseRankConditions(document, start, end, fmt.Sprintf("phase=%d", phaseIndex+1))
|
|
if err != nil {
|
|
return AntonPhase{}, err
|
|
}
|
|
|
|
// [PHASE COIN LIMIT] is declared only by Luke's second phase; every Anton
|
|
// phase omits it and keeps the zero.
|
|
coinIndex, hasCoinLimit, err := antonOptionalUniqueSection(document, "phase coin limit", start, end)
|
|
if err != nil {
|
|
return AntonPhase{}, err
|
|
}
|
|
if hasCoinLimit {
|
|
if phase.CoinLimit, err = antonPositiveIntAt(document, coinIndex, "phase coin limit"); err != nil {
|
|
return AntonPhase{}, err
|
|
}
|
|
}
|
|
|
|
minimalIndex, err := antonUniqueSection(document, "minimal dungeon clear count", start, end)
|
|
if err != nil {
|
|
return AntonPhase{}, err
|
|
}
|
|
if minimalIndex <= rewardBlocks[0].close {
|
|
return AntonPhase{}, antonShape("phase", fmt.Sprintf("phase=%d minimum clear count must follow rewards", phaseIndex+1))
|
|
}
|
|
phase.MinimalDungeonClearCount, err = antonPositiveIntAt(document, minimalIndex, "minimal dungeon clear count")
|
|
if err != nil {
|
|
return AntonPhase{}, err
|
|
}
|
|
|
|
dungeonBlocks, err := antonFindBlocks(document, "dungeon info", start, end)
|
|
if err != nil {
|
|
return AntonPhase{}, err
|
|
}
|
|
if len(dungeonBlocks) == 0 {
|
|
return AntonPhase{}, antonMissing("dungeon info", fmt.Sprintf("phase=%d", phaseIndex+1))
|
|
}
|
|
if dungeonBlocks[0].open <= minimalIndex {
|
|
return AntonPhase{}, antonShape("phase", fmt.Sprintf("phase=%d dungeons must follow minimum clear count", phaseIndex+1))
|
|
}
|
|
phase.Dungeons = make([]AntonDungeonInfo, 0, len(dungeonBlocks))
|
|
for _, dungeonBlock := range dungeonBlocks {
|
|
dungeon, parseErr := antonParseDungeonWithPartyMax(document, dungeonBlock, phaseIndex, memberMax, allowZeroPartyMax)
|
|
if parseErr != nil {
|
|
return AntonPhase{}, parseErr
|
|
}
|
|
if firstPhase, exists := seenDungeons[dungeon.Index]; exists {
|
|
return AntonPhase{}, fmt.Errorf("%w: path=%s dungeon=%d first_phase=%d duplicate_phase=%d", ErrAntonPVFDuplicateDungeon, AntonPVFPath, dungeon.Index, firstPhase+1, phaseIndex+1)
|
|
}
|
|
seenDungeons[dungeon.Index] = phaseIndex
|
|
phase.Dungeons = append(phase.Dungeons, dungeon)
|
|
}
|
|
if phase.MinimalDungeonClearCount > len(phase.Dungeons) {
|
|
return AntonPhase{}, antonInvalid("minimal dungeon clear count", phase.MinimalDungeonClearCount, fmt.Sprintf("phase=%d want<=dungeons(%d)", phaseIndex+1, len(phase.Dungeons)))
|
|
}
|
|
return phase, nil
|
|
}
|
|
|
|
func antonParseSingleModeName(document *dnfpvf.Document, block antonSectionBlock) (string, error) {
|
|
if err := antonRequireEmptySection(document, block.open); err != nil {
|
|
return "", err
|
|
}
|
|
if err := antonRequireEmptySection(document, block.close); err != nil {
|
|
return "", err
|
|
}
|
|
start, end := block.open+1, block.close
|
|
nameIndex, err := antonUniqueSection(document, "phase name", start, end)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
nameTokens, err := antonSectionTokens(document, nameIndex)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(nameTokens) != 1 || nameTokens[0].Kind != dnfpvf.TokenString || strings.TrimSpace(nameTokens[0].Value) == "" {
|
|
return "", antonShape("phase name", "single phase want=one nonempty string")
|
|
}
|
|
return nameTokens[0].Value, nil
|
|
}
|
|
|
|
func antonParseRewards(document *dnfpvf.Document, block antonSectionBlock, phaseIndex int) ([]AntonStateReward, error) {
|
|
if err := antonRequireEmptySection(document, block.open); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := antonRequireEmptySection(document, block.close); err != nil {
|
|
return nil, err
|
|
}
|
|
rewards := make([]AntonStateReward, 0, block.close-block.open-1)
|
|
for sectionIndex := block.open + 1; sectionIndex < block.close; sectionIndex++ {
|
|
if antonSectionName(document.Sections[sectionIndex].Name) != "state reward" {
|
|
return nil, antonShape("clear reward item", fmt.Sprintf("phase=%d unexpected_section=%q", phaseIndex+1, document.Sections[sectionIndex].Name))
|
|
}
|
|
tokens, err := antonSectionTokens(document, sectionIndex)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(tokens) != 5 || tokens[0].Kind != dnfpvf.TokenString || strings.TrimSpace(tokens[0].Value) == "" {
|
|
return nil, antonShape("state reward", fmt.Sprintf("phase=%d values=%d want=kind+4_ints", phaseIndex+1, len(tokens)))
|
|
}
|
|
rank, err := antonIntToken(tokens[1], "state reward rank")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if rank < -1 {
|
|
return nil, antonInvalid("state reward rank", rank, "want>=-1")
|
|
}
|
|
weight, err := antonPositiveIntToken(tokens[2], "state reward weight")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
itemID, err := antonPositiveUint32Token(tokens[3], "state reward definition item id")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
flag, err := antonIntToken(tokens[4], "state reward flag")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if flag != 0 && flag != 1 {
|
|
return nil, antonInvalid("state reward flag", flag, "want=0_or_1")
|
|
}
|
|
rewards = append(rewards, AntonStateReward{
|
|
Kind: tokens[0].Value, Rank: rank, Weight: weight, DefinitionItemID: itemID, Flag: byte(flag),
|
|
})
|
|
}
|
|
if len(rewards) == 0 {
|
|
return nil, antonMissing("state reward", fmt.Sprintf("phase=%d", phaseIndex+1))
|
|
}
|
|
return rewards, nil
|
|
}
|
|
|
|
func antonParseDungeon(document *dnfpvf.Document, block antonSectionBlock, phaseIndex, memberMax int) (AntonDungeonInfo, error) {
|
|
return antonParseDungeonWithPartyMax(document, block, phaseIndex, memberMax, false)
|
|
}
|
|
|
|
func antonParseDungeonWithPartyMax(document *dnfpvf.Document, block antonSectionBlock, phaseIndex, memberMax int, allowZeroPartyMax bool) (AntonDungeonInfo, error) {
|
|
if err := antonRequireEmptySection(document, block.open); err != nil {
|
|
return AntonDungeonInfo{}, err
|
|
}
|
|
if err := antonRequireEmptySection(document, block.close); err != nil {
|
|
return AntonDungeonInfo{}, err
|
|
}
|
|
start, end := block.open+1, block.close
|
|
indexSection, err := antonUniqueSection(document, "index", start, end)
|
|
if err != nil {
|
|
return AntonDungeonInfo{}, err
|
|
}
|
|
indexTokens, err := antonSectionTokens(document, indexSection)
|
|
if err != nil {
|
|
return AntonDungeonInfo{}, err
|
|
}
|
|
if len(indexTokens) != 1 {
|
|
return AntonDungeonInfo{}, antonShape("index", fmt.Sprintf("phase=%d values=%d want=1", phaseIndex+1, len(indexTokens)))
|
|
}
|
|
dungeonID, err := antonPositiveUint32Token(indexTokens[0], "dungeon index")
|
|
if err != nil {
|
|
return AntonDungeonInfo{}, err
|
|
}
|
|
|
|
initSection, err := antonUniqueSection(document, "init state", start, end)
|
|
if err != nil {
|
|
return AntonDungeonInfo{}, err
|
|
}
|
|
initTokens, err := antonSectionTokens(document, initSection)
|
|
if err != nil {
|
|
return AntonDungeonInfo{}, err
|
|
}
|
|
if len(initTokens) != 1 || initTokens[0].Kind != dnfpvf.TokenString {
|
|
return AntonDungeonInfo{}, antonShape("init state", fmt.Sprintf("dungeon=%d want=one string", dungeonID))
|
|
}
|
|
state := AntonDungeonState(strings.ToLower(strings.TrimSpace(initTokens[0].Value)))
|
|
if state != AntonDungeonOpen && state != AntonDungeonHide && state != AntonDungeonClosed {
|
|
return AntonDungeonInfo{}, antonInvalid("init state", initTokens[0].Value, fmt.Sprintf("dungeon=%d", dungeonID))
|
|
}
|
|
|
|
hpInfoSection, err := antonUniqueSection(document, "hp info", start, end)
|
|
if err != nil {
|
|
return AntonDungeonInfo{}, err
|
|
}
|
|
if err := antonRequireEmptySection(document, hpInfoSection); err != nil {
|
|
return AntonDungeonInfo{}, err
|
|
}
|
|
hpSectionName := fmt.Sprintf("%d dgn hp", dungeonID)
|
|
hpSection, err := antonUniqueSection(document, hpSectionName, start, end)
|
|
if err != nil {
|
|
return AntonDungeonInfo{}, err
|
|
}
|
|
hp, err := antonPositiveIntAt(document, hpSection, hpSectionName)
|
|
if err != nil {
|
|
return AntonDungeonInfo{}, err
|
|
}
|
|
|
|
partyMaxSection, err := antonUniqueSection(document, "party max", start, end)
|
|
if err != nil {
|
|
return AntonDungeonInfo{}, err
|
|
}
|
|
partyMax, err := antonNonNegativeIntAt(document, partyMaxSection, "party max")
|
|
if err != nil {
|
|
return AntonDungeonInfo{}, err
|
|
}
|
|
if partyMax < 0 || (!allowZeroPartyMax && partyMax == 0) || partyMax > memberMax {
|
|
return AntonDungeonInfo{}, antonInvalid("party max", partyMax, fmt.Sprintf("dungeon=%d want<=member_max(%d)", dungeonID, memberMax))
|
|
}
|
|
|
|
if !(indexSection < initSection && initSection < hpInfoSection && hpInfoSection < hpSection && hpSection < partyMaxSection) {
|
|
return AntonDungeonInfo{}, antonShape("dungeon info", fmt.Sprintf("dungeon=%d fields_out_of_order", dungeonID))
|
|
}
|
|
|
|
var lockoutType [2]int
|
|
lockoutSection, hasLockout, err := antonOptionalUniqueSection(document, "lockout type", start, end)
|
|
if err != nil {
|
|
return AntonDungeonInfo{}, err
|
|
}
|
|
if hasLockout {
|
|
lockoutTokens, tokenErr := antonSectionTokens(document, lockoutSection)
|
|
if tokenErr != nil {
|
|
return AntonDungeonInfo{}, tokenErr
|
|
}
|
|
if len(lockoutTokens) != len(lockoutType) {
|
|
return AntonDungeonInfo{}, antonShape("lockout type",
|
|
fmt.Sprintf("dungeon=%d values=%d want=%d", dungeonID, len(lockoutTokens), len(lockoutType)))
|
|
}
|
|
for position, token := range lockoutTokens {
|
|
value, valueErr := antonIntToken(token, "lockout type")
|
|
if valueErr != nil {
|
|
return AntonDungeonInfo{}, valueErr
|
|
}
|
|
if value < 0 {
|
|
return AntonDungeonInfo{}, antonInvalid("lockout type", value, fmt.Sprintf("dungeon=%d want_nonnegative", dungeonID))
|
|
}
|
|
lockoutType[position] = value
|
|
}
|
|
}
|
|
|
|
// [DESTROY OBJECT INFO] has no closing tag, so its object-group markers are
|
|
// every remaining section of this [DUNGEON INFO] block. They carry no
|
|
// values; the name is the payload.
|
|
var destroyObjects []string
|
|
destroySection, hasDestroyObjects, err := antonOptionalUniqueSection(document, "destroy object info", start, end)
|
|
if err != nil {
|
|
return AntonDungeonInfo{}, err
|
|
}
|
|
strictEnd := end
|
|
if hasDestroyObjects {
|
|
if err := antonRequireEmptySection(document, destroySection); err != nil {
|
|
return AntonDungeonInfo{}, err
|
|
}
|
|
if destroySection < partyMaxSection {
|
|
return AntonDungeonInfo{}, antonShape("destroy object info",
|
|
fmt.Sprintf("dungeon=%d must follow party max", dungeonID))
|
|
}
|
|
strictEnd = destroySection
|
|
for sectionIndex := destroySection + 1; sectionIndex < end; sectionIndex++ {
|
|
if err := antonRequireEmptySection(document, sectionIndex); err != nil {
|
|
return AntonDungeonInfo{}, err
|
|
}
|
|
destroyObjects = append(destroyObjects, antonSectionName(document.Sections[sectionIndex].Name))
|
|
}
|
|
if len(destroyObjects) == 0 {
|
|
return AntonDungeonInfo{}, antonMissing("destroy object info", fmt.Sprintf("dungeon=%d object groups", dungeonID))
|
|
}
|
|
}
|
|
|
|
allowed := map[string]struct{}{
|
|
"index": {}, "init state": {}, "hp info": {}, hpSectionName: {}, "party max": {},
|
|
"lockout type": {},
|
|
}
|
|
for sectionIndex := start; sectionIndex < strictEnd; sectionIndex++ {
|
|
if _, ok := allowed[antonSectionName(document.Sections[sectionIndex].Name)]; !ok {
|
|
return AntonDungeonInfo{}, antonShape("dungeon info", fmt.Sprintf("dungeon=%d unexpected_section=%q", dungeonID, document.Sections[sectionIndex].Name))
|
|
}
|
|
}
|
|
return AntonDungeonInfo{
|
|
Index: dungeonID,
|
|
InitState: state,
|
|
HP: hp,
|
|
PartyMax: partyMax,
|
|
LockoutType: lockoutType,
|
|
DestroyObjects: destroyObjects,
|
|
}, nil
|
|
}
|
|
|
|
func antonFindBlocks(document *dnfpvf.Document, name string, start, end int) ([]antonSectionBlock, error) {
|
|
want := antonSectionName(name)
|
|
closeName := "/" + want
|
|
openIndex := -1
|
|
blocks := make([]antonSectionBlock, 0)
|
|
for index := start; index < end; index++ {
|
|
sectionName := antonSectionName(document.Sections[index].Name)
|
|
switch sectionName {
|
|
case want:
|
|
if openIndex >= 0 {
|
|
return nil, antonShape(name, fmt.Sprintf("nested_open_at=%d", index))
|
|
}
|
|
openIndex = index
|
|
case closeName:
|
|
if openIndex < 0 {
|
|
return nil, antonShape(name, fmt.Sprintf("unmatched_close_at=%d", index))
|
|
}
|
|
blocks = append(blocks, antonSectionBlock{open: openIndex, close: index})
|
|
openIndex = -1
|
|
}
|
|
}
|
|
if openIndex >= 0 {
|
|
return nil, antonShape(name, fmt.Sprintf("unclosed_open_at=%d", openIndex))
|
|
}
|
|
return blocks, nil
|
|
}
|
|
|
|
func antonReadPositiveIntSection(document *dnfpvf.Document, name string, start, end int) (int, error) {
|
|
sectionIndex, err := antonUniqueSection(document, name, start, end)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return antonPositiveIntAt(document, sectionIndex, name)
|
|
}
|
|
|
|
func antonReadPositiveIntListSection(document *dnfpvf.Document, name string, start, end, want int) ([]int, error) {
|
|
sectionIndex, err := antonUniqueSection(document, name, start, end)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tokens, err := antonSectionTokens(document, sectionIndex)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(tokens) != want {
|
|
return nil, antonShape(name, fmt.Sprintf("values=%d want=%d", len(tokens), want))
|
|
}
|
|
values := make([]int, 0, len(tokens))
|
|
for _, token := range tokens {
|
|
value, valueErr := antonPositiveIntToken(token, name)
|
|
if valueErr != nil {
|
|
return nil, valueErr
|
|
}
|
|
values = append(values, value)
|
|
}
|
|
return values, nil
|
|
}
|
|
|
|
func antonPositiveIntAt(document *dnfpvf.Document, sectionIndex int, field string) (int, error) {
|
|
tokens, err := antonSectionTokens(document, sectionIndex)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if len(tokens) != 1 {
|
|
return 0, antonShape(field, fmt.Sprintf("values=%d want=1", len(tokens)))
|
|
}
|
|
return antonPositiveIntToken(tokens[0], field)
|
|
}
|
|
|
|
func antonNonNegativeIntAt(document *dnfpvf.Document, sectionIndex int, field string) (int, error) {
|
|
tokens, err := antonSectionTokens(document, sectionIndex)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if len(tokens) != 1 {
|
|
return 0, antonShape(field, fmt.Sprintf("values=%d want=1", len(tokens)))
|
|
}
|
|
value, err := antonIntToken(tokens[0], field)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if value < 0 {
|
|
return 0, antonInvalid(field, value, "want_nonnegative")
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func antonUniqueSection(document *dnfpvf.Document, name string, start, end int) (int, error) {
|
|
found, ok, err := antonOptionalUniqueSection(document, name, start, end)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
if !ok {
|
|
return -1, antonMissing(name, "required")
|
|
}
|
|
return found, nil
|
|
}
|
|
|
|
func antonOptionalUniqueSection(document *dnfpvf.Document, name string, start, end int) (int, bool, error) {
|
|
want := antonSectionName(name)
|
|
found := -1
|
|
for index := start; index < end; index++ {
|
|
if antonSectionName(document.Sections[index].Name) != want {
|
|
continue
|
|
}
|
|
if found >= 0 {
|
|
return -1, false, antonShape(name, fmt.Sprintf("duplicate_at=%d,%d", found, index))
|
|
}
|
|
found = index
|
|
}
|
|
if found < 0 {
|
|
return -1, false, nil
|
|
}
|
|
return found, true, nil
|
|
}
|
|
|
|
func antonSectionTokens(document *dnfpvf.Document, sectionIndex int) ([]dnfpvf.Token, error) {
|
|
if sectionIndex < 0 || sectionIndex >= len(document.Sections) {
|
|
return nil, antonShape("section", fmt.Sprintf("index=%d sections=%d", sectionIndex, len(document.Sections)))
|
|
}
|
|
section := document.Sections[sectionIndex]
|
|
if section.Start < 0 || section.End < section.Start || section.End > len(document.Tokens) {
|
|
return nil, antonShape(section.Name, fmt.Sprintf("token_range=%d:%d tokens=%d", section.Start, section.End, len(document.Tokens)))
|
|
}
|
|
return document.Tokens[section.Start:section.End], nil
|
|
}
|
|
|
|
func antonRequireEmptySection(document *dnfpvf.Document, sectionIndex int) error {
|
|
tokens, err := antonSectionTokens(document, sectionIndex)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(tokens) != 0 {
|
|
return antonShape(document.Sections[sectionIndex].Name, fmt.Sprintf("values=%d want=0", len(tokens)))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func antonIntToken(token dnfpvf.Token, field string) (int, error) {
|
|
if token.Kind != dnfpvf.TokenInt || token.Int < math.MinInt32 || token.Int > math.MaxInt32 {
|
|
return 0, antonInvalid(field, token.Raw, "want=int32")
|
|
}
|
|
return int(token.Int), nil
|
|
}
|
|
|
|
func antonPositiveIntToken(token dnfpvf.Token, field string) (int, error) {
|
|
value, err := antonIntToken(token, field)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if value <= 0 {
|
|
return 0, antonInvalid(field, value, "want_positive")
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func antonPositiveUint32Token(token dnfpvf.Token, field string) (uint32, error) {
|
|
if token.Kind != dnfpvf.TokenInt || token.Int <= 0 || token.Int > math.MaxUint32 {
|
|
return 0, antonInvalid(field, token.Raw, "want=positive_uint32")
|
|
}
|
|
return uint32(token.Int), nil
|
|
}
|
|
|
|
func antonMissing(section, context string) error {
|
|
return fmt.Errorf("%w: path=%s section=%s context=%s", ErrAntonPVFSectionMissing, AntonPVFPath, section, context)
|
|
}
|
|
|
|
func antonShape(section, detail string) error {
|
|
return fmt.Errorf("%w: path=%s section=%s %s", ErrAntonPVFSectionShape, AntonPVFPath, section, detail)
|
|
}
|
|
|
|
func antonInvalid(field string, value any, detail string) error {
|
|
return fmt.Errorf("%w: path=%s field=%s value=%v %s", ErrAntonPVFValueInvalid, AntonPVFPath, field, value, detail)
|
|
}
|
|
|
|
func antonPathKey(value string) string {
|
|
value = strings.TrimSpace(strings.ReplaceAll(value, "\\", "/"))
|
|
value = strings.TrimLeft(value, "/")
|
|
return strings.ToLower(value)
|
|
}
|
|
|
|
func antonSectionName(value string) string {
|
|
return strings.ToLower(strings.TrimSpace(value))
|
|
}
|