Files
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

265 lines
11 KiB
Go

package ruleconfig
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"strings"
"testing"
)
const annotationFixture = `{"version":1,"fields":[{"id":"shared","match":{"glob":"a/test.etc"},"target":{"kind":"token","section":"name","index":0},"annotation":{"type":"text","title":"shared"},"preview":{"provider":"equ","role":"name","group":"header","format":"text"}}],"rules":[{"id":"note","match":{"glob":"a/test.etc"},"target":{"kind":"path"},"annotation":{"type":"text","title":"builtin"}},{"id":"dependent","field":"shared","match":{},"target":{},"annotation":{"title":"","type":""}}]}`
const renderFixture = `{"version":1,"rules":[{"id":"format","match":{},"target":{"kind":"section","section":"name"},"format":{"tokensPerLine":2}},{"id":"other","match":{},"target":{"kind":"section","section":"other"},"format":{"tokensPerLine":1}}]}`
func fixtureStore(t *testing.T, development bool) *Store {
t.Helper()
s := New(development, t.TempDir(), t.TempDir())
s.builtin = map[string][]byte{"annotations.json": []byte(annotationFixture), "rendering.json": []byte(renderFixture), "paths.json": []byte(`{"version":1,"rules":[{"id":"generated","match":{"glob":"b"},"target":{"kind":"path"},"annotation":{"type":"text","title":"generated"}}]}`), "sections.json": []byte(`{"version":1,"rules":[]}`), "lists.json": []byte(`{"version":1,"relations":{}}`)}
for name, data := range s.builtin {
if err := os.WriteFile(filepath.Join(s.SourceDir, name), data, 0o600); err != nil {
t.Fatal(err)
}
}
return s
}
func load(t *testing.T, s *Store, category string) *Snapshot {
t.Helper()
snap, err := s.Load(category)
if err != nil {
t.Fatal(err)
}
return snap
}
func apply(t *testing.T, s *Store, category, kind, id, action, raw string) *Snapshot {
t.Helper()
snap := load(t, s, category)
next, err := s.Mutate(Mutation{Category: category, Revision: snap.Revision, Kind: kind, ID: id, Action: action, JSON: raw})
if err != nil {
t.Fatal(err)
}
return next
}
func row(t *testing.T, snap *Snapshot, id string) Entry {
t.Helper()
for _, e := range snap.Entries {
if e.ID == id {
return e
}
}
t.Fatalf("missing entry %s", id)
return Entry{}
}
func TestProductionOverrideOrderDisableRestore(t *testing.T) {
s := fixtureStore(t, false)
override := `{"id":"format","match":{},"target":{"kind":"section","section":"name"},"format":{"tokensPerLine":5}}`
snap := apply(t, s, "rendering", "rule", "format", "save", override)
_, engine, err := snap.Engines("rendering")
if err != nil {
t.Fatal(err)
}
if engine.SectionFormat("a/test.etc", "name").TokensPerLine != 5 || identity(snap.merged.Rules[0]) != "format" || !row(t, snap, "format").Overridden {
t.Fatal("override not in original position")
}
snap = apply(t, s, "rendering", "rule", "new", "save", strings.Replace(override, `"format"`, `"new"`, 1))
if len(snap.merged.Rules) != 3 || identity(snap.merged.Rules[2]) != "new" {
t.Fatal("different IDs must coexist")
}
snap = apply(t, s, "rendering", "rule", "format", "delete", "")
if row(t, snap, "format").Overridden {
t.Fatal("delete override must restore builtin")
}
snap = apply(t, s, "rendering", "rule", "format", "disable", "")
if !row(t, snap, "format").Disabled || len(snap.merged.Rules) != 2 {
t.Fatal("builtin not suppressed")
}
snap = apply(t, s, "rendering", "rule", "format", "restore", "")
if row(t, snap, "format").Disabled {
t.Fatal("builtin not restored")
}
apply(t, s, "rendering", "rule", "new", "delete", "")
data, err := os.ReadFile(filepath.Join(s.SourceDir, "rendering.json"))
if err != nil || string(data) != renderFixture {
t.Fatal("production changed source")
}
}
func TestSharedFieldCascadePreservesIndependentSuppression(t *testing.T) {
s := fixtureStore(t, false)
apply(t, s, "annotations", "rule", "dependent", "disable", "")
snap := apply(t, s, "annotations", "field", "shared", "disable", "")
if !row(t, snap, "dependent").Disabled || len(snap.merged.Fields) != 0 {
t.Fatal("field/dependency not suppressed")
}
snap = apply(t, s, "annotations", "field", "shared", "restore", "")
if row(t, snap, "shared").Disabled || !row(t, snap, "dependent").Disabled {
t.Fatal("restoring field erased independent rule suppression")
}
apply(t, s, "annotations", "rule", "dependent", "restore", "")
apply(t, s, "annotations", "field", "shared", "disable", "")
snap = apply(t, s, "annotations", "field", "shared", "restore", "")
if row(t, snap, "dependent").Disabled {
t.Fatal("cascade not restored")
}
}
func TestDevelopmentWritesOriginalSource(t *testing.T) {
s := fixtureStore(t, true)
apply(t, s, "annotations", "rule", "generated", "save", `{"id":"generated","match":{"glob":"b"},"target":{"kind":"path"},"annotation":{"type":"text","title":"edited"}}`)
data, _ := os.ReadFile(filepath.Join(s.SourceDir, "paths.json"))
if !strings.Contains(string(data), "edited") {
t.Fatal("did not write generated source")
}
data, _ = os.ReadFile(filepath.Join(s.SourceDir, "annotations.json"))
if string(data) != annotationFixture {
t.Fatal("rewrote wrong source")
}
apply(t, s, "annotations", "rule", "generated", "delete", "")
snap := load(t, s, "annotations")
for _, e := range snap.Entries {
if e.ID == "generated" {
t.Fatal("not deleted")
}
}
if files, _ := os.ReadDir(s.UserDir); len(files) != 0 {
t.Fatal("development wrote user directory")
}
s.SourceDir = ""
if _, err := s.Load("annotations"); err == nil {
t.Fatal("missing source must fail")
}
}
func TestMutationConflictInvalidInputAndWriteFailure(t *testing.T) {
s := fixtureStore(t, false)
snap := load(t, s, "rendering")
invalid := Mutation{Category: "rendering", Revision: snap.Revision, Kind: "rule", ID: "format", Action: "save", JSON: `{"id":"format","match":{},"target":{"kind":"section","section":"name"},"format":{"tokensPerLine":0}}`}
if _, err := s.Mutate(invalid); err == nil {
t.Fatal("invalid rule accepted")
}
if _, err := os.Stat(filepath.Join(s.UserDir, "rendering.user.json")); !os.IsNotExist(err) {
t.Fatal("invalid rule persisted")
}
valid := invalid
valid.JSON = strings.Replace(invalid.JSON, `"tokensPerLine":0`, `"tokensPerLine":3`, 1)
s.Writer = func(string, []byte) error { return errors.New("disk full") }
if _, err := s.Mutate(valid); err == nil {
t.Fatal("write failure hidden")
}
s.Writer = atomicWrite
apply(t, s, "rendering", "rule", "format", "save", valid.JSON)
if _, err := s.Mutate(valid); err == nil || !strings.Contains(err.Error(), "外部修改") {
t.Fatal("stale revision accepted")
}
path := filepath.Join(s.UserDir, "rendering.user.json")
os.WriteFile(path, []byte("{"), 0o600)
if _, err := s.Mutate(valid); err == nil {
t.Fatal("corrupted overlay overwritten")
}
data, _ := os.ReadFile(path)
if string(data) != "{" {
t.Fatal("damaged file changed")
}
}
func TestLegacyMigrationImportsDifferencesAndIsIdempotent(t *testing.T) {
s := fixtureStore(t, false)
legacy := strings.Replace(renderFixture, `"tokensPerLine":2`, `"tokensPerLine":7`, 1)
legacyPath := filepath.Join(s.UserDir, "rendering.json")
os.WriteFile(legacyPath, []byte(legacy), 0o600)
if err := s.Migrate("rendering"); err != nil {
t.Fatal(err)
}
data, _ := os.ReadFile(filepath.Join(s.UserDir, "rendering.user.json"))
var overlay Document
if err := json.Unmarshal(data, &overlay); err != nil {
t.Fatal(err)
}
if len(overlay.Rules) != 1 || len(overlay.DisabledIDs) != 0 {
t.Fatal("migration copied unchanged entries")
}
if err := s.Migrate("rendering"); err != nil {
t.Fatal(err)
}
next, _ := os.ReadFile(filepath.Join(s.UserDir, "rendering.user.json"))
if string(data) != string(next) {
t.Fatal("migration not idempotent")
}
old, _ := os.ReadFile(legacyPath)
if string(old) != legacy {
t.Fatal("legacy changed")
}
// An omitted builtin in a legacy mirror is not a deletion.
s = fixtureStore(t, false)
os.WriteFile(filepath.Join(s.UserDir, "rendering.json"), []byte(`{"version":1,"rules":[]}`), 0o600)
if err := s.Migrate("rendering"); err != nil {
t.Fatal(err)
}
if len(load(t, s, "rendering").merged.Rules) != 2 {
t.Fatal("missing legacy rows became deletions")
}
}
func TestLegacyRelationsRemainAvailableReadOnly(t *testing.T) {
s := fixtureStore(t, false)
legacy := `{"version":1,"rules":[{"id":"ref","match":{},"target":{"kind":"token","section":"name","index":0},"annotation":{"type":"reference","title":"ref","relation":"custom"}}]}`
os.WriteFile(filepath.Join(s.UserDir, "annotations.json"), []byte(legacy), 0o600)
os.WriteFile(filepath.Join(s.UserDir, "lists.json"), []byte(`{"version":1,"relations":{"custom":{"listPath":"test.lst","idToken":0,"pathToken":1,"recordTokens":2,"nameSection":"name"}}}`), 0o600)
if err := s.Migrate("annotations"); err != nil {
t.Fatal(err)
}
snap := load(t, s, "annotations")
if _, ok := snap.merged.Relations["custom"]; !ok {
t.Fatal("legacy relation lost")
}
}
func TestSuppressedFieldHandlesNewBuiltinDependencies(t *testing.T) {
s := fixtureStore(t, false)
apply(t, s, "annotations", "field", "shared", "disable", "")
var d Document
json.Unmarshal(s.builtin["annotations.json"], &d)
d.Rules = append(d.Rules, json.RawMessage(`{"id":"new-dependent","field":"shared","match":{},"target":{},"annotation":{"title":"","type":""}}`))
data, _ := json.Marshal(d)
s.builtin["annotations.json"] = data
snap := load(t, s, "annotations")
if !row(t, snap, "new-dependent").Disabled {
t.Fatal("upgrade dependency not suppressed")
}
}
func TestMatches(t *testing.T) {
for _, tc := range []struct {
glob, file string
ext []string
want bool
}{{"a/**", "a/deep/x.etc", nil, true}, {"a/**", "a2/x.etc", nil, false}, {"a/*", "a/deep/x.etc", nil, false}, {"", "A/X.ETC", []string{".etc"}, true}, {"a", "a/child", nil, false}} {
if got := Matches(tc.glob, tc.file, tc.ext); got != tc.want {
t.Fatalf("%q %q: %v", tc.glob, tc.file, got)
}
}
}
func TestUserFieldSuppressRestoreAndCascadeDelete(t *testing.T) {
s := fixtureStore(t, false)
field := `{"id":"custom","match":{},"target":{"kind":"token","section":"custom","index":0},"annotation":{"title":"custom","type":"text"}}`
apply(t, s, "annotations", "field", "custom", "save", field)
apply(t, s, "annotations", "rule", "custom-ref", "save", `{"id":"custom-ref","field":"custom","match":{},"target":{},"annotation":{"title":"","type":""}}`)
snap := apply(t, s, "annotations", "field", "custom", "disable", "")
if !row(t, snap, "custom").Disabled || !row(t, snap, "custom-ref").Disabled {
t.Fatal("user field/dependency not suppressed")
}
snap = apply(t, s, "annotations", "field", "custom", "restore", "")
if row(t, snap, "custom").Disabled || row(t, snap, "custom-ref").Disabled {
t.Fatal("user field/dependency not restored")
}
snap = apply(t, s, "annotations", "field", "custom", "delete", "")
for _, e := range snap.Entries {
if e.ID == "custom" || e.ID == "custom-ref" {
t.Fatal("user field/dependency not deleted")
}
}
}
func TestDevelopmentFieldDeletionRemovesSameSourceDependencies(t *testing.T) {
s := fixtureStore(t, true)
snap := apply(t, s, "annotations", "field", "shared", "delete", "")
for _, e := range snap.Entries {
if e.ID == "shared" || e.ID == "dependent" {
t.Fatal("source field/dependency not deleted")
}
}
}