服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
230 lines
7.4 KiB
Go
230 lines
7.4 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"sync"
|
|
|
|
dnfdrops "longheng.io/server/internal/modules/dnf/drop"
|
|
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
|
)
|
|
|
|
const (
|
|
currentDungeonDropDenominator = 10000
|
|
currentDungeonExplicitPoolRate = 1000
|
|
)
|
|
|
|
var currentDungeonDropDifficultyBonus = [...]float64{1.0, 1.2, 1.4, 1.6, 1.8}
|
|
|
|
var (
|
|
errCurrentDungeonDropRulesUnavailable = errors.New("current dungeon drop PVF rules are unavailable")
|
|
errCurrentDungeonClearRewardRulesUnavailable = errors.New("current dungeon clear-reward PVF rules are unavailable")
|
|
errCurrentDungeonGoldReferenceInvalid = errors.New("current dungeon gold PVF reference is invalid")
|
|
errCurrentDungeonMapDropCountInvalid = errors.New("current dungeon map drop count is invalid")
|
|
)
|
|
|
|
func currentDungeonMapDropAttempts(value int64, kind string) (int, error) {
|
|
if value < 0 || value > math.MaxUint8 {
|
|
return 0, fmt.Errorf("%w: kind=%s count=%d", errCurrentDungeonMapDropCountInvalid, kind, value)
|
|
}
|
|
return int(value), nil
|
|
}
|
|
|
|
// currentDungeonDropLCG is the room PRNG algorithm used by the verified C#
|
|
// domain implementation. Its state is persisted in the room visit, not
|
|
// reconstructed from a monster key, so sequential kills consume the same
|
|
// deterministic stream as the room run.
|
|
type currentDungeonDropLCG struct {
|
|
seed uint32
|
|
}
|
|
|
|
func newCurrentDungeonDropLCG(seed uint32) *currentDungeonDropLCG {
|
|
return ¤tDungeonDropLCG{seed: seed}
|
|
}
|
|
|
|
func (rng *currentDungeonDropLCG) Seed() uint32 {
|
|
if rng == nil {
|
|
return 0
|
|
}
|
|
return rng.seed
|
|
}
|
|
|
|
func (rng *currentDungeonDropLCG) NextUint32() uint32 {
|
|
if rng == nil {
|
|
return 0
|
|
}
|
|
v2 := rng.seed*1103515245 + 12345
|
|
v3 := v2*1103515245 + 12345
|
|
v4 := v3*1103515245 + 12345
|
|
rng.seed = v4
|
|
hi2 := (v2 >> 16) & 0x7ff
|
|
hi3 := (v3 >> 16) & 0x3ff
|
|
hi4 := (v4 >> 16) & 0x3ff
|
|
return (((hi2 << 10) ^ hi3) << 10) | hi4
|
|
}
|
|
|
|
func (rng *currentDungeonDropLCG) Next(max int) int {
|
|
value := rng.NextUint32()
|
|
if max <= 0 {
|
|
return int(value)
|
|
}
|
|
return int(value % uint32(max))
|
|
}
|
|
|
|
type currentDungeonGoldReference struct {
|
|
Level int64
|
|
Base int64
|
|
VariancePct int64
|
|
}
|
|
|
|
type currentDungeonGoldReferenceCacheEntry struct {
|
|
byLevel map[int64]currentDungeonGoldReference
|
|
err error
|
|
}
|
|
|
|
type currentDungeonMonsterDropRulesCacheEntry struct {
|
|
rules *dnfdrops.MonsterRules
|
|
err error
|
|
}
|
|
|
|
type currentDungeonClearRewardRulesCacheEntry struct {
|
|
rules *dnfdrops.ClearRewardRules
|
|
err error
|
|
}
|
|
|
|
var currentDungeonGoldReferencesByCatalog sync.Map
|
|
var currentDungeonMonsterDropRulesByCatalog sync.Map
|
|
var currentDungeonClearRewardRulesByCatalog sync.Map
|
|
|
|
func currentDungeonMonsterDropRules(catalog *pvfDungeonMonsterCatalog) (*dnfdrops.MonsterRules, error) {
|
|
if catalog == nil || catalog.source == nil {
|
|
return nil, errCurrentDungeonDropRulesUnavailable
|
|
}
|
|
if cached, found := currentDungeonMonsterDropRulesByCatalog.Load(catalog); found {
|
|
entry := cached.(currentDungeonMonsterDropRulesCacheEntry)
|
|
return entry.rules, entry.err
|
|
}
|
|
rules, err := dnfdrops.LoadMonsterRules(catalog.source)
|
|
entry := currentDungeonMonsterDropRulesCacheEntry{rules: rules, err: err}
|
|
actual, _ := currentDungeonMonsterDropRulesByCatalog.LoadOrStore(catalog, entry)
|
|
entry = actual.(currentDungeonMonsterDropRulesCacheEntry)
|
|
if entry.err != nil {
|
|
return nil, fmt.Errorf("%w: %v", errCurrentDungeonDropRulesUnavailable, entry.err)
|
|
}
|
|
return entry.rules, nil
|
|
}
|
|
|
|
func currentDungeonClearRewardRules(catalog *pvfDungeonMonsterCatalog) (*dnfdrops.ClearRewardRules, error) {
|
|
if catalog == nil || catalog.source == nil {
|
|
return nil, errCurrentDungeonClearRewardRulesUnavailable
|
|
}
|
|
if cached, found := currentDungeonClearRewardRulesByCatalog.Load(catalog); found {
|
|
entry := cached.(currentDungeonClearRewardRulesCacheEntry)
|
|
return entry.rules, entry.err
|
|
}
|
|
rules, err := dnfdrops.LoadClearRewardRules(catalog.source)
|
|
entry := currentDungeonClearRewardRulesCacheEntry{rules: rules, err: err}
|
|
actual, _ := currentDungeonClearRewardRulesByCatalog.LoadOrStore(catalog, entry)
|
|
entry = actual.(currentDungeonClearRewardRulesCacheEntry)
|
|
if entry.err != nil {
|
|
return nil, fmt.Errorf("%w: %v", errCurrentDungeonClearRewardRulesUnavailable, entry.err)
|
|
}
|
|
return entry.rules, nil
|
|
}
|
|
|
|
func currentDungeonGoldReferences(catalog *pvfDungeonMonsterCatalog) (map[int64]currentDungeonGoldReference, error) {
|
|
if catalog == nil || catalog.source == nil {
|
|
return nil, errCurrentDungeonGoldReferenceInvalid
|
|
}
|
|
if cached, found := currentDungeonGoldReferencesByCatalog.Load(catalog); found {
|
|
entry := cached.(currentDungeonGoldReferenceCacheEntry)
|
|
return entry.byLevel, entry.err
|
|
}
|
|
entry := currentDungeonGoldReferenceCacheEntry{}
|
|
text, err := catalog.source.ReadText(dungeonCardGoldReferencePath)
|
|
if err == nil {
|
|
var document *dnfpvf.Document
|
|
document, err = dnfpvf.Parse(dungeonCardGoldReferencePath, text)
|
|
if err == nil {
|
|
values, parseErr := dungeonCardPVFSectionInts(document, "gold drop ref table")
|
|
if parseErr != nil {
|
|
err = parseErr
|
|
} else if len(values)%3 != 0 {
|
|
err = fmt.Errorf("%w: rows=%d", errCurrentDungeonGoldReferenceInvalid, len(values))
|
|
} else {
|
|
entry.byLevel = make(map[int64]currentDungeonGoldReference, len(values)/3)
|
|
for index := 0; index < len(values); index += 3 {
|
|
row := currentDungeonGoldReference{Level: values[index], Base: values[index+1], VariancePct: values[index+2]}
|
|
if row.Level <= 0 || row.Base <= 0 || row.VariancePct < 0 || row.VariancePct > 100 || entry.byLevel[row.Level].Level != 0 {
|
|
err = fmt.Errorf("%w: row=%+v", errCurrentDungeonGoldReferenceInvalid, row)
|
|
break
|
|
}
|
|
entry.byLevel[row.Level] = row
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if err != nil {
|
|
entry.err = fmt.Errorf("%w: %v", errCurrentDungeonGoldReferenceInvalid, err)
|
|
}
|
|
actual, _ := currentDungeonGoldReferencesByCatalog.LoadOrStore(catalog, entry)
|
|
entry = actual.(currentDungeonGoldReferenceCacheEntry)
|
|
return entry.byLevel, entry.err
|
|
}
|
|
|
|
func currentDungeonDropRate(base int64, typeBonus float64, difficultyBonus float64) int {
|
|
if base <= 0 || math.IsNaN(typeBonus) || math.IsInf(typeBonus, 0) || typeBonus <= 0 ||
|
|
math.IsNaN(difficultyBonus) || math.IsInf(difficultyBonus, 0) || difficultyBonus <= 0 {
|
|
return 0
|
|
}
|
|
value := math.Floor(float64(base) * typeBonus * difficultyBonus)
|
|
if value <= 0 {
|
|
return 0
|
|
}
|
|
if value >= currentDungeonDropDenominator {
|
|
return currentDungeonDropDenominator
|
|
}
|
|
return int(value)
|
|
}
|
|
|
|
func currentDungeonDropDifficultyRate(index int) float64 {
|
|
if index < 0 || index >= len(currentDungeonDropDifficultyBonus) {
|
|
return 1.0
|
|
}
|
|
return currentDungeonDropDifficultyBonus[index]
|
|
}
|
|
|
|
func currentDungeonDropRarity(rng *currentDungeonDropLCG, rules *dnfdrops.MonsterRules) int64 {
|
|
if rng == nil || rules == nil {
|
|
return 0
|
|
}
|
|
thresholds := rules.RawTables().RarityDecision[0]
|
|
roll := int64(rng.Next(1000000)) + 1
|
|
for rarity, threshold := range thresholds {
|
|
if roll <= threshold {
|
|
return int64(rarity)
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func currentDungeonGoldAmount(rng *currentDungeonDropLCG, reference currentDungeonGoldReference, difficulty float64) uint32 {
|
|
if rng == nil || reference.Base <= 0 || reference.VariancePct < 0 || difficulty <= 0 {
|
|
return 0
|
|
}
|
|
variance := int64(0)
|
|
if reference.VariancePct > 0 {
|
|
rangeSize := reference.VariancePct*2 + 1
|
|
variance = (int64(rng.Next(int(rangeSize))) - reference.VariancePct) * reference.Base / 100
|
|
}
|
|
value := int64(math.Floor(float64(reference.Base+variance) * difficulty))
|
|
if value < 1 {
|
|
value = 1
|
|
}
|
|
if value > math.MaxUint32 {
|
|
return math.MaxUint32
|
|
}
|
|
return uint32(value)
|
|
}
|