服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
198 lines
5.7 KiB
Go
198 lines
5.7 KiB
Go
package rendering
|
|
|
|
import (
|
|
"path"
|
|
"strings"
|
|
)
|
|
|
|
// Engine is an immutable, compiled set of script rendering rules.
|
|
type Engine struct {
|
|
document Document
|
|
rules []compiledRule
|
|
}
|
|
|
|
type compiledRule struct {
|
|
rule Rule
|
|
extensions map[string]struct{}
|
|
specificity int
|
|
order int
|
|
}
|
|
|
|
// Compile validates and compiles a rendering rule document.
|
|
func Compile(document Document) (*Engine, error) {
|
|
if document.Rules == nil {
|
|
document.Rules = make([]Rule, 0)
|
|
}
|
|
if err := Validate(document); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rules := make([]compiledRule, 0, len(document.Rules))
|
|
for order, rule := range document.Rules {
|
|
extensions := make(map[string]struct{}, len(rule.Match.Extensions))
|
|
for _, extension := range rule.Match.Extensions {
|
|
extensions[strings.ToLower(strings.TrimSpace(extension))] = struct{}{}
|
|
}
|
|
specificity := 0
|
|
if len(rule.Match.Extensions) > 0 {
|
|
specificity++
|
|
}
|
|
if strings.TrimSpace(rule.Match.Glob) != "" {
|
|
specificity++
|
|
}
|
|
rules = append(rules, compiledRule{
|
|
rule: rule,
|
|
extensions: extensions,
|
|
specificity: specificity,
|
|
order: order,
|
|
})
|
|
}
|
|
return &Engine{document: document, rules: rules}, nil
|
|
}
|
|
|
|
// Document returns the source document used to build the engine.
|
|
func (e *Engine) Document() Document {
|
|
if e == nil {
|
|
return Document{}
|
|
}
|
|
document := e.document
|
|
document.Rules = append([]Rule(nil), e.document.Rules...)
|
|
for index := range document.Rules {
|
|
if widths := document.Rules[index].Format.TokensPerLineByVersion; widths != nil {
|
|
cloned := make(map[string]int, len(widths))
|
|
for version, width := range widths {
|
|
cloned[version] = width
|
|
}
|
|
document.Rules[index].Format.TokensPerLineByVersion = cloned
|
|
}
|
|
document.Rules[index].Match.Extensions = append([]string(nil), document.Rules[index].Match.Extensions...)
|
|
document.Rules[index].Format.StandaloneValues = append([]int32(nil), document.Rules[index].Format.StandaloneValues...)
|
|
document.Rules[index].Format.NestedSections = append([]string(nil), document.Rules[index].Format.NestedSections...)
|
|
if value := document.Rules[index].Format.TokensPerLineIndex; value != nil {
|
|
copy := *value
|
|
document.Rules[index].Format.TokensPerLineIndex = ©
|
|
}
|
|
}
|
|
return document
|
|
}
|
|
|
|
// FileFormat resolves the best file-scoped rule for filePath. A zero value
|
|
// means that the decoder should use its ordinary layout.
|
|
func (e *Engine) FileFormat(filePath string) FormatSpec {
|
|
return e.resolve(filePath, "", "file")
|
|
}
|
|
|
|
// SectionFormat resolves the best section-scoped rule for a section name.
|
|
// Section names are intentionally matched without considering nesting or
|
|
// occurrence, matching the annotation engine's V1 semantics.
|
|
func (e *Engine) SectionFormat(filePath, section string) FormatSpec {
|
|
return e.resolve(filePath, section, "section")
|
|
}
|
|
|
|
func (e *Engine) resolve(filePath, section, kind string) FormatSpec {
|
|
if e == nil {
|
|
return FormatSpec{}
|
|
}
|
|
bestSpecificity := -1
|
|
bestOrder := -1
|
|
var format FormatSpec
|
|
for _, compiled := range e.rules {
|
|
rule := compiled.rule
|
|
if rule.Target.Kind != kind || !compiled.matches(filePath) {
|
|
continue
|
|
}
|
|
if kind == "section" && !strings.EqualFold(strings.TrimSpace(rule.Target.Section), strings.TrimSpace(section)) {
|
|
continue
|
|
}
|
|
if compiled.specificity < bestSpecificity ||
|
|
(compiled.specificity == bestSpecificity && compiled.order < bestOrder) {
|
|
continue
|
|
}
|
|
bestSpecificity = compiled.specificity
|
|
bestOrder = compiled.order
|
|
format = compiled.rule.Format
|
|
}
|
|
return format
|
|
}
|
|
|
|
func (r compiledRule) matches(filePath string) bool {
|
|
filePath = normalizePath(filePath)
|
|
if len(r.extensions) > 0 {
|
|
if _, ok := r.extensions[strings.ToLower(path.Ext(filePath))]; !ok {
|
|
return false
|
|
}
|
|
}
|
|
glob := strings.TrimSpace(r.rule.Match.Glob)
|
|
return glob == "" || globMatch(glob, filePath)
|
|
}
|
|
|
|
func normalizePath(value string) string {
|
|
return strings.ToLower(strings.Trim(strings.ReplaceAll(value, "\\", "/"), "/"))
|
|
}
|
|
|
|
func globMatch(pattern, value string) bool {
|
|
patternParts := splitPath(normalizePath(pattern))
|
|
valueParts := splitPath(normalizePath(value))
|
|
type state struct{ pattern, value int }
|
|
memo := make(map[state]bool)
|
|
seen := make(map[state]bool)
|
|
var match func(int, int) bool
|
|
match = func(patternIndex, valueIndex int) bool {
|
|
key := state{pattern: patternIndex, value: valueIndex}
|
|
if seen[key] {
|
|
return memo[key]
|
|
}
|
|
seen[key] = true
|
|
if patternIndex == len(patternParts) {
|
|
memo[key] = valueIndex == len(valueParts)
|
|
return memo[key]
|
|
}
|
|
if patternParts[patternIndex] == "**" {
|
|
if match(patternIndex+1, valueIndex) ||
|
|
(valueIndex < len(valueParts) && match(patternIndex, valueIndex+1)) {
|
|
memo[key] = true
|
|
}
|
|
return memo[key]
|
|
}
|
|
if valueIndex >= len(valueParts) {
|
|
return false
|
|
}
|
|
matched, err := path.Match(patternParts[patternIndex], valueParts[valueIndex])
|
|
if err == nil && matched {
|
|
memo[key] = match(patternIndex+1, valueIndex+1)
|
|
}
|
|
return memo[key]
|
|
}
|
|
return match(0, 0)
|
|
}
|
|
|
|
func splitPath(value string) []string {
|
|
if value == "" {
|
|
return nil
|
|
}
|
|
return strings.Split(value, "/")
|
|
}
|
|
|
|
// EffectiveRuleID reports the selected rule for management UI diagnostics.
|
|
func (e *Engine) EffectiveRuleID(filePath, section, kind string) string {
|
|
if e == nil {
|
|
return ""
|
|
}
|
|
bestSpecificity, bestOrder := -1, -1
|
|
id := ""
|
|
for _, compiled := range e.rules {
|
|
r := compiled.rule
|
|
if r.Target.Kind != kind || !compiled.matches(filePath) {
|
|
continue
|
|
}
|
|
if kind == "section" && !strings.EqualFold(strings.TrimSpace(r.Target.Section), strings.TrimSpace(section)) {
|
|
continue
|
|
}
|
|
if compiled.specificity < bestSpecificity || (compiled.specificity == bestSpecificity && compiled.order < bestOrder) {
|
|
continue
|
|
}
|
|
bestSpecificity, bestOrder, id = compiled.specificity, compiled.order, r.ID
|
|
}
|
|
return id
|
|
}
|