服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
287 lines
8.1 KiB
Go
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
|
|
}
|