Files
90CNServer/pvf/services/rule_service.go
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

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
}
}
}