Files
90CNServer/server/internal/services/dnfbridge/dungeon_drop_dimension.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

441 lines
17 KiB
Go

package dnfbridge
import (
"errors"
"fmt"
"sort"
"strings"
"sync"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
platformpvf "longheng.io/server/internal/platform/pvf"
)
const (
currentDungeonDimensionGatePath = "Etc/DimensionGate.etc"
currentDungeonDimensionInfoPath = "Etc/DungeonEtc/DimensionDungeonInfo.etc"
currentDungeonDimensionDropListPath = "Etc/DimensionGateDropList.etc"
currentDungeonDimensionDropExcludePath = "Etc/DimensionGateDropList_Exclude.etc"
currentDungeonClearRewardRulePath = "Etc/ItemDropInfo_ClearReward.etc"
// The same-version reference server adds an area material to the ordinary
// monster-template pool with weight 100. The weight only selects within
// the pool; the existing 1000/10000 outer roll remains the drop rate.
currentDungeonDimensionMaterialWeight uint32 = 100
)
var errCurrentDungeonDimensionDropInvalid = errors.New("current dungeon dimension drop PVF data is invalid")
type currentDungeonDimensionDropEntry struct {
Group int
ItemID uint32
}
type currentDungeonDimensionLevelThreshold struct {
Tier int
MinimumLevel int
}
type currentDungeonDimensionChronicleKey struct {
Job byte
GrowType byte
}
type currentDungeonDimensionChroniclePool struct {
Normal []uint32
Set []uint32
}
// currentDungeonDimensionChronicleRate is the exact three-column row from
// ItemDropInfo_ClearReward.etc [chronicle set reward rate]. RewardPercent is
// used by the clear-card path. SetPercent chooses the rarer set pool after a
// monster's ordinary PVF equipment roll has already succeeded.
type currentDungeonDimensionChronicleRate struct {
RewardPercent int
SetPercent int
}
type currentDungeonDimensionDropCatalog struct {
LegacyDungeons map[int64]struct{}
ExactTiers map[int64]int
LevelThresholds []currentDungeonDimensionLevelThreshold
ItemsByTier map[int][]currentDungeonDimensionDropEntry
ChronicleByCareer map[currentDungeonDimensionChronicleKey]currentDungeonDimensionChroniclePool
ChronicleRates map[int]currentDungeonDimensionChronicleRate
}
type currentDungeonDimensionDropCacheEntry struct {
catalog *currentDungeonDimensionDropCatalog
err error
}
var currentDungeonDimensionDropsByMonsterCatalog sync.Map
func currentDungeonDimensionDrops(catalog *pvfDungeonMonsterCatalog) (*currentDungeonDimensionDropCatalog, error) {
if catalog == nil || catalog.source == nil {
return nil, errDungeonDropSourceRequired
}
if cached, found := currentDungeonDimensionDropsByMonsterCatalog.Load(catalog); found {
entry := cached.(currentDungeonDimensionDropCacheEntry)
return entry.catalog, entry.err
}
loaded, err := loadCurrentDungeonDimensionDrops(catalog.source)
entry := currentDungeonDimensionDropCacheEntry{catalog: loaded, err: err}
actual, _ := currentDungeonDimensionDropsByMonsterCatalog.LoadOrStore(catalog, entry)
entry = actual.(currentDungeonDimensionDropCacheEntry)
return entry.catalog, entry.err
}
func loadCurrentDungeonDimensionDrops(source dnfpvf.Source) (*currentDungeonDimensionDropCatalog, error) {
if source == nil {
return nil, errDungeonDropSourceRequired
}
catalog := &currentDungeonDimensionDropCatalog{
LegacyDungeons: make(map[int64]struct{}),
ExactTiers: make(map[int64]int),
ItemsByTier: make(map[int][]currentDungeonDimensionDropEntry),
ChronicleByCareer: make(map[currentDungeonDimensionChronicleKey]currentDungeonDimensionChroniclePool),
ChronicleRates: make(map[int]currentDungeonDimensionChronicleRate),
}
gateText, err := source.ReadText(currentDungeonDimensionGatePath)
if err != nil && !currentDungeonDimensionDropDocumentMissing(err) {
return nil, fmt.Errorf("read %s: %w", currentDungeonDimensionGatePath, err)
}
if strings.TrimSpace(gateText) != "" {
document, parseErr := dnfpvf.Parse(currentDungeonDimensionGatePath, gateText)
if parseErr != nil {
return nil, parseErr
}
values, sectionErr := currentDungeonDimensionSectionInts(document, "dimensiondungeon list", false)
if sectionErr != nil {
return nil, sectionErr
}
if len(values)%3 != 0 {
return nil, fmt.Errorf("%w: %s [dimensiondungeon list] values=%d", errCurrentDungeonDimensionDropInvalid, currentDungeonDimensionGatePath, len(values))
}
for index := 0; index < len(values); index += 3 {
dungeonID := values[index]
if dungeonID <= 0 {
return nil, fmt.Errorf("%w: %s dungeon_id=%d", errCurrentDungeonDimensionDropInvalid, currentDungeonDimensionGatePath, dungeonID)
}
catalog.LegacyDungeons[dungeonID] = struct{}{}
}
}
infoText, err := source.ReadText(currentDungeonDimensionInfoPath)
if err != nil && !currentDungeonDimensionDropDocumentMissing(err) {
return nil, fmt.Errorf("read %s: %w", currentDungeonDimensionInfoPath, err)
}
if strings.TrimSpace(infoText) == "" {
return catalog, nil
}
document, err := dnfpvf.Parse(currentDungeonDimensionInfoPath, infoText)
if err != nil {
return nil, err
}
infoValues, err := currentDungeonDimensionSectionInts(document, "dimension dungeon info", false)
if err != nil {
return nil, err
}
if len(infoValues)%2 != 0 {
return nil, fmt.Errorf("%w: %s [dimension dungeon info] values=%d", errCurrentDungeonDimensionDropInvalid, currentDungeonDimensionInfoPath, len(infoValues))
}
for index := 0; index < len(infoValues); index += 2 {
tier, dungeonID := infoValues[index], infoValues[index+1]
if tier < 0 || tier > int64(^uint8(0)) || dungeonID <= 0 {
return nil, fmt.Errorf("%w: %s tier=%d dungeon_id=%d", errCurrentDungeonDimensionDropInvalid, currentDungeonDimensionInfoPath, tier, dungeonID)
}
if _, duplicate := catalog.ExactTiers[dungeonID]; duplicate {
return nil, fmt.Errorf("%w: %s duplicate dungeon_id=%d", errCurrentDungeonDimensionDropInvalid, currentDungeonDimensionInfoPath, dungeonID)
}
catalog.ExactTiers[dungeonID] = int(tier)
}
thresholdValues, err := currentDungeonDimensionSectionInts(document, "dungeon difficulty condition info", false)
if err != nil {
return nil, err
}
if len(thresholdValues)%2 != 0 {
return nil, fmt.Errorf("%w: %s [dungeon difficulty condition info] values=%d", errCurrentDungeonDimensionDropInvalid, currentDungeonDimensionInfoPath, len(thresholdValues))
}
for index := 0; index < len(thresholdValues); index += 2 {
tier, level := thresholdValues[index], thresholdValues[index+1]
if tier < 0 || tier > int64(^uint8(0)) || level < 0 || level > int64(^uint8(0)) {
return nil, fmt.Errorf("%w: %s threshold tier=%d level=%d", errCurrentDungeonDimensionDropInvalid, currentDungeonDimensionInfoPath, tier, level)
}
catalog.LevelThresholds = append(catalog.LevelThresholds, currentDungeonDimensionLevelThreshold{Tier: int(tier), MinimumLevel: int(level)})
}
sort.SliceStable(catalog.LevelThresholds, func(i, j int) bool {
if catalog.LevelThresholds[i].MinimumLevel == catalog.LevelThresholds[j].MinimumLevel {
return catalog.LevelThresholds[i].Tier < catalog.LevelThresholds[j].Tier
}
return catalog.LevelThresholds[i].MinimumLevel < catalog.LevelThresholds[j].MinimumLevel
})
dropValues, err := currentDungeonDimensionSectionInts(document, "drop item Info", false)
if err != nil {
return nil, err
}
if len(dropValues)%3 != 0 {
return nil, fmt.Errorf("%w: %s [drop item Info] values=%d", errCurrentDungeonDimensionDropInvalid, currentDungeonDimensionInfoPath, len(dropValues))
}
for index := 0; index < len(dropValues); index += 3 {
tier, group, itemID := dropValues[index], dropValues[index+1], dropValues[index+2]
if tier < 0 || tier > int64(^uint8(0)) || group < 0 || group > int64(^uint8(0)) || itemID <= 0 || itemID > int64(^uint32(0)) {
return nil, fmt.Errorf("%w: %s drop tier=%d group=%d item_id=%d", errCurrentDungeonDimensionDropInvalid, currentDungeonDimensionInfoPath, tier, group, itemID)
}
catalog.ItemsByTier[int(tier)] = append(catalog.ItemsByTier[int(tier)], currentDungeonDimensionDropEntry{Group: int(group), ItemID: uint32(itemID)})
}
if err := loadCurrentDungeonDimensionChronicleDrops(source, catalog); err != nil {
return nil, err
}
return catalog, nil
}
func loadCurrentDungeonDimensionChronicleDrops(source dnfpvf.Source, catalog *currentDungeonDimensionDropCatalog) error {
if source == nil || catalog == nil {
return errDungeonDropSourceRequired
}
dropText, err := source.ReadText(currentDungeonDimensionDropListPath)
if err != nil {
if currentDungeonDimensionDropDocumentMissing(err) {
return nil
}
return fmt.Errorf("read %s: %w", currentDungeonDimensionDropListPath, err)
}
if strings.TrimSpace(dropText) == "" {
return nil
}
excluded := make(map[uint32]struct{})
excludeText, err := source.ReadText(currentDungeonDimensionDropExcludePath)
if err != nil {
return fmt.Errorf("read %s: %w", currentDungeonDimensionDropExcludePath, err)
}
excludeDocument, err := dnfpvf.Parse(currentDungeonDimensionDropExcludePath, excludeText)
if err != nil {
return err
}
excludeValues, err := currentDungeonDimensionSectionInts(excludeDocument, "exclude list", false)
if err != nil {
return err
}
for _, itemID := range excludeValues {
if itemID <= 0 || itemID > int64(^uint32(0)) {
return fmt.Errorf("%w: %s item_id=%d", errCurrentDungeonDimensionDropInvalid, currentDungeonDimensionDropExcludePath, itemID)
}
excluded[uint32(itemID)] = struct{}{}
}
document, err := dnfpvf.Parse(currentDungeonDimensionDropListPath, dropText)
if err != nil {
return err
}
for sectionIndex := 0; sectionIndex < len(document.Sections); sectionIndex++ {
section := document.Sections[sectionIndex]
if !strings.EqualFold(strings.TrimSpace(section.Name), "chronicle grow type") {
continue
}
careerValues := magicBoxSectionInts(document, section)
if len(careerValues) != 2 || careerValues[0] < 0 || careerValues[0] > int64(^uint8(0)) || careerValues[1] < 0 || careerValues[1] > int64(^uint8(0)) {
return fmt.Errorf("%w: %s section=%d career=%v", errCurrentDungeonDimensionDropInvalid, currentDungeonDimensionDropListPath, sectionIndex, careerValues)
}
key := currentDungeonDimensionChronicleKey{Job: byte(careerValues[0]), GrowType: byte(careerValues[1])}
if _, duplicate := catalog.ChronicleByCareer[key]; duplicate {
return fmt.Errorf("%w: %s duplicate job=%d grow_type=%d", errCurrentDungeonDimensionDropInvalid, currentDungeonDimensionDropListPath, key.Job, key.GrowType)
}
pool := currentDungeonDimensionChroniclePool{}
normalFound, setFound, closed := false, false, false
for nestedIndex := sectionIndex + 1; nestedIndex < len(document.Sections); nestedIndex++ {
nested := document.Sections[nestedIndex]
name := strings.ToLower(strings.TrimSpace(nested.Name))
if name == "/chronicle grow type" {
sectionIndex = nestedIndex
closed = true
break
}
var destination *[]uint32
switch name {
case "normal chronicle list":
if normalFound {
return fmt.Errorf("%w: %s duplicate normal list job=%d grow_type=%d", errCurrentDungeonDimensionDropInvalid, currentDungeonDimensionDropListPath, key.Job, key.GrowType)
}
normalFound = true
destination = &pool.Normal
case "set chronicle list":
if setFound {
return fmt.Errorf("%w: %s duplicate set list job=%d grow_type=%d", errCurrentDungeonDimensionDropInvalid, currentDungeonDimensionDropListPath, key.Job, key.GrowType)
}
setFound = true
destination = &pool.Set
default:
continue
}
for _, itemID := range magicBoxSectionInts(document, nested) {
if itemID <= 0 || itemID > int64(^uint32(0)) {
return fmt.Errorf("%w: %s job=%d grow_type=%d item_id=%d", errCurrentDungeonDimensionDropInvalid, currentDungeonDimensionDropListPath, key.Job, key.GrowType, itemID)
}
id := uint32(itemID)
if _, blocked := excluded[id]; blocked {
continue
}
*destination = append(*destination, id)
}
}
if !closed || !normalFound || !setFound {
return fmt.Errorf("%w: %s incomplete job=%d grow_type=%d", errCurrentDungeonDimensionDropInvalid, currentDungeonDimensionDropListPath, key.Job, key.GrowType)
}
catalog.ChronicleByCareer[key] = pool
}
if len(catalog.ChronicleByCareer) == 0 {
return fmt.Errorf("%w: %s has no career rows", errCurrentDungeonDimensionDropInvalid, currentDungeonDimensionDropListPath)
}
rateText, err := source.ReadText(currentDungeonClearRewardRulePath)
if err != nil {
return fmt.Errorf("read %s: %w", currentDungeonClearRewardRulePath, err)
}
rateDocument, err := dnfpvf.Parse(currentDungeonClearRewardRulePath, rateText)
if err != nil {
return err
}
rateValues, err := currentDungeonDimensionSectionInts(rateDocument, "chronicle set reward rate", false)
if err != nil {
return err
}
if len(rateValues)%3 != 0 {
return fmt.Errorf("%w: %s [chronicle set reward rate] values=%d", errCurrentDungeonDimensionDropInvalid, currentDungeonClearRewardRulePath, len(rateValues))
}
for index := 0; index < len(rateValues); index += 3 {
tier, rewardPercent, setPercent := rateValues[index], rateValues[index+1], rateValues[index+2]
if tier < 0 || tier > int64(^uint8(0)) || rewardPercent < 0 || rewardPercent > 100 || setPercent < 0 || setPercent > 100 {
return fmt.Errorf("%w: %s tier=%d reward=%d set=%d", errCurrentDungeonDimensionDropInvalid, currentDungeonClearRewardRulePath, tier, rewardPercent, setPercent)
}
if _, duplicate := catalog.ChronicleRates[int(tier)]; duplicate {
return fmt.Errorf("%w: %s duplicate tier=%d", errCurrentDungeonDimensionDropInvalid, currentDungeonClearRewardRulePath, tier)
}
catalog.ChronicleRates[int(tier)] = currentDungeonDimensionChronicleRate{RewardPercent: int(rewardPercent), SetPercent: int(setPercent)}
}
return nil
}
func currentDungeonDimensionTier(catalog *currentDungeonDimensionDropCatalog, dungeonID int64, characterLevel int) (int, bool) {
if catalog == nil || dungeonID <= 0 {
return 0, false
}
tier, matched := catalog.ExactTiers[dungeonID]
if matched {
return tier, true
}
if _, matched = catalog.LegacyDungeons[dungeonID]; !matched {
return 0, false
}
for _, threshold := range catalog.LevelThresholds {
if characterLevel < threshold.MinimumLevel {
break
}
tier = threshold.Tier
}
return tier, true
}
func currentDungeonDimensionDropPool(
catalog *currentDungeonDimensionDropCatalog,
dungeonID int64,
characterLevel int,
) (int, []dungeonMonsterDropPoolEntry, bool, error) {
if catalog == nil || dungeonID <= 0 {
return 0, nil, false, nil
}
tier, matched := currentDungeonDimensionTier(catalog, dungeonID, characterLevel)
if !matched {
return 0, nil, false, nil
}
selectedTier := -1
for candidate := range catalog.ItemsByTier {
if candidate <= tier && candidate > selectedTier && len(catalog.ItemsByTier[candidate]) > 0 {
selectedTier = candidate
}
}
if selectedTier < 0 {
return tier, nil, true, fmt.Errorf("%w: dungeon_id=%d tier=%d has no drop-item row", errCurrentDungeonDimensionDropInvalid, dungeonID, tier)
}
entries := catalog.ItemsByTier[selectedTier]
pool := make([]dungeonMonsterDropPoolEntry, 0, len(entries))
for _, entry := range entries {
pool = append(pool, dungeonMonsterDropPoolEntry{
ItemID: entry.ItemID,
Weight: currentDungeonDimensionMaterialWeight,
})
}
return selectedTier, pool, true, nil
}
func currentDungeonDimensionChronicleItem(
catalog *currentDungeonDimensionDropCatalog,
dungeonID int64,
characterLevel int,
job byte,
growType byte,
rng *currentDungeonDropLCG,
applyRewardRate bool,
) (int, uint32, string, bool, bool, error) {
tier, matched := currentDungeonDimensionTier(catalog, dungeonID, characterLevel)
if !matched {
return 0, 0, "", false, false, nil
}
if rng == nil {
return tier, 0, "", true, false, errCurrentDungeonDimensionDropInvalid
}
pool, found := catalog.ChronicleByCareer[currentDungeonDimensionChronicleKey{Job: job, GrowType: growType}]
if !found || (len(pool.Normal) == 0 && len(pool.Set) == 0) {
return tier, 0, "", true, false, nil
}
rate, found := catalog.ChronicleRates[tier]
if !found {
return tier, 0, "", true, false, fmt.Errorf("%w: dungeon_id=%d tier=%d has no chronicle rate", errCurrentDungeonDimensionDropInvalid, dungeonID, tier)
}
if applyRewardRate && (rate.RewardPercent == 0 || rng.Next(100) >= rate.RewardPercent) {
return tier, 0, "", true, false, nil
}
selectedPool := pool.Normal
group := "normal"
if len(pool.Set) > 0 && rate.SetPercent > 0 && rng.Next(100) < rate.SetPercent {
selectedPool = pool.Set
group = "set"
}
if len(selectedPool) == 0 {
selectedPool = pool.Set
group = "set"
}
if len(selectedPool) == 0 {
return tier, 0, "", true, false, nil
}
return tier, selectedPool[rng.Next(len(selectedPool))], group, true, true, nil
}
func currentDungeonDimensionSectionInts(document *dnfpvf.Document, section string, optional bool) ([]int64, error) {
if document == nil {
return nil, fmt.Errorf("%w: section=%s document=nil", errCurrentDungeonDimensionDropInvalid, section)
}
tokens, found := document.Section(section)
if !found {
if optional {
return nil, nil
}
return nil, fmt.Errorf("%w: %s section=[%s] missing", errCurrentDungeonDimensionDropInvalid, document.Path, section)
}
values := make([]int64, 0, len(tokens))
for _, token := range tokens {
if token.Kind == dnfpvf.TokenInt {
values = append(values, token.Int)
}
}
return values, nil
}
func currentDungeonDimensionDropDocumentMissing(err error) bool {
return errors.Is(err, dnfpvf.ErrDocNotFound) || errors.Is(err, platformpvf.ErrFileNotFound)
}