服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
311 lines
12 KiB
Go
311 lines
12 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"path"
|
|
"strings"
|
|
|
|
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
|
)
|
|
|
|
const currentVendingMachineListPVFPath = "etc/vendingmachine.lst"
|
|
|
|
var (
|
|
errCurrentVendingMachineCatalogUnavailable = errors.New("dnf current vending-machine PVF catalog is unavailable")
|
|
errCurrentVendingMachineDefinitionInvalid = errors.New("dnf current vending-machine PVF definition is invalid")
|
|
errCurrentVendingMachineGroupUnavailable = errors.New("dnf current vending-machine group is unavailable")
|
|
)
|
|
|
|
// currentVendingMachineOutput is one relative-weight row from a PVF [output]
|
|
// section. Current NoPack always includes Weight in the draw table; JackpotFlag
|
|
// additionally adds the item/count to its separate machine-announcement list.
|
|
type currentVendingMachineOutput struct {
|
|
ItemID uint32
|
|
Weight uint64
|
|
Count uint32
|
|
JackpotFlag byte
|
|
}
|
|
|
|
type currentVendingMachineGroup struct {
|
|
MachineID uint32
|
|
GroupID uint32
|
|
PVFPath string
|
|
MaterialItemID uint32
|
|
MaterialCount uint32
|
|
Outputs []currentVendingMachineOutput
|
|
TotalWeight uint64
|
|
}
|
|
|
|
type currentVendingMachineCatalog struct {
|
|
groups map[uint32]map[uint32]currentVendingMachineGroup
|
|
}
|
|
|
|
func newCurrentVendingMachineCatalog(source dnfpvf.Source) (*currentVendingMachineCatalog, error) {
|
|
if source == nil {
|
|
return nil, errCurrentVendingMachineCatalogUnavailable
|
|
}
|
|
listText, err := source.ReadText(currentVendingMachineListPVFPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: read %s: %v", errCurrentVendingMachineCatalogUnavailable, currentVendingMachineListPVFPath, err)
|
|
}
|
|
listDocument, err := dnfpvf.Parse(currentVendingMachineListPVFPath, listText)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: parse %s: %v", errCurrentVendingMachineCatalogUnavailable, currentVendingMachineListPVFPath, err)
|
|
}
|
|
entries := dnfpvf.ParseList(listDocument)
|
|
if len(entries) == 0 {
|
|
return nil, fmt.Errorf("%w: %s contains no machines", errCurrentVendingMachineCatalogUnavailable, currentVendingMachineListPVFPath)
|
|
}
|
|
|
|
catalog := ¤tVendingMachineCatalog{groups: make(map[uint32]map[uint32]currentVendingMachineGroup)}
|
|
listedMachines := make(map[uint32]struct{}, len(entries))
|
|
for _, entry := range entries {
|
|
if entry.ID <= 0 || entry.ID > math.MaxUint32 {
|
|
return nil, fmt.Errorf("%w: machine id=%d is outside u32", errCurrentVendingMachineDefinitionInvalid, entry.ID)
|
|
}
|
|
machineID := uint32(entry.ID)
|
|
if _, duplicate := listedMachines[machineID]; duplicate {
|
|
return nil, fmt.Errorf("%w: duplicate machine id=%d", errCurrentVendingMachineDefinitionInvalid, machineID)
|
|
}
|
|
listedMachines[machineID] = struct{}{}
|
|
|
|
documentPath, document, readErr := readCurrentVendingMachineDocument(source, entry.Path)
|
|
if readErr != nil {
|
|
return nil, fmt.Errorf("%w: machine=%d path=%s: %v", errCurrentVendingMachineCatalogUnavailable, machineID, entry.Path, readErr)
|
|
}
|
|
groups, parseErr := parseCurrentVendingMachineDefinition(machineID, documentPath, document)
|
|
if parseErr != nil {
|
|
return nil, parseErr
|
|
}
|
|
// The runtime list contains an intentionally empty dummy definition. Empty
|
|
// [item group] blocks do not create a requestable machine or group.
|
|
if len(groups) == 0 {
|
|
continue
|
|
}
|
|
catalog.groups[machineID] = groups
|
|
}
|
|
if len(catalog.groups) == 0 {
|
|
return nil, fmt.Errorf("%w: no usable machine groups", errCurrentVendingMachineCatalogUnavailable)
|
|
}
|
|
return catalog, nil
|
|
}
|
|
|
|
func (c *currentVendingMachineCatalog) Resolve(machineID, groupID uint32) (currentVendingMachineGroup, error) {
|
|
if c == nil || c.groups == nil {
|
|
return currentVendingMachineGroup{}, errCurrentVendingMachineCatalogUnavailable
|
|
}
|
|
groups, ok := c.groups[machineID]
|
|
if !ok {
|
|
return currentVendingMachineGroup{}, fmt.Errorf("%w: machine=%d group=%d", errCurrentVendingMachineGroupUnavailable, machineID, groupID)
|
|
}
|
|
group, ok := groups[groupID]
|
|
if !ok {
|
|
return currentVendingMachineGroup{}, fmt.Errorf("%w: machine=%d group=%d", errCurrentVendingMachineGroupUnavailable, machineID, groupID)
|
|
}
|
|
group.Outputs = append([]currentVendingMachineOutput(nil), group.Outputs...)
|
|
return group, nil
|
|
}
|
|
|
|
func (c *currentVendingMachineCatalog) MachineCount() int {
|
|
if c == nil {
|
|
return 0
|
|
}
|
|
return len(c.groups)
|
|
}
|
|
|
|
func readCurrentVendingMachineDocument(source dnfpvf.Source, listedPath string) (string, *dnfpvf.Document, error) {
|
|
cleaned := cleanCurrentVendingMachinePath(listedPath)
|
|
if cleaned == "" || !strings.EqualFold(path.Ext(cleaned), ".vm") {
|
|
return "", nil, fmt.Errorf("%w: listed path=%q is not a .vm document", errCurrentVendingMachineDefinitionInvalid, listedPath)
|
|
}
|
|
candidates := []string{cleaned}
|
|
if !strings.HasPrefix(strings.ToLower(cleaned), "etc/") {
|
|
candidates = append(candidates, path.Join("etc", cleaned))
|
|
}
|
|
var lastErr error
|
|
for _, candidate := range candidates {
|
|
text, err := source.ReadText(candidate)
|
|
if err != nil {
|
|
lastErr = err
|
|
continue
|
|
}
|
|
document, err := dnfpvf.Parse(candidate, text)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
return candidate, document, nil
|
|
}
|
|
return "", nil, lastErr
|
|
}
|
|
|
|
func cleanCurrentVendingMachinePath(value string) string {
|
|
value = strings.TrimSpace(strings.Trim(value, "`\"'"))
|
|
value = strings.ReplaceAll(value, "\\", "/")
|
|
value = strings.TrimPrefix(value, "./")
|
|
return strings.TrimPrefix(value, "/")
|
|
}
|
|
|
|
type currentVendingMachineGroupSections struct {
|
|
groupNum []dnfpvf.Token
|
|
material []dnfpvf.Token
|
|
output []dnfpvf.Token
|
|
seen map[string]struct{}
|
|
}
|
|
|
|
func parseCurrentVendingMachineDefinition(
|
|
machineID uint32,
|
|
documentPath string,
|
|
document *dnfpvf.Document,
|
|
) (map[uint32]currentVendingMachineGroup, error) {
|
|
if machineID == 0 || document == nil {
|
|
return nil, fmt.Errorf("%w: machine=%d path=%s", errCurrentVendingMachineDefinitionInvalid, machineID, documentPath)
|
|
}
|
|
groups := make(map[uint32]currentVendingMachineGroup)
|
|
var current *currentVendingMachineGroupSections
|
|
for _, section := range document.Sections {
|
|
name := strings.ToLower(strings.TrimSpace(section.Name))
|
|
switch name {
|
|
case "item group":
|
|
if current != nil {
|
|
return nil, fmt.Errorf("%w: machine=%d path=%s nested [item group]", errCurrentVendingMachineDefinitionInvalid, machineID, documentPath)
|
|
}
|
|
current = ¤tVendingMachineGroupSections{seen: make(map[string]struct{}, 3)}
|
|
case "/item group":
|
|
if current == nil {
|
|
return nil, fmt.Errorf("%w: machine=%d path=%s unexpected [/item group]", errCurrentVendingMachineDefinitionInvalid, machineID, documentPath)
|
|
}
|
|
group, empty, err := buildCurrentVendingMachineGroup(machineID, documentPath, *current)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !empty {
|
|
if _, duplicate := groups[group.GroupID]; duplicate {
|
|
return nil, fmt.Errorf("%w: machine=%d duplicate group=%d", errCurrentVendingMachineDefinitionInvalid, machineID, group.GroupID)
|
|
}
|
|
groups[group.GroupID] = group
|
|
}
|
|
current = nil
|
|
case "group num", "material", "output":
|
|
if current == nil {
|
|
return nil, fmt.Errorf("%w: machine=%d path=%s [%s] is outside [item group]", errCurrentVendingMachineDefinitionInvalid, machineID, documentPath, name)
|
|
}
|
|
if _, duplicate := current.seen[name]; duplicate {
|
|
return nil, fmt.Errorf("%w: machine=%d path=%s duplicate [%s]", errCurrentVendingMachineDefinitionInvalid, machineID, documentPath, name)
|
|
}
|
|
current.seen[name] = struct{}{}
|
|
tokens, err := currentVendingMachineSectionTokens(document, section)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: machine=%d path=%s [%s]: %v", errCurrentVendingMachineDefinitionInvalid, machineID, documentPath, name, err)
|
|
}
|
|
switch name {
|
|
case "group num":
|
|
current.groupNum = tokens
|
|
case "material":
|
|
current.material = tokens
|
|
case "output":
|
|
current.output = tokens
|
|
}
|
|
}
|
|
}
|
|
if current != nil {
|
|
return nil, fmt.Errorf("%w: machine=%d path=%s unclosed [item group]", errCurrentVendingMachineDefinitionInvalid, machineID, documentPath)
|
|
}
|
|
return groups, nil
|
|
}
|
|
|
|
func currentVendingMachineSectionTokens(document *dnfpvf.Document, section dnfpvf.Section) ([]dnfpvf.Token, error) {
|
|
if document == nil || section.Start < 0 || section.End < section.Start || section.End > len(document.Tokens) {
|
|
return nil, errors.New("invalid token range")
|
|
}
|
|
tokens := append([]dnfpvf.Token(nil), document.Tokens[section.Start:section.End]...)
|
|
for _, token := range tokens {
|
|
if token.Kind != dnfpvf.TokenInt {
|
|
return nil, fmt.Errorf("line=%d column=%d token=%q is not an integer", token.Line, token.Column, token.Raw)
|
|
}
|
|
}
|
|
return tokens, nil
|
|
}
|
|
|
|
func buildCurrentVendingMachineGroup(
|
|
machineID uint32,
|
|
documentPath string,
|
|
sections currentVendingMachineGroupSections,
|
|
) (currentVendingMachineGroup, bool, error) {
|
|
if len(sections.seen) == 0 {
|
|
return currentVendingMachineGroup{}, true, nil
|
|
}
|
|
if len(sections.groupNum) != 1 || len(sections.material) != 2 || len(sections.output) == 0 || len(sections.output)%4 != 0 {
|
|
return currentVendingMachineGroup{}, false, fmt.Errorf(
|
|
"%w: machine=%d path=%s group_num=%d material=%d output=%d",
|
|
errCurrentVendingMachineDefinitionInvalid,
|
|
machineID,
|
|
documentPath,
|
|
len(sections.groupNum),
|
|
len(sections.material),
|
|
len(sections.output),
|
|
)
|
|
}
|
|
groupIDValue := sections.groupNum[0].Int
|
|
materialIDValue := sections.material[0].Int
|
|
materialCountValue := sections.material[1].Int
|
|
if groupIDValue <= 0 || groupIDValue > math.MaxUint32 {
|
|
return currentVendingMachineGroup{}, false, fmt.Errorf("%w: machine=%d group=%d is outside u32", errCurrentVendingMachineDefinitionInvalid, machineID, groupIDValue)
|
|
}
|
|
if materialIDValue <= 0 || materialIDValue > math.MaxUint32 || materialCountValue <= 0 || materialCountValue > math.MaxUint32 {
|
|
return currentVendingMachineGroup{}, false, fmt.Errorf(
|
|
"%w: machine=%d group=%d material=%d count=%d",
|
|
errCurrentVendingMachineDefinitionInvalid,
|
|
machineID,
|
|
groupIDValue,
|
|
materialIDValue,
|
|
materialCountValue,
|
|
)
|
|
}
|
|
|
|
group := currentVendingMachineGroup{
|
|
MachineID: machineID,
|
|
GroupID: uint32(groupIDValue),
|
|
PVFPath: documentPath,
|
|
MaterialItemID: uint32(materialIDValue),
|
|
MaterialCount: uint32(materialCountValue),
|
|
Outputs: make([]currentVendingMachineOutput, 0, len(sections.output)/4),
|
|
}
|
|
positiveWeight := false
|
|
for index := 0; index < len(sections.output); index += 4 {
|
|
itemIDValue := sections.output[index].Int
|
|
weightValue := sections.output[index+1].Int
|
|
countValue := sections.output[index+2].Int
|
|
flagValue := sections.output[index+3].Int
|
|
if itemIDValue <= 0 || itemIDValue > math.MaxUint32 {
|
|
return currentVendingMachineGroup{}, false, fmt.Errorf("%w: machine=%d group=%d output=%d item=%d", errCurrentVendingMachineDefinitionInvalid, machineID, group.GroupID, index/4, itemIDValue)
|
|
}
|
|
if weightValue < 0 {
|
|
return currentVendingMachineGroup{}, false, fmt.Errorf("%w: machine=%d group=%d output=%d weight=%d", errCurrentVendingMachineDefinitionInvalid, machineID, group.GroupID, index/4, weightValue)
|
|
}
|
|
if countValue <= 0 || countValue > math.MaxUint32 {
|
|
return currentVendingMachineGroup{}, false, fmt.Errorf("%w: machine=%d group=%d output=%d count=%d", errCurrentVendingMachineDefinitionInvalid, machineID, group.GroupID, index/4, countValue)
|
|
}
|
|
if flagValue != 0 && flagValue != 1 {
|
|
return currentVendingMachineGroup{}, false, fmt.Errorf("%w: machine=%d group=%d output=%d jackpot_flag=%d", errCurrentVendingMachineDefinitionInvalid, machineID, group.GroupID, index/4, flagValue)
|
|
}
|
|
weight := uint64(weightValue)
|
|
if math.MaxUint64-group.TotalWeight < weight {
|
|
return currentVendingMachineGroup{}, false, fmt.Errorf("%w: machine=%d group=%d total weight overflows u64", errCurrentVendingMachineDefinitionInvalid, machineID, group.GroupID)
|
|
}
|
|
group.TotalWeight += weight
|
|
positiveWeight = positiveWeight || weight > 0
|
|
group.Outputs = append(group.Outputs, currentVendingMachineOutput{
|
|
ItemID: uint32(itemIDValue),
|
|
Weight: weight,
|
|
Count: uint32(countValue),
|
|
JackpotFlag: byte(flagValue),
|
|
})
|
|
}
|
|
if !positiveWeight {
|
|
return currentVendingMachineGroup{}, false, fmt.Errorf("%w: machine=%d group=%d has no positive output weight", errCurrentVendingMachineDefinitionInvalid, machineID, group.GroupID)
|
|
}
|
|
return group, false, nil
|
|
}
|