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

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

287 lines
8.1 KiB
Go

package hellparty
import (
"errors"
"fmt"
"math"
"sort"
"strings"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
)
const ConfigPath = "etc/hellparty.etc"
var (
ErrSourceRequired = errors.New("dnf hell party PVF source is required")
ErrCatalogInvalid = errors.New("dnf hell party catalog is invalid")
)
type TextSource interface {
ReadText(string) (string, error)
}
// DifficultyRule preserves the five current-PVF values without assigning
// names to fields whose business meaning has not been confirmed in 90CN.
// Value0 is separately exposed as RewardRollCount because the current PVF and
// reference runtime agree on that use; the other values remain opaque.
type DifficultyRule struct {
Key string
RewardRollCount uint32
Values [5]int64
}
type ActorDefinition struct {
Code uint32
EntityType uint8
}
type Group struct {
ID uint32
Actors map[string][]ActorDefinition
}
type Catalog struct {
difficulties map[string]DifficultyRule
groups map[uint32]Group
}
func Load(source TextSource) (*Catalog, error) {
if source == nil {
return nil, ErrSourceRequired
}
text, err := source.ReadText(ConfigPath)
if err != nil {
return nil, fmt.Errorf("read %s: %w", ConfigPath, err)
}
return Parse(text)
}
func Parse(text string) (*Catalog, error) {
document, err := dnfpvf.Parse(ConfigPath, text)
if err != nil {
return nil, fmt.Errorf("%w: parse %s: %v", ErrCatalogInvalid, ConfigPath, err)
}
return ParseDocument(document)
}
func ParseDocument(document *dnfpvf.Document) (*Catalog, error) {
if document == nil {
return nil, fmt.Errorf("%w: document is nil", ErrCatalogInvalid)
}
difficulties, err := parseDifficulties(document)
if err != nil {
return nil, err
}
groups, err := parseGroups(document)
if err != nil {
return nil, err
}
if len(difficulties) == 0 || len(groups) == 0 {
return nil, fmt.Errorf("%w: difficulties=%d groups=%d", ErrCatalogInvalid, len(difficulties), len(groups))
}
return &Catalog{difficulties: difficulties, groups: groups}, nil
}
func (c *Catalog) Difficulty(key string) (DifficultyRule, bool) {
if c == nil {
return DifficultyRule{}, false
}
rule, ok := c.difficulties[normalizeKey(key)]
return rule, ok
}
func (c *Catalog) Group(id uint32) (Group, bool) {
if c == nil {
return Group{}, false
}
group, ok := c.groups[id]
return cloneGroup(group), ok
}
func (c *Catalog) GroupIDs() []uint32 {
if c == nil {
return nil
}
ids := make([]uint32, 0, len(c.groups))
for id := range c.groups {
ids = append(ids, id)
}
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
return ids
}
func (c *Catalog) Snapshot() (difficultyRules, groups, actors int) {
if c == nil {
return 0, 0, 0
}
for _, group := range c.groups {
for _, entries := range group.Actors {
actors += len(entries)
}
}
return len(c.difficulties), len(c.groups), actors
}
func parseDifficulties(document *dnfpvf.Document) (map[string]DifficultyRule, error) {
tokens, ok := uniqueSection(document, "difficulty")
if !ok {
return nil, fmt.Errorf("%w: missing or duplicate [difficulty]", ErrCatalogInvalid)
}
out := make(map[string]DifficultyRule)
for index := 0; index < len(tokens); {
token := tokens[index]
if token.Kind == dnfpvf.TokenSection {
break
}
key, isKey := tokenKey(token)
if !isKey {
// The live 90CN table begins with one row-count integer. Preserve
// forward compatibility by ignoring numeric metadata before a key.
if token.Kind != dnfpvf.TokenInt {
return nil, fmt.Errorf("%w: [difficulty] token=%q before key", ErrCatalogInvalid, token.Raw)
}
index++
continue
}
index++
if _, exists := out[key]; exists {
return nil, fmt.Errorf("%w: duplicate difficulty=%s", ErrCatalogInvalid, key)
}
var values [5]int64
for valueIndex := range values {
if index >= len(tokens) || tokens[index].Kind != dnfpvf.TokenInt {
return nil, fmt.Errorf("%w: difficulty=%s value_index=%d", ErrCatalogInvalid, key, valueIndex)
}
values[valueIndex] = tokens[index].Int
index++
}
if values[0] < 0 || values[0] > math.MaxUint32 {
return nil, fmt.Errorf("%w: difficulty=%s reward_roll_count=%d", ErrCatalogInvalid, key, values[0])
}
out[key] = DifficultyRule{Key: key, RewardRollCount: uint32(values[0]), Values: values}
}
return out, nil
}
func parseGroups(document *dnfpvf.Document) (map[uint32]Group, error) {
out := make(map[uint32]Group)
var pendingID uint32
for _, section := range document.Sections {
switch normalizeSection(section.Name) {
case "group index":
if pendingID != 0 {
return nil, fmt.Errorf("%w: group=%d has no [group]", ErrCatalogInvalid, pendingID)
}
tokens := sectionTokens(document, section)
if len(tokens) != 1 || tokens[0].Kind != dnfpvf.TokenInt || tokens[0].Int <= 0 || tokens[0].Int > math.MaxUint32 {
return nil, fmt.Errorf("%w: invalid [group index]", ErrCatalogInvalid)
}
pendingID = uint32(tokens[0].Int)
case "group":
if pendingID == 0 {
continue
}
if _, exists := out[pendingID]; exists {
return nil, fmt.Errorf("%w: duplicate group=%d", ErrCatalogInvalid, pendingID)
}
actors, err := parseGroupActors(sectionTokens(document, section), pendingID)
if err != nil {
return nil, err
}
out[pendingID] = Group{ID: pendingID, Actors: actors}
pendingID = 0
}
}
if pendingID != 0 {
return nil, fmt.Errorf("%w: group=%d has no [group]", ErrCatalogInvalid, pendingID)
}
return out, nil
}
func parseGroupActors(tokens []dnfpvf.Token, groupID uint32) (map[string][]ActorDefinition, error) {
out := make(map[string][]ActorDefinition)
currentKey := ""
for index := 0; index < len(tokens); {
if tokens[index].Kind == dnfpvf.TokenSection {
break
}
if key, ok := tokenKey(tokens[index]); ok {
currentKey = key
if _, exists := out[currentKey]; exists {
return nil, fmt.Errorf("%w: group=%d duplicate mode=%s", ErrCatalogInvalid, groupID, currentKey)
}
out[currentKey] = nil
index++
continue
}
if currentKey == "" || index+1 >= len(tokens) || tokens[index].Kind != dnfpvf.TokenInt || tokens[index+1].Kind != dnfpvf.TokenInt {
return nil, fmt.Errorf("%w: group=%d actor pair at token=%d", ErrCatalogInvalid, groupID, index)
}
code, entityType := tokens[index].Int, tokens[index+1].Int
if code <= 0 || code > math.MaxUint32 || (entityType != 0 && entityType != 1) {
return nil, fmt.Errorf("%w: group=%d code=%d entity_type=%d", ErrCatalogInvalid, groupID, code, entityType)
}
out[currentKey] = append(out[currentKey], ActorDefinition{Code: uint32(code), EntityType: uint8(entityType)})
index += 2
}
if len(out) == 0 {
return nil, fmt.Errorf("%w: group=%d has no modes", ErrCatalogInvalid, groupID)
}
for key, actors := range out {
if len(actors) == 0 {
return nil, fmt.Errorf("%w: group=%d mode=%s has no actors", ErrCatalogInvalid, groupID, key)
}
}
return out, nil
}
func uniqueSection(document *dnfpvf.Document, name string) ([]dnfpvf.Token, bool) {
key := normalizeSection(name)
var result []dnfpvf.Token
count := 0
for _, section := range document.Sections {
if normalizeSection(section.Name) != key {
continue
}
count++
result = sectionTokens(document, section)
}
return result, count == 1
}
func sectionTokens(document *dnfpvf.Document, section dnfpvf.Section) []dnfpvf.Token {
if section.Start < 0 || section.End < section.Start || section.End > len(document.Tokens) {
return nil
}
return document.Tokens[section.Start:section.End]
}
func tokenKey(token dnfpvf.Token) (string, bool) {
if token.Kind != dnfpvf.TokenString && token.Kind != dnfpvf.TokenIdent {
return "", false
}
key := normalizeKey(token.Value)
return key, len(key) == 1 && key[0] >= 'A' && key[0] <= 'Z'
}
func normalizeKey(value string) string {
return strings.ToUpper(strings.TrimSpace(value))
}
func normalizeSection(value string) string {
return strings.ToLower(strings.Join(strings.Fields(strings.TrimSpace(value)), " "))
}
func cloneGroup(group Group) Group {
if group.Actors == nil {
return group
}
source := group.Actors
group.Actors = make(map[string][]ActorDefinition, len(source))
for key, actors := range source {
group.Actors[key] = append([]ActorDefinition(nil), actors...)
}
return group
}