Files
90CNServer/pvf/services/autosave_real_test.go
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

126 lines
3.6 KiB
Go

package services
import (
"os"
"path/filepath"
"strings"
"testing"
"pvfine/internal/pvf"
)
// Real-archive coverage for the backup slot: a large archive (with real script
// payloads and string pools) has to snapshot without touching the source file
// and replay back into a fresh session.
func TestAutosaveRealArchiveRecovery(t *testing.T) {
filename := os.Getenv("PVF_TESTFILE")
if filename == "" {
t.Skip("PVF_TESTFILE 未设置")
}
const target = "equipment/character/common/amulet/100300001.equ"
const probePath = "zz_test/autosave-probe.stk"
const replacementScript = "[name]\n`测试物品`\n[grade]\n74\n[rarity]\n3\n"
// Present in the sample revisions but not part of the assertion: it only
// exercises the deleted-entry replay when it is there.
const deletedPath = "aicharacter/aicharacter.kor.str"
settings := newSettingsService(filepath.Join(t.TempDir(), "settings.json"))
appSettings := DefaultAppSettings()
appSettings.AutosaveEnabled = true
cachePath := filepath.Join(t.TempDir(), "backup", "autosave.pvf")
appSettings.AutosavePath = cachePath
if err := settings.SaveSettings(appSettings); err != nil {
t.Fatal(err)
}
first := NewCore()
t.Cleanup(first.closeArchive)
if _, err := first.openArchive(filename); err != nil {
t.Fatal(err)
}
index, ok := first.archive.Find(target)
if !ok {
t.Skip("样例缺少目标文件")
}
if err := NewEditorService(first).SetText(index, replacementScript); err != nil {
t.Fatal(err)
}
added, err := NewArchiveService(first).CreateFile(probePath, pvf.TypeScript)
if err != nil {
t.Fatal(err)
}
if err := NewEditorService(first).SetText(added.FileIndex, "[name]\n`缓存探针`"); err != nil {
t.Fatal(err)
}
deleted := ""
if other, ok := first.archive.Find(deletedPath); ok {
if _, err := NewArchiveService(first).DeleteFiles([]int32{other}); err != nil {
t.Fatal(err)
}
deleted = deletedPath
}
before, err := os.Stat(filename)
if err != nil {
t.Fatal(err)
}
if err := NewAutosaveService(first, settings).CreateSnapshot(); err != nil {
t.Fatal(err)
}
if info := first.archive.Info(); info.ModifiedCount == 0 {
t.Fatal("snapshot cleared the unsaved state of the live archive")
}
after, err := os.Stat(filename)
if err != nil {
t.Fatal(err)
}
if before.Size() != after.Size() || !before.ModTime().Equal(after.ModTime()) {
t.Fatal("snapshot wrote to the real archive")
}
first.closeArchive()
second := NewCore()
t.Cleanup(second.closeArchive)
recovery := NewAutosaveService(second, settings)
pending, err := recovery.PendingRecovery()
if err != nil {
t.Fatal(err)
}
if pending == nil || !pending.SourceExists || pending.SourcePath != filename {
t.Fatalf("pending recovery = %#v", pending)
}
if pending.PendingFiles < 2 {
t.Fatalf("pending files = %d, want the edited and the added entry", pending.PendingFiles)
}
if _, err := recovery.Restore(); err != nil {
t.Fatal(err)
}
restored, ok := second.archive.Find(target)
if !ok {
t.Fatal("restored archive lost the edited file")
}
meta, err := NewEditorService(second).GetFile(restored)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(meta.Text, "测试物品") || !meta.Modified {
t.Fatalf("restored file = %#v", meta)
}
if _, ok := second.archive.Find(probePath); !ok {
t.Fatal("restored archive lost the added file")
}
if deleted != "" {
if _, ok := second.archive.Find(deleted); ok {
t.Fatalf("restored archive kept the deleted file %q", deleted)
}
}
if _, err := recovery.Discard(); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(cachePath); !os.IsNotExist(err) {
t.Fatalf("backup survived the discard: %v", err)
}
}