DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

@@ -0,0 +1,310 @@
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 := &currentVendingMachineCatalog{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 = &currentVendingMachineGroupSections{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
}