服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
734 lines
29 KiB
Go
734 lines
29 KiB
Go
package dnfbridge
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"math"
|
||
"path"
|
||
"strings"
|
||
"sync"
|
||
|
||
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
||
dnfraid "longheng.io/server/internal/modules/dnf/raid"
|
||
)
|
||
|
||
const currentAntonRewardPhaseCount = 2
|
||
|
||
// currentLukeRewardPhaseCount / currentFiendWarRewardPhaseCount 是这两张奖励表
|
||
// **各自**声明的阶段数。原来是拿安东的 2 当通用常量,超时空只有 1 个 [PHASE]
|
||
// (fiendwar.etc 的 `[PHASE MAX] 1`),于是它整条发奖在提交时被拒:
|
||
//
|
||
// 真机 2026-10-02 13:52:48(raid 12058625,16 分制里的第 2 局,5 分 39 秒通关):
|
||
// game-raid-reward-auto-flip-commit-failed flipped=4
|
||
// error=anton reward PVF definition set is invalid: phases=1 want=2
|
||
// → 四个人的翻牌都翻开了(flipped=4)却被整批回滚,客户端只看到一个「0」,
|
||
// 这就是操作员看到的「翻牌翻不开」。超时空的最后一次翻牌本来就是**攻坚结束**
|
||
// 的那一次(与卢克/安徒恩二阶段结束同一条路:movie → 倒计时 → 自动翻开 → 提交)。
|
||
const (
|
||
currentLukeRewardPhaseCount = 2
|
||
currentFiendWarRewardPhaseCount = 1
|
||
)
|
||
|
||
var currentAntonRewardDefinitionOrder = [...]uint32{
|
||
10094735,
|
||
10094733,
|
||
10157773,
|
||
10157775,
|
||
10157776,
|
||
10157777,
|
||
10157778,
|
||
10161936,
|
||
10157783,
|
||
10157784,
|
||
10157785,
|
||
10157786,
|
||
}
|
||
|
||
var (
|
||
errAntonRewardPVFCatalogRequired = errors.New("anton reward PVF item catalog is required")
|
||
errAntonRewardPVFConfigRequired = errors.New("anton reward PVF config is required")
|
||
errAntonRewardPVFDefinitionSet = errors.New("anton reward PVF definition set is invalid")
|
||
errAntonRewardPVFDefinitionMissing = errors.New("anton reward PVF definition item is unresolved")
|
||
errAntonRewardPVFSectionShape = errors.New("anton reward PVF int data shape is invalid")
|
||
errAntonRewardPVFValueInvalid = errors.New("anton reward PVF value is invalid")
|
||
errAntonRewardPVFFinalItemUnresolved = errors.New("anton reward PVF final item is unresolved")
|
||
errAntonRewardPVFNestingInvalid = errors.New("anton reward PVF nesting is invalid")
|
||
errAntonRewardPVFRNGRequired = errors.New("anton reward PVF RNG is required")
|
||
errAntonRewardPVFPoolUnresolved = errors.New("anton reward PVF pool is unresolved")
|
||
)
|
||
|
||
// antonRewardPVFRNG is deliberately tiny so runtime code can inject a seeded
|
||
// generator and tests can pin every outer and inner weighted choice.
|
||
type antonRewardPVFRNG interface {
|
||
Uint64() uint64
|
||
}
|
||
|
||
// antonRewardPVFEntry preserves one exact {final item, weight, count} triple
|
||
// from a definition item's [int data] section. Duplicate rows remain distinct.
|
||
type antonRewardPVFEntry struct {
|
||
ItemID uint32
|
||
Weight uint32
|
||
Count uint32
|
||
}
|
||
|
||
type antonRewardPVFDefinition struct {
|
||
DefinitionItemID uint32
|
||
Kind dungeonDropItemKind
|
||
PVFPath string
|
||
DirectFinal bool
|
||
Entries []antonRewardPVFEntry
|
||
TotalWeight uint64
|
||
}
|
||
|
||
type antonRewardPVFPoolEntry struct {
|
||
DefinitionItemID uint32
|
||
Rank int
|
||
Weight uint32
|
||
Flag byte
|
||
}
|
||
|
||
type antonRewardPVFPool struct {
|
||
PhaseIndex int
|
||
Kind string
|
||
Entries []antonRewardPVFPoolEntry
|
||
TotalWeight uint64
|
||
}
|
||
|
||
type antonRewardPVFPoolKey struct {
|
||
phaseIndex int
|
||
kind string
|
||
}
|
||
|
||
// antonRewardPVFResolved is a final grant instruction. DefinitionItemID and
|
||
// NestedDefinitionItemID are evidence only. ItemID is either a leaf from an
|
||
// [int data] table or the definition item itself when the PVF declares a
|
||
// directly grantable [booster info] reward box.
|
||
type antonRewardPVFResolved struct {
|
||
PhaseIndex int
|
||
PoolKind string
|
||
PoolWeight uint32
|
||
Rank int
|
||
Flag byte
|
||
DefinitionItemID uint32
|
||
DefinitionPVFPath string
|
||
NestedDefinitionItemID uint32
|
||
NestedDefinitionPVFPath string
|
||
ItemID uint32
|
||
Count uint32
|
||
ItemKind dungeonDropItemKind
|
||
ItemPVFPath string
|
||
}
|
||
|
||
type antonRewardPVFCatalogSnapshot struct {
|
||
Definitions int
|
||
DefinitionEntries int
|
||
PhasePools int
|
||
PhasePoolEntries int
|
||
NestedReferences int
|
||
}
|
||
|
||
// antonRewardPVFCatalog contains the current 90 Anton reward definitions for
|
||
// both the team pools and the native single-mode pool.
|
||
// It does not mutate inventory or own raid state.
|
||
type antonRewardPVFCatalog struct {
|
||
definitions map[uint32]antonRewardPVFDefinition
|
||
definitionOrder []uint32
|
||
pools map[antonRewardPVFPoolKey]antonRewardPVFPool
|
||
poolOrder []antonRewardPVFPoolKey
|
||
itemRefs map[uint32]dungeonDropItemReference
|
||
source dnfpvf.Source
|
||
finalMu sync.Mutex
|
||
finalCache map[uint32]dungeonDropItemDefinition
|
||
}
|
||
|
||
// newAntonRewardPVFCatalog reads the definition documents referenced by the
|
||
// current team and single-mode anton.etc pools. The existing item catalog has
|
||
// already paid the list-index cost; no final item document is opened here.
|
||
// newAntonRewardPVFCatalog reads the definition documents referenced by the
|
||
// 安东奖励表。期望的阶段数由**每个团本自己**给出(安东/卢克 2、超时空 1)。
|
||
func newAntonRewardPVFCatalog(items *pvfDungeonDropCatalog, config *dnfraid.AntonConfig) (*antonRewardPVFCatalog, error) {
|
||
return newRaidRewardPVFCatalog(items, config, currentAntonRewardDefinitionOrder[:], true, currentAntonRewardPhaseCount)
|
||
}
|
||
|
||
func newRaidRewardPVFCatalog(items *pvfDungeonDropCatalog, config *dnfraid.AntonConfig, requiredDefinitions []uint32, requireAll bool, expectedPhases int) (*antonRewardPVFCatalog, error) {
|
||
if items == nil {
|
||
return nil, errAntonRewardPVFCatalogRequired
|
||
}
|
||
if config == nil {
|
||
return nil, errAntonRewardPVFConfigRequired
|
||
}
|
||
|
||
items.mu.Lock()
|
||
source := items.source
|
||
itemRefs := make(map[uint32]dungeonDropItemReference, len(items.itemRefs))
|
||
for itemID, reference := range items.itemRefs {
|
||
itemRefs[itemID] = reference
|
||
}
|
||
items.mu.Unlock()
|
||
if source == nil {
|
||
return nil, errAntonRewardPVFCatalogRequired
|
||
}
|
||
|
||
pools, poolOrder, err := buildRaidRewardPVFPools(config, requiredDefinitions, requireAll, expectedPhases)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
definitionOrder := raidRewardDefinitionOrder(config, requiredDefinitions)
|
||
catalog := &antonRewardPVFCatalog{
|
||
definitions: make(map[uint32]antonRewardPVFDefinition, len(definitionOrder)),
|
||
definitionOrder: definitionOrder,
|
||
pools: pools,
|
||
poolOrder: poolOrder,
|
||
itemRefs: itemRefs,
|
||
source: source,
|
||
finalCache: make(map[uint32]dungeonDropItemDefinition),
|
||
}
|
||
for _, definitionItemID := range catalog.definitionOrder {
|
||
definition, loadErr := loadAntonRewardPVFDefinition(source, itemRefs, definitionItemID)
|
||
if loadErr != nil {
|
||
return nil, loadErr
|
||
}
|
||
catalog.definitions[definitionItemID] = definition
|
||
}
|
||
if err := catalog.validateFinalReferences(); err != nil {
|
||
return nil, err
|
||
}
|
||
return catalog, nil
|
||
}
|
||
|
||
func antonRewardDefinitionOrder(config *dnfraid.AntonConfig) []uint32 {
|
||
return raidRewardDefinitionOrder(config, currentAntonRewardDefinitionOrder[:])
|
||
}
|
||
|
||
func raidRewardDefinitionOrder(config *dnfraid.AntonConfig, requiredDefinitions []uint32) []uint32 {
|
||
order := append([]uint32(nil), requiredDefinitions...)
|
||
seen := make(map[uint32]struct{}, len(order))
|
||
for _, id := range order {
|
||
seen[id] = struct{}{}
|
||
}
|
||
appendRewards := func(rewards []dnfraid.AntonStateReward) {
|
||
for _, reward := range rewards {
|
||
if reward.DefinitionItemID == 0 {
|
||
continue
|
||
}
|
||
if _, exists := seen[reward.DefinitionItemID]; exists {
|
||
continue
|
||
}
|
||
seen[reward.DefinitionItemID] = struct{}{}
|
||
order = append(order, reward.DefinitionItemID)
|
||
}
|
||
}
|
||
if config != nil {
|
||
for _, phase := range config.Phases {
|
||
appendRewards(phase.Rewards)
|
||
}
|
||
if config.SingleMode != nil {
|
||
appendRewards(config.SingleMode.Phase.Rewards)
|
||
}
|
||
}
|
||
return order
|
||
}
|
||
|
||
func buildAntonRewardPVFPools(config *dnfraid.AntonConfig) (map[antonRewardPVFPoolKey]antonRewardPVFPool, []antonRewardPVFPoolKey, error) {
|
||
return buildRaidRewardPVFPools(config, currentAntonRewardDefinitionOrder[:], true, currentAntonRewardPhaseCount)
|
||
}
|
||
|
||
func buildRaidRewardPVFPools(config *dnfraid.AntonConfig, requiredDefinitions []uint32, requireAll bool, expectedPhases int) (map[antonRewardPVFPoolKey]antonRewardPVFPool, []antonRewardPVFPoolKey, error) {
|
||
if len(config.Phases) != expectedPhases {
|
||
return nil, nil, fmt.Errorf("%w: phases=%d want=%d", errAntonRewardPVFDefinitionSet, len(config.Phases), expectedPhases)
|
||
}
|
||
allowed := make(map[uint32]struct{}, len(requiredDefinitions))
|
||
for _, itemID := range requiredDefinitions {
|
||
allowed[itemID] = struct{}{}
|
||
}
|
||
seenDefinitions := make(map[uint32]struct{}, len(allowed))
|
||
pools := make(map[antonRewardPVFPoolKey]antonRewardPVFPool)
|
||
poolOrder := make([]antonRewardPVFPoolKey, 0, 6)
|
||
poolDefinitions := make(map[antonRewardPVFPoolKey]map[uint32]struct{})
|
||
for phaseIndex, phase := range config.Phases {
|
||
if len(phase.Rewards) == 0 {
|
||
return nil, nil, fmt.Errorf("%w: phase=%d has no rewards", errAntonRewardPVFDefinitionSet, phaseIndex)
|
||
}
|
||
for rewardIndex, reward := range phase.Rewards {
|
||
kind := normalizeAntonRewardPVFPoolKind(reward.Kind)
|
||
if kind == "" || reward.DefinitionItemID == 0 || reward.Weight <= 0 || uint64(reward.Weight) > math.MaxUint32 {
|
||
return nil, nil, fmt.Errorf("%w: phase=%d reward=%d kind=%q definition=%d weight=%d", errAntonRewardPVFValueInvalid, phaseIndex, rewardIndex, reward.Kind, reward.DefinitionItemID, reward.Weight)
|
||
}
|
||
if requireAll {
|
||
if _, ok := allowed[reward.DefinitionItemID]; !ok {
|
||
return nil, nil, fmt.Errorf("%w: phase=%d reward=%d unexpected_definition=%d", errAntonRewardPVFDefinitionSet, phaseIndex, rewardIndex, reward.DefinitionItemID)
|
||
}
|
||
}
|
||
key := antonRewardPVFPoolKey{phaseIndex: phaseIndex, kind: kind}
|
||
pool, found := pools[key]
|
||
if !found {
|
||
pool = antonRewardPVFPool{PhaseIndex: phaseIndex, Kind: kind}
|
||
poolOrder = append(poolOrder, key)
|
||
poolDefinitions[key] = make(map[uint32]struct{})
|
||
}
|
||
if _, duplicate := poolDefinitions[key][reward.DefinitionItemID]; duplicate {
|
||
return nil, nil, fmt.Errorf("%w: phase=%d kind=%s duplicate_definition=%d", errAntonRewardPVFDefinitionSet, phaseIndex, kind, reward.DefinitionItemID)
|
||
}
|
||
weight := uint32(reward.Weight)
|
||
if math.MaxUint64-pool.TotalWeight < uint64(weight) {
|
||
return nil, nil, fmt.Errorf("%w: phase=%d kind=%s total_weight_overflow", errAntonRewardPVFValueInvalid, phaseIndex, kind)
|
||
}
|
||
pool.Entries = append(pool.Entries, antonRewardPVFPoolEntry{
|
||
DefinitionItemID: reward.DefinitionItemID,
|
||
Rank: reward.Rank,
|
||
Weight: weight,
|
||
Flag: reward.Flag,
|
||
})
|
||
pool.TotalWeight += uint64(weight)
|
||
pools[key] = pool
|
||
poolDefinitions[key][reward.DefinitionItemID] = struct{}{}
|
||
seenDefinitions[reward.DefinitionItemID] = struct{}{}
|
||
}
|
||
}
|
||
if requireAll {
|
||
for _, definitionItemID := range requiredDefinitions {
|
||
if _, found := seenDefinitions[definitionItemID]; !found {
|
||
return nil, nil, fmt.Errorf("%w: missing_definition=%d", errAntonRewardPVFDefinitionSet, definitionItemID)
|
||
}
|
||
}
|
||
}
|
||
if config.SingleMode != nil {
|
||
singlePhaseIndex := len(config.Phases)
|
||
phase := config.SingleMode.Phase
|
||
if len(phase.Rewards) == 0 {
|
||
return nil, nil, fmt.Errorf("%w: single phase has no rewards", errAntonRewardPVFDefinitionSet)
|
||
}
|
||
for rewardIndex, reward := range phase.Rewards {
|
||
kind := normalizeAntonRewardPVFPoolKind(reward.Kind)
|
||
if kind == "" || reward.DefinitionItemID == 0 || reward.Weight <= 0 || uint64(reward.Weight) > math.MaxUint32 {
|
||
return nil, nil, fmt.Errorf("%w: single reward=%d kind=%q definition=%d weight=%d", errAntonRewardPVFValueInvalid, rewardIndex, reward.Kind, reward.DefinitionItemID, reward.Weight)
|
||
}
|
||
key := antonRewardPVFPoolKey{phaseIndex: singlePhaseIndex, kind: kind}
|
||
pool, found := pools[key]
|
||
if !found {
|
||
pool = antonRewardPVFPool{PhaseIndex: singlePhaseIndex, Kind: kind}
|
||
poolOrder = append(poolOrder, key)
|
||
poolDefinitions[key] = make(map[uint32]struct{})
|
||
}
|
||
if _, duplicate := poolDefinitions[key][reward.DefinitionItemID]; duplicate {
|
||
return nil, nil, fmt.Errorf("%w: single kind=%s duplicate_definition=%d", errAntonRewardPVFDefinitionSet, kind, reward.DefinitionItemID)
|
||
}
|
||
weight := uint32(reward.Weight)
|
||
if math.MaxUint64-pool.TotalWeight < uint64(weight) {
|
||
return nil, nil, fmt.Errorf("%w: single kind=%s total_weight_overflow", errAntonRewardPVFValueInvalid, kind)
|
||
}
|
||
pool.Entries = append(pool.Entries, antonRewardPVFPoolEntry{
|
||
DefinitionItemID: reward.DefinitionItemID,
|
||
Rank: reward.Rank,
|
||
Weight: weight,
|
||
Flag: reward.Flag,
|
||
})
|
||
pool.TotalWeight += uint64(weight)
|
||
pools[key] = pool
|
||
poolDefinitions[key][reward.DefinitionItemID] = struct{}{}
|
||
}
|
||
}
|
||
return pools, poolOrder, nil
|
||
}
|
||
|
||
func loadAntonRewardPVFDefinition(source dnfpvf.Source, itemRefs map[uint32]dungeonDropItemReference, definitionItemID uint32) (antonRewardPVFDefinition, error) {
|
||
reference, found := itemRefs[definitionItemID]
|
||
if !found {
|
||
return antonRewardPVFDefinition{}, fmt.Errorf("%w: definition=%d", errAntonRewardPVFDefinitionMissing, definitionItemID)
|
||
}
|
||
documentPath, err := antonRewardPVFReferencePath(reference)
|
||
if err != nil {
|
||
return antonRewardPVFDefinition{}, fmt.Errorf("%w: definition=%d: %v", errAntonRewardPVFDefinitionMissing, definitionItemID, err)
|
||
}
|
||
text, err := source.ReadText(documentPath)
|
||
if err != nil {
|
||
return antonRewardPVFDefinition{}, fmt.Errorf("read anton reward definition=%d path=%s: %w", definitionItemID, documentPath, err)
|
||
}
|
||
document, err := dnfpvf.Parse(documentPath, text)
|
||
if err != nil {
|
||
return antonRewardPVFDefinition{}, fmt.Errorf("parse anton reward definition=%d path=%s: %w", definitionItemID, documentPath, err)
|
||
}
|
||
directFinal := raidRewardPVFHasSection(document, "booster info") && !raidRewardPVFHasSection(document, "int data")
|
||
entries := []antonRewardPVFEntry{{ItemID: definitionItemID, Weight: 1, Count: 1}}
|
||
totalWeight := uint64(1)
|
||
if !directFinal {
|
||
entries, totalWeight, err = parseAntonRewardPVFEntries(document, definitionItemID)
|
||
if err != nil {
|
||
return antonRewardPVFDefinition{}, err
|
||
}
|
||
}
|
||
return antonRewardPVFDefinition{
|
||
DefinitionItemID: definitionItemID,
|
||
Kind: reference.kind,
|
||
PVFPath: documentPath,
|
||
DirectFinal: directFinal,
|
||
Entries: entries,
|
||
TotalWeight: totalWeight,
|
||
}, nil
|
||
}
|
||
|
||
func raidRewardPVFHasSection(document *dnfpvf.Document, name string) bool {
|
||
if document == nil {
|
||
return false
|
||
}
|
||
want := normalizeAntonRewardPVFSection(name)
|
||
for _, section := range document.Sections {
|
||
if normalizeAntonRewardPVFSection(section.Name) == want {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func parseAntonRewardPVFEntries(document *dnfpvf.Document, definitionItemID uint32) ([]antonRewardPVFEntry, uint64, error) {
|
||
if document == nil {
|
||
return nil, 0, fmt.Errorf("%w: definition=%d nil_document", errAntonRewardPVFSectionShape, definitionItemID)
|
||
}
|
||
openIndex, closeIndex := -1, -1
|
||
for index, section := range document.Sections {
|
||
switch normalizeAntonRewardPVFSection(section.Name) {
|
||
case "int data":
|
||
if openIndex >= 0 {
|
||
return nil, 0, fmt.Errorf("%w: definition=%d duplicate_int_data", errAntonRewardPVFSectionShape, definitionItemID)
|
||
}
|
||
openIndex = index
|
||
case "/int data":
|
||
if closeIndex >= 0 {
|
||
return nil, 0, fmt.Errorf("%w: definition=%d duplicate_int_data_close", errAntonRewardPVFSectionShape, definitionItemID)
|
||
}
|
||
closeIndex = index
|
||
}
|
||
}
|
||
if openIndex < 0 || closeIndex != openIndex+1 {
|
||
return nil, 0, fmt.Errorf("%w: definition=%d int_data=%d close=%d", errAntonRewardPVFSectionShape, definitionItemID, openIndex, closeIndex)
|
||
}
|
||
closingTokens, err := antonRewardPVFSectionTokens(document, closeIndex)
|
||
if err != nil || len(closingTokens) != 0 {
|
||
return nil, 0, fmt.Errorf("%w: definition=%d closing_values=%d", errAntonRewardPVFSectionShape, definitionItemID, len(closingTokens))
|
||
}
|
||
tokens, err := antonRewardPVFSectionTokens(document, openIndex)
|
||
if err != nil {
|
||
return nil, 0, err
|
||
}
|
||
return parseRaidRewardTripleTokens(tokens, definitionItemID)
|
||
}
|
||
|
||
func parseRaidRewardTripleTokens(tokens []dnfpvf.Token, definitionItemID uint32) ([]antonRewardPVFEntry, uint64, error) {
|
||
if len(tokens) == 0 || len(tokens)%3 != 0 {
|
||
return nil, 0, fmt.Errorf("%w: definition=%d values=%d row_width=3", errAntonRewardPVFSectionShape, definitionItemID, len(tokens))
|
||
}
|
||
values := make([]int64, 0, len(tokens))
|
||
for tokenIndex, token := range tokens {
|
||
if token.Kind != dnfpvf.TokenInt {
|
||
return nil, 0, fmt.Errorf("%w: definition=%d token=%d kind=%s", errAntonRewardPVFSectionShape, definitionItemID, tokenIndex, token.Kind)
|
||
}
|
||
values = append(values, token.Int)
|
||
}
|
||
entries := make([]antonRewardPVFEntry, 0, len(values)/3)
|
||
var totalWeight uint64
|
||
for offset := 0; offset < len(values); offset += 3 {
|
||
itemID, weight, count := values[offset], values[offset+1], values[offset+2]
|
||
if itemID <= 0 || itemID > math.MaxUint32 || weight <= 0 || weight > math.MaxUint32 || count <= 0 || count > math.MaxUint32 {
|
||
return nil, 0, fmt.Errorf("%w: definition=%d row=%d item=%d weight=%d count=%d", errAntonRewardPVFValueInvalid, definitionItemID, offset/3, itemID, weight, count)
|
||
}
|
||
if math.MaxUint64-totalWeight < uint64(weight) {
|
||
return nil, 0, fmt.Errorf("%w: definition=%d total_weight_overflow", errAntonRewardPVFValueInvalid, definitionItemID)
|
||
}
|
||
entries = append(entries, antonRewardPVFEntry{ItemID: uint32(itemID), Weight: uint32(weight), Count: uint32(count)})
|
||
totalWeight += uint64(weight)
|
||
}
|
||
return entries, totalWeight, nil
|
||
}
|
||
|
||
func antonRewardPVFSectionTokens(document *dnfpvf.Document, sectionIndex int) ([]dnfpvf.Token, error) {
|
||
if document == nil || sectionIndex < 0 || sectionIndex >= len(document.Sections) {
|
||
return nil, fmt.Errorf("%w: section_index=%d", errAntonRewardPVFSectionShape, sectionIndex)
|
||
}
|
||
section := document.Sections[sectionIndex]
|
||
if section.Start < 0 || section.End < section.Start || section.End > len(document.Tokens) {
|
||
return nil, fmt.Errorf("%w: section=%s range=%d:%d tokens=%d", errAntonRewardPVFSectionShape, section.Name, section.Start, section.End, len(document.Tokens))
|
||
}
|
||
return document.Tokens[section.Start:section.End], nil
|
||
}
|
||
|
||
func (c *antonRewardPVFCatalog) validateFinalReferences() error {
|
||
for _, definitionItemID := range c.definitionOrder {
|
||
definition := c.definitions[definitionItemID]
|
||
if definition.DirectFinal {
|
||
if _, err := c.finalItemReference(definitionItemID); err != nil {
|
||
return fmt.Errorf("definition=%d direct_final: %w", definitionItemID, err)
|
||
}
|
||
continue
|
||
}
|
||
for entryIndex, entry := range definition.Entries {
|
||
nested, isNested := c.definitions[entry.ItemID]
|
||
if !isNested {
|
||
if _, err := c.finalItemReference(entry.ItemID); err != nil {
|
||
return fmt.Errorf("definition=%d entry=%d: %w", definitionItemID, entryIndex, err)
|
||
}
|
||
continue
|
||
}
|
||
if nested.DefinitionItemID == definitionItemID {
|
||
return fmt.Errorf("%w: definition=%d self_reference", errAntonRewardPVFNestingInvalid, definitionItemID)
|
||
}
|
||
if nested.DirectFinal {
|
||
if _, err := c.finalItemReference(nested.DefinitionItemID); err != nil {
|
||
return fmt.Errorf("definition=%d entry=%d nested_direct=%d: %w", definitionItemID, entryIndex, nested.DefinitionItemID, err)
|
||
}
|
||
continue
|
||
}
|
||
for nestedIndex, nestedEntry := range nested.Entries {
|
||
if _, deeper := c.definitions[nestedEntry.ItemID]; deeper {
|
||
return fmt.Errorf("%w: definition=%d entry=%d nested=%d nested_entry=%d deeper=%d", errAntonRewardPVFNestingInvalid, definitionItemID, entryIndex, nested.DefinitionItemID, nestedIndex, nestedEntry.ItemID)
|
||
}
|
||
if uint64(entry.Count)*uint64(nestedEntry.Count) > math.MaxUint32 {
|
||
return fmt.Errorf("%w: definition=%d nested=%d count_overflow=%d*%d", errAntonRewardPVFValueInvalid, definitionItemID, nested.DefinitionItemID, entry.Count, nestedEntry.Count)
|
||
}
|
||
if _, err := c.finalItemReference(nestedEntry.ItemID); err != nil {
|
||
return fmt.Errorf("definition=%d entry=%d nested=%d nested_entry=%d: %w", definitionItemID, entryIndex, nested.DefinitionItemID, nestedIndex, err)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (c *antonRewardPVFCatalog) finalItemReference(itemID uint32) (dungeonDropItemReference, error) {
|
||
reference, found := c.itemRefs[itemID]
|
||
if !found {
|
||
return dungeonDropItemReference{}, fmt.Errorf("%w: item=%d", errAntonRewardPVFFinalItemUnresolved, itemID)
|
||
}
|
||
if _, err := antonRewardPVFReferencePath(reference); err != nil {
|
||
return dungeonDropItemReference{}, fmt.Errorf("%w: item=%d: %v", errAntonRewardPVFFinalItemUnresolved, itemID, err)
|
||
}
|
||
return reference, nil
|
||
}
|
||
|
||
func (c *antonRewardPVFCatalog) Definition(definitionItemID uint32) (antonRewardPVFDefinition, bool) {
|
||
if c == nil {
|
||
return antonRewardPVFDefinition{}, false
|
||
}
|
||
definition, found := c.definitions[definitionItemID]
|
||
if !found {
|
||
return antonRewardPVFDefinition{}, false
|
||
}
|
||
definition.Entries = append([]antonRewardPVFEntry(nil), definition.Entries...)
|
||
return definition, true
|
||
}
|
||
|
||
func (c *antonRewardPVFCatalog) DefinitionIDs() []uint32 {
|
||
if c == nil {
|
||
return nil
|
||
}
|
||
return append([]uint32(nil), c.definitionOrder...)
|
||
}
|
||
|
||
func (c *antonRewardPVFCatalog) Pool(phaseIndex int, kind string) (antonRewardPVFPool, bool) {
|
||
if c == nil {
|
||
return antonRewardPVFPool{}, false
|
||
}
|
||
key := antonRewardPVFPoolKey{phaseIndex: phaseIndex, kind: normalizeAntonRewardPVFPoolKind(kind)}
|
||
pool, found := c.pools[key]
|
||
if !found {
|
||
return antonRewardPVFPool{}, false
|
||
}
|
||
pool.Entries = append([]antonRewardPVFPoolEntry(nil), pool.Entries...)
|
||
return pool, true
|
||
}
|
||
|
||
func (c *antonRewardPVFCatalog) ResolvePool(phaseIndex int, kind string, rng antonRewardPVFRNG) (antonRewardPVFResolved, error) {
|
||
if c == nil {
|
||
return antonRewardPVFResolved{}, errAntonRewardPVFCatalogRequired
|
||
}
|
||
if rng == nil {
|
||
return antonRewardPVFResolved{}, errAntonRewardPVFRNGRequired
|
||
}
|
||
pool, found := c.Pool(phaseIndex, kind)
|
||
if !found || len(pool.Entries) == 0 || pool.TotalWeight == 0 {
|
||
return antonRewardPVFResolved{}, fmt.Errorf("%w: phase=%d kind=%q", errAntonRewardPVFPoolUnresolved, phaseIndex, kind)
|
||
}
|
||
selected := selectAntonRewardPVFPoolEntry(pool, rng.Uint64())
|
||
resolved, err := c.ResolveDefinition(selected.DefinitionItemID, rng)
|
||
if err != nil {
|
||
return antonRewardPVFResolved{}, err
|
||
}
|
||
resolved.PhaseIndex = phaseIndex
|
||
resolved.PoolKind = pool.Kind
|
||
resolved.PoolWeight = selected.Weight
|
||
resolved.Rank = selected.Rank
|
||
resolved.Flag = selected.Flag
|
||
return resolved, nil
|
||
}
|
||
|
||
func (c *antonRewardPVFCatalog) ResolveDefinition(definitionItemID uint32, rng antonRewardPVFRNG) (antonRewardPVFResolved, error) {
|
||
if c == nil {
|
||
return antonRewardPVFResolved{}, errAntonRewardPVFCatalogRequired
|
||
}
|
||
if rng == nil {
|
||
return antonRewardPVFResolved{}, errAntonRewardPVFRNGRequired
|
||
}
|
||
definition, found := c.definitions[definitionItemID]
|
||
if !found || len(definition.Entries) == 0 || definition.TotalWeight == 0 {
|
||
return antonRewardPVFResolved{}, fmt.Errorf("%w: definition=%d", errAntonRewardPVFDefinitionMissing, definitionItemID)
|
||
}
|
||
selected := selectAntonRewardPVFDefinitionEntry(definition, rng.Uint64())
|
||
count := selected.Count
|
||
resolved := antonRewardPVFResolved{
|
||
PhaseIndex: -1,
|
||
DefinitionItemID: definitionItemID,
|
||
DefinitionPVFPath: definition.PVFPath,
|
||
}
|
||
if definition.DirectFinal {
|
||
selected = antonRewardPVFEntry{ItemID: definition.DefinitionItemID, Weight: 1, Count: 1}
|
||
count = 1
|
||
}
|
||
if nested, isNested := c.definitions[selected.ItemID]; isNested {
|
||
if nested.DirectFinal {
|
||
resolved.NestedDefinitionItemID = nested.DefinitionItemID
|
||
resolved.NestedDefinitionPVFPath = nested.PVFPath
|
||
selected = antonRewardPVFEntry{ItemID: nested.DefinitionItemID, Weight: 1, Count: count}
|
||
} else {
|
||
nestedSelected := selectAntonRewardPVFDefinitionEntry(nested, rng.Uint64())
|
||
if _, deeper := c.definitions[nestedSelected.ItemID]; deeper {
|
||
return antonRewardPVFResolved{}, fmt.Errorf("%w: definition=%d nested=%d deeper=%d", errAntonRewardPVFNestingInvalid, definitionItemID, nested.DefinitionItemID, nestedSelected.ItemID)
|
||
}
|
||
count64 := uint64(count) * uint64(nestedSelected.Count)
|
||
if count64 == 0 || count64 > math.MaxUint32 {
|
||
return antonRewardPVFResolved{}, fmt.Errorf("%w: definition=%d nested=%d count=%d", errAntonRewardPVFValueInvalid, definitionItemID, nested.DefinitionItemID, count64)
|
||
}
|
||
resolved.NestedDefinitionItemID = nested.DefinitionItemID
|
||
resolved.NestedDefinitionPVFPath = nested.PVFPath
|
||
selected = nestedSelected
|
||
count = uint32(count64)
|
||
}
|
||
}
|
||
|
||
if stillDefinition, exists := c.definitions[selected.ItemID]; exists && !stillDefinition.DirectFinal {
|
||
return antonRewardPVFResolved{}, fmt.Errorf("%w: definition=%d final=%d", errAntonRewardPVFNestingInvalid, definitionItemID, selected.ItemID)
|
||
}
|
||
finalReference, err := c.finalItemReference(selected.ItemID)
|
||
if err != nil {
|
||
return antonRewardPVFResolved{}, err
|
||
}
|
||
finalPath, err := antonRewardPVFReferencePath(finalReference)
|
||
if err != nil {
|
||
return antonRewardPVFResolved{}, err
|
||
}
|
||
resolved.ItemID = selected.ItemID
|
||
resolved.Count = count
|
||
resolved.ItemKind = finalReference.kind
|
||
resolved.ItemPVFPath = finalPath
|
||
return resolved, nil
|
||
}
|
||
|
||
// ResolveFinalItem expands metadata for one already selected final item. The
|
||
// underlying resolver opens only that item's indexed document and caches it;
|
||
// it never initializes the generic dungeon-drop pools.
|
||
func (c *antonRewardPVFCatalog) ResolveFinalItem(itemID uint32) (dungeonDropItemDefinition, error) {
|
||
if c == nil || c.source == nil {
|
||
return dungeonDropItemDefinition{}, errAntonRewardPVFCatalogRequired
|
||
}
|
||
if definition, isDefinition := c.definitions[itemID]; isDefinition && !definition.DirectFinal {
|
||
return dungeonDropItemDefinition{}, fmt.Errorf("%w: definition=%d is not final", errAntonRewardPVFNestingInvalid, itemID)
|
||
}
|
||
reference, err := c.finalItemReference(itemID)
|
||
if err != nil {
|
||
return dungeonDropItemDefinition{}, err
|
||
}
|
||
exactPath, err := antonRewardPVFReferencePath(reference)
|
||
if err != nil {
|
||
return dungeonDropItemDefinition{}, err
|
||
}
|
||
c.finalMu.Lock()
|
||
defer c.finalMu.Unlock()
|
||
if cached, found := c.finalCache[itemID]; found {
|
||
return cached, nil
|
||
}
|
||
// Supplying the already rooted path makes ResolveItem's first and only
|
||
// successful read the exact selected document. The one-item catalog cannot
|
||
// initialize monster or generic item pools.
|
||
single := &pvfDungeonDropCatalog{
|
||
source: c.source,
|
||
itemRefs: map[uint32]dungeonDropItemReference{
|
||
itemID: {kind: reference.kind, path: exactPath},
|
||
},
|
||
itemCache: make(map[uint32]dungeonDropItemDefinition, 1),
|
||
}
|
||
definition, err := single.ResolveItem(itemID)
|
||
if err != nil {
|
||
return dungeonDropItemDefinition{}, fmt.Errorf("resolve Anton final item=%d: %w", itemID, err)
|
||
}
|
||
c.finalCache[itemID] = definition
|
||
return definition, nil
|
||
}
|
||
|
||
func (c *antonRewardPVFCatalog) Snapshot() antonRewardPVFCatalogSnapshot {
|
||
if c == nil {
|
||
return antonRewardPVFCatalogSnapshot{}
|
||
}
|
||
snapshot := antonRewardPVFCatalogSnapshot{Definitions: len(c.definitions), PhasePools: len(c.pools)}
|
||
for _, definition := range c.definitions {
|
||
snapshot.DefinitionEntries += len(definition.Entries)
|
||
for _, entry := range definition.Entries {
|
||
if _, nested := c.definitions[entry.ItemID]; nested {
|
||
snapshot.NestedReferences++
|
||
}
|
||
}
|
||
}
|
||
for _, pool := range c.pools {
|
||
snapshot.PhasePoolEntries += len(pool.Entries)
|
||
}
|
||
return snapshot
|
||
}
|
||
|
||
func selectAntonRewardPVFPoolEntry(pool antonRewardPVFPool, random uint64) antonRewardPVFPoolEntry {
|
||
want := random % pool.TotalWeight
|
||
var cumulative uint64
|
||
for _, entry := range pool.Entries {
|
||
cumulative += uint64(entry.Weight)
|
||
if want < cumulative {
|
||
return entry
|
||
}
|
||
}
|
||
return pool.Entries[len(pool.Entries)-1]
|
||
}
|
||
|
||
func selectAntonRewardPVFDefinitionEntry(definition antonRewardPVFDefinition, random uint64) antonRewardPVFEntry {
|
||
want := random % definition.TotalWeight
|
||
var cumulative uint64
|
||
for _, entry := range definition.Entries {
|
||
cumulative += uint64(entry.Weight)
|
||
if want < cumulative {
|
||
return entry
|
||
}
|
||
}
|
||
return definition.Entries[len(definition.Entries)-1]
|
||
}
|
||
|
||
func antonRewardPVFReferencePath(reference dungeonDropItemReference) (string, error) {
|
||
if reference.kind != dungeonDropItemStackable && reference.kind != dungeonDropItemEquipment {
|
||
return "", fmt.Errorf("unsupported_kind=%q", reference.kind)
|
||
}
|
||
clean := cleanDungeonDropPath(reference.path)
|
||
if clean == "" {
|
||
return "", errors.New("empty_path")
|
||
}
|
||
root := string(reference.kind)
|
||
if !strings.HasPrefix(strings.ToLower(clean), strings.ToLower(root)+"/") {
|
||
clean = path.Join(root, clean)
|
||
}
|
||
clean = path.Clean(clean)
|
||
if clean == "." || clean == ".." || strings.HasPrefix(clean, "../") || strings.HasPrefix(clean, "/") {
|
||
return "", fmt.Errorf("path_outside_item_root=%q", clean)
|
||
}
|
||
return clean, nil
|
||
}
|
||
|
||
func normalizeAntonRewardPVFPoolKind(value string) string {
|
||
return strings.ToLower(strings.TrimSpace(value))
|
||
}
|
||
|
||
func normalizeAntonRewardPVFSection(value string) string {
|
||
return strings.ToLower(strings.TrimSpace(value))
|
||
}
|