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,426 @@
package raid
import (
"errors"
"fmt"
"sort"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
)
// LukeCardSetPVFPath is the tarot table the client's CardSetWindow draws from.
//
// It is a document of its own: luke.etc owns the phase graph and the
// [CARD SET MAP INFO] record list, while this file owns the 8 pattern slots and
// the 22 major arcana. The client loads it into LukeCardSet (window+0x1a8) and
// looks up the notification's (dungeonID, patternIndex) pair in the pattern map
// at config+0x14 (tools\_rev_卢克塔罗牌_1181规格_20260929.md §7.2), and each
// card slot's P in the card map at config+0x00 (§7.3, §8.2).
const LukeCardSetPVFPath = "etc/raid/luke/LukeCardSet.etc"
var (
ErrLukeCardSetSourceRequired = errors.New("luke card set PVF source is required")
ErrLukeCardSetDocumentInvalid = errors.New("luke card set PVF document is invalid")
)
// LukeCardPosition is one `[CARD POS]` (x, y) pair.
//
// Coordinates sit on a 0..4 grid in half steps (0, 0.50, 1, 1.50 ...), which is
// why they are floats and not ints. The client reads them from this table
// itself -- the notification body never carries coordinates -- so they are
// parsed here for counting (the slot count K) and for diagnostics only.
type LukeCardPosition struct {
X float64
Y float64
}
// LukeCardPattern is one `[PATTERN] <dungeonID> <patternIndex>` block, the tarot
// display of one phase-one sanctum: which buff-system icon it shows, where its
// seven card slots sit, and the tooltip the window prints.
//
// patternIndex is the notification's field B and dungeonID its field A; together
// they are the 8-byte key the client's pattern map is built from (the two dwords
// pushed at 0x03926BD6).
type LukeCardPattern struct {
DungeonID uint32
Index int
ImagePath string
ImageIndices []int
CardPositions []LukeCardPosition
Tooltip string
}
// LukeCard is one `[CARD] <monsterID>` block: a major arcanum. MonsterID is the
// u32 the notification puts in every card slot (field P).
type LukeCard struct {
MonsterID uint32
ImagePath string
ImageIndex int
Tooltip string
}
// LukeCardSetConfig owns etc/raid/luke/LukeCardSet.etc. Both tables are global:
// the 22 cards are not per-pattern (the `[PATTERN]` blocks carry no `[CARD]`
// child at all), and the `[CARD POS]` pairs are the seven slots *inside* one
// pattern, not seven cards.
type LukeCardSetConfig struct {
// Patterns is every `[PATTERN]` block in source order.
Patterns []LukeCardPattern
// Cards is every `[CARD]` block in source order (the 22 major arcana).
Cards []LukeCard
// cardsByMonsterID indexes Cards by the notification's P field. The client
// silently draws nothing for a P that misses, so the caller must send only
// keys that are present here.
cardsByMonsterID map[uint32]int
}
func LoadLukeCardSet(source dnfpvf.Source) (*LukeCardSetConfig, error) {
if source == nil {
return nil, ErrLukeCardSetSourceRequired
}
text, err := source.ReadText(LukeCardSetPVFPath)
if err != nil {
return nil, fmt.Errorf("read luke card set PVF %q: %w", LukeCardSetPVFPath, err)
}
document, err := dnfpvf.Parse(LukeCardSetPVFPath, text)
if err != nil {
return nil, fmt.Errorf("parse luke card set PVF %q: %w", LukeCardSetPVFPath, err)
}
return ParseLukeCardSet(document)
}
func ParseLukeCardSet(document *dnfpvf.Document) (*LukeCardSetConfig, error) {
if document == nil || antonPathKey(document.Path) != antonPathKey(LukeCardSetPVFPath) {
path := ""
if document != nil {
path = document.Path
}
return nil, fmt.Errorf("%w: path=%q want=%q", ErrLukeCardSetDocumentInvalid, path, LukeCardSetPVFPath)
}
patternBlocks, err := antonFindBlocks(document, "pattern", 0, len(document.Sections))
if err != nil {
return nil, fmt.Errorf("%w: pattern blocks: %v", ErrLukeCardSetDocumentInvalid, err)
}
if len(patternBlocks) == 0 {
return nil, fmt.Errorf("%w: no [PATTERN] block", ErrLukeCardSetDocumentInvalid)
}
config := &LukeCardSetConfig{
Patterns: make([]LukeCardPattern, 0, len(patternBlocks)),
cardsByMonsterID: make(map[uint32]int, 32),
}
// The client's pattern map holds one entry per `[PATTERN]` block and a
// duplicate key would overwrite rather than extend it, so a repeated
// (dungeonID, index) pair is a table this server cannot describe faithfully.
patternKeys := make(map[[2]int64]struct{}, len(patternBlocks))
for _, block := range patternBlocks {
pattern, parseErr := parseLukeCardPattern(document, block)
if parseErr != nil {
return nil, parseErr
}
key := [2]int64{int64(pattern.DungeonID), int64(pattern.Index)}
if _, duplicate := patternKeys[key]; duplicate {
return nil, fmt.Errorf("%w: duplicate [PATTERN] dungeon=%d index=%d",
ErrLukeCardSetDocumentInvalid, pattern.DungeonID, pattern.Index)
}
patternKeys[key] = struct{}{}
config.Patterns = append(config.Patterns, pattern)
}
cardBlocks, err := antonFindBlocks(document, "card", 0, len(document.Sections))
if err != nil {
return nil, fmt.Errorf("%w: card blocks: %v", ErrLukeCardSetDocumentInvalid, err)
}
if len(cardBlocks) == 0 {
return nil, fmt.Errorf("%w: no [CARD] block", ErrLukeCardSetDocumentInvalid)
}
for _, block := range cardBlocks {
card, parseErr := parseLukeCard(document, block)
if parseErr != nil {
return nil, parseErr
}
if _, duplicate := config.cardsByMonsterID[card.MonsterID]; duplicate {
return nil, fmt.Errorf("%w: duplicate [CARD] monster=%d", ErrLukeCardSetDocumentInvalid, card.MonsterID)
}
config.cardsByMonsterID[card.MonsterID] = len(config.Cards)
config.Cards = append(config.Cards, card)
}
return config, nil
}
// Pattern returns the pattern of one sanctum and pattern index.
func (c *LukeCardSetConfig) Pattern(dungeonID uint32, index int) (LukeCardPattern, bool) {
if c == nil {
return LukeCardPattern{}, false
}
for _, pattern := range c.Patterns {
if pattern.DungeonID == dungeonID && pattern.Index == index {
return pattern, true
}
}
return LukeCardPattern{}, false
}
// PatternIndices lists one sanctum's pattern indices in ascending order: the
// index positions a caller may pick a pattern from. 5054 -> [0 1],
// 5055 -> [0 1 2 3], 5056 -> [0 1] in the shipped PVF.
func (c *LukeCardSetConfig) PatternIndices(dungeonID uint32) []int {
if c == nil {
return nil
}
indices := make([]int, 0, 4)
for _, pattern := range c.Patterns {
if pattern.DungeonID == dungeonID {
indices = append(indices, pattern.Index)
}
}
sort.Ints(indices)
return indices
}
// CardMonsterIDs returns every `[CARD]` key in source order: the only values the
// notification's P field may carry.
func (c *LukeCardSetConfig) CardMonsterIDs() []uint32 {
if c == nil {
return nil
}
ids := make([]uint32, 0, len(c.Cards))
for _, card := range c.Cards {
ids = append(ids, card.MonsterID)
}
return ids
}
// HasCard reports whether a monster id is one of the `[CARD]` keys.
func (c *LukeCardSetConfig) HasCard(monsterID uint32) bool {
if c == nil {
return false
}
if _, ok := c.cardsByMonsterID[monsterID]; ok {
return true
}
// A config assembled without the loader (tests, or a caller mirroring another
// document) has no index built; fall back to the blocks themselves, the same
// way Pattern and PatternIndices read their slices directly.
for _, card := range c.Cards {
if card.MonsterID == monsterID {
return true
}
}
return false
}
// parseLukeCardPattern reads one `[PATTERN] <dungeonID> <index>` block: its two
// key values sit on the opening section, and only [IMAGE] / [CARD POS] /
// [TOOLTIP TEXT] may appear inside.
func parseLukeCardPattern(document *dnfpvf.Document, block antonSectionBlock) (LukeCardPattern, error) {
tokens, err := antonSectionTokens(document, block.open)
if err != nil {
return LukeCardPattern{}, fmt.Errorf("%w: pattern key: %v", ErrLukeCardSetDocumentInvalid, err)
}
if len(tokens) != 2 {
return LukeCardPattern{}, fmt.Errorf("%w: pattern key values=%d want=2",
ErrLukeCardSetDocumentInvalid, len(tokens))
}
dungeonID, err := antonPositiveUint32Token(tokens[0], "pattern dungeon")
if err != nil {
return LukeCardPattern{}, fmt.Errorf("%w: pattern dungeon: %v", ErrLukeCardSetDocumentInvalid, err)
}
index, err := antonIntToken(tokens[1], "pattern index")
if err != nil {
return LukeCardPattern{}, fmt.Errorf("%w: pattern index: %v", ErrLukeCardSetDocumentInvalid, err)
}
if index < 0 {
return LukeCardPattern{}, fmt.Errorf("%w: pattern index=%d want_nonnegative", ErrLukeCardSetDocumentInvalid, index)
}
pattern := LukeCardPattern{DungeonID: dungeonID, Index: index}
seen := map[string]bool{}
for sectionIndex := block.open + 1; sectionIndex < block.close; sectionIndex++ {
name := antonSectionName(document.Sections[sectionIndex].Name)
switch name {
case "image", "card pos", "tooltip text":
case "/card pos":
// The closing tag of [CARD POS]; it carries no value.
continue
default:
return LukeCardPattern{}, fmt.Errorf("%w: pattern dungeon=%d index=%d unexpected section=%q",
ErrLukeCardSetDocumentInvalid, dungeonID, index, document.Sections[sectionIndex].Name)
}
if seen[name] {
return LukeCardPattern{}, fmt.Errorf("%w: pattern dungeon=%d index=%d duplicate section=%q",
ErrLukeCardSetDocumentInvalid, dungeonID, index, name)
}
seen[name] = true
sectionTokens, tokenErr := antonSectionTokens(document, sectionIndex)
if tokenErr != nil {
return LukeCardPattern{}, fmt.Errorf("%w: pattern %s: %v", ErrLukeCardSetDocumentInvalid, name, tokenErr)
}
switch name {
case "image":
path, indices, imageErr := lukeCardSetImage(sectionTokens, "pattern image")
if imageErr != nil {
return LukeCardPattern{}, imageErr
}
pattern.ImagePath = path
pattern.ImageIndices = indices
case "card pos":
positions, positionErr := lukeCardSetPositions(sectionTokens)
if positionErr != nil {
return LukeCardPattern{}, positionErr
}
pattern.CardPositions = positions
case "tooltip text":
text, textErr := lukeCardSetTooltip(sectionTokens)
if textErr != nil {
return LukeCardPattern{}, textErr
}
pattern.Tooltip = text
}
}
if pattern.ImagePath == "" {
return LukeCardPattern{}, fmt.Errorf("%w: pattern dungeon=%d index=%d has no [IMAGE]",
ErrLukeCardSetDocumentInvalid, dungeonID, index)
}
if len(pattern.CardPositions) == 0 {
return LukeCardPattern{}, fmt.Errorf("%w: pattern dungeon=%d index=%d has no [CARD POS]",
ErrLukeCardSetDocumentInvalid, dungeonID, index)
}
return pattern, nil
}
// parseLukeCard reads one `[CARD] <monsterID>` block. Its key is a single
// number, which is why the card table cannot be keyed like the 8-byte pattern
// table (see the 1181 spec, §7.3).
func parseLukeCard(document *dnfpvf.Document, block antonSectionBlock) (LukeCard, error) {
tokens, err := antonSectionTokens(document, block.open)
if err != nil {
return LukeCard{}, fmt.Errorf("%w: card key: %v", ErrLukeCardSetDocumentInvalid, err)
}
if len(tokens) != 1 {
return LukeCard{}, fmt.Errorf("%w: card key values=%d want=1", ErrLukeCardSetDocumentInvalid, len(tokens))
}
monsterID, err := antonPositiveUint32Token(tokens[0], "card monster")
if err != nil {
return LukeCard{}, fmt.Errorf("%w: card monster: %v", ErrLukeCardSetDocumentInvalid, err)
}
card := LukeCard{MonsterID: monsterID}
for sectionIndex := block.open + 1; sectionIndex < block.close; sectionIndex++ {
name := antonSectionName(document.Sections[sectionIndex].Name)
if name != "image" && name != "tooltip text" {
return LukeCard{}, fmt.Errorf("%w: card monster=%d unexpected section=%q",
ErrLukeCardSetDocumentInvalid, monsterID, document.Sections[sectionIndex].Name)
}
sectionTokens, tokenErr := antonSectionTokens(document, sectionIndex)
if tokenErr != nil {
return LukeCard{}, fmt.Errorf("%w: card %s: %v", ErrLukeCardSetDocumentInvalid, name, tokenErr)
}
if name == "image" {
if card.ImagePath != "" {
return LukeCard{}, fmt.Errorf("%w: card monster=%d duplicate [IMAGE]", ErrLukeCardSetDocumentInvalid, monsterID)
}
path, indices, imageErr := lukeCardSetImage(sectionTokens, "card image")
if imageErr != nil {
return LukeCard{}, imageErr
}
// A card names exactly one frame; the two-number form belongs to the
// patterns, whose [IMAGE] holds the buff icon pair.
if len(indices) != 1 {
return LukeCard{}, fmt.Errorf("%w: card monster=%d image indices=%d want=1",
ErrLukeCardSetDocumentInvalid, monsterID, len(indices))
}
card.ImagePath = path
card.ImageIndex = indices[0]
continue
}
text, textErr := lukeCardSetTooltip(sectionTokens)
if textErr != nil {
return LukeCard{}, textErr
}
card.Tooltip = text
}
if card.ImagePath == "" {
return LukeCard{}, fmt.Errorf("%w: card monster=%d has no [IMAGE]", ErrLukeCardSetDocumentInvalid, monsterID)
}
return card, nil
}
// lukeCardSetImage reads `[IMAGE]`: one path token followed by a VARIABLE number
// of image indices. `5054 *` names one index, `5055 *` / `5056 *` name two
// (`2 3`, `4 5`, ...), so the indices must not be read as a fixed pair.
func lukeCardSetImage(tokens []dnfpvf.Token, field string) (string, []int, error) {
if len(tokens) < 2 {
return "", nil, fmt.Errorf("%w: %s values=%d want>=2", ErrLukeCardSetDocumentInvalid, field, len(tokens))
}
if tokens[0].Kind != dnfpvf.TokenString {
return "", nil, fmt.Errorf("%w: %s path kind=%q want=string",
ErrLukeCardSetDocumentInvalid, field, tokens[0].Kind)
}
path := tokens[0].Value
if path == "" {
return "", nil, fmt.Errorf("%w: %s empty path", ErrLukeCardSetDocumentInvalid, field)
}
indices := make([]int, 0, len(tokens)-1)
for _, token := range tokens[1:] {
index, err := antonIntToken(token, field)
if err != nil {
return "", nil, fmt.Errorf("%w: %s: %v", ErrLukeCardSetDocumentInvalid, field, err)
}
if index < 0 {
return "", nil, fmt.Errorf("%w: %s index=%d want_nonnegative", ErrLukeCardSetDocumentInvalid, field, index)
}
indices = append(indices, index)
}
return path, indices, nil
}
// lukeCardSetPositions reads `[CARD POS]`: 2n values, n (x, y) pairs. Half-step
// coordinates arrive as float tokens, whole ones as int tokens, so both kinds
// are accepted and both are required to be non-negative.
func lukeCardSetPositions(tokens []dnfpvf.Token) ([]LukeCardPosition, error) {
if len(tokens) == 0 {
return nil, fmt.Errorf("%w: empty [CARD POS]", ErrLukeCardSetDocumentInvalid)
}
if len(tokens)%2 != 0 {
return nil, fmt.Errorf("%w: [CARD POS] values=%d want_even", ErrLukeCardSetDocumentInvalid, len(tokens))
}
positions := make([]LukeCardPosition, 0, len(tokens)/2)
for index := 0; index < len(tokens); index += 2 {
x, err := lukeCardSetCoordinate(tokens[index])
if err != nil {
return nil, err
}
y, err := lukeCardSetCoordinate(tokens[index+1])
if err != nil {
return nil, err
}
positions = append(positions, LukeCardPosition{X: x, Y: y})
}
return positions, nil
}
func lukeCardSetCoordinate(token dnfpvf.Token) (float64, error) {
var value float64
switch token.Kind {
case dnfpvf.TokenInt:
value = float64(token.Int)
case dnfpvf.TokenFloat:
value = token.Float
default:
return 0, fmt.Errorf("%w: [CARD POS] value=%q kind=%q want=number",
ErrLukeCardSetDocumentInvalid, token.Raw, token.Kind)
}
if value < 0 {
return 0, fmt.Errorf("%w: [CARD POS] value=%v want_nonnegative", ErrLukeCardSetDocumentInvalid, value)
}
return value, nil
}
// lukeCardSetTooltip reads `[TOOLTIP TEXT]`, which is exactly one quoted string.
func lukeCardSetTooltip(tokens []dnfpvf.Token) (string, error) {
if len(tokens) != 1 {
return "", fmt.Errorf("%w: [TOOLTIP TEXT] values=%d want=1", ErrLukeCardSetDocumentInvalid, len(tokens))
}
if tokens[0].Kind != dnfpvf.TokenString {
return "", fmt.Errorf("%w: [TOOLTIP TEXT] kind=%q want=string", ErrLukeCardSetDocumentInvalid, tokens[0].Kind)
}
return tokens[0].Value, nil
}