服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
299 lines
8.2 KiB
Go
299 lines
8.2 KiB
Go
package annotations
|
|
|
|
import (
|
|
"fmt"
|
|
"path"
|
|
"sort"
|
|
"strings"
|
|
|
|
"pvfine/internal/pvf"
|
|
)
|
|
|
|
// PreviewFieldValue is one occurrence of a shared field in an editor view.
|
|
// Values contains the selected token(s). For a repeated record target it
|
|
// contains the complete record so consumers can use context and neighboring
|
|
// values (for example, skill context + skill id + level).
|
|
type PreviewFieldValue struct {
|
|
Field FieldDefinition
|
|
Values []string
|
|
Context string
|
|
Start int
|
|
End int
|
|
}
|
|
|
|
// resolveFieldRule lets legacy rules opt into the shared field definition
|
|
// without changing the existing rule JSON shape. A rule's id, description and
|
|
// group remain local; non-empty target/match/annotation values override the
|
|
// field when supplied.
|
|
func resolveFieldRule(rule Rule, fields []FieldDefinition) (Rule, error) {
|
|
fieldID := strings.TrimSpace(rule.Field)
|
|
if fieldID == "" {
|
|
return rule, nil
|
|
}
|
|
for _, field := range fields {
|
|
if field.ID != fieldID {
|
|
continue
|
|
}
|
|
resolved := Rule{
|
|
ID: rule.ID,
|
|
PVFVersions: rule.PVFVersions,
|
|
Description: rule.Description,
|
|
Field: rule.Field,
|
|
Match: field.Match,
|
|
Target: field.Target,
|
|
Annotation: field.Annotation,
|
|
Group: rule.Group,
|
|
}
|
|
if len(rule.Match.Extensions) > 0 || rule.Match.Glob != "" {
|
|
resolved.Match = rule.Match
|
|
}
|
|
if rule.Target.Kind != "" {
|
|
resolved.Target = rule.Target
|
|
}
|
|
if rule.Annotation.Title != "" || rule.Annotation.Type != "" ||
|
|
rule.Annotation.Content != "" || len(rule.Annotation.Values) > 0 ||
|
|
rule.Annotation.Relation != "" || rule.Annotation.PathRoot != "" || rule.Annotation.InlineImage {
|
|
resolved.Annotation = rule.Annotation
|
|
}
|
|
return resolved, nil
|
|
}
|
|
return Rule{}, fmt.Errorf("引用了不存在的共享字段: %s", fieldID)
|
|
}
|
|
|
|
// PreviewFields returns the provider-specific shared fields that match a
|
|
// path, in stable display order. The returned definitions are copies of the
|
|
// immutable engine document and are safe for callers to retain.
|
|
func (e *Engine) PreviewFields(filePath, provider string) []FieldDefinition {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
items := make([]FieldDefinition, 0)
|
|
for _, field := range e.document.Fields {
|
|
if field.Preview == nil || !previewSupportsProvider(field.Preview, provider) {
|
|
continue
|
|
}
|
|
if !fieldMatches(field.Match, filePath, false) {
|
|
continue
|
|
}
|
|
items = append(items, field)
|
|
}
|
|
sort.SliceStable(items, func(i, j int) bool {
|
|
left, right := items[i].Preview, items[j].Preview
|
|
if left.Order != right.Order {
|
|
return left.Order < right.Order
|
|
}
|
|
return items[i].ID < items[j].ID
|
|
})
|
|
return items
|
|
}
|
|
|
|
func previewSupportsProvider(preview *PreviewSpec, provider string) bool {
|
|
provider = strings.TrimSpace(provider)
|
|
if preview == nil || provider == "" {
|
|
return false
|
|
}
|
|
for _, candidate := range preview.providerNames() {
|
|
if strings.EqualFold(candidate, provider) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ExtractPreviewFields projects a ScriptView through provider field targets.
|
|
// It deliberately shares the same target semantics as annotations, including
|
|
// repeated records, offsets and dynamic record widths.
|
|
func (e *Engine) ExtractPreviewFields(filePath string, view pvf.ScriptView, provider string) []PreviewFieldValue {
|
|
fields := e.PreviewFields(filePath, provider)
|
|
values := make([]PreviewFieldValue, 0)
|
|
for _, field := range fields {
|
|
for _, occurrence := range extractField(field, view) {
|
|
values = append(values, occurrence)
|
|
}
|
|
}
|
|
return values
|
|
}
|
|
|
|
func extractField(field FieldDefinition, view pvf.ScriptView) []PreviewFieldValue {
|
|
target := field.Target
|
|
if target.Kind == "section" {
|
|
result := make([]PreviewFieldValue, 0)
|
|
for _, sectionID := range sectionIDs(view, target.Section) {
|
|
tokens := sectionTokens(view, target.Section, sectionID)
|
|
if len(tokens) == 0 {
|
|
continue
|
|
}
|
|
result = append(result, PreviewFieldValue{
|
|
Field: field, Values: valuesOf(tokens),
|
|
Start: tokens[0].Start, End: tokens[len(tokens)-1].End,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
if target.Kind != "token" {
|
|
return nil
|
|
}
|
|
|
|
result := make([]PreviewFieldValue, 0)
|
|
for _, sectionID := range sectionIDs(view, target.Section) {
|
|
tokens := sectionTokens(view, target.Section, sectionID)
|
|
if target.Index != nil && target.RecordTokens > 0 {
|
|
recordTokens := target.RecordTokens
|
|
if target.TokensPerLineIndex != nil {
|
|
recordTokens = repeatedRecordTokens(target, tokens)
|
|
}
|
|
if recordTokens <= 0 {
|
|
continue
|
|
}
|
|
for _, group := range repeatedTokenGroups(target, tokens) {
|
|
for start := group.start; start+recordTokens <= group.end; start += recordTokens {
|
|
offset := *target.Index
|
|
if offset < 0 || offset >= recordTokens {
|
|
continue
|
|
}
|
|
record := tokens[start : start+recordTokens]
|
|
context := ""
|
|
if target.ContextIndex != nil && *target.ContextIndex >= 0 && *target.ContextIndex < len(record) {
|
|
context = record[*target.ContextIndex].Value
|
|
}
|
|
result = append(result, PreviewFieldValue{
|
|
Field: field, Values: valuesOf(record), Context: context,
|
|
Start: record[offset].Start, End: record[offset].End,
|
|
})
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
if target.Index != nil {
|
|
index := *target.Index
|
|
if index >= 0 && index < len(tokens) {
|
|
token := tokens[index]
|
|
result = append(result, PreviewFieldValue{Field: field, Values: []string{token.Value}, Start: token.Start, End: token.End})
|
|
}
|
|
continue
|
|
}
|
|
if target.Range == nil {
|
|
continue
|
|
}
|
|
start := maxInt(0, target.Range.Start)
|
|
end := minInt(len(tokens), target.Range.EndExclusive)
|
|
if start >= end {
|
|
continue
|
|
}
|
|
result = append(result, PreviewFieldValue{
|
|
Field: field, Values: valuesOf(tokens[start:end]),
|
|
Start: tokens[start].Start, End: tokens[end-1].End,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
func fieldMatches(match MatchSpec, filePath string, isDir bool) bool {
|
|
filePath = normalizeFieldPath(filePath)
|
|
if len(match.Extensions) > 0 {
|
|
if isDir {
|
|
return false
|
|
}
|
|
matched := false
|
|
for _, extension := range match.Extensions {
|
|
if strings.EqualFold(path.Ext(filePath), strings.TrimSpace(extension)) {
|
|
matched = true
|
|
break
|
|
}
|
|
}
|
|
if !matched {
|
|
return false
|
|
}
|
|
}
|
|
if match.Glob == "" {
|
|
return true
|
|
}
|
|
return globFieldMatch(match.Glob, filePath)
|
|
}
|
|
|
|
func globFieldMatch(pattern, value string) bool {
|
|
patternParts := strings.Split(normalizeFieldPath(pattern), "/")
|
|
valueParts := strings.Split(normalizeFieldPath(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(pi, vi int) bool {
|
|
key := state{pi, vi}
|
|
if seen[key] {
|
|
return memo[key]
|
|
}
|
|
seen[key] = true
|
|
if pi == len(patternParts) {
|
|
memo[key] = vi == len(valueParts)
|
|
return memo[key]
|
|
}
|
|
if patternParts[pi] == "**" {
|
|
memo[key] = match(pi+1, vi) || (vi < len(valueParts) && match(pi, vi+1))
|
|
return memo[key]
|
|
}
|
|
if vi >= len(valueParts) {
|
|
return false
|
|
}
|
|
matched, err := path.Match(patternParts[pi], valueParts[vi])
|
|
if err == nil && matched {
|
|
memo[key] = match(pi+1, vi+1)
|
|
}
|
|
return memo[key]
|
|
}
|
|
return match(0, 0)
|
|
}
|
|
|
|
func normalizeFieldPath(value string) string {
|
|
return strings.ToLower(strings.Trim(strings.ReplaceAll(value, "\\", "/"), "/"))
|
|
}
|
|
|
|
func sectionIDs(view pvf.ScriptView, name string) []int {
|
|
result := make([]int, 0)
|
|
seen := make(map[int]struct{})
|
|
for _, element := range view.Elements {
|
|
if element.Kind != pvf.ScriptElementSection || !strings.EqualFold(element.Section, name) {
|
|
continue
|
|
}
|
|
if _, ok := seen[element.SectionID]; ok {
|
|
continue
|
|
}
|
|
seen[element.SectionID] = struct{}{}
|
|
result = append(result, element.SectionID)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func sectionTokens(view pvf.ScriptView, name string, sectionID int) []pvf.ScriptElement {
|
|
result := make([]pvf.ScriptElement, 0)
|
|
for _, element := range view.Elements {
|
|
if element.Kind == pvf.ScriptElementToken && element.SectionID == sectionID && strings.EqualFold(element.Section, name) {
|
|
result = append(result, element)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func valuesOf(elements []pvf.ScriptElement) []string {
|
|
result := make([]string, len(elements))
|
|
for i, element := range elements {
|
|
result[i] = element.Value
|
|
}
|
|
return result
|
|
}
|
|
|
|
func maxInt(a, b int) int {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func minInt(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|