Files
90CNServer/server/internal/modules/dnf/pvf/parse.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

224 lines
5.3 KiB
Go

package pvf
import (
"fmt"
"strconv"
"strings"
"unicode"
)
// Parse 把单个 PVF 文本文档解析成通用 token 和 section。
func Parse(docPath, text string) (*Document, error) {
docPath = cleanPath(docPath)
if docPath == "" {
return nil, ErrPathRequired
}
parser := docParser{
doc: Document{Path: docPath},
}
lines := strings.Split(text, "\n")
for idx, line := range lines {
if err := parser.parseLine(idx+1, line); err != nil {
return nil, err
}
}
if parser.pending != nil {
return nil, fmt.Errorf("dnf pvf string at %d:%d is not closed", parser.pending.line, parser.pending.column)
}
parser.closeSection()
return &parser.doc, nil
}
type docParser struct {
doc Document
pending *pendingString
}
type pendingString struct {
quote byte
line int
column int
raw strings.Builder
value strings.Builder
}
func (p *docParser) parseLine(lineNo int, line string) error {
if p.pending != nil {
return p.continueString(lineNo, line)
}
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, "//") {
return nil
}
if strings.HasPrefix(trimmed, "[") {
if end := strings.IndexByte(trimmed, ']'); end > 0 {
column := strings.Index(line, "[") + 1
name := strings.TrimSpace(trimmed[1:end])
p.addToken(Token{
Kind: TokenSection,
Raw: trimmed[:end+1],
Value: name,
Line: lineNo,
Column: column,
})
return p.scanValues(lineNo, column+end+1, trimmed[end+1:])
}
}
column := len(line) - len(strings.TrimLeftFunc(line, unicode.IsSpace)) + 1
return p.scanValues(lineNo, column, trimmed)
}
func (p *docParser) addToken(token Token) {
if token.Kind == TokenSection {
p.closeSection()
p.doc.Sections = append(p.doc.Sections, Section{
Name: token.Value,
Start: len(p.doc.Tokens) + 1,
})
}
p.doc.Tokens = append(p.doc.Tokens, token)
}
func (p *docParser) closeSection() {
if len(p.doc.Sections) == 0 {
return
}
idx := len(p.doc.Sections) - 1
if p.doc.Sections[idx].End == 0 {
p.doc.Sections[idx].End = len(p.doc.Tokens)
}
}
func (p *docParser) scanValues(lineNo, baseColumn int, line string) error {
for pos := 0; pos < len(line); {
if isSpace(line[pos]) {
pos++
continue
}
if pos+1 < len(line) && line[pos] == '/' && line[pos+1] == '/' {
return nil
}
if line[pos] == '#' {
return nil
}
column := baseColumn + pos
switch line[pos] {
case '`', '\'', '"':
token, next, complete := p.beginString(lineNo, column, line, pos)
if !complete {
return nil
}
p.addToken(token)
pos = next
case '{', '}', '=', ',', ':':
p.addToken(Token{Kind: TokenSymbol, Raw: line[pos : pos+1], Value: line[pos : pos+1], Line: lineNo, Column: column})
pos++
default:
raw, next := scanBare(line, pos)
if raw == "" {
pos++
continue
}
p.addToken(parseBare(lineNo, column, raw))
pos = next
}
}
return nil
}
func (p *docParser) beginString(lineNo, column int, line string, start int) (Token, int, bool) {
pending := &pendingString{quote: line[start], line: lineNo, column: column}
pending.raw.WriteByte(line[start])
next, complete := appendStringSegment(pending, line, start+1)
if !complete {
p.pending = pending
return Token{}, 0, false
}
return pending.token(), next, true
}
func (p *docParser) continueString(lineNo int, line string) error {
p.pending.raw.WriteByte('\n')
p.pending.value.WriteByte('\n')
next, complete := appendStringSegment(p.pending, line, 0)
if !complete {
return nil
}
token := p.pending.token()
p.pending = nil
p.addToken(token)
return p.scanValues(lineNo, next+1, line[next:])
}
func appendStringSegment(pending *pendingString, line string, start int) (int, bool) {
for pos := start; pos < len(line); pos++ {
value := line[pos]
pending.raw.WriteByte(value)
if value == pending.quote {
return pos + 1, true
}
if pending.quote != '`' && value == '\\' && pos+1 < len(line) {
pos++
pending.raw.WriteByte(line[pos])
pending.value.WriteByte(line[pos])
continue
}
pending.value.WriteByte(value)
}
return len(line), false
}
func (p *pendingString) token() Token {
return Token{
Kind: TokenString,
Raw: p.raw.String(),
Value: p.value.String(),
Line: p.line,
Column: p.column,
}
}
func scanBare(line string, start int) (string, int) {
pos := start
for pos < len(line) {
if isSpace(line[pos]) || strings.ContainsRune("{},:=", rune(line[pos])) {
break
}
if pos+1 < len(line) && line[pos] == '/' && line[pos+1] == '/' {
break
}
pos++
}
return line[start:pos], pos
}
func parseBare(lineNo, column int, raw string) Token {
if value, ok := parseInt(raw); ok {
return Token{Kind: TokenInt, Raw: raw, Value: raw, Int: value, Line: lineNo, Column: column}
}
if value, ok := parseFloat(raw); ok {
return Token{Kind: TokenFloat, Raw: raw, Value: raw, Float: value, Line: lineNo, Column: column}
}
return Token{Kind: TokenIdent, Raw: raw, Value: raw, Line: lineNo, Column: column}
}
func parseInt(raw string) (int64, bool) {
if strings.ContainsAny(raw, ".eE") {
return 0, false
}
value, err := strconv.ParseInt(raw, 10, 64)
return value, err == nil
}
func parseFloat(raw string) (float64, bool) {
if !strings.ContainsAny(raw, ".eE") {
return 0, false
}
value, err := strconv.ParseFloat(raw, 64)
return value, err == nil
}
func isSpace(value byte) bool {
return value == ' ' || value == '\t' || value == '\r'
}