DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,453 @@
|
||||
package drop
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
||||
)
|
||||
|
||||
const ClearRewardRulePath = "Etc/ItemDropInfo_ClearReward.etc"
|
||||
|
||||
const (
|
||||
// ClearRewardItemRateDenominator is the probability scale used by the
|
||||
// ordinary dungeon-clear equipment roll in the current PVF.
|
||||
ClearRewardItemRateDenominator = uint32(10_000)
|
||||
clearRewardBonusDenominator = float64(1_000)
|
||||
)
|
||||
|
||||
var (
|
||||
ErrClearRewardRuleSourceRequired = errors.New("dnf clear-reward rule PVF source is required")
|
||||
ErrClearRewardRuleSectionMissing = errors.New("dnf clear-reward rule PVF section is missing")
|
||||
ErrClearRewardRuleSectionShape = errors.New("dnf clear-reward rule PVF section shape is invalid")
|
||||
ErrClearRewardRuleLookupMissing = errors.New("dnf clear-reward rule lookup is missing")
|
||||
)
|
||||
|
||||
// ClearRewardItemReference preserves one exact three-column
|
||||
// [item drop ref table] row. The two values remain opaque until the current
|
||||
// client confirms their item-level interpretation.
|
||||
type ClearRewardItemReference struct {
|
||||
LookupKey int64
|
||||
ValueA int64
|
||||
ValueB int64
|
||||
}
|
||||
|
||||
// ClearRewardRuleSnapshot reports only loaded PVF source coverage.
|
||||
type ClearRewardRuleSnapshot struct {
|
||||
MapRateRows int
|
||||
DifficultyBonusRows int
|
||||
RarityThresholdRows int
|
||||
PartyBonusRows int
|
||||
GoldDifficultyRows int
|
||||
GoldCardCostRows int
|
||||
ItemReferenceRows int
|
||||
}
|
||||
|
||||
// ClearRewardRawTables is a value-copy of fixed-size clear-reward source
|
||||
// tables.
|
||||
type ClearRewardRawTables struct {
|
||||
ItemRateByMapCount map[int64]int64
|
||||
DifficultyDropBonus [5]int64
|
||||
RarityDecision [7]int64
|
||||
PartyMemberDropBonus [4]float64
|
||||
GoldDifficultyDropBonus [5]float64
|
||||
GoldCardCostByLevel map[int64]int64
|
||||
}
|
||||
|
||||
// ClearRewardSourcePlan is a pure lookup. It neither rolls a card reward nor
|
||||
// charges gold; callers can use it to correlate a real-client packet trace
|
||||
// with the exact rows from the runtime PVF.
|
||||
type ClearRewardSourcePlan struct {
|
||||
Level int64
|
||||
DifficultyIndex int
|
||||
PartyMembers int
|
||||
VisitedRoomCount int
|
||||
ItemRate int64
|
||||
DifficultyBonus int64
|
||||
PartyBonus float64
|
||||
GoldDifficultyRate float64
|
||||
GoldCardCost int64
|
||||
ItemReference ClearRewardItemReference
|
||||
ItemRateFound bool
|
||||
DifficultyFound bool
|
||||
PartyBonusFound bool
|
||||
GoldDifficultyFound bool
|
||||
GoldCardCostFound bool
|
||||
ItemReferenceFound bool
|
||||
}
|
||||
|
||||
// ClearRewardRule is the resolved ordinary end-of-dungeon card rule. The
|
||||
// equipment rate and rarity thresholds are read from the active PVF; it does
|
||||
// not select or grant an item.
|
||||
type ClearRewardRule struct {
|
||||
EquipmentRate uint32
|
||||
GradeDown int64
|
||||
GradeUp int64
|
||||
RarityThresholds [7]uint32
|
||||
RarityScale uint32
|
||||
PaidCardCost uint32
|
||||
GoldDifficultyRate float64
|
||||
MapCount int
|
||||
}
|
||||
|
||||
// ClearRewardRules is a read-only typed view of
|
||||
// Etc/ItemDropInfo_ClearReward.etc. The data can be safely inspected by
|
||||
// card-reward and diagnostics code without owning card selection or rewards.
|
||||
type ClearRewardRules struct {
|
||||
itemRateByMapCount map[int64]int64
|
||||
difficultyDropBonus [5]int64
|
||||
rarityDecision [7]int64
|
||||
partyMemberDropBonus [4]float64
|
||||
goldDifficultyDropBonus [5]float64
|
||||
goldCardCostByLevel map[int64]int64
|
||||
itemReferences map[int64]ClearRewardItemReference
|
||||
}
|
||||
|
||||
func LoadClearRewardRules(source dnfpvf.Source) (*ClearRewardRules, error) {
|
||||
if source == nil {
|
||||
return nil, ErrClearRewardRuleSourceRequired
|
||||
}
|
||||
text, err := source.ReadText(ClearRewardRulePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read %s: %w", ClearRewardRulePath, err)
|
||||
}
|
||||
document, err := dnfpvf.Parse(ClearRewardRulePath, text)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse %s: %w", ClearRewardRulePath, err)
|
||||
}
|
||||
|
||||
rules := &ClearRewardRules{
|
||||
itemRateByMapCount: make(map[int64]int64, 5),
|
||||
goldCardCostByLevel: make(map[int64]int64),
|
||||
itemReferences: make(map[int64]ClearRewardItemReference),
|
||||
}
|
||||
if err := loadClearRewardMapRates(document, rules); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := fillClearRewardIntArray(document, "dungeon difficulty drop bonusrate", rules.difficultyDropBonus[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for index, value := range rules.difficultyDropBonus {
|
||||
if value < 0 {
|
||||
return nil, fmt.Errorf("%w: section=dungeon difficulty drop bonusrate row=%d value=%d", ErrClearRewardRuleSectionShape, index, value)
|
||||
}
|
||||
}
|
||||
if err := fillClearRewardIntArray(document, "basis of rarity dicision", rules.rarityDecision[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for index, value := range rules.rarityDecision {
|
||||
if value < 0 || index > 0 && value < rules.rarityDecision[index-1] {
|
||||
return nil, fmt.Errorf("%w: section=basis of rarity dicision row=%d value=%d", ErrClearRewardRuleSectionShape, index, value)
|
||||
}
|
||||
}
|
||||
if rules.rarityDecision[len(rules.rarityDecision)-1] == 0 {
|
||||
return nil, fmt.Errorf("%w: section=basis of rarity dicision final_threshold=0", ErrClearRewardRuleSectionShape)
|
||||
}
|
||||
if err := fillClearRewardNumberArray(document, "party member drop bonusrate", rules.partyMemberDropBonus[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := fillClearRewardNumberArray(document, "dungeon difficulty gold drop bonusrate", rules.goldDifficultyDropBonus[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := loadClearRewardGoldCardCosts(document, rules); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := loadClearRewardItemReferences(document, rules); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func loadClearRewardMapRates(document *dnfpvf.Document, rules *ClearRewardRules) error {
|
||||
values, err := clearRewardIntegers(document, "reward item rate per map max count")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(values) != 10 {
|
||||
return fmt.Errorf("%w: section=reward item rate per map max count values=%d row_width=2", ErrClearRewardRuleSectionShape, len(values))
|
||||
}
|
||||
for offset := 0; offset < len(values); offset += 2 {
|
||||
mapCount, rate := values[offset], values[offset+1]
|
||||
wantMapCount := int64(offset/2 + 1)
|
||||
if mapCount != wantMapCount || rate < 0 {
|
||||
return fmt.Errorf("%w: section=reward item rate per map max count row=%d count=%d rate=%d", ErrClearRewardRuleSectionShape, offset/2, mapCount, rate)
|
||||
}
|
||||
rules.itemRateByMapCount[mapCount] = rate
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadClearRewardGoldCardCosts(document *dnfpvf.Document, rules *ClearRewardRules) error {
|
||||
values, err := clearRewardIntegers(document, "gold card cost table")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(values) == 0 || len(values)%2 != 0 {
|
||||
return fmt.Errorf("%w: section=gold card cost table values=%d row_width=2", ErrClearRewardRuleSectionShape, len(values))
|
||||
}
|
||||
var previous int64
|
||||
for offset := 0; offset < len(values); offset += 2 {
|
||||
level, cost := values[offset], values[offset+1]
|
||||
if level <= 0 || cost <= 0 || offset != 0 && level <= previous {
|
||||
return fmt.Errorf("%w: section=gold card cost table row=%d level=%d cost=%d", ErrClearRewardRuleSectionShape, offset/2, level, cost)
|
||||
}
|
||||
rules.goldCardCostByLevel[level] = cost
|
||||
previous = level
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadClearRewardItemReferences(document *dnfpvf.Document, rules *ClearRewardRules) error {
|
||||
values, err := clearRewardIntegers(document, "item drop ref table")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(values) == 0 || len(values)%3 != 0 {
|
||||
return fmt.Errorf("%w: section=item drop ref table values=%d row_width=3", ErrClearRewardRuleSectionShape, len(values))
|
||||
}
|
||||
var previous int64
|
||||
for offset := 0; offset < len(values); offset += 3 {
|
||||
row := ClearRewardItemReference{LookupKey: values[offset], ValueA: values[offset+1], ValueB: values[offset+2]}
|
||||
if row.LookupKey <= 0 || row.ValueA < 0 || row.ValueB < 0 || offset != 0 && row.LookupKey <= previous {
|
||||
return fmt.Errorf("%w: section=item drop ref table row=%d value=%+v", ErrClearRewardRuleSectionShape, offset/3, row)
|
||||
}
|
||||
rules.itemReferences[row.LookupKey] = row
|
||||
previous = row.LookupKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func clearRewardIntegers(document *dnfpvf.Document, section string) ([]int64, error) {
|
||||
tokens, found := document.Section(section)
|
||||
if !found {
|
||||
return nil, fmt.Errorf("%w: section=%s", ErrClearRewardRuleSectionMissing, section)
|
||||
}
|
||||
if len(tokens) == 0 {
|
||||
return nil, fmt.Errorf("%w: section=%s empty", ErrClearRewardRuleSectionShape, section)
|
||||
}
|
||||
values := make([]int64, 0, len(tokens))
|
||||
for index, token := range tokens {
|
||||
if token.Kind != dnfpvf.TokenInt {
|
||||
return nil, fmt.Errorf("%w: section=%s token=%d kind=%s", ErrClearRewardRuleSectionShape, section, index, token.Kind)
|
||||
}
|
||||
values = append(values, token.Int)
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func clearRewardNumbers(document *dnfpvf.Document, section string) ([]float64, error) {
|
||||
tokens, found := document.Section(section)
|
||||
if !found {
|
||||
return nil, fmt.Errorf("%w: section=%s", ErrClearRewardRuleSectionMissing, section)
|
||||
}
|
||||
if len(tokens) == 0 {
|
||||
return nil, fmt.Errorf("%w: section=%s empty", ErrClearRewardRuleSectionShape, section)
|
||||
}
|
||||
values := make([]float64, 0, len(tokens))
|
||||
for index, token := range tokens {
|
||||
var value float64
|
||||
switch token.Kind {
|
||||
case dnfpvf.TokenInt:
|
||||
value = float64(token.Int)
|
||||
case dnfpvf.TokenFloat:
|
||||
value = token.Float
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: section=%s token=%d kind=%s", ErrClearRewardRuleSectionShape, section, index, token.Kind)
|
||||
}
|
||||
if math.IsNaN(value) || math.IsInf(value, 0) || value <= 0 {
|
||||
return nil, fmt.Errorf("%w: section=%s token=%d value=%v", ErrClearRewardRuleSectionShape, section, index, value)
|
||||
}
|
||||
values = append(values, value)
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func fillClearRewardIntArray(document *dnfpvf.Document, section string, destination []int64) error {
|
||||
values, err := clearRewardIntegers(document, section)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(values) != len(destination) {
|
||||
return fmt.Errorf("%w: section=%s values=%d want=%d", ErrClearRewardRuleSectionShape, section, len(values), len(destination))
|
||||
}
|
||||
copy(destination, values)
|
||||
return nil
|
||||
}
|
||||
|
||||
func fillClearRewardNumberArray(document *dnfpvf.Document, section string, destination []float64) error {
|
||||
values, err := clearRewardNumbers(document, section)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(values) != len(destination) {
|
||||
return fmt.Errorf("%w: section=%s values=%d want=%d", ErrClearRewardRuleSectionShape, section, len(values), len(destination))
|
||||
}
|
||||
copy(destination, values)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rules *ClearRewardRules) ItemReference(level int64) (ClearRewardItemReference, bool) {
|
||||
if rules == nil {
|
||||
return ClearRewardItemReference{}, false
|
||||
}
|
||||
reference, found := rules.itemReferences[level]
|
||||
return reference, found
|
||||
}
|
||||
|
||||
// PartyMemberRate is the PVF [party member drop bonusrate] multiplier for a
|
||||
// party size, clamped to the four rows the table defines. The clear-card gold
|
||||
// formula applies it alongside the difficulty rate; a size outside the table
|
||||
// contributes no bonus rather than zeroing the reward.
|
||||
func (rules *ClearRewardRules) PartyMemberRate(partyMembers int) float64 {
|
||||
if rules == nil {
|
||||
return 1
|
||||
}
|
||||
if partyMembers < 1 {
|
||||
partyMembers = 1
|
||||
} else if partyMembers > len(rules.partyMemberDropBonus) {
|
||||
partyMembers = len(rules.partyMemberDropBonus)
|
||||
}
|
||||
rate := rules.partyMemberDropBonus[partyMembers-1]
|
||||
if rate <= 0 {
|
||||
return 1
|
||||
}
|
||||
return rate
|
||||
}
|
||||
|
||||
func (rules *ClearRewardRules) PlanSource(level int64, difficultyIndex, partyMembers, visitedRoomCount int) ClearRewardSourcePlan {
|
||||
plan := ClearRewardSourcePlan{
|
||||
Level: level,
|
||||
DifficultyIndex: difficultyIndex,
|
||||
PartyMembers: partyMembers,
|
||||
VisitedRoomCount: visitedRoomCount,
|
||||
}
|
||||
if rules == nil {
|
||||
return plan
|
||||
}
|
||||
plan.ItemRate, plan.ItemRateFound = rules.itemRateByMapCount[int64(visitedRoomCount)]
|
||||
if difficultyIndex >= 0 && difficultyIndex < len(rules.difficultyDropBonus) {
|
||||
plan.DifficultyBonus = rules.difficultyDropBonus[difficultyIndex]
|
||||
plan.DifficultyFound = true
|
||||
plan.GoldDifficultyRate = rules.goldDifficultyDropBonus[difficultyIndex]
|
||||
plan.GoldDifficultyFound = true
|
||||
}
|
||||
if partyMembers >= 1 && partyMembers <= len(rules.partyMemberDropBonus) {
|
||||
plan.PartyBonus = rules.partyMemberDropBonus[partyMembers-1]
|
||||
plan.PartyBonusFound = true
|
||||
}
|
||||
plan.GoldCardCost, plan.GoldCardCostFound = rules.goldCardCostByLevel[level]
|
||||
plan.ItemReference, plan.ItemReferenceFound = rules.ItemReference(level)
|
||||
return plan
|
||||
}
|
||||
|
||||
// Resolve applies the formula used by the current server/client clear-card
|
||||
// flow: room rate multiplied by the additive difficulty bonus (1/1000 scale)
|
||||
// and the party-size multiplier, capped to the PVF's 1/10000 probability
|
||||
// denominator. Normal difficulty has a zero additive bonus, not a zero
|
||||
// multiplier.
|
||||
func (rules *ClearRewardRules) Resolve(level int64, difficultyIndex, partyMembers, visitedRoomCount int) (ClearRewardRule, error) {
|
||||
if rules == nil || level <= 0 {
|
||||
return ClearRewardRule{}, ErrClearRewardRuleSourceRequired
|
||||
}
|
||||
if difficultyIndex < 0 || difficultyIndex >= len(rules.difficultyDropBonus) {
|
||||
difficultyIndex = 0
|
||||
}
|
||||
if partyMembers < 1 {
|
||||
partyMembers = 1
|
||||
} else if partyMembers > len(rules.partyMemberDropBonus) {
|
||||
partyMembers = len(rules.partyMemberDropBonus)
|
||||
}
|
||||
if visitedRoomCount < 1 {
|
||||
visitedRoomCount = 1
|
||||
} else if visitedRoomCount > len(rules.itemRateByMapCount) {
|
||||
visitedRoomCount = len(rules.itemRateByMapCount)
|
||||
}
|
||||
plan := rules.PlanSource(level, difficultyIndex, partyMembers, visitedRoomCount)
|
||||
if !plan.ItemRateFound || !plan.DifficultyFound || !plan.PartyBonusFound ||
|
||||
!plan.GoldDifficultyFound || !plan.GoldCardCostFound || !plan.ItemReferenceFound {
|
||||
return ClearRewardRule{}, fmt.Errorf(
|
||||
"%w: level=%d difficulty=%d party=%d rooms=%d coverage=%+v",
|
||||
ErrClearRewardRuleLookupMissing, level, difficultyIndex, partyMembers, visitedRoomCount, plan,
|
||||
)
|
||||
}
|
||||
if plan.ItemRate < 0 || plan.DifficultyBonus < 0 || plan.PartyBonus <= 0 ||
|
||||
math.IsNaN(plan.PartyBonus) || math.IsInf(plan.PartyBonus, 0) ||
|
||||
plan.GoldCardCost <= 0 || plan.GoldCardCost > math.MaxUint32 ||
|
||||
plan.ItemReference.ValueA < 0 || plan.ItemReference.ValueB < 0 {
|
||||
return ClearRewardRule{}, fmt.Errorf("%w: resolved source=%+v", ErrClearRewardRuleSectionShape, plan)
|
||||
}
|
||||
raw := rules.RawTables()
|
||||
var rarityThresholds [7]uint32
|
||||
for index, threshold := range raw.RarityDecision {
|
||||
if threshold < 0 || threshold > math.MaxUint32 {
|
||||
return ClearRewardRule{}, fmt.Errorf(
|
||||
"%w: section=basis of rarity dicision row=%d value=%d",
|
||||
ErrClearRewardRuleSectionShape, index, threshold,
|
||||
)
|
||||
}
|
||||
rarityThresholds[index] = uint32(threshold)
|
||||
}
|
||||
rarityScale := rarityThresholds[len(rarityThresholds)-1]
|
||||
if rarityScale == 0 {
|
||||
return ClearRewardRule{}, fmt.Errorf("%w: section=basis of rarity dicision final_threshold=0", ErrClearRewardRuleSectionShape)
|
||||
}
|
||||
rate := math.Floor(float64(plan.ItemRate) *
|
||||
(clearRewardBonusDenominator + float64(plan.DifficultyBonus)) /
|
||||
clearRewardBonusDenominator * plan.PartyBonus)
|
||||
if rate < 0 {
|
||||
rate = 0
|
||||
} else if rate > float64(ClearRewardItemRateDenominator) {
|
||||
rate = float64(ClearRewardItemRateDenominator)
|
||||
}
|
||||
return ClearRewardRule{
|
||||
EquipmentRate: uint32(rate),
|
||||
GradeDown: plan.ItemReference.ValueA,
|
||||
GradeUp: plan.ItemReference.ValueB,
|
||||
RarityThresholds: rarityThresholds,
|
||||
RarityScale: rarityScale,
|
||||
PaidCardCost: uint32(plan.GoldCardCost),
|
||||
GoldDifficultyRate: plan.GoldDifficultyRate,
|
||||
MapCount: visitedRoomCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rules *ClearRewardRules) Snapshot() ClearRewardRuleSnapshot {
|
||||
if rules == nil {
|
||||
return ClearRewardRuleSnapshot{}
|
||||
}
|
||||
return ClearRewardRuleSnapshot{
|
||||
MapRateRows: len(rules.itemRateByMapCount),
|
||||
DifficultyBonusRows: len(rules.difficultyDropBonus),
|
||||
RarityThresholdRows: len(rules.rarityDecision),
|
||||
PartyBonusRows: len(rules.partyMemberDropBonus),
|
||||
GoldDifficultyRows: len(rules.goldDifficultyDropBonus),
|
||||
GoldCardCostRows: len(rules.goldCardCostByLevel),
|
||||
ItemReferenceRows: len(rules.itemReferences),
|
||||
}
|
||||
}
|
||||
|
||||
func (rules *ClearRewardRules) RawTables() ClearRewardRawTables {
|
||||
if rules == nil {
|
||||
return ClearRewardRawTables{}
|
||||
}
|
||||
return ClearRewardRawTables{
|
||||
ItemRateByMapCount: cloneClearRewardIntMap(rules.itemRateByMapCount),
|
||||
DifficultyDropBonus: rules.difficultyDropBonus,
|
||||
RarityDecision: rules.rarityDecision,
|
||||
PartyMemberDropBonus: rules.partyMemberDropBonus,
|
||||
GoldDifficultyDropBonus: rules.goldDifficultyDropBonus,
|
||||
GoldCardCostByLevel: cloneClearRewardIntMap(rules.goldCardCostByLevel),
|
||||
}
|
||||
}
|
||||
|
||||
func cloneClearRewardIntMap(source map[int64]int64) map[int64]int64 {
|
||||
if len(source) == 0 {
|
||||
return nil
|
||||
}
|
||||
clone := make(map[int64]int64, len(source))
|
||||
for key, value := range source {
|
||||
clone[key] = value
|
||||
}
|
||||
return clone
|
||||
}
|
||||
Reference in new issue
Block a user