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,762 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/alignedcmd"
|
||||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||||
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
||||
)
|
||||
|
||||
const (
|
||||
currentRandomOptionOverallPath = "etc/randomoption/randomizedoptionoverall2.etc"
|
||||
currentRandomOptionQuantityPath = "etc/randomoption/optionquantity.etc"
|
||||
currentRandomOptionSelectPath = "etc/randomoption/optiongroupselection.etc"
|
||||
currentRandomOptionGroupingPath = "etc/randomoption/optiongrouping.etc"
|
||||
currentRandomOptionListPath = "etc/randomoption/randomoption.lst"
|
||||
)
|
||||
|
||||
// Current randomizedoptionoverall2.etc section names used by the seal-grade
|
||||
// reset (op455). [postfix grade modification] rows are
|
||||
// "rarity grade minPct maxPct weight"; the two exemption sections are flat
|
||||
// option-id lists.
|
||||
const (
|
||||
currentRandomOptionGradeSection = "postfix grade modification"
|
||||
currentRandomOptionUnmodifiableSection = "unable to modify postfix grade"
|
||||
currentRandomOptionGradelessSection = "gradeless postfix"
|
||||
// [different weight] rows are "rarity slot minPct maxPct": the percentile
|
||||
// window each 1-based seal slot rolls its VALUE in. This is the table the
|
||||
// client's own CNRDRandomOptionManager::importRandomOptionsScript parses
|
||||
// next to [postfix grade modification], and the two live in the same
|
||||
// 0..100 percentile space (rare slot 3 tops out at 55, the rare S band at
|
||||
// 56). [changed postfix] carries the same shape for op437 when present.
|
||||
currentRandomOptionDifferentWeightSection = "different weight"
|
||||
currentRandomOptionChangedPostfixSection = "changed postfix"
|
||||
)
|
||||
|
||||
type currentRandomOptionConfig struct {
|
||||
quantity map[int64][]alignedcmd.RandomOptionWeightedQuantity
|
||||
initialSelect map[int64]map[string][3]int64
|
||||
modifiedSelect map[int64]map[string][3]int64
|
||||
groups map[int64][]currentRandomOptionWeightedID
|
||||
optionPaths map[int64]string
|
||||
breakSealCosts []currentRandomOptionBreakCost
|
||||
modifyCosts []currentRandomOptionModifyCost
|
||||
gradeBands map[int64][]alignedcmd.RandomOptionGradeBand
|
||||
unmodifiable map[byte]struct{}
|
||||
gradeless map[byte]struct{}
|
||||
slotWindows map[int64][]alignedcmd.RandomOptionPercentWindow
|
||||
changeWindows map[int64][]alignedcmd.RandomOptionPercentWindow
|
||||
}
|
||||
|
||||
type currentRandomOptionWeightedID struct {
|
||||
optionID int64
|
||||
weight int64
|
||||
}
|
||||
|
||||
type currentRandomOptionBreakCost struct {
|
||||
rarity int64
|
||||
level int64
|
||||
optionSlot int64
|
||||
cost int64
|
||||
}
|
||||
|
||||
type currentRandomOptionModifyCost struct {
|
||||
level int64
|
||||
commonCost int64
|
||||
uniqueCost int64
|
||||
}
|
||||
|
||||
func (s *Service) alignedRandomOptionResolverForCommand(opcode dnfenum.CmdPacket) (alignedcmd.RandomOptionResolver, error) {
|
||||
if opcode != dnfenum.CmdPacketUnsealRandomOption &&
|
||||
opcode != dnfenum.CmdPacketChangeRandomOption &&
|
||||
opcode != dnfenum.CmdPacketResetRandomOption {
|
||||
return nil, nil
|
||||
}
|
||||
catalog, err := s.currentPVFItemCatalog()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if catalog == nil {
|
||||
return nil, errDungeonDropSourceRequired
|
||||
}
|
||||
s.initialEquipmentMu.Lock()
|
||||
source, err := s.initialEquipmentArchiveLocked()
|
||||
s.initialEquipmentMu.Unlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config, err := loadCurrentRandomOptionConfig(source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func(targetItemID int64) (alignedcmd.RandomOptionResolution, error) {
|
||||
return resolveCurrentRandomOptionMetadata(catalog, source, config, targetItemID)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func loadCurrentRandomOptionConfig(source dnfpvf.Source) (*currentRandomOptionConfig, error) {
|
||||
if source == nil {
|
||||
return nil, dnfpvf.ErrSourceRequired
|
||||
}
|
||||
overall, err := parseCurrentRandomOptionDocument(source, currentRandomOptionOverallPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
quantity, err := parseCurrentRandomOptionDocument(source, currentRandomOptionQuantityPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
selection, err := parseCurrentRandomOptionDocument(source, currentRandomOptionSelectPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
grouping, err := parseCurrentRandomOptionDocument(source, currentRandomOptionGroupingPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list, err := parseCurrentRandomOptionDocument(source, currentRandomOptionListPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := ¤tRandomOptionConfig{
|
||||
quantity: parseCurrentRandomOptionQuantities(quantity),
|
||||
initialSelect: make(map[int64]map[string][3]int64),
|
||||
modifiedSelect: make(map[int64]map[string][3]int64),
|
||||
groups: parseCurrentRandomOptionGroups(grouping),
|
||||
optionPaths: make(map[int64]string),
|
||||
gradeBands: parseCurrentRandomOptionGradeBands(overall),
|
||||
unmodifiable: parseCurrentRandomOptionOptionIDs(overall, currentRandomOptionUnmodifiableSection),
|
||||
gradeless: parseCurrentRandomOptionOptionIDs(overall, currentRandomOptionGradelessSection),
|
||||
slotWindows: parseCurrentRandomOptionSlotWindows(overall, currentRandomOptionDifferentWeightSection),
|
||||
changeWindows: parseCurrentRandomOptionSlotWindows(overall, currentRandomOptionChangedPostfixSection),
|
||||
}
|
||||
parseCurrentRandomOptionSelections(selection, config.initialSelect, config.modifiedSelect)
|
||||
config.breakSealCosts, config.modifyCosts = parseCurrentRandomOptionCosts(overall)
|
||||
for _, entry := range dnfpvf.ParseList(list) {
|
||||
if entry.ID <= 0 || strings.TrimSpace(entry.Path) == "" {
|
||||
continue
|
||||
}
|
||||
config.optionPaths[entry.ID] = path.Clean(path.Join(path.Dir(currentRandomOptionListPath), entry.Path))
|
||||
}
|
||||
if len(config.quantity[2]) == 0 || len(config.quantity[3]) == 0 {
|
||||
return nil, fmt.Errorf("random-option quantity config missing rare or unique weights")
|
||||
}
|
||||
if len(config.initialSelect) == 0 || len(config.modifiedSelect) == 0 || len(config.groups) == 0 || len(config.optionPaths) == 0 {
|
||||
return nil, fmt.Errorf("random-option config incomplete: initial=%d modified=%d groups=%d options=%d", len(config.initialSelect), len(config.modifiedSelect), len(config.groups), len(config.optionPaths))
|
||||
}
|
||||
if len(config.breakSealCosts) == 0 || len(config.modifyCosts) == 0 {
|
||||
return nil, fmt.Errorf("random-option cost config incomplete: break=%d modify=%d", len(config.breakSealCosts), len(config.modifyCosts))
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func parseCurrentRandomOptionDocument(source dnfpvf.Source, documentPath string) (*dnfpvf.Document, error) {
|
||||
text, err := source.ReadText(documentPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read random-option PVF %s: %w", documentPath, err)
|
||||
}
|
||||
document, err := dnfpvf.Parse(documentPath, text)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse random-option PVF %s: %w", documentPath, err)
|
||||
}
|
||||
return document, nil
|
||||
}
|
||||
|
||||
// parseCurrentRandomOptionGradeBands reads [postfix grade modification]:
|
||||
// one row per "rarity grade minPct maxPct weight". Grade 1..4 are the C/B/A/S
|
||||
// seal grades, so the percentage bands are the real grade boundaries the client
|
||||
// paints next to each seal option.
|
||||
func parseCurrentRandomOptionGradeBands(document *dnfpvf.Document) map[int64][]alignedcmd.RandomOptionGradeBand {
|
||||
out := make(map[int64][]alignedcmd.RandomOptionGradeBand, 2)
|
||||
for _, section := range document.Sections {
|
||||
if currentRandomOptionSectionKey(section.Name) != currentRandomOptionGradeSection {
|
||||
continue
|
||||
}
|
||||
values := currentRandomOptionSectionInts(document, section)
|
||||
for index := 0; index+4 < len(values); index += 5 {
|
||||
rarity, grade, minPct, maxPct, weight := values[index], values[index+1], values[index+2], values[index+3], values[index+4]
|
||||
if (rarity != 2 && rarity != 3) || grade < 1 || grade > 4 {
|
||||
continue
|
||||
}
|
||||
if minPct < 0 || maxPct < minPct || maxPct > 100 || weight <= 0 {
|
||||
continue
|
||||
}
|
||||
out[rarity] = append(out[rarity], alignedcmd.RandomOptionGradeBand{
|
||||
Grade: grade,
|
||||
MinPct: minPct,
|
||||
MaxPct: maxPct,
|
||||
Weight: weight,
|
||||
})
|
||||
}
|
||||
}
|
||||
for rarity, bands := range out {
|
||||
sort.Slice(bands, func(i, j int) bool { return bands[i].Grade < bands[j].Grade })
|
||||
out[rarity] = bands
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// parseCurrentRandomOptionOptionIDs reads a flat option-id list section such as
|
||||
// [unable to modify postfix grade] or [gradeless postfix].
|
||||
func parseCurrentRandomOptionOptionIDs(document *dnfpvf.Document, sectionName string) map[byte]struct{} {
|
||||
out := make(map[byte]struct{})
|
||||
for _, section := range document.Sections {
|
||||
if currentRandomOptionSectionKey(section.Name) != sectionName {
|
||||
continue
|
||||
}
|
||||
for _, value := range currentRandomOptionSectionInts(document, section) {
|
||||
if value > 0 && value <= math.MaxUint8 {
|
||||
out[byte(value)] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// parseCurrentRandomOptionSlotWindows reads [different weight] (and its op437
|
||||
// sibling [changed postfix] when the PVF ships one): rows of
|
||||
// "rarity slot minPct maxPct" giving the percentile window each 1-based seal
|
||||
// slot rolls the option VALUE in. The current PVF has no [changed postfix], so
|
||||
// the change flow reuses these windows; see the owner for the fallback.
|
||||
func parseCurrentRandomOptionSlotWindows(document *dnfpvf.Document, sectionName string) map[int64][]alignedcmd.RandomOptionPercentWindow {
|
||||
out := make(map[int64][]alignedcmd.RandomOptionPercentWindow, 2)
|
||||
for _, section := range document.Sections {
|
||||
if currentRandomOptionSectionKey(section.Name) != sectionName {
|
||||
continue
|
||||
}
|
||||
values := currentRandomOptionSectionInts(document, section)
|
||||
for index := 0; index+3 < len(values); index += 4 {
|
||||
rarity, slot, minPct, maxPct := values[index], values[index+1], values[index+2], values[index+3]
|
||||
if (rarity != 2 && rarity != 3) || slot < 1 || slot > 3 {
|
||||
continue
|
||||
}
|
||||
if minPct < 0 || maxPct < minPct || maxPct > 100 {
|
||||
continue
|
||||
}
|
||||
out[rarity] = append(out[rarity], alignedcmd.RandomOptionPercentWindow{
|
||||
Slot: slot,
|
||||
MinPct: minPct,
|
||||
MaxPct: maxPct,
|
||||
})
|
||||
}
|
||||
}
|
||||
for rarity, windows := range out {
|
||||
sort.Slice(windows, func(i, j int) bool { return windows[i].Slot < windows[j].Slot })
|
||||
out[rarity] = windows
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseCurrentRandomOptionQuantities(document *dnfpvf.Document) map[int64][]alignedcmd.RandomOptionWeightedQuantity {
|
||||
out := make(map[int64][]alignedcmd.RandomOptionWeightedQuantity, 2)
|
||||
for _, section := range document.Sections {
|
||||
var rarity int64
|
||||
switch currentRandomOptionSectionKey(section.Name) {
|
||||
case "common":
|
||||
rarity = 2
|
||||
case "unique":
|
||||
rarity = 3
|
||||
default:
|
||||
continue
|
||||
}
|
||||
values := currentRandomOptionSectionInts(document, section)
|
||||
for index := 0; index+1 < len(values); index += 2 {
|
||||
quantity, weight := values[index], values[index+1]
|
||||
if quantity >= 1 && quantity <= 3 && weight > 0 {
|
||||
out[rarity] = append(out[rarity], alignedcmd.RandomOptionWeightedQuantity{Quantity: byte(quantity), Weight: weight})
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseCurrentRandomOptionSelections(document *dnfpvf.Document, initial map[int64]map[string][3]int64, modified map[int64]map[string][3]int64) {
|
||||
for _, section := range document.Sections {
|
||||
key := currentRandomOptionSectionKey(section.Name)
|
||||
var target map[int64]map[string][3]int64
|
||||
switch {
|
||||
case strings.Contains(key, "modified option selection"):
|
||||
target = modified
|
||||
case strings.Contains(key, "choose option group"):
|
||||
target = initial
|
||||
default:
|
||||
continue
|
||||
}
|
||||
values := currentRandomOptionSectionValues(document, section)
|
||||
for index, value := range values {
|
||||
if value.text == "" || index < 2 || index+1 >= len(values) {
|
||||
continue
|
||||
}
|
||||
rarity, rarityOK := values[index-2].integer()
|
||||
marker, markerOK := values[index-1].integer()
|
||||
if !rarityOK || !markerOK || marker != -1 || (rarity != 2 && rarity != 3) {
|
||||
continue
|
||||
}
|
||||
// The runtime row starts with two control integers (1,3) after the
|
||||
// equipment key. The first three 1000-series integers are the real
|
||||
// group IDs; later integers are flags/defaults and must be ignored.
|
||||
var groups [3]int64
|
||||
groupCount := 0
|
||||
for cursor := index + 1; cursor < len(values) && groupCount < len(groups); cursor++ {
|
||||
group, ok := values[cursor].integer()
|
||||
if ok && group >= 1000 && group < 2000 {
|
||||
groups[groupCount] = group
|
||||
groupCount++
|
||||
}
|
||||
}
|
||||
if groupCount != len(groups) {
|
||||
continue
|
||||
}
|
||||
equipmentKey := normalizeCurrentRandomOptionKey(value.text)
|
||||
if equipmentKey == "" {
|
||||
continue
|
||||
}
|
||||
if target[rarity] == nil {
|
||||
target[rarity] = make(map[string][3]int64)
|
||||
}
|
||||
target[rarity][equipmentKey] = groups
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseCurrentRandomOptionGroups(document *dnfpvf.Document) map[int64][]currentRandomOptionWeightedID {
|
||||
out := make(map[int64][]currentRandomOptionWeightedID)
|
||||
for _, section := range document.Sections {
|
||||
if currentRandomOptionSectionKey(section.Name) != "option group" {
|
||||
continue
|
||||
}
|
||||
values := currentRandomOptionSectionInts(document, section)
|
||||
if len(values) < 3 {
|
||||
continue
|
||||
}
|
||||
groupID := values[0]
|
||||
for index := 1; index+1 < len(values); index += 2 {
|
||||
optionID, weight := values[index], values[index+1]
|
||||
if groupID > 0 && optionID > 0 && weight > 0 {
|
||||
out[groupID] = append(out[groupID], currentRandomOptionWeightedID{optionID: optionID, weight: weight})
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseCurrentRandomOptionCosts(document *dnfpvf.Document) ([]currentRandomOptionBreakCost, []currentRandomOptionModifyCost) {
|
||||
var breaks []currentRandomOptionBreakCost
|
||||
var modifies []currentRandomOptionModifyCost
|
||||
for _, section := range document.Sections {
|
||||
values := currentRandomOptionSectionInts(document, section)
|
||||
switch currentRandomOptionSectionKey(section.Name) {
|
||||
case "break seal cost":
|
||||
for index := 0; index+3 < len(values); index += 4 {
|
||||
row := currentRandomOptionBreakCost{rarity: values[index], level: values[index+1], optionSlot: values[index+2], cost: values[index+3]}
|
||||
if (row.rarity == 2 || row.rarity == 3) && row.level >= 0 && row.optionSlot >= 0 && row.cost >= 0 {
|
||||
breaks = append(breaks, row)
|
||||
}
|
||||
}
|
||||
case "option modification":
|
||||
for index := 0; index+2 < len(values); index += 3 {
|
||||
row := currentRandomOptionModifyCost{level: values[index], commonCost: values[index+1], uniqueCost: values[index+2]}
|
||||
if row.level >= 0 && row.commonCost >= 0 && row.uniqueCost >= 0 {
|
||||
modifies = append(modifies, row)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return breaks, modifies
|
||||
}
|
||||
|
||||
func resolveCurrentRandomOptionMetadata(catalog *pvfDungeonDropCatalog, source dnfpvf.Source, config *currentRandomOptionConfig, targetItemID int64) (alignedcmd.RandomOptionResolution, error) {
|
||||
resolution := alignedcmd.RandomOptionResolution{}
|
||||
if catalog == nil || source == nil || config == nil {
|
||||
return resolution, errDungeonDropSourceRequired
|
||||
}
|
||||
if targetItemID <= 0 || targetItemID > math.MaxUint32 {
|
||||
return resolution, nil
|
||||
}
|
||||
definition, err := catalog.ResolveItem(uint32(targetItemID))
|
||||
if err != nil {
|
||||
if errors.Is(err, errDungeonDropItemUnresolved) {
|
||||
return resolution, nil
|
||||
}
|
||||
return resolution, fmt.Errorf("resolve random-option target item=%d: %w", targetItemID, err)
|
||||
}
|
||||
resolution.TargetKind = string(definition.Kind)
|
||||
resolution.TargetPVFPath = definition.PVFPath
|
||||
if definition.Kind != dungeonDropItemEquipment {
|
||||
return resolution, nil
|
||||
}
|
||||
document, err := parseCurrentRandomOptionDocument(source, definition.PVFPath)
|
||||
if err != nil {
|
||||
return alignedcmd.RandomOptionResolution{}, err
|
||||
}
|
||||
minimumLevel, minimumFound := document.Int("minimum level")
|
||||
rarity, rarityFound := document.Int("rarity")
|
||||
randomOption, randomFound := document.Int("random option")
|
||||
equipmentType, typeFound := document.Text("equipment type")
|
||||
if !minimumFound || !rarityFound || !randomFound || !typeFound || randomOption <= 0 || (rarity != 2 && rarity != 3) {
|
||||
return resolution, nil
|
||||
}
|
||||
resolution.TargetMinimumLevel = minimumLevel
|
||||
resolution.TargetRarity = rarity
|
||||
|
||||
keys := currentRandomOptionEquipmentKeys(definition.PVFPath, equipmentType)
|
||||
initialGroups, initialKey, initialFound := currentRandomOptionSelectedGroups(config.initialSelect[rarity], keys)
|
||||
modifiedGroups, _, modifiedFound := currentRandomOptionSelectedGroups(config.modifiedSelect[rarity], keys)
|
||||
if !initialFound || !modifiedFound {
|
||||
return resolution, nil
|
||||
}
|
||||
resolution.TargetEquipmentKey = initialKey
|
||||
resolution.QuantityWeights = append([]alignedcmd.RandomOptionWeightedQuantity(nil), config.quantity[rarity]...)
|
||||
resolution.InitialGroups, err = resolveCurrentRandomOptionGroups(source, config, initialGroups, minimumLevel)
|
||||
if err != nil {
|
||||
return alignedcmd.RandomOptionResolution{}, fmt.Errorf("resolve initial random-option groups item=%d key=%s: %w", targetItemID, initialKey, err)
|
||||
}
|
||||
resolution.ModifiedGroups, err = resolveCurrentRandomOptionGroups(source, config, modifiedGroups, minimumLevel)
|
||||
if err != nil {
|
||||
return alignedcmd.RandomOptionResolution{}, fmt.Errorf("resolve modified random-option groups item=%d key=%s: %w", targetItemID, initialKey, err)
|
||||
}
|
||||
resolution.BreakSealGoldCost, err = currentRandomOptionBreakSealCost(config.breakSealCosts, rarity, minimumLevel)
|
||||
if err != nil {
|
||||
return alignedcmd.RandomOptionResolution{}, err
|
||||
}
|
||||
resolution.ModificationGoldCost, err = currentRandomOptionModificationCost(config.modifyCosts, rarity, minimumLevel)
|
||||
if err != nil {
|
||||
return alignedcmd.RandomOptionResolution{}, err
|
||||
}
|
||||
resolution.Eligible = len(resolution.QuantityWeights) > 0 && len(resolution.InitialGroups) == 3 && len(resolution.ModifiedGroups) == 3
|
||||
// op455 (装备品级调整箱调整封印属性品级) needs the seal-grade windows for
|
||||
// this item's rarity plus the two PVF exemption lists.
|
||||
if bands := config.gradeBands[rarity]; len(bands) > 0 {
|
||||
resolution.ResetGrades = append([]alignedcmd.RandomOptionGradeBand(nil), bands...)
|
||||
}
|
||||
resolution.GradeUnmodifiable = config.unmodifiable
|
||||
resolution.GradeGradeless = config.gradeless
|
||||
// 词条那两个字节存的是百分比(客户端常量 MAX_OPTION_VALUE=100),所以解封 /
|
||||
// 变更必须掷"百分位"而不是写 PVF 的 (min,max) 原值:窗口来自 [different weight]
|
||||
// 的按槽位表。缺表时留空,owner 侧走退化路径并记日志。
|
||||
resolution.InitialSlotWindows = append([]alignedcmd.RandomOptionPercentWindow(nil), config.slotWindows[rarity]...)
|
||||
resolution.ChangeSlotWindows = append([]alignedcmd.RandomOptionPercentWindow(nil), config.changeWindows[rarity]...)
|
||||
return resolution, nil
|
||||
}
|
||||
|
||||
func resolveCurrentRandomOptionGroups(source dnfpvf.Source, config *currentRandomOptionConfig, groups [3]int64, level int64) ([][]alignedcmd.RandomOptionCandidate, error) {
|
||||
out := make([][]alignedcmd.RandomOptionCandidate, 0, len(groups))
|
||||
for _, groupID := range groups {
|
||||
weighted := config.groups[groupID]
|
||||
if len(weighted) == 0 {
|
||||
return nil, fmt.Errorf("group %d missing", groupID)
|
||||
}
|
||||
candidates := make([]alignedcmd.RandomOptionCandidate, 0, len(weighted))
|
||||
for _, entry := range weighted {
|
||||
optionPath := config.optionPaths[entry.optionID]
|
||||
if optionPath == "" {
|
||||
return nil, fmt.Errorf("group %d option %d path missing", groupID, entry.optionID)
|
||||
}
|
||||
optionType, value1, value2, err := resolveCurrentRandomOptionValues(source, optionPath, level)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("group %d option %d: %w", groupID, entry.optionID, err)
|
||||
}
|
||||
candidates = append(candidates, alignedcmd.RandomOptionCandidate{Type: optionType, Value1: value1, Value2: value2, Weight: entry.weight})
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return nil, fmt.Errorf("group %d has no candidates", groupID)
|
||||
}
|
||||
out = append(out, candidates)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func resolveCurrentRandomOptionValues(source dnfpvf.Source, optionPath string, targetLevel int64) (byte, byte, byte, error) {
|
||||
document, err := parseCurrentRandomOptionDocument(source, optionPath)
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
optionID, found := document.Int("option")
|
||||
if !found || optionID <= 0 || optionID > math.MaxUint8 {
|
||||
return 0, 0, 0, fmt.Errorf("option type invalid: %d found=%t", optionID, found)
|
||||
}
|
||||
dungeonStart, dungeonEnd := -1, len(document.Sections)
|
||||
for index, section := range document.Sections {
|
||||
name := currentRandomOptionSectionKey(section.Name)
|
||||
if dungeonStart < 0 {
|
||||
if name == "dungeon" {
|
||||
dungeonStart = index + 1
|
||||
}
|
||||
continue
|
||||
}
|
||||
if name == "/dungeon" {
|
||||
dungeonEnd = index
|
||||
break
|
||||
}
|
||||
}
|
||||
if dungeonStart < 0 || dungeonStart > dungeonEnd {
|
||||
return 0, 0, 0, fmt.Errorf("option %d dungeon value block missing", optionID)
|
||||
}
|
||||
bestLevel := int64(-1)
|
||||
var bestValues []currentRandomOptionNumber
|
||||
elementProperty, categorical := currentRandomOptionElementProperty(optionID)
|
||||
for index := dungeonStart; index < dungeonEnd; index++ {
|
||||
section := document.Sections[index]
|
||||
if currentRandomOptionSectionKey(section.Name) != "level" {
|
||||
continue
|
||||
}
|
||||
levelValues := currentRandomOptionSectionInts(document, section)
|
||||
if len(levelValues) == 0 || levelValues[0] < 0 || levelValues[0] > targetLevel || levelValues[0] < bestLevel {
|
||||
continue
|
||||
}
|
||||
var values []currentRandomOptionNumber
|
||||
var elementValues []string
|
||||
for cursor := index + 1; cursor < dungeonEnd; cursor++ {
|
||||
name := currentRandomOptionSectionKey(document.Sections[cursor].Name)
|
||||
if name == "level" || name == "/level" {
|
||||
break
|
||||
}
|
||||
values = append(values, currentRandomOptionSectionNumbers(document, document.Sections[cursor])...)
|
||||
if name == "elemental property" || name == "/elemental property" {
|
||||
for _, value := range currentRandomOptionSectionValues(document, document.Sections[cursor]) {
|
||||
if !value.isInteger && strings.TrimSpace(value.text) != "" {
|
||||
elementValues = append(elementValues, normalizeCurrentRandomOptionKey(value.text))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if categorical {
|
||||
if len(elementValues) == 2 && elementValues[0] == elementProperty && elementValues[1] == elementProperty {
|
||||
bestLevel = levelValues[0]
|
||||
bestValues = []currentRandomOptionNumber{{value: 1}, {value: 1}}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if len(values) >= 2 {
|
||||
bestLevel = levelValues[0]
|
||||
bestValues = values[len(values)-2:]
|
||||
}
|
||||
}
|
||||
if bestLevel < 0 || len(bestValues) != 2 {
|
||||
return 0, 0, 0, fmt.Errorf("option %d has no values for target level %d (best_level=%d values=%v)", optionID, targetLevel, bestLevel, bestValues)
|
||||
}
|
||||
value1, err := encodeCurrentRandomOptionNumber(bestValues[0])
|
||||
if err != nil {
|
||||
return 0, 0, 0, fmt.Errorf("option %d value1: %w", optionID, err)
|
||||
}
|
||||
value2, err := encodeCurrentRandomOptionNumber(bestValues[1])
|
||||
if err != nil {
|
||||
return 0, 0, 0, fmt.Errorf("option %d value2: %w", optionID, err)
|
||||
}
|
||||
return byte(optionID), value1, value2, nil
|
||||
}
|
||||
|
||||
// The runtime PVF represents the four elemental-attack options with two
|
||||
// repeated textual endpoints instead of numeric scalars. The option ID itself
|
||||
// identifies the element, while the current item row still has one byte for
|
||||
// each endpoint. Encode the two validated endpoint-presence markers as 1/1.
|
||||
// Keep this closed list strict so unrelated malformed text options still fail.
|
||||
func currentRandomOptionElementProperty(optionID int64) (string, bool) {
|
||||
switch optionID {
|
||||
case 244:
|
||||
return "fireelement", true
|
||||
case 245:
|
||||
return "waterelement", true
|
||||
case 246:
|
||||
return "darkelement", true
|
||||
case 247:
|
||||
return "lightelement", true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
// Current item rows have one byte per random-option scalar and the current EXE
|
||||
// forwards that byte unchanged into its random-option model. Preserve signed
|
||||
// negative values as two's-complement bytes, truncate fractional PVF scalars
|
||||
// toward zero, and saturate values outside the one-byte wire domain.
|
||||
func encodeCurrentRandomOptionNumber(value currentRandomOptionNumber) (byte, error) {
|
||||
encoded := math.Trunc(value.value)
|
||||
if math.IsNaN(encoded) || math.IsInf(encoded, 0) {
|
||||
return 0, fmt.Errorf("PVF scalar %v is not finite", value.value)
|
||||
}
|
||||
if encoded < math.MinInt8 {
|
||||
encoded = math.MinInt8
|
||||
}
|
||||
if encoded > math.MaxUint8 {
|
||||
encoded = math.MaxUint8
|
||||
}
|
||||
if encoded < 0 {
|
||||
return byte(int8(int64(encoded))), nil
|
||||
}
|
||||
return byte(encoded), nil
|
||||
}
|
||||
|
||||
func currentRandomOptionBreakSealCost(rows []currentRandomOptionBreakCost, rarity int64, level int64) (int64, error) {
|
||||
bestLevel := int64(-1)
|
||||
cost := int64(0)
|
||||
found := false
|
||||
for _, row := range rows {
|
||||
if row.rarity == rarity && row.optionSlot == 1 && row.level <= level && row.level >= bestLevel {
|
||||
bestLevel, cost, found = row.level, row.cost, true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return 0, fmt.Errorf("random-option break-seal cost missing for rarity=%d level=%d", rarity, level)
|
||||
}
|
||||
return cost, nil
|
||||
}
|
||||
|
||||
func currentRandomOptionModificationCost(rows []currentRandomOptionModifyCost, rarity int64, level int64) (int64, error) {
|
||||
bestLevel := int64(-1)
|
||||
cost := int64(0)
|
||||
found := false
|
||||
for _, row := range rows {
|
||||
if row.level <= level && row.level >= bestLevel {
|
||||
bestLevel, found = row.level, true
|
||||
if rarity >= 3 {
|
||||
cost = row.uniqueCost
|
||||
} else {
|
||||
cost = row.commonCost
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return 0, fmt.Errorf("random-option modification cost missing for rarity=%d level=%d", rarity, level)
|
||||
}
|
||||
return cost, nil
|
||||
}
|
||||
|
||||
func currentRandomOptionEquipmentKeys(pvfPath string, equipmentType string) []string {
|
||||
keys := make(map[string]struct{})
|
||||
add := func(value string) {
|
||||
if normalized := normalizeCurrentRandomOptionKey(value); normalized != "" {
|
||||
keys[normalized] = struct{}{}
|
||||
}
|
||||
}
|
||||
normalizedEquipmentType := normalizeCurrentRandomOptionKey(equipmentType)
|
||||
add(normalizedEquipmentType)
|
||||
segments := strings.Split(strings.ToLower(strings.ReplaceAll(pvfPath, "\\", "/")), "/")
|
||||
for index, segment := range segments {
|
||||
switch segment {
|
||||
case "weapon":
|
||||
if index+1 < len(segments) {
|
||||
weapon := normalizeCurrentRandomOptionKey(segments[index+1])
|
||||
add(weapon)
|
||||
switch weapon {
|
||||
case "boxglove":
|
||||
add("bglove")
|
||||
case "hsword":
|
||||
add("lswd")
|
||||
case "beamsword":
|
||||
add("beamswd")
|
||||
case "twinsword":
|
||||
add("twinswd")
|
||||
case "chakram":
|
||||
add("chakraweapon")
|
||||
}
|
||||
}
|
||||
case "cloth", "leather", "larmor", "harmor", "plate":
|
||||
prefix := map[string]string{"cloth": "cl", "leather": "lt", "larmor": "la", "harmor": "ha", "plate": "mt"}[segment]
|
||||
// Current PVF armor paths use both material/slot and slot/material
|
||||
// layouts. The live failed rows are the latter, for example
|
||||
// common/shoes/leather/100260138.equ. Derive the canonical
|
||||
// random-option key from [equipment type] first, then retain both
|
||||
// adjacent path orders for older resources.
|
||||
add(prefix + normalizedEquipmentType)
|
||||
if index > 0 && !strings.HasSuffix(segments[index-1], ".equ") {
|
||||
add(prefix + normalizeCurrentRandomOptionKey(segments[index-1]))
|
||||
}
|
||||
if index+1 < len(segments) {
|
||||
if !strings.HasSuffix(segments[index+1], ".equ") {
|
||||
add(prefix + normalizeCurrentRandomOptionKey(segments[index+1]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out := make([]string, 0, len(keys))
|
||||
for key := range keys {
|
||||
out = append(out, key)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func currentRandomOptionSelectedGroups(entries map[string][3]int64, keys []string) ([3]int64, string, bool) {
|
||||
for _, key := range keys {
|
||||
if groups, ok := entries[key]; ok {
|
||||
return groups, key, true
|
||||
}
|
||||
}
|
||||
return [3]int64{}, "", false
|
||||
}
|
||||
|
||||
type currentRandomOptionValue struct {
|
||||
text string
|
||||
intValue int64
|
||||
isInteger bool
|
||||
}
|
||||
|
||||
type currentRandomOptionNumber struct {
|
||||
value float64
|
||||
}
|
||||
|
||||
func (v currentRandomOptionValue) integer() (int64, bool) { return v.intValue, v.isInteger }
|
||||
|
||||
func currentRandomOptionSectionValues(document *dnfpvf.Document, section dnfpvf.Section) []currentRandomOptionValue {
|
||||
if document == nil || section.Start < 0 || section.Start > section.End || section.End > len(document.Tokens) {
|
||||
return nil
|
||||
}
|
||||
values := make([]currentRandomOptionValue, 0, section.End-section.Start)
|
||||
for _, token := range document.Tokens[section.Start:section.End] {
|
||||
switch token.Kind {
|
||||
case dnfpvf.TokenInt:
|
||||
values = append(values, currentRandomOptionValue{text: token.Value, intValue: token.Int, isInteger: true})
|
||||
case dnfpvf.TokenString, dnfpvf.TokenIdent:
|
||||
values = append(values, currentRandomOptionValue{text: token.Value})
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func currentRandomOptionSectionInts(document *dnfpvf.Document, section dnfpvf.Section) []int64 {
|
||||
values := currentRandomOptionSectionValues(document, section)
|
||||
ints := make([]int64, 0, len(values))
|
||||
for _, value := range values {
|
||||
if integer, ok := value.integer(); ok {
|
||||
ints = append(ints, integer)
|
||||
}
|
||||
}
|
||||
return ints
|
||||
}
|
||||
|
||||
func currentRandomOptionSectionNumbers(document *dnfpvf.Document, section dnfpvf.Section) []currentRandomOptionNumber {
|
||||
if document == nil || section.Start < 0 || section.Start > section.End || section.End > len(document.Tokens) {
|
||||
return nil
|
||||
}
|
||||
values := make([]currentRandomOptionNumber, 0, section.End-section.Start)
|
||||
for _, token := range document.Tokens[section.Start:section.End] {
|
||||
switch token.Kind {
|
||||
case dnfpvf.TokenInt:
|
||||
values = append(values, currentRandomOptionNumber{value: float64(token.Int)})
|
||||
case dnfpvf.TokenFloat:
|
||||
values = append(values, currentRandomOptionNumber{value: token.Float})
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func currentRandomOptionSectionKey(value string) string {
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
}
|
||||
|
||||
func normalizeCurrentRandomOptionKey(value string) string {
|
||||
value = strings.ToLower(strings.TrimSpace(value))
|
||||
value = strings.Trim(value, "`[](){}<> ")
|
||||
value = strings.ReplaceAll(value, "_", "")
|
||||
value = strings.ReplaceAll(value, "-", "")
|
||||
value = strings.ReplaceAll(value, " ", "")
|
||||
return value
|
||||
}
|
||||
Reference in new issue
Block a user