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