DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

+344
View File
@@ -0,0 +1,344 @@
// Author: flobit
package services
import (
"path/filepath"
"strings"
"testing"
"time"
"pvfine/internal/pvf"
)
func TestArchiveDiffStringPoolDifferenceAndMerge(t *testing.T) {
// 创建基础归档 A
dir := t.TempDir()
pathA := filepath.Join(dir, "base.pvf")
pathB := filepath.Join(dir, "other.pvf")
a := pvf.New()
// 写入脚本,引用 A 中的字符串
if _, err := a.AddFileText("etc/test.etc", "[name]\n`hello_from_A`\n", pvf.TypeScript); err != nil {
t.Fatal(err)
}
if _, err := a.AddFileText("etc/same.etc", "[name]\n`common_string`\n", pvf.TypeScript); err != nil {
t.Fatal(err)
}
if err := a.SaveAs(pathA); err != nil {
t.Fatal(err)
}
// 创建对比归档 B,字符串池内容不同
b := pvf.New()
if _, err := b.AddFileText("etc/test.etc", "[name]\n`hello_from_B`\n", pvf.TypeScript); err != nil {
t.Fatal(err)
}
if _, err := b.AddFileText("etc/same.etc", "[name]\n`common_string`\n", pvf.TypeScript); err != nil {
t.Fatal(err)
}
if _, err := b.AddFileText("etc/added.etc", "[name]\n`newly_added`\n", pvf.TypeScript); err != nil {
t.Fatal(err)
}
if err := b.SaveAs(pathB); err != nil {
t.Fatal(err)
}
core := NewCore()
openA, err := pvf.Open(pathA)
if err != nil {
t.Fatal(err)
}
if err := core.setArchive(openA); err != nil {
t.Fatal(err)
}
defer core.closeArchive()
diffService := NewArchiveDiffService(core)
status, err := diffService.Start(ArchiveDiffRequest{Path: pathB})
if err != nil {
t.Fatalf("Start diff failed: %v", err)
}
// 等待比较完成
var finalStatus *ArchiveDiffStatus
for i := 0; i < 50; i++ {
s, err := diffService.Status(status.ID)
if err != nil {
t.Fatal(err)
}
if s.State == "ready" {
finalStatus = s
break
}
if s.State == "error" {
t.Fatalf("diff failed with error: %s", s.Error)
}
time.Sleep(20 * time.Millisecond)
}
if finalStatus == nil {
t.Fatal("diff timed out")
}
// 验证差异结果:
// 默认方向下 other(B) 为 before,current(A) 为 after:
// etc/test.etc 因字符串池不同识别为 Modified;
// etc/added.etc 在 B 中存在而在 A 中不存在,在默认方向下标记为 Deleted;
// etc/same.etc 内容相同,不出现在差异中。
if finalStatus.Modified != 1 {
t.Fatalf("Modified count = %d, want 1", finalStatus.Modified)
}
if finalStatus.Deleted != 1 {
t.Fatalf("Deleted count = %d, want 1", finalStatus.Deleted)
}
// 验证 FlatFiles 全量与关键字过滤
flatAll, err := diffService.FlatFiles(status.ID, "", "all")
if err != nil {
t.Fatal(err)
}
if len(flatAll) != 2 {
t.Fatalf("FlatFiles count = %d, want 2", len(flatAll))
}
flatFilteredTest, err := diffService.FlatFiles(status.ID, "test", "all")
if err != nil {
t.Fatal(err)
}
if len(flatFilteredTest) != 1 || flatFilteredTest[0].Path != "etc/test.etc" || flatFilteredTest[0].Name != "test.etc" {
t.Fatalf("FlatFiles filtered by 'test' unexpected: %+v", flatFilteredTest)
}
flatNone, err := diffService.FlatFiles(status.ID, "nonexistent_query", "all")
if err != nil {
t.Fatal(err)
}
if len(flatNone) != 0 {
t.Fatalf("FlatFiles filtered by nonexistent should be empty, got: %+v", flatNone)
}
// 验证 FilteredChildren
allChildren, err := diffService.FilteredChildren(status.ID, "", "all")
if err != nil || len(allChildren) != 1 || allChildren[0].Name != "etc" || allChildren[0].Count != 2 {
t.Fatalf("FilteredChildren all failed: %+v", allChildren)
}
modChildren, err := diffService.FilteredChildren(status.ID, "", "modify")
if err != nil || len(modChildren) != 1 || modChildren[0].Name != "etc" || modChildren[0].Count != 1 {
t.Fatalf("FilteredChildren modify failed: %+v", modChildren)
}
delChildren, err := diffService.FilteredChildren(status.ID, "", "delete")
if err != nil || len(delChildren) != 1 || delChildren[0].Name != "etc" || delChildren[0].Count != 1 {
t.Fatalf("FilteredChildren delete failed: %+v", delChildren)
}
addChildren, err := diffService.FilteredChildren(status.ID, "", "add")
if err != nil || len(addChildren) != 0 {
t.Fatalf("FilteredChildren add should be empty, got: %+v", addChildren)
}
// 测试 MergeFiles:将 B 中的 etc/test.etc 合并到 A
res, err := diffService.MergeFiles(status.ID, []string{"etc/test.etc"})
if err != nil {
t.Fatalf("MergeFiles failed: %v", err)
}
if res.SuccessCount != 1 || len(res.MergedPaths) != 1 {
t.Fatalf("Merge result unexpected: %+v", res)
}
// 验证 A 中的内容已经被成功更新为 B 中的字符串
idx, ok := core.archive.Find("etc/test.etc")
if !ok {
t.Fatal("etc/test.etc not found in archive A after merge")
}
mergedText, err := core.archive.CanonicalText(idx)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(mergedText, "hello_from_B") {
t.Fatalf("merged text = %q, want hello_from_B", mergedText)
}
// 测试 MergeText:修改单文件文本(仍与 B 有差异)
newContent := "[name]\n\t`manually_edited`\n"
if err := diffService.MergeText(status.ID, "etc/test.etc", newContent); err != nil {
t.Fatalf("MergeText failed: %v", err)
}
files, err := diffService.FlatFiles(status.ID, "test.etc", "modify")
if err != nil || len(files) != 1 {
t.Fatalf("re-edited merged file missing from diff: files=%+v err=%v", files, err)
}
detail, err := diffService.FileDiff(status.ID, "etc/test.etc")
if err != nil || detail.Operation != "modify" || !strings.Contains(detail.AfterText, "manually_edited") {
t.Fatalf("re-edited file detail: %+v err=%v", detail, err)
}
updated, err := diffService.Status(status.ID)
if err != nil || updated.Modified != 1 {
t.Fatalf("re-edited file statistics: %+v err=%v", updated, err)
}
exports := NewExportService(core)
preview, err := exports.Prepare(ExportRequest{Source: "archive-diff", DiffID: status.ID, Mode: "direct", Scopes: []string{"etc/test.etc"}})
if err != nil {
t.Fatal(err)
}
root, err := exports.executeTo(preview.ID, t.TempDir())
if err != nil {
t.Fatal(err)
}
if text := readExportFile(t, root, "etc/test.etc"); !strings.Contains(text, "manually_edited") {
t.Fatalf("export lost re-edited merged file: %q", text)
}
// 测试通过目录路径合并整个目录下的所有差异文件(包含仍有剩余差异的 test.etc 与未合并的 added.etc)
dirMergeRes, err := diffService.MergeFiles(status.ID, []string{"etc"})
if err != nil {
t.Fatalf("MergeFiles by directory failed: %v", err)
}
if dirMergeRes.SuccessCount != 2 {
t.Fatalf("Directory merge expected 2 remaining files merged, got: %+v", dirMergeRes)
}
}
func TestArchiveDiffPartialSavePreservesRemainingDiff(t *testing.T) {
dir := t.TempDir()
pathA := filepath.Join(dir, "base.pvf")
pathB := filepath.Join(dir, "other.pvf")
a := pvf.New()
if _, err := a.AddFileText("etc/test.etc", "[name]\n`val_A`\n[price]\n`100`\n", pvf.TypeScript); err != nil {
t.Fatal(err)
}
if err := a.SaveAs(pathA); err != nil {
t.Fatal(err)
}
b := pvf.New()
if _, err := b.AddFileText("etc/test.etc", "[name]\n`val_B`\n[price]\n`200`\n", pvf.TypeScript); err != nil {
t.Fatal(err)
}
if err := b.SaveAs(pathB); err != nil {
t.Fatal(err)
}
core := NewCore()
openA, err := pvf.Open(pathA)
if err != nil {
t.Fatal(err)
}
if err := core.setArchive(openA); err != nil {
t.Fatal(err)
}
defer core.closeArchive()
diffService := NewArchiveDiffService(core)
status, err := diffService.Start(ArchiveDiffRequest{Path: pathB})
if err != nil {
t.Fatal(err)
}
for i := 0; i < 50; i++ {
s, _ := diffService.Status(status.ID)
if s.State == "ready" {
break
}
time.Sleep(20 * time.Millisecond)
}
// 局部保存:只合入 [name],但 [price] 仍然不同
partialContent := "[name]\n`val_B`\n[price]\n`100`\n"
if err := diffService.MergeText(status.ID, "etc/test.etc", partialContent); err != nil {
t.Fatal(err)
}
// 验证局部保存后,etc/test.etc 仍然保留在差异列表中(因为 price 依然有差异)
flat, err := diffService.FlatFiles(status.ID, "", "all")
if err != nil {
t.Fatal(err)
}
if len(flat) != 1 || flat[0].Path != "etc/test.etc" {
t.Fatalf("expected etc/test.etc to remain in diff list, got %+v", flat)
}
// 完全保存:内容完全与 B 一致
fullContent := "[name]\n`val_B`\n[price]\n`200`\n"
if err := diffService.MergeText(status.ID, "etc/test.etc", fullContent); err != nil {
t.Fatal(err)
}
// 验证完全一致后,etc/test.etc 从差异列表中移除
flatAfter, err := diffService.FlatFiles(status.ID, "", "all")
if err != nil {
t.Fatal(err)
}
if len(flatAfter) != 0 {
t.Fatalf("expected etc/test.etc to be removed from diff list, got %+v", flatAfter)
}
}
func TestArchiveDiffMergeOneFilePreservesOtherDrafts(t *testing.T) {
dir := t.TempDir()
pathA := filepath.Join(dir, "base.pvf")
pathB := filepath.Join(dir, "other.pvf")
a := pvf.New()
if _, err := a.AddFileText("etc/file1.etc", "[name]\n`file1_orig`\n", pvf.TypeScript); err != nil {
t.Fatal(err)
}
if _, err := a.AddFileText("etc/file2.etc", "[name]\n`file2_orig`\n", pvf.TypeScript); err != nil {
t.Fatal(err)
}
if err := a.SaveAs(pathA); err != nil {
t.Fatal(err)
}
b := pvf.New()
if _, err := b.AddFileText("etc/file1.etc", "[name]\n`file1_other`\n", pvf.TypeScript); err != nil {
t.Fatal(err)
}
if _, err := b.AddFileText("etc/file2.etc", "[name]\n`file2_other`\n", pvf.TypeScript); err != nil {
t.Fatal(err)
}
if err := b.SaveAs(pathB); err != nil {
t.Fatal(err)
}
core := NewCore()
openA, err := pvf.Open(pathA)
if err != nil {
t.Fatal(err)
}
if err := core.setArchive(openA); err != nil {
t.Fatal(err)
}
defer core.closeArchive()
diffService := NewArchiveDiffService(core)
f1Idx, _ := core.archive.Find("etc/file1.etc")
f2Idx, _ := core.archive.Find("etc/file2.etc")
// 传入 file1 和 file2 的编辑器草稿
status, err := diffService.Start(ArchiveDiffRequest{
Path: pathB,
Drafts: []ArchiveDiffDraft{
{FileIndex: f1Idx, Path: "etc/file1.etc", Text: "[name]\n`file1_draft`\n"},
{FileIndex: f2Idx, Path: "etc/file2.etc", Text: "[name]\n`file2_draft`\n"},
},
})
if err != nil {
t.Fatal(err)
}
for i := 0; i < 50; i++ {
s, _ := diffService.Status(status.ID)
if s.State == "ready" {
break
}
time.Sleep(20 * time.Millisecond)
}
// 合并 file1
_, err = diffService.MergeFiles(status.ID, []string{"etc/file1.etc"})
if err != nil {
t.Fatal(err)
}
// 获取 file2 的详情,验证 file2 的对比底稿仍为 file2_draft,而不是回退成磁盘的 file2_orig
detail2, err := diffService.FileDiff(status.ID, "etc/file2.etc")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(detail2.AfterText, "file2_draft") {
t.Fatalf("expected file2 AfterText to preserve draft file2_draft, got: %q", detail2.AfterText)
}
}