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

472 lines
14 KiB
Go

package services
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
"pvfine/internal/pvf"
)
func TestVersionServiceImportUndoAndCommit(t *testing.T) {
c, _, _ := versionServiceFixture(t)
versions := NewVersionService(c)
if _, err := versions.Initialize(); err != nil {
t.Fatal(err)
}
sourcePath := filepath.Join(t.TempDir(), "imported.equ")
if err := os.WriteFile(sourcePath, []byte("[name]\n`导入文件`"), 0o644); err != nil {
t.Fatal(err)
}
imports := NewArchiveService(c)
result, err := imports.ImportFiles([]string{sourcePath}, "", ImportModeText)
if err != nil {
t.Fatal(err)
}
if result.ImportedCount != 1 || result.OverwrittenCount != 0 {
t.Fatalf("import result = %#v", result)
}
status := versions.Status()
if status.ChangedFiles != 1 || status.PendingChangeSets != 1 {
t.Fatalf("import version status = %#v", status)
}
if _, err := versions.Undo(); err != nil {
t.Fatal(err)
}
if _, ok := c.archive.Find("imported.equ"); ok {
t.Fatal("undo did not remove imported file")
}
status = versions.Status()
if status.ChangedFiles != 0 || status.PendingChangeSets != 0 {
t.Fatalf("undo version status = %#v", status)
}
if _, err := imports.ImportFiles([]string{sourcePath}, "", ImportModeText); err != nil {
t.Fatal(err)
}
commit, err := versions.Commit("导入文件")
if err != nil {
t.Fatal(err)
}
if commit.ChangeCount != 1 || versions.Status().ChangedFiles != 0 {
t.Fatalf("import commit = %#v status=%#v", commit, versions.Status())
}
}
func versionServiceFixture(t *testing.T) (*core, string, int32) {
t.Helper()
path := writeSearchFixture(t, "version.pvf")
c := NewCore()
a, err := pvf.Open(path)
if err != nil {
t.Fatal(err)
}
if err := c.setArchive(a); err != nil {
t.Fatal(err)
}
index, ok := a.Find("equipment/character/common/amulet/1008.equ")
if !ok {
t.Fatal("version fixture file missing")
}
t.Cleanup(c.closeArchive)
return c, path, index
}
func waitForVersionLoad(t *testing.T, c *core) VersionStatus {
t.Helper()
deadline := time.Now().Add(10 * time.Second)
service := NewVersionService(c)
for time.Now().Before(deadline) {
status := service.Status()
if !status.Loading {
return status
}
time.Sleep(time.Millisecond)
}
t.Fatalf("version load did not finish: %#v", service.Status())
return VersionStatus{}
}
func TestVersionServiceCommitSaveHistoryCheckout(t *testing.T) {
c, _, index := versionServiceFixture(t)
versions := NewVersionService(c)
initialStatus, err := versions.Initialize()
if err != nil {
t.Fatal(err)
}
status := *initialStatus
if !status.Enabled || status.ChangedFiles != 0 || status.NeedsSave {
t.Fatalf("initial status = %#v", status)
}
if err := NewEditorService(c).SetText(index, "[name]\n`修改后的项链`\n[grade]\n2"); err != nil {
t.Fatal(err)
}
status = versions.Status()
if status.ChangedFiles != 1 || status.PendingChangeSets != 1 || !status.NeedsSave {
t.Fatalf("edited status = %#v", status)
}
first, err := versions.Commit("修改项链属性")
if err != nil {
t.Fatal(err)
}
status = versions.Status()
if first.ChangeCount != 1 || status.ChangedFiles != 0 || !status.NeedsSave {
t.Fatalf("committed status = %#v commit=%#v", status, first)
}
if _, err := NewEditorService(c).Save(); err != nil {
t.Fatal(err)
}
status = versions.Status()
if status.NeedsSave {
t.Fatalf("saved status = %#v", status)
}
history, err := versions.History(0, 10)
if err != nil || len(history.Commits) != 2 {
t.Fatalf("history = %#v err=%v", history, err)
}
diff, err := versions.Diff(first.ID, "equipment/character/common/amulet/1008.equ")
if err != nil {
t.Fatal(err)
}
if !diff.TextAvailable || !strings.Contains(diff.BeforeText, "烈火之心项链") || !strings.Contains(diff.AfterText, "修改后的项链") {
t.Fatalf("diff = %#v", diff)
}
if err := NewEditorService(c).SetText(index, "[name]\n`第三次修改`\n[grade]\n3"); err != nil {
t.Fatal(err)
}
if _, err := versions.Undo(); err != nil {
t.Fatal(err)
}
text, err := c.archive.Text(index)
if err != nil || !strings.Contains(text, "修改后的项链") {
t.Fatalf("undo text = %q err=%v", text, err)
}
rootID := history.Commits[len(history.Commits)-1].ID
if _, err := versions.Checkout(rootID); err != nil {
t.Fatal(err)
}
rootIndex, ok := c.archive.Find("equipment/character/common/amulet/1008.equ")
if !ok {
t.Fatal("checked out file missing")
}
text, err = c.archive.Text(rootIndex)
if err != nil || !strings.Contains(text, "烈火之心项链") {
t.Fatalf("checkout text = %q err=%v", text, err)
}
if versions.Status().HeadID != first.ID || versions.Status().ChangedFiles != 1 {
t.Fatalf("checkout status = %#v", versions.Status())
}
if _, err := versions.Discard(); err != nil {
t.Fatal(err)
}
current, ok := c.archive.Find("equipment/character/common/amulet/1008.equ")
if !ok {
t.Fatal("discarded file missing")
}
text, err = c.archive.Text(current)
if err != nil || !strings.Contains(text, "修改后的项链") {
t.Fatalf("discard text = %q err=%v", text, err)
}
}
func TestVersionServiceStatusTracksIncrementalRevert(t *testing.T) {
c, _, index := versionServiceFixture(t)
versions := NewVersionService(c)
if _, err := versions.Initialize(); err != nil {
t.Fatal(err)
}
original, err := c.archive.Text(index)
if err != nil {
t.Fatal(err)
}
if err := NewEditorService(c).SetText(index, "[name]\n`临时修改`"); err != nil {
t.Fatal(err)
}
status := versions.Status()
if status.ChangedFiles != 1 || !status.NeedsSave {
t.Fatalf("modified status = %#v", status)
}
if err := NewEditorService(c).SetText(index, original); err != nil {
t.Fatal(err)
}
status = versions.Status()
if status.ChangedFiles != 0 || status.NeedsSave {
t.Fatalf("reverted status = %#v", status)
}
}
func TestVersionServiceDiffWorkingAndRestorePath(t *testing.T) {
c, _, index := versionServiceFixture(t)
versions := NewVersionService(c)
if _, err := versions.Initialize(); err != nil {
t.Fatal(err)
}
original, err := c.archive.Text(index)
if err != nil {
t.Fatal(err)
}
updated := "[name]\n`工作区临时修改`\n[grade]\n8"
if err := NewEditorService(c).SetText(index, updated); err != nil {
t.Fatal(err)
}
diff, err := versions.DiffWorking("equipment/character/common/amulet/1008.equ")
if err != nil {
t.Fatal(err)
}
if !diff.TextAvailable || !strings.Contains(diff.BeforeText, "烈火之心项链") || !strings.Contains(diff.AfterText, "工作区临时修改") {
t.Fatalf("working diff = %#v", diff)
}
status, err := versions.RestorePath("equipment/character/common/amulet/1008.equ")
if err != nil {
t.Fatal(err)
}
if status.ChangedFiles != 0 || status.PendingChangeSets != 2 {
t.Fatalf("restored status = %#v", status)
}
restored, err := c.archive.Text(index)
if err != nil || restored != original {
t.Fatalf("restored text = %q err=%v", restored, err)
}
if _, err := versions.Undo(); err != nil {
t.Fatal(err)
}
current := versions.Status()
if current.ChangedFiles != 1 || current.PendingChangeSets != 1 {
t.Fatalf("undo restored status = %#v", current)
}
undone, err := c.archive.Text(index)
if err != nil || !strings.Contains(undone, "工作区临时修改") {
t.Fatalf("undo restored text = %q err=%v", undone, err)
}
}
func TestVersionServiceRemoveDeletesOnlySidecar(t *testing.T) {
c, path, _ := versionServiceFixture(t)
versions := NewVersionService(c)
if _, err := versions.Initialize(); err != nil {
t.Fatal(err)
}
sidecar := path + ".pvfine"
if _, err := os.Stat(sidecar); err != nil {
t.Fatalf("sidecar was not created: %v", err)
}
status, err := versions.Remove()
if err != nil {
t.Fatal(err)
}
if status == nil || status.Enabled {
t.Fatalf("removed status = %#v", status)
}
if _, err := os.Stat(sidecar); !os.IsNotExist(err) {
t.Fatalf("sidecar still exists or stat failed: %v", err)
}
if c.archive == nil {
t.Fatal("removing version control closed the archive")
}
if _, err := os.Stat(path); err != nil {
t.Fatalf("PVF was removed: %v", err)
}
}
func TestVersionServiceDiscoversRepositoryOnOpen(t *testing.T) {
c, path, index := versionServiceFixture(t)
versions := NewVersionService(c)
if _, err := versions.Initialize(); err != nil {
t.Fatal(err)
}
if err := NewEditorService(c).SetText(index, "[name]\n`持久化版本`\n[grade]\n5"); err != nil {
t.Fatal(err)
}
commit, err := versions.Commit("持久化版本")
if err != nil {
t.Fatal(err)
}
if _, err := NewEditorService(c).Save(); err != nil {
t.Fatal(err)
}
c.closeArchive()
archiveService := NewArchiveService(c)
if _, err := archiveService.Open(path); err != nil {
t.Fatal(err)
}
status := waitForVersionLoad(t, c)
if !status.Enabled || status.HeadID != commit.ID || status.ChangedFiles != 0 || status.NeedsSave {
t.Fatalf("reopened status = %#v", status)
}
openedIndex, ok := c.archive.Find("equipment/character/common/amulet/1008.equ")
if !ok {
t.Fatal("reopened file missing")
}
text, err := c.archive.Text(openedIndex)
if err != nil || !strings.Contains(text, "持久化版本") {
t.Fatalf("reopened text = %q err=%v", text, err)
}
if err := NewEditorService(c).SetText(openedIndex, "[name]\n`重新编辑`"); err != nil {
t.Fatal(err)
}
if status := versions.Status(); status.ChangedFiles != 1 || status.PendingChangeSets != 1 {
t.Fatalf("reopened edit status = %#v", status)
}
}
func TestVersionServiceRecoversCommittedWorktreeOnOpen(t *testing.T) {
c, path, index := versionServiceFixture(t)
versions := NewVersionService(c)
if _, err := versions.Initialize(); err != nil {
t.Fatal(err)
}
if err := NewEditorService(c).SetText(index, "[name]\n`尚未保存但已提交`\n[grade]\n6"); err != nil {
t.Fatal(err)
}
commit, err := versions.Commit("未保存提交")
if err != nil {
t.Fatal(err)
}
// The packed PVF remains at the base state. Reopening it should recover
// the committed logical state into memory without rewriting the file.
c.closeArchive()
if _, err := NewArchiveService(c).Open(path); err != nil {
t.Fatal(err)
}
status := waitForVersionLoad(t, c)
if status.HeadID != commit.ID || status.ChangedFiles != 0 || !status.NeedsSave {
t.Fatalf("recovered status = %#v", status)
}
openedIndex, ok := c.archive.Find("equipment/character/common/amulet/1008.equ")
if !ok {
t.Fatal("recovered file missing")
}
text, err := c.archive.Text(openedIndex)
if err != nil || !strings.Contains(text, "尚未保存但已提交") {
t.Fatalf("recovered text = %q err=%v", text, err)
}
}
func TestCloseArchiveAfterRecoveredVersionLoad(t *testing.T) {
c, path, index := versionServiceFixture(t)
versions := NewVersionService(c)
if _, err := versions.Initialize(); err != nil {
t.Fatal(err)
}
if err := NewEditorService(c).SetText(index, "[name]\n`恢复并立即关闭`"); err != nil {
t.Fatal(err)
}
if _, err := versions.Commit("恢复并关闭"); err != nil {
t.Fatal(err)
}
c.closeArchive()
for n := 0; n < 10; n++ {
if _, err := NewArchiveService(c).Open(path); err != nil {
t.Fatal(err)
}
waitForVersionLoad(t, c)
closed := make(chan struct{})
go func() {
c.closeArchive()
close(closed)
}()
select {
case <-closed:
case <-time.After(5 * time.Second):
t.Fatal("closing waited on the version task that was waiting to start the index")
}
}
}
func TestVersionServiceRecordsBatchAsOneChangeSet(t *testing.T) {
c, _, _ := versionServiceFixture(t)
versions := NewVersionService(c)
if _, err := versions.Initialize(); err != nil {
t.Fatal(err)
}
a := c.archive
first, ok := a.Find("equipment/equipment.lst")
if !ok {
t.Fatal("first batch file missing")
}
second, ok := a.Find("stackable/stackable.lst")
if !ok {
t.Fatal("second batch file missing")
}
page, err := NewBatchService(c).Preview(BatchRequest{
Mode: BatchModeText,
Paths: []string{a.Path(first), a.Path(second)},
Text: &TextReplaceSpec{Find: "1008", Replacement: "2008"},
})
if err != nil {
t.Fatal(err)
}
if _, err := NewBatchService(c).Apply(page.PlanID, []int32{first, second}); err != nil {
t.Fatal(err)
}
status := versions.Status()
if status.ChangedFiles != 2 || status.PendingChangeSets != 1 {
t.Fatalf("batch status = %#v", status)
}
commit, err := versions.Commit("批量替换编号")
if err != nil {
t.Fatal(err)
}
if commit.ChangeCount != 2 || versions.Status().ChangedFiles != 0 {
t.Fatalf("batch commit = %#v status=%#v", commit, versions.Status())
}
}
func TestVersionServiceCheckoutRestoresAddedFile(t *testing.T) {
c, _, _ := versionServiceFixture(t)
versions := NewVersionService(c)
if _, err := versions.Initialize(); err != nil {
t.Fatal(err)
}
node, err := NewArchiveService(c).CreateFile("custom/generated.stk", pvf.TypeScript)
if err != nil {
t.Fatal(err)
}
if err := NewEditorService(c).SetText(node.FileIndex, "[name]\n`生成文件`"); err != nil {
t.Fatal(err)
}
commit, err := versions.Commit("新增生成文件")
if err != nil {
t.Fatal(err)
}
if _, err := NewEditorService(c).Save(); err != nil {
t.Fatal(err)
}
history, err := versions.History(0, 10)
if err != nil || len(history.Commits) != 2 {
t.Fatalf("history = %#v err=%v", history, err)
}
rootID := history.Commits[len(history.Commits)-1].ID
if _, err := versions.Checkout(rootID); err != nil {
t.Fatal(err)
}
if _, ok := c.archive.Find("custom/generated.stk"); ok {
t.Fatal("added file remained after root checkout")
}
if _, err := versions.Checkout(commit.ID); err != nil {
t.Fatal(err)
}
index, ok := c.archive.Find("custom/generated.stk")
if !ok {
t.Fatal("added file missing after commit checkout")
}
text, err := c.archive.Text(index)
if err != nil || !strings.Contains(text, "生成文件") {
t.Fatalf("added file text = %q err=%v", text, err)
}
}