DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,316 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"pvfine/internal/pvf"
|
||||
)
|
||||
|
||||
func semanticDeltaFixture(t *testing.T) (*core, *sqliteArchiveIndex) {
|
||||
t.Helper()
|
||||
a := pvf.New()
|
||||
mustAddText(t, a, "equipment/first.equ", "[name]\n`First`\n[rarity]\n2", pvf.TypeScript)
|
||||
mustAddText(t, a, "equipment/second.equ", "[name]\n`Second`", pvf.TypeScript)
|
||||
mustAddText(t, a, "equipment/equipment.lst", "1\t`first.equ`\n2\t`second.equ`\n3\t`future.equ`", pvf.TypeScript)
|
||||
mustAddText(t, a, "stackable/stackable.lst", "9\t`../equipment/first.equ`", pvf.TypeScript)
|
||||
c := NewCore()
|
||||
if err := c.setArchive(a); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
index, _, err := openSQLiteArchiveIndex(a)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.diskIndex = index
|
||||
c.installDiskArchiveIndexesLocked(a)
|
||||
c.mu.Unlock()
|
||||
t.Cleanup(c.closeArchive)
|
||||
c.startSearchIndex()
|
||||
waitForSearchIndex(t, c)
|
||||
return c, index
|
||||
}
|
||||
|
||||
func assertSemanticDeltaMatchesFull(t *testing.T, c *core, index *sqliteArchiveIndex) {
|
||||
t.Helper()
|
||||
waitForSearchIndex(t, c)
|
||||
c.mu.RLock()
|
||||
snapshot := c.archive.CloneForBatch()
|
||||
specs := c.searchableListSpecsLocked()
|
||||
c.mu.RUnlock()
|
||||
oracle := deltaIndexFixture(t, snapshot)
|
||||
t.Cleanup(oracle.close)
|
||||
fake := &core{archive: snapshot, diskIndex: oracle, indexGen: 1, indexStatus: IndexStatus{State: IndexStateBuilding}}
|
||||
if _, _, err := oracle.buildSemantic(context.Background(), fake, snapshot, specs, 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, query := range []string{
|
||||
`SELECT file_index,name,record_id,path,category,list_path,size,data_type FROM records ORDER BY file_index,category,list_path,record_id`,
|
||||
`SELECT file_index,tag_id,name,category FROM tags ORDER BY file_index,category,tag_id`,
|
||||
`SELECT * FROM visuals ORDER BY file_index`,
|
||||
`SELECT * FROM semantic_dependencies ORDER BY file_index,dependency_path`,
|
||||
`SELECT value FROM meta WHERE key='semantic_count'`,
|
||||
} {
|
||||
c.mu.RLock()
|
||||
actual := deltaQuery(t, index.db, query)
|
||||
c.mu.RUnlock()
|
||||
expected := deltaQuery(t, oracle.db, query)
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
t.Fatalf("delta differs from full rebuild: %s\nactual=%v\nexpected=%v", query, actual, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLiteSemanticDeltaMatchesFullAndPreservesUntouchedRows(t *testing.T) {
|
||||
c, index := semanticDeltaFixture(t)
|
||||
originalDB := index.db
|
||||
untouched := deltaQuery(t, index.db, `SELECT id FROM records WHERE path='equipment/second.equ'`)
|
||||
first, _ := c.archive.Find("equipment/first.equ")
|
||||
if err := NewEditorService(c).SetText(first, "[name]\n`Updated`\n[rarity]\n4"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertSemanticDeltaMatchesFull(t, c, index)
|
||||
if index.db != originalDB || !reflect.DeepEqual(untouched, deltaQuery(t, index.db, `SELECT id FROM records WHERE path='equipment/second.equ'`)) {
|
||||
t.Fatal("single-file update rebuilt unrelated semantic rows")
|
||||
}
|
||||
created, err := NewArchiveService(c).CreateFile("equipment/future.equ", pvf.TypeScript)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := NewEditorService(c).SetText(created.FileIndex, "[name]\n`Future`"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertSemanticDeltaMatchesFull(t, c, index)
|
||||
list, _ := c.archive.Find("equipment/equipment.lst")
|
||||
if err := NewEditorService(c).SetText(list, "2\t`second.equ`\n3\t`future.equ`"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertSemanticDeltaMatchesFull(t, c, index)
|
||||
first, _ = c.archive.Find("equipment/first.equ")
|
||||
if _, err := NewArchiveService(c).DeleteFiles([]int32{first}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertSemanticDeltaMatchesFull(t, c, index)
|
||||
}
|
||||
|
||||
func TestSQLiteSemanticDeltaRollbackRetainsDirtyQueue(t *testing.T) {
|
||||
c, index := semanticDeltaFixture(t)
|
||||
first, _ := c.archive.Find("equipment/first.equ")
|
||||
c.mu.Lock()
|
||||
before := deltaQuery(t, index.db, `SELECT file_index,name FROM records ORDER BY id`)
|
||||
if err := c.archive.SetText(first, "[name]\n`After failure`"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
index.queueSemanticMutations(c.archive.MutationsSince(0))
|
||||
c.archive.ClearMutations()
|
||||
c.indexGen++
|
||||
gen := c.indexGen
|
||||
c.indexStatus = IndexStatus{State: IndexStateBuilding}
|
||||
specs := c.searchableListSpecsLocked()
|
||||
if _, err := index.db.Exec(`CREATE TRIGGER fail_semantic BEFORE INSERT ON records WHEN NEW.name='After failure' BEGIN SELECT RAISE(ABORT,'injected failure'); END`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := c.archive
|
||||
c.mu.Unlock()
|
||||
if _, _, err := index.buildSemanticDelta(context.Background(), c, a, specs, gen); err == nil {
|
||||
t.Fatal("failed delta reported success")
|
||||
}
|
||||
if !reflect.DeepEqual(before, deltaQuery(t, index.db, `SELECT file_index,name FROM records ORDER BY id`)) || len(index.pendingPaths) == 0 {
|
||||
t.Fatal("failed transaction changed records or lost pending work")
|
||||
}
|
||||
if _, err := index.db.Exec(`DROP TRIGGER fail_semantic`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err := index.buildSemanticDelta(context.Background(), c, a, specs, gen); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.indexStatus = IndexStatus{State: IndexStateReady}
|
||||
c.mu.Unlock()
|
||||
assertSemanticDeltaMatchesFull(t, c, index)
|
||||
}
|
||||
|
||||
func TestSQLiteDirtySearchUsesPathsAndRejectsOldPagination(t *testing.T) {
|
||||
c, index := semanticDeltaFixture(t)
|
||||
svc := NewArchiveService(c)
|
||||
first, err := svc.Search("equipment", 0, 1)
|
||||
if err != nil || first.NextCursor < 0 {
|
||||
t.Fatalf("first page=%#v err=%v", first, err)
|
||||
}
|
||||
c.mu.Lock()
|
||||
index.queueSemanticMutations(pvf.MutationSummary{Files: []pvf.FileMutation{{Path: "equipment/first.equ", Kind: pvf.MutationModified}}})
|
||||
c.indexStatus = IndexStatus{State: IndexStateBuilding}
|
||||
c.mu.Unlock()
|
||||
if name, err := index.indexedNames(0); err != ErrSearchIndexing || name != "" {
|
||||
t.Fatalf("dirty semantic name=%q err=%v", name, err)
|
||||
}
|
||||
if _, err := svc.Search("equipment", first.NextCursor, 1); err == nil {
|
||||
t.Fatal("old semantic cursor was accepted during path fallback")
|
||||
}
|
||||
partial, err := svc.Search("equipment/first", 0, 10)
|
||||
if err != nil || len(partial.Hits) != 1 || partial.Hits[0].ID != "" || partial.Hits[0].Name != "first.equ" {
|
||||
t.Fatalf("path fallback=%#v err=%v", partial, err)
|
||||
}
|
||||
if _, err := svc.SearchQuestTargets("dungeon", "1", 0, 10); err == nil {
|
||||
t.Fatal("exact semantic query presented incomplete results as ready")
|
||||
}
|
||||
c.startSearchIndex()
|
||||
waitForSearchIndex(t, c)
|
||||
if _, err := svc.Search("equipment", partial.NextCursor, 10); partial.NextCursor > 0 && err == nil {
|
||||
t.Fatal("path cursor survived publication of semantic rows")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLiteSemanticDeltaUpdatesLocalizedPlaceholderDependencies(t *testing.T) {
|
||||
c, a, itemIndex, _ := paged110LayoutFixture(t)
|
||||
index, _, err := openSQLiteArchiveIndex(a)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.diskIndex = index
|
||||
c.installDiskArchiveIndexesLocked(a)
|
||||
c.mu.Unlock()
|
||||
t.Cleanup(c.closeArchive)
|
||||
c.startSearchIndex()
|
||||
waitForSearchIndex(t, c)
|
||||
originalDB := index.db
|
||||
if err := NewEditorService(c).SetPlaceholderText(itemIndex, 3, "name_514530375", "增量本地化名称"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertSemanticDeltaMatchesFull(t, c, index)
|
||||
result, err := NewArchiveService(c).Search("增量本地化名称", 0, 10)
|
||||
if err != nil || len(result.Hits) != 1 {
|
||||
t.Fatalf("localized delta search=%#v err=%v", result, err)
|
||||
}
|
||||
if index.db != originalDB {
|
||||
t.Fatal("string-table edit copied the full semantic database")
|
||||
}
|
||||
}
|
||||
|
||||
// Compare identical one-file updates on a synthetic 100,000-entry archive.
|
||||
// Fixture preparation and initial indexing are outside the measured interval.
|
||||
func BenchmarkSQLiteSemanticRefresh(b *testing.B) {
|
||||
for _, mode := range []string{"full", "delta"} {
|
||||
b.Run(mode, func(b *testing.B) {
|
||||
a := pvf.New()
|
||||
var list strings.Builder
|
||||
for n := 0; n < 100000; n++ {
|
||||
path := fmt.Sprintf("equipment/%06d.equ", n)
|
||||
a.AddFile(path, nil, pvf.TypeScript)
|
||||
fmt.Fprintf(&list, "%d\t`%06d.equ`\n", n, n)
|
||||
}
|
||||
if _, err := a.AddFileText("equipment/equipment.lst", list.String(), pvf.TypeScript); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
index := deltaIndexFixture(b, a)
|
||||
b.Cleanup(index.close)
|
||||
c := &core{archive: a, diskIndex: index, indexGen: 1, indexStatus: IndexStatus{State: IndexStateBuilding}}
|
||||
specs := []searchableListSpec{{"equipment/equipment.lst", SearchCategoryEquipment}}
|
||||
if _, _, err := index.buildSemantic(context.Background(), c, a, specs, 1); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
b.StopTimer()
|
||||
if err := a.SetText(0, fmt.Sprintf("[name]\n`updated %d`", n)); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
index.queueSemanticMutations(a.MutationsSince(0))
|
||||
a.ClearMutations()
|
||||
b.StartTimer()
|
||||
var err error
|
||||
if mode == "full" {
|
||||
_, _, err = index.buildSemantic(context.Background(), c, a, specs, 1)
|
||||
} else {
|
||||
_, _, err = index.buildSemanticDelta(context.Background(), c, a, specs, 1)
|
||||
}
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLiteCacheIdentityChecksContentWithUnchangedFileMetadata(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "identity.pvf")
|
||||
a := pvf.New()
|
||||
mustAddText(t, a, "equipment/a.equ", "[name]\n`Alpha`", pvf.TypeScript)
|
||||
if err := a.SaveAs(path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
before, _, err := archiveIndexIdentity(a)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b := pvf.New()
|
||||
mustAddText(t, b, "equipment/a.equ", "[name]\n`Bravo`", pvf.TypeScript)
|
||||
if err := b.SaveAs(path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Chtimes(path, stat.ModTime(), stat.ModTime()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
afterStat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if afterStat.Size() != stat.Size() || a.Header().BodySize != b.Header().BodySize {
|
||||
t.Fatal("fixture must retain the cheap cache identity metadata")
|
||||
}
|
||||
loaded, err := pvf.Open(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
after, _, err := archiveIndexIdentity(loaded)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if before == after {
|
||||
t.Fatal("content replacement reused an unrelated cache")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLiteRapidEditsRetainLatestSemanticSnapshot(t *testing.T) {
|
||||
c, index := semanticDeltaFixture(t)
|
||||
first, _ := c.archive.Find("equipment/first.equ")
|
||||
for n := 0; n < 40; n++ {
|
||||
if err := NewEditorService(c).SetText(first, fmt.Sprintf("[name]\n`Latest %d`", n)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
assertSemanticDeltaMatchesFull(t, c, index)
|
||||
result, err := NewArchiveService(c).Search("Latest 39", 0, 10)
|
||||
if err != nil || len(result.Hits) != 2 {
|
||||
t.Fatalf("latest queued snapshot=%#v err=%v", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLiteForcedRebuildSurvivesConcurrentEdits(t *testing.T) {
|
||||
c, index := semanticDeltaFixture(t)
|
||||
before := index.db
|
||||
if _, err := NewArchiveService(c).RebuildSearchIndex(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first, _ := c.archive.Find("equipment/first.equ")
|
||||
for n := 0; n < 12; n++ {
|
||||
if err := NewEditorService(c).SetText(first, fmt.Sprintf("[name]\n`Forced latest %d`", n)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
assertSemanticDeltaMatchesFull(t, c, index)
|
||||
if index.db == before {
|
||||
t.Fatal("queued edits downgraded the explicit forced rebuild")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user