服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
126 lines
3.6 KiB
Go
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)
|
|
}
|
|
}
|