Files
90CNServer/server/internal/services/dnfbridge/anton_reward_pvf.go
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

734 lines
29 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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))
}