服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
272 lines
7.9 KiB
Go
272 lines
7.9 KiB
Go
package services
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
annotationrules "pvfine/internal/annotations"
|
|
"pvfine/internal/buildmode"
|
|
"pvfine/internal/ruleconfig"
|
|
)
|
|
|
|
// RuleService exposes only rule identities and contents, never arbitrary paths.
|
|
type RuleService struct{ c *core }
|
|
|
|
func NewRuleService(c *core) *RuleService { return &RuleService{c: c} }
|
|
|
|
type RuleTarget struct {
|
|
Category string `json:"category"`
|
|
Path string `json:"path"`
|
|
Section string `json:"section"`
|
|
Directory bool `json:"directory"`
|
|
}
|
|
type RuleQueryResult struct {
|
|
Revision string `json:"revision"`
|
|
Development bool `json:"development"`
|
|
Entries []ruleconfig.Entry `json:"entries"`
|
|
Relations []string `json:"relations"`
|
|
}
|
|
|
|
func (s *RuleService) StartupWarnings() []string { return append([]string{}, s.c.ruleWarnings...) }
|
|
func (s *RuleService) Query(target RuleTarget) (RuleQueryResult, error) {
|
|
if s.c.ruleStore == nil {
|
|
return RuleQueryResult{}, fmt.Errorf("规则存储不可用")
|
|
}
|
|
snap, err := s.c.ruleStore.Load(target.Category)
|
|
if err != nil {
|
|
return RuleQueryResult{}, err
|
|
}
|
|
s.c.mu.RLock()
|
|
version := s.c.archive.ClientVersion()
|
|
archiveOpen := s.c.archive != nil
|
|
s.c.mu.RUnlock()
|
|
result := RuleQueryResult{Revision: snap.Revision, Development: snap.Development, Entries: []ruleconfig.Entry{}, Relations: []string{}}
|
|
ann, renderer, err := snap.Engines(target.Category)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
applicable := map[string]bool{}
|
|
activeFields := map[string]bool{}
|
|
if ann != nil {
|
|
if archiveOpen {
|
|
ann = ann.ForVersion(version)
|
|
}
|
|
applicable = ann.ApplicableRuleIDs(target.Path, target.Section, target.Directory)
|
|
for _, field := range ann.Document().Fields {
|
|
if field.Preview != nil {
|
|
activeFields[field.ID] = true
|
|
}
|
|
}
|
|
}
|
|
fields := map[string]json.RawMessage{}
|
|
for _, entry := range snap.Entries {
|
|
if entry.Kind == "field" {
|
|
fields[entry.ID] = json.RawMessage(entry.JSON)
|
|
}
|
|
}
|
|
for _, entry := range snap.Entries {
|
|
var r struct {
|
|
Field string `json:"field"`
|
|
PVFVersions []string `json:"pvfVersions"`
|
|
Match annotationrules.MatchSpec `json:"match"`
|
|
Target annotationrules.TargetSpec `json:"target"`
|
|
}
|
|
if err := json.Unmarshal([]byte(entry.JSON), &r); err != nil {
|
|
return result, err
|
|
}
|
|
if r.Field != "" {
|
|
var f struct {
|
|
Match annotationrules.MatchSpec `json:"match"`
|
|
Target annotationrules.TargetSpec `json:"target"`
|
|
}
|
|
_ = json.Unmarshal(fields[r.Field], &f)
|
|
if r.Target.Kind == "" {
|
|
r.Target = f.Target
|
|
}
|
|
if r.Match.Glob == "" && len(r.Match.Extensions) == 0 {
|
|
r.Match = f.Match
|
|
}
|
|
}
|
|
// A directory rendering entry describes descendant files; use the rule's
|
|
// glob plus a representative child for extension-only rules.
|
|
matches := ruleconfig.Matches(r.Match.Glob, target.Path, r.Match.Extensions)
|
|
if target.Directory && target.Category == "rendering" {
|
|
matches = false
|
|
candidates := []string{target.Path + "/example"}
|
|
for _, extension := range r.Match.Extensions {
|
|
candidates = append(candidates, target.Path+"/example"+extension)
|
|
}
|
|
for _, candidate := range candidates {
|
|
matches = matches || ruleconfig.Matches(r.Match.Glob, candidate, r.Match.Extensions)
|
|
}
|
|
prefix := strings.TrimSuffix(target.Path, "/") + "/"
|
|
matches = matches || strings.HasPrefix(strings.ToLower(r.Match.Glob), strings.ToLower(prefix))
|
|
}
|
|
if !matches {
|
|
continue
|
|
}
|
|
if target.Section != "" {
|
|
if !strings.EqualFold(r.Target.Section, target.Section) {
|
|
continue
|
|
}
|
|
} else if target.Category == "annotations" && r.Target.Kind != "path" {
|
|
continue
|
|
}
|
|
if target.Directory && target.Category == "annotations" && len(r.Match.Extensions) > 0 {
|
|
continue
|
|
}
|
|
if len(r.PVFVersions) > 0 && version != "" {
|
|
ok := false
|
|
for _, v := range r.PVFVersions {
|
|
ok = ok || v == version
|
|
}
|
|
entry.Effective = entry.Effective && ok
|
|
}
|
|
if ann != nil {
|
|
id := entry.ID
|
|
if entry.Kind == "field" {
|
|
id = "field:" + id
|
|
}
|
|
entry.Effective = entry.Effective && (applicable[id] || (entry.Kind == "field" && activeFields[entry.ID]))
|
|
}
|
|
if renderer != nil && !target.Directory {
|
|
chosen := renderer.EffectiveRuleID(target.Path, r.Target.Section, r.Target.Kind)
|
|
entry.Effective = entry.Effective && chosen == entry.ID
|
|
}
|
|
result.Entries = append(result.Entries, entry)
|
|
}
|
|
if ann != nil {
|
|
for name := range ann.Document().Relations {
|
|
result.Relations = append(result.Relations, name)
|
|
}
|
|
}
|
|
sort.Strings(result.Relations)
|
|
return result, nil
|
|
}
|
|
|
|
// Apply validates, saves and activates a rule operation. A stale revision is rejected.
|
|
func (s *RuleService) Apply(m ruleconfig.Mutation) error {
|
|
if s.c.ruleStore == nil {
|
|
return fmt.Errorf("规则存储不可用")
|
|
}
|
|
// Serialize disk mutation and engine activation, including concurrent reloads.
|
|
s.c.ruleMu.Lock()
|
|
defer s.c.ruleMu.Unlock()
|
|
snap, err := s.c.ruleStore.Mutate(m)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.activate(m.Category, snap)
|
|
}
|
|
func (s *RuleService) reload(category string) error {
|
|
s.c.ruleMu.Lock()
|
|
defer s.c.ruleMu.Unlock()
|
|
snap, err := s.c.ruleStore.Load(category)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.activate(category, snap)
|
|
}
|
|
func (s *RuleService) activate(category string, snap *ruleconfig.Snapshot) error {
|
|
ann, renderer, err := snap.Engines(category)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c := s.c
|
|
c.mu.Lock()
|
|
archiveOpen := c.archive != nil
|
|
specChanged := false
|
|
if ann != nil {
|
|
old := searchIndexSpecFingerprint(c.searchableListSpecsLocked())
|
|
c.annotationSnapshot = snap
|
|
c.annotationEngine = ann.ForVersion(c.archive.ClientVersion())
|
|
c.annotationErr = nil
|
|
c.annotationRelations = make(map[string]map[string]*relationTarget)
|
|
c.pathAnnotations = nil
|
|
if archiveOpen {
|
|
c.pathAnnotations = buildPathAnnotations(c.annotationEngine, c.dirChildren)
|
|
}
|
|
specChanged = old != searchIndexSpecFingerprint(c.searchableListSpecsLocked())
|
|
if archiveOpen && specChanged {
|
|
c.markAdvancedSearchStaleLocked(c.archive)
|
|
}
|
|
if archiveOpen && specChanged && c.diskIndex != nil {
|
|
c.diskIndex.dirty = true
|
|
c.diskIndex.semanticEpoch++
|
|
}
|
|
}
|
|
if renderer != nil {
|
|
c.renderingEngine = renderer
|
|
c.renderingErr = nil
|
|
if archiveOpen {
|
|
c.bindRenderingEngineLocked(c.archive)
|
|
}
|
|
if c.versionBaseArchive != nil {
|
|
c.versionBaseArchive.SetScriptRenderer(renderer)
|
|
}
|
|
c.batchRevision++
|
|
c.batchPlan = nil
|
|
c.invalidateScriptLocked()
|
|
}
|
|
c.editorAnnotation = editorAnnotationCache{}
|
|
c.mu.Unlock()
|
|
if archiveOpen && specChanged {
|
|
c.startSearchIndex()
|
|
}
|
|
if ann != nil {
|
|
doc := ann.Document()
|
|
emitEvent("annotations:reloaded", AnnotationReloadResult{RuleCount: len(doc.Rules), RelationCount: len(doc.Relations)})
|
|
}
|
|
if renderer != nil {
|
|
emitEvent("rendering:reloaded", RenderingReloadResult{RuleCount: len(renderer.Document().Rules)})
|
|
}
|
|
return nil
|
|
}
|
|
func (c *core) initializeRuleStore() {
|
|
configDir, err := os.UserConfigDir()
|
|
if err != nil {
|
|
c.ruleWarnings = append(c.ruleWarnings, err.Error())
|
|
return
|
|
}
|
|
sourceDir := ""
|
|
if buildmode.Development {
|
|
if source, ok := annotationrules.FindSourcePath(); ok {
|
|
sourceDir = filepath.Dir(source)
|
|
}
|
|
}
|
|
c.loadInitialRules(ruleconfig.New(buildmode.Development, sourceDir, filepath.Join(configDir, "pvfine")))
|
|
}
|
|
|
|
func (c *core) loadInitialRules(store *ruleconfig.Store) {
|
|
c.ruleStore = store
|
|
for _, category := range []string{"annotations", "rendering"} {
|
|
if err := c.ruleStore.Migrate(category); err != nil {
|
|
c.ruleWarnings = append(c.ruleWarnings, err.Error())
|
|
continue
|
|
}
|
|
snap, err := c.ruleStore.Load(category)
|
|
if err != nil {
|
|
c.ruleWarnings = append(c.ruleWarnings, err.Error())
|
|
continue
|
|
}
|
|
ann, renderer, err := snap.Engines(category)
|
|
if err != nil {
|
|
c.ruleWarnings = append(c.ruleWarnings, err.Error())
|
|
continue
|
|
}
|
|
if ann != nil {
|
|
c.annotationSnapshot = snap
|
|
c.annotationEngine = ann
|
|
c.annotationErr = nil
|
|
}
|
|
if renderer != nil {
|
|
c.renderingEngine = renderer
|
|
c.renderingErr = nil
|
|
}
|
|
}
|
|
}
|