服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
331 lines
8.2 KiB
Go
331 lines
8.2 KiB
Go
package pvf
|
|
|
|
import (
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
ScriptElementSection = "section"
|
|
ScriptElementToken = "token"
|
|
)
|
|
|
|
// ScriptView is a tolerant semantic projection of the exact editor text.
|
|
// Start and End are JavaScript UTF-16 code-unit offsets after normalizing line
|
|
// endings the same way as the editor (CRLF and CR each occupy one unit).
|
|
type ScriptView struct {
|
|
Text string
|
|
Elements []ScriptElement
|
|
}
|
|
|
|
// ScriptElement describes one opening section tag or one direct value token.
|
|
type ScriptElement struct {
|
|
Kind string
|
|
TokenType int32
|
|
Value string
|
|
Section string
|
|
SectionID int
|
|
SectionPath []string
|
|
Index int
|
|
Start int
|
|
End int
|
|
}
|
|
|
|
type scriptLexeme struct {
|
|
isSection bool
|
|
closing bool
|
|
name string
|
|
tokenType int32
|
|
value string
|
|
start int
|
|
end int
|
|
}
|
|
|
|
type semanticSection struct {
|
|
id int
|
|
name string
|
|
path []string
|
|
paired bool
|
|
tokenCount int
|
|
}
|
|
|
|
// ParseScriptView parses decompiled or edited TypeScript text without changing
|
|
// its formatting. Malformed fragments are skipped in the same tolerant spirit
|
|
// as encodeScript, while valid tokens retain their exact editor positions.
|
|
func ParseScriptView(text string) ScriptView {
|
|
return ParseScriptViewWithNestedSections(text, nil)
|
|
}
|
|
|
|
// ParseScriptViewWithNestedSections preserves an unpaired parent while parsing
|
|
// paired child sections explicitly declared by the active rendering rules.
|
|
func ParseScriptViewWithNestedSections(text string, isNested func(parent, child string) bool) ScriptView {
|
|
lexemes := lexScriptText(text)
|
|
pairedNames := make(map[string]bool)
|
|
for _, lexeme := range lexemes {
|
|
if lexeme.isSection && lexeme.closing {
|
|
pairedNames[lexeme.name] = true
|
|
}
|
|
}
|
|
|
|
elements := make([]ScriptElement, 0, len(lexemes))
|
|
sections := make([]semanticSection, 0)
|
|
nextSectionID := 1
|
|
for _, lexeme := range lexemes {
|
|
if lexeme.isSection {
|
|
if lexeme.closing {
|
|
for len(sections) > 0 && !sections[len(sections)-1].paired {
|
|
sections = sections[:len(sections)-1]
|
|
}
|
|
for i := len(sections) - 1; i >= 0; i-- {
|
|
if sections[i].paired && sections[i].name == lexeme.name {
|
|
sections = sections[:i]
|
|
break
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
for len(sections) > 0 && !sections[len(sections)-1].paired {
|
|
parent := sections[len(sections)-1].name
|
|
if pairedNames[lexeme.name] && isNested != nil && isNested(parent, lexeme.name) {
|
|
break
|
|
}
|
|
sections = sections[:len(sections)-1]
|
|
}
|
|
sectionPath := make([]string, 0, len(sections)+1)
|
|
if len(sections) > 0 {
|
|
sectionPath = append(sectionPath, sections[len(sections)-1].path...)
|
|
}
|
|
sectionPath = append(sectionPath, lexeme.name)
|
|
sectionID := nextSectionID
|
|
nextSectionID++
|
|
elements = append(elements, ScriptElement{
|
|
Kind: ScriptElementSection,
|
|
TokenType: 3,
|
|
Value: lexeme.name,
|
|
Section: lexeme.name,
|
|
SectionID: sectionID,
|
|
SectionPath: append([]string(nil), sectionPath...),
|
|
Index: -1,
|
|
Start: lexeme.start,
|
|
End: lexeme.end,
|
|
})
|
|
sections = append(sections, semanticSection{
|
|
id: sectionID,
|
|
name: lexeme.name,
|
|
path: sectionPath,
|
|
paired: pairedNames[lexeme.name],
|
|
})
|
|
continue
|
|
}
|
|
|
|
element := ScriptElement{
|
|
Kind: ScriptElementToken,
|
|
TokenType: lexeme.tokenType,
|
|
Value: lexeme.value,
|
|
Index: -1,
|
|
Start: lexeme.start,
|
|
End: lexeme.end,
|
|
}
|
|
if len(sections) > 0 {
|
|
current := §ions[len(sections)-1]
|
|
element.Section = current.name
|
|
element.SectionID = current.id
|
|
element.SectionPath = append([]string(nil), current.path...)
|
|
element.Index = current.tokenCount
|
|
current.tokenCount++
|
|
}
|
|
elements = append(elements, element)
|
|
}
|
|
|
|
return ScriptView{Text: text, Elements: elements}
|
|
}
|
|
|
|
func lexScriptText(text string) []scriptLexeme {
|
|
rs := []rune(text)
|
|
offsets := editorOffsets(rs)
|
|
isWS := func(r rune) bool {
|
|
return r == ' ' || r == '\t' || r == '\n' || r == '\r' || r == '\v' || r == '\f'
|
|
}
|
|
|
|
lexemes := make([]scriptLexeme, 0)
|
|
for i := 0; i < len(rs); {
|
|
if isWS(rs[i]) {
|
|
i++
|
|
continue
|
|
}
|
|
if rs[i] == '/' && i+1 < len(rs) {
|
|
if rs[i+1] == '/' {
|
|
for i < len(rs) && rs[i] != '\n' {
|
|
i++
|
|
}
|
|
continue
|
|
}
|
|
if rs[i+1] == '*' {
|
|
i += 2
|
|
for i+1 < len(rs) && !(rs[i] == '*' && rs[i+1] == '/') {
|
|
i++
|
|
}
|
|
i = min(i+2, len(rs))
|
|
continue
|
|
}
|
|
}
|
|
if rs[i] == '#' {
|
|
for i < len(rs) && rs[i] != '\n' {
|
|
i++
|
|
}
|
|
continue
|
|
}
|
|
if rs[i] == '`' {
|
|
start := i
|
|
value, next, _ := readBacktickString(rs, i)
|
|
lexemes = append(lexemes, scriptLexeme{
|
|
tokenType: 6,
|
|
value: value,
|
|
start: offsets[start],
|
|
end: offsets[next],
|
|
})
|
|
i = next
|
|
continue
|
|
}
|
|
if rs[i] == '{' {
|
|
start := i
|
|
if end := findMarkerEnd(rs, i+1); end >= 0 {
|
|
if typ, value, ok := parseTextMarker(string(rs[start : end+1])); ok {
|
|
lexemes = append(lexemes, scriptLexeme{
|
|
tokenType: typ,
|
|
value: value,
|
|
start: offsets[start],
|
|
end: offsets[end+1],
|
|
})
|
|
i = end + 1
|
|
continue
|
|
}
|
|
}
|
|
i++
|
|
continue
|
|
}
|
|
if rs[i] == '[' {
|
|
start := i
|
|
if end := indexRune(rs, i+1, ']'); end > i {
|
|
tag := string(rs[start : end+1])
|
|
name, closing, ok := parseSectionTag(tag)
|
|
lexemes = append(lexemes, scriptLexeme{
|
|
isSection: ok,
|
|
closing: closing,
|
|
name: name,
|
|
tokenType: 3,
|
|
value: tag,
|
|
start: offsets[start],
|
|
end: offsets[end+1],
|
|
})
|
|
i = end + 1
|
|
continue
|
|
}
|
|
i++
|
|
continue
|
|
}
|
|
|
|
start := i
|
|
for i < len(rs) && !isWS(rs[i]) && rs[i] != '`' && rs[i] != '{' && rs[i] != '[' {
|
|
i++
|
|
}
|
|
if i == start {
|
|
i++
|
|
continue
|
|
}
|
|
raw := string(rs[start:i])
|
|
typ, value := normalizeTextToken(raw)
|
|
lexemes = append(lexemes, scriptLexeme{
|
|
tokenType: typ,
|
|
value: value,
|
|
start: offsets[start],
|
|
end: offsets[i],
|
|
})
|
|
}
|
|
return lexemes
|
|
}
|
|
|
|
// editorOffsets maps offsets in the original Go string to offsets in the
|
|
// editor's normalized JavaScript string. CodeMirror treats CRLF and lone CR
|
|
// as a single line break, so the carriage return in a CRLF pair contributes
|
|
// no additional code unit.
|
|
func editorOffsets(rs []rune) []int {
|
|
offsets := make([]int, len(rs)+1)
|
|
for i, r := range rs {
|
|
if r == '\r' && i+1 < len(rs) && rs[i+1] == '\n' {
|
|
offsets[i+1] = offsets[i]
|
|
continue
|
|
}
|
|
offsets[i+1] = offsets[i] + utf16RuneLen(r)
|
|
}
|
|
return offsets
|
|
}
|
|
|
|
// parseTextMarker parses `{N=`...`}` block-value markers, where N is 5/7 for
|
|
// the 90US token streams and 8/10 for the newer Paged110 ones.
|
|
func parseTextMarker(marker string) (int32, string, bool) {
|
|
if len(marker) < 4 || marker[0] != '{' || marker[len(marker)-1] != '}' {
|
|
return 0, "", false
|
|
}
|
|
inner := marker[1 : len(marker)-1]
|
|
eq := strings.IndexByte(inner, '=')
|
|
if eq <= 0 {
|
|
return 0, "", false
|
|
}
|
|
n, err := strconv.Atoi(inner[:eq])
|
|
if err != nil || (n != 5 && n != 7 && n != 8 && n != 10) {
|
|
return 0, "", false
|
|
}
|
|
typ := int32(n)
|
|
inner = strings.TrimSpace(inner[eq+1:])
|
|
innerRunes := []rune(inner)
|
|
if value, next, ok := readBacktickString(innerRunes, 0); ok && next == len(innerRunes) {
|
|
return typ, value, true
|
|
}
|
|
return typ, inner, true
|
|
}
|
|
|
|
func normalizeTextToken(raw string) (int32, string) {
|
|
if _, _, ok := ParsePlaceholder(raw); ok {
|
|
return 8, raw
|
|
}
|
|
if value, err := strconv.ParseInt(raw, 10, 32); err == nil {
|
|
return 0, strconv.FormatInt(value, 10)
|
|
}
|
|
if value, err := strconv.ParseFloat(raw, 32); err == nil {
|
|
return 2, strconv.FormatFloat(float64(float32(value)), 'g', -1, 32)
|
|
}
|
|
return 3, raw
|
|
}
|
|
|
|
func utf16RuneLen(r rune) int {
|
|
if r > math.MaxUint16 {
|
|
return 2
|
|
}
|
|
return 1
|
|
}
|
|
|
|
// ScriptSectionClosingOffsets maps opening tags to their matching closing tag
|
|
// positions in editor UTF-16 units. The lexer excludes comments and strings.
|
|
func ScriptSectionClosingOffsets(text, section string) map[int]int {
|
|
result := map[int]int{}
|
|
stack := []int{}
|
|
for _, lexeme := range lexScriptText(text) {
|
|
if !lexeme.isSection || lexeme.name != section {
|
|
continue
|
|
}
|
|
if !lexeme.closing {
|
|
stack = append(stack, lexeme.start)
|
|
continue
|
|
}
|
|
if len(stack) == 0 {
|
|
continue
|
|
}
|
|
result[stack[len(stack)-1]] = lexeme.start
|
|
stack = stack[:len(stack)-1]
|
|
}
|
|
return result
|
|
}
|