Files
90CNServer/pvf/internal/ruleconfig/store.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

781 lines
21 KiB
Go

// Package ruleconfig manages editable source documents and production overlays.
package ruleconfig
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
"path"
"path/filepath"
"reflect"
"strings"
"sync"
appconfig "pvfine/config"
"pvfine/internal/annotations"
"pvfine/internal/rendering"
)
type Document struct {
Version int `json:"version"`
Description string `json:"description,omitempty"`
Rules []json.RawMessage `json:"rules"`
Fields []json.RawMessage `json:"fields,omitempty"`
Relations map[string]annotations.RelationSpec `json:"relations,omitempty"`
DisabledIDs []string `json:"disabledIds,omitempty"`
DisabledFieldIDs []string `json:"disabledFieldIds,omitempty"`
// Dependencies retain the provenance of a field's cascading suppression.
FieldSuppressions map[string][]string `json:"fieldSuppressions,omitempty"`
}
type Entry struct {
ID string `json:"id"`
Kind string `json:"kind"`
JSON string `json:"json"`
Source string `json:"source"`
Overridden bool `json:"overridden"`
Disabled bool `json:"disabled"`
Effective bool `json:"effective"`
Dependencies []string `json:"dependencies"`
}
type Snapshot struct {
annotationEngine *annotations.Engine
renderingEngine *rendering.Engine
Revision string `json:"revision"`
Development bool `json:"development"`
Entries []Entry `json:"entries"`
documents map[string]Document
base Document
merged Document
}
type Store struct {
builtin map[string][]byte
mu sync.Mutex
Development bool
SourceDir string
UserDir string
// Writer may be replaced in tests to verify persistence failures.
Writer func(string, []byte) error
}
func New(development bool, sourceDir, userDir string) *Store {
return &Store{Development: development, SourceDir: sourceDir, UserDir: userDir, Writer: atomicWrite}
}
func decode(data []byte) (Document, error) {
var d Document
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()
if err := dec.Decode(&d); err != nil {
return d, err
}
if err := dec.Decode(new(any)); err != io.EOF {
return d, fmt.Errorf("只能包含一个 JSON 文档")
}
if d.Version != 1 {
return d, fmt.Errorf("version 必须为 1")
}
return d, nil
}
func identity(raw json.RawMessage) string {
var r struct {
ID string `json:"id"`
}
_ = json.Unmarshal(raw, &r)
return r.ID
}
func fieldRef(raw json.RawMessage) string {
if !bytes.Contains(raw, []byte(`"field"`)) {
return ""
}
var r struct {
Field string `json:"field"`
}
_ = json.Unmarshal(raw, &r)
return strings.TrimSpace(r.Field)
}
func contains(xs []string, x string) bool {
for _, v := range xs {
if v == x {
return true
}
}
return false
}
func without(xs []string, x string) []string {
out := []string{}
for _, v := range xs {
if v != x {
out = append(out, v)
}
}
return out
}
func upsert(xs []json.RawMessage, raw json.RawMessage) []json.RawMessage {
for i, r := range xs {
if identity(r) == identity(raw) {
xs[i] = raw
return xs
}
}
return append(xs, raw)
}
func remove(xs []json.RawMessage, id string) []json.RawMessage {
out := []json.RawMessage{}
for _, r := range xs {
if identity(r) != id {
out = append(out, r)
}
}
return out
}
func mergeRows(base, user []json.RawMessage, disabled []string) []json.RawMessage {
out := []json.RawMessage{}
seen := map[string]bool{}
for _, r := range base {
id := identity(r)
seen[id] = true
if contains(disabled, id) {
continue
}
for _, u := range user {
if identity(u) == id {
r = u
break
}
}
out = append(out, r)
}
for _, r := range user {
if !seen[identity(r)] && !contains(disabled, identity(r)) {
out = append(out, r)
}
}
return out
}
func validateOverlay(d Document, category string) error {
for name, rows := range map[string][]json.RawMessage{"rules": d.Rules, "fields": d.Fields} {
seen := map[string]bool{}
for _, r := range rows {
id := identity(r)
if strings.TrimSpace(id) == "" || seen[id] {
return fmt.Errorf("%s ID 为空或重复: %s", name, id)
}
seen[id] = true
}
}
if category == "rendering" && (len(d.Fields) > 0 || len(d.Relations) > 0 || len(d.DisabledFieldIDs) > 0 || len(d.FieldSuppressions) > 0) {
return fmt.Errorf("渲染配置不支持共享字段或关联列表")
}
for _, rows := range [][]string{d.DisabledIDs, d.DisabledFieldIDs} {
seen := map[string]bool{}
for _, id := range rows {
if id == "" || seen[id] {
return fmt.Errorf("屏蔽 ID 为空或重复")
}
seen[id] = true
}
}
for field, ids := range d.FieldSuppressions {
if !contains(d.DisabledFieldIDs, field) {
return fmt.Errorf("字段依赖屏蔽缺少字段屏蔽: %s", field)
}
for _, id := range ids {
if id == "" {
return fmt.Errorf("依赖规则 ID 不能为空")
}
}
}
return nil
}
func (s *Store) read(category string) (*Snapshot, error) {
if category != "annotations" && category != "rendering" {
return nil, fmt.Errorf("未知规则类别")
}
if s.Development && s.SourceDir == "" {
return nil, fmt.Errorf("开发环境未找到源规则目录")
}
snap := &Snapshot{Development: s.Development, Entries: []Entry{}, documents: map[string]Document{}}
h := sha256.New()
sources := []struct {
name string
data []byte
}{{category + ".json", appconfig.AnnotationsJSON}}
if category == "rendering" {
sources[0].data = appconfig.RenderingJSON
} else {
sources = append(sources, struct {
name string
data []byte
}{"paths.json", appconfig.PathsJSON}, struct {
name string
data []byte
}{"sections.json", appconfig.SectionsJSON})
}
base := Document{Version: 1, Rules: []json.RawMessage{}}
seenRules := map[string]bool{}
seenFields := map[string]bool{}
for _, src := range sources {
data := src.data
if override, ok := s.builtin[src.name]; ok {
data = override
}
if s.Development {
var err error
data, err = os.ReadFile(filepath.Join(s.SourceDir, src.name))
if err != nil {
return nil, fmt.Errorf("读取 %s: %w", src.name, err)
}
}
h.Write([]byte(src.name))
h.Write(data)
d, err := decode(data)
if err != nil {
return nil, fmt.Errorf("解析 %s: %w", src.name, err)
}
snap.documents[src.name] = d
for _, r := range d.Rules {
id := identity(r)
if seenRules[id] {
continue
}
seenRules[id] = true
base.Rules = append(base.Rules, r)
snap.Entries = append(snap.Entries, Entry{ID: id, Kind: "rule", JSON: string(r), Source: src.name, Dependencies: []string{}})
}
for _, r := range d.Fields {
id := identity(r)
if seenFields[id] {
return nil, fmt.Errorf("共享字段 ID 重复: %s", id)
}
seenFields[id] = true
base.Fields = append(base.Fields, r)
snap.Entries = append(snap.Entries, Entry{ID: id, Kind: "field", JSON: string(r), Source: src.name, Dependencies: []string{}})
}
}
if category == "annotations" {
data := appconfig.ListsJSON
if override, ok := s.builtin["lists.json"]; ok {
data = override
}
if s.Development {
var err error
data, err = os.ReadFile(filepath.Join(s.SourceDir, "lists.json"))
if err != nil {
return nil, err
}
}
h.Write(data)
lists, err := annotations.ParseLists(data)
if err != nil {
return nil, err
}
base.Relations = lists.Relations
}
snap.base = base
overlayName := category + ".user.json"
overlay := Document{Version: 1, Rules: []json.RawMessage{}}
if !s.Development {
data, err := os.ReadFile(filepath.Join(s.UserDir, overlayName))
if err == nil {
overlay, err = decode(data)
if err != nil {
return nil, fmt.Errorf("用户规则损坏,请先修复 %s: %w", overlayName, err)
}
h.Write(data)
} else if !os.IsNotExist(err) {
return nil, err
} else {
h.Write([]byte("missing"))
}
if err := validateOverlay(overlay, category); err != nil {
return nil, err
}
snap.documents[overlayName] = overlay
}
merged := base
merged.Rules = mergeRows(base.Rules, overlay.Rules, overlay.DisabledIDs)
merged.Fields = mergeRows(base.Fields, overlay.Fields, overlay.DisabledFieldIDs)
merged.Rules = excludeFieldDependencies(merged.Rules, overlay.DisabledFieldIDs)
for _, ids := range overlay.FieldSuppressions {
for _, id := range ids {
merged.Rules = remove(merged.Rules, id)
}
}
if len(overlay.Relations) > 0 {
merged.Relations = map[string]annotations.RelationSpec{}
for k, v := range base.Relations {
merged.Relations[k] = v
}
for k, v := range overlay.Relations {
merged.Relations[k] = v
}
}
ann, renderer, err := Engines(category, merged)
if err != nil {
return nil, err
}
snap.annotationEngine = ann
snap.renderingEngine = renderer
snap.merged = merged
for i := range snap.Entries {
e := &snap.Entries[i]
rows := overlay.Rules
disabled := overlay.DisabledIDs
if e.Kind == "field" {
rows = overlay.Fields
disabled = overlay.DisabledFieldIDs
}
e.Disabled = contains(disabled, e.ID)
if e.Kind == "rule" {
for _, ids := range overlay.FieldSuppressions {
e.Disabled = e.Disabled || contains(ids, e.ID)
}
}
for _, r := range rows {
if identity(r) == e.ID {
e.JSON = string(r)
e.Overridden = true
break
}
}
}
for kind, rows := range map[string][]json.RawMessage{"rule": overlay.Rules, "field": overlay.Fields} {
for _, r := range rows {
exists := false
for _, e := range snap.Entries {
if e.Kind == kind && e.ID == identity(r) {
exists = true
break
}
}
if !exists {
snap.Entries = append(snap.Entries, Entry{ID: identity(r), Kind: kind, JSON: string(r), Source: overlayName, Dependencies: []string{}})
}
}
}
dependencyIDs := map[string][]string{}
for _, r := range mergeRows(base.Rules, overlay.Rules, nil) {
if field := fieldRef(r); field != "" {
dependencyIDs[field] = append(dependencyIDs[field], identity(r))
}
}
for i := range snap.Entries {
e := &snap.Entries[i]
if e.Kind == "rule" {
e.Disabled = contains(overlay.DisabledIDs, e.ID) || contains(overlay.DisabledFieldIDs, fieldRef(json.RawMessage(e.JSON)))
for _, ids := range overlay.FieldSuppressions {
e.Disabled = e.Disabled || contains(ids, e.ID)
}
} else {
e.Disabled = contains(overlay.DisabledFieldIDs, e.ID)
}
if e.Kind == "field" {
e.Dependencies = append(e.Dependencies, dependencyIDs[e.ID]...)
}
e.Effective = !e.Disabled
}
snap.Revision = hex.EncodeToString(h.Sum(nil))
return snap, nil
}
func Compile(category string, d Document) error {
_, _, err := Engines(category, d)
return err
}
func Engines(category string, d Document) (*annotations.Engine, *rendering.Engine, error) {
if d.Rules == nil {
d.Rules = []json.RawMessage{}
}
data, err := json.Marshal(d)
if err != nil {
return nil, nil, err
}
if category == "annotations" {
doc, err := annotations.Parse(data)
if err != nil {
return nil, nil, err
}
e, err := annotations.Compile(doc)
return e, nil, err
}
e, err := rendering.Parse(data)
return nil, e, err
}
func (s *Store) Load(category string) (*Snapshot, error) {
s.mu.Lock()
defer s.mu.Unlock()
return s.read(category)
}
func (snap *Snapshot) Engines(category string) (*annotations.Engine, *rendering.Engine, error) {
if (category == "annotations" && snap.annotationEngine != nil) || (category == "rendering" && snap.renderingEngine != nil) {
return snap.annotationEngine, snap.renderingEngine, nil
}
return Engines(category, snap.merged)
}
type Mutation struct {
Category string `json:"category"`
Revision string `json:"revision"`
Kind string `json:"kind"`
ID string `json:"id"`
Action string `json:"action"`
JSON string `json:"json"`
}
func (s *Store) Mutate(m Mutation) (*Snapshot, error) {
s.mu.Lock()
defer s.mu.Unlock()
snap, err := s.read(m.Category)
if err != nil {
return nil, err
}
if m.Revision != snap.Revision {
return nil, fmt.Errorf("规则已被外部修改,请重新加载后再保存")
}
if m.Kind != "rule" && m.Kind != "field" {
return nil, fmt.Errorf("未知规则类型")
}
if m.Category == "rendering" && m.Kind != "rule" {
return nil, fmt.Errorf("渲染不支持共享字段")
}
var entry *Entry
for i := range snap.Entries {
e := &snap.Entries[i]
if e.Kind == m.Kind && e.ID == m.ID {
entry = e
break
}
}
if m.Action != "save" && entry == nil {
return nil, fmt.Errorf("规则不存在")
}
name := m.Category + ".user.json"
if s.Development {
name = m.Category + ".json"
if entry != nil {
name = entry.Source
}
}
d := snap.documents[name]
rows := &d.Rules
if m.Kind == "field" {
rows = &d.Fields
}
switch m.Action {
case "save":
raw := json.RawMessage(m.JSON)
if !json.Valid(raw) || identity(raw) != m.ID || strings.TrimSpace(m.ID) == "" {
return nil, fmt.Errorf("规则 JSON 无效,或 ID 与原规则不一致")
}
*rows = upsert(*rows, raw)
if m.Kind == "field" {
d.DisabledFieldIDs = without(d.DisabledFieldIDs, m.ID)
delete(d.FieldSuppressions, m.ID)
} else {
d.DisabledIDs = without(d.DisabledIDs, m.ID)
}
case "delete":
if s.Development {
*rows = remove(*rows, m.ID)
if m.Kind == "field" {
for _, dependency := range snap.Entries {
if dependency.Kind == "rule" && fieldRef(json.RawMessage(dependency.JSON)) == m.ID && dependency.Source != name {
return nil, fmt.Errorf("共享字段有其他来源的引用,请先解除引用: %s", dependency.ID)
}
}
d.Rules = excludeFieldDependencies(d.Rules, []string{m.ID})
}
} else if entry.Overridden || entry.Source == name {
*rows = remove(*rows, m.ID)
if m.Kind == "field" && entry.Source == name {
d.Rules = excludeFieldDependencies(d.Rules, []string{m.ID})
d.DisabledFieldIDs = without(d.DisabledFieldIDs, m.ID)
delete(d.FieldSuppressions, m.ID)
} else if m.Kind == "rule" && entry.Source == name {
d.DisabledIDs = without(d.DisabledIDs, m.ID)
}
} else {
disable(&d, m, snap)
}
case "disable":
if s.Development {
return nil, fmt.Errorf("开发环境请直接删除规则")
}
disable(&d, m, snap)
case "restore":
if s.Development {
return nil, fmt.Errorf("开发环境不支持恢复内置")
}
if entry.Source != name {
*rows = remove(*rows, m.ID)
}
if m.Kind == "field" {
d.DisabledFieldIDs = without(d.DisabledFieldIDs, m.ID)
delete(d.FieldSuppressions, m.ID)
} else {
d.DisabledIDs = without(d.DisabledIDs, m.ID)
}
default:
return nil, fmt.Errorf("未知操作")
}
if d.Rules == nil {
d.Rules = []json.RawMessage{}
}
if err := validateOverlay(d, m.Category); err != nil {
return nil, err
}
// Validate the prospective configuration without persisting invalid data.
candidate := snap.base
if s.Development {
candidate.Rules = nil
candidate.Fields = nil
known := map[string]bool{}
names := []string{m.Category + ".json"}
if m.Category == "annotations" {
names = append(names, "paths.json", "sections.json")
}
for _, src := range names {
doc := snap.documents[src]
if src == name {
doc = d
}
for _, r := range doc.Rules {
if !known[identity(r)] {
candidate.Rules = append(candidate.Rules, r)
known[identity(r)] = true
}
}
candidate.Fields = append(candidate.Fields, doc.Fields...)
}
} else {
candidate.Rules = mergeRows(candidate.Rules, d.Rules, d.DisabledIDs)
candidate.Fields = mergeRows(candidate.Fields, d.Fields, d.DisabledFieldIDs)
candidate.Rules = excludeFieldDependencies(candidate.Rules, d.DisabledFieldIDs)
for _, ids := range d.FieldSuppressions {
for _, id := range ids {
candidate.Rules = remove(candidate.Rules, id)
}
}
if len(d.Relations) > 0 {
candidate.Relations = map[string]annotations.RelationSpec{}
for k, v := range snap.base.Relations {
candidate.Relations[k] = v
}
for k, v := range d.Relations {
candidate.Relations[k] = v
}
}
}
if err := Compile(m.Category, candidate); err != nil {
return nil, err
}
data, err := json.MarshalIndent(d, "", " ")
if err != nil {
return nil, err
}
dir := s.UserDir
if s.Development {
dir = s.SourceDir
}
latest, err := s.read(m.Category)
if err != nil {
return nil, err
}
if latest.Revision != snap.Revision {
return nil, fmt.Errorf("规则已被外部修改,请重新加载后再保存")
}
if err := s.Writer(filepath.Join(dir, name), append(data, '\n')); err != nil {
return nil, fmt.Errorf("保存规则失败: %w", err)
}
return s.read(m.Category)
}
func disable(d *Document, m Mutation, snap *Snapshot) {
if m.Kind == "field" {
if !contains(d.DisabledFieldIDs, m.ID) {
d.DisabledFieldIDs = append(d.DisabledFieldIDs, m.ID)
}
if d.FieldSuppressions == nil {
d.FieldSuppressions = map[string][]string{}
}
ids := []string{}
for _, r := range mergeRows(snap.base.Rules, d.Rules, nil) {
if fieldRef(r) == m.ID {
ids = append(ids, identity(r))
}
}
d.FieldSuppressions[m.ID] = ids
} else if !contains(d.DisabledIDs, m.ID) {
d.DisabledIDs = append(d.DisabledIDs, m.ID)
}
}
func atomicWrite(filename string, data []byte) error {
if err := os.MkdirAll(filepath.Dir(filename), 0o700); err != nil {
return err
}
f, err := os.CreateTemp(filepath.Dir(filename), ".rules-*.tmp")
if err != nil {
return err
}
defer os.Remove(f.Name())
mode := os.FileMode(0o600)
if info, err := os.Stat(filename); err == nil {
mode = info.Mode().Perm()
}
if err = f.Chmod(mode); err == nil {
_, err = f.Write(data)
}
if err == nil {
err = f.Sync()
}
closeErr := f.Close()
if err != nil {
return err
}
if closeErr != nil {
return closeErr
}
return os.Rename(f.Name(), filename)
}
// Migrate imports only differences; absence in a legacy mirror is never a deletion.
func (s *Store) Migrate(category string) error {
if s.Development {
return nil
}
s.mu.Lock()
defer s.mu.Unlock()
filename := filepath.Join(s.UserDir, category+".user.json")
if _, err := os.Stat(filename); err == nil {
return nil
} else if !os.IsNotExist(err) {
return err
}
legacyData, err := os.ReadFile(filepath.Join(s.UserDir, category+".json"))
if os.IsNotExist(err) {
return nil
}
if err != nil {
return err
}
legacy, err := decode(legacyData)
if err != nil {
return fmt.Errorf("旧规则迁移失败: %w", err)
}
snap, err := s.read(category)
if err != nil {
return err
}
overlay := Document{Version: 1, Rules: []json.RawMessage{}}
different := func(rows, base []json.RawMessage) []json.RawMessage {
out := []json.RawMessage{}
for _, r := range rows {
same := false
for _, b := range base {
if identity(b) == identity(r) {
var a, c any
_ = json.Unmarshal(r, &a)
_ = json.Unmarshal(b, &c)
same = reflect.DeepEqual(a, c)
break
}
}
if !same {
out = append(out, r)
}
}
return out
}
overlay.Rules = different(legacy.Rules, snap.base.Rules)
overlay.Fields = different(legacy.Fields, snap.base.Fields)
if category == "annotations" {
relations := legacy.Relations
if data, e := os.ReadFile(filepath.Join(s.UserDir, "lists.json")); e == nil {
lists, e := annotations.ParseLists(data)
if e != nil {
return e
}
relations = lists.Relations
} else if !os.IsNotExist(e) {
return e
}
for k, v := range relations {
if b, ok := snap.base.Relations[k]; !ok || !reflect.DeepEqual(v, b) {
if overlay.Relations == nil {
overlay.Relations = map[string]annotations.RelationSpec{}
}
overlay.Relations[k] = v
}
}
}
if err := validateOverlay(overlay, category); err != nil {
return err
}
candidate := snap.base
candidate.Rules = mergeRows(candidate.Rules, overlay.Rules, nil)
candidate.Fields = mergeRows(candidate.Fields, overlay.Fields, nil)
if len(overlay.Relations) > 0 {
candidate.Relations = map[string]annotations.RelationSpec{}
for k, v := range snap.base.Relations {
candidate.Relations[k] = v
}
for k, v := range overlay.Relations {
candidate.Relations[k] = v
}
}
if err := Compile(category, candidate); err != nil {
return fmt.Errorf("旧规则迁移失败: %w", err)
}
data, err := json.MarshalIndent(overlay, "", " ")
if err != nil {
return err
}
return s.Writer(filename, append(data, '\n'))
}
// Matches follows the archive-relative, case-insensitive glob semantics.
func Matches(glob, file string, extensions []string) bool {
file = strings.ToLower(strings.Trim(strings.ReplaceAll(file, "\\", "/"), "/"))
if len(extensions) > 0 {
ok := false
for _, e := range extensions {
ok = ok || strings.EqualFold(strings.TrimSpace(e), path.Ext(file))
}
if !ok {
return false
}
}
if strings.TrimSpace(glob) == "" {
return true
}
parts := strings.Split(strings.ToLower(strings.Trim(strings.ReplaceAll(glob, "\\", "/"), "/")), "/")
segments := strings.Split(file, "/")
var match func(int, int) bool
match = func(i, j int) bool {
if i == len(parts) {
return j == len(segments)
}
if parts[i] == "**" {
return match(i+1, j) || (j < len(segments) && match(i, j+1))
}
if j == len(segments) {
return false
}
ok, _ := path.Match(parts[i], segments[j])
return ok && match(i+1, j+1)
}
return match(0, 0)
}
func excludeFieldDependencies(rows []json.RawMessage, disabled []string) []json.RawMessage {
result := []json.RawMessage{}
for _, row := range rows {
if !contains(disabled, fieldRef(row)) {
result = append(result, row)
}
}
return result
}