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

+360
View File
@@ -0,0 +1,360 @@
import { beforeEach, expect, test, vi } from "vitest";
import { createPinia, setActivePinia } from "pinia";
import { nextTick } from "vue";
const api = vi.hoisted(() => ({ GetFileBasic: vi.fn(), GetFile: vi.fn(), GetAnnotationsForText: vi.fn(), showArchiveEditor: vi.fn(), ApplyShopEdit: vi.fn(), refreshInfo: vi.fn(), Release: vi.fn(), ResolveFiles: vi.fn() }));
vi.mock("@wailsio/runtime", () => ({ Events: { On: vi.fn() } }));
vi.mock("../bindings/pvfine/services", () => ({ EditorService: api, ArchiveService: api, FileGUIService: api, ArchiveDiffService: api }));
vi.mock("../src/stores/archive", () => ({ useArchiveStore: () => ({ refreshInfo: api.refreshInfo }) }));
vi.mock("../src/stores/script", () => ({ useScriptStore: () => api }));
vi.mock("../src/stores/explorer", () => ({
useExplorerStore: () => ({ getFilePath: (index: number) => `map/${index}.lst`, selectedKey: null }),
}));
import { useEditorStore } from "../src/stores/editor";
import { longLineThreshold } from "../src/editorWrapping";
import { useArchiveDiffStore } from "../src/stores/archiveDiff";
function deferred<T>() {
let resolve!: (value: T) => void;
const promise = new Promise<T>((done) => { resolve = done; });
return { promise, resolve };
}
function meta(index: number, text = "1 `map/file.map`") {
return { index, path: `map/${index}.lst`, text, editable: true, dataType: 1, size: text.length, tags: [], annotations: [], modified: false };
}
beforeEach(() => {
setActivePinia(createPinia());
vi.resetAllMocks();
api.GetFile.mockResolvedValue(null);
api.GetAnnotationsForText.mockResolvedValue([]);
api.Release.mockResolvedValue(undefined);
api.ResolveFiles.mockResolvedValue([]);
});
test("对比草稿参与所有关闭路径的脏检查,取消保留,确认释放会话", () => {
const editor = useEditorStore();
const diff = useArchiveDiffStore();
editor.openCompareTab();
diff.setDraft("a.equ", "edited", "original");
expect(editor.dirtyCount).toBe(1);
editor.requestCloseTab(-999);
expect(editor.pendingClose).toMatchObject({ kind: "tab", index: -999 });
editor.cancelPendingClose();
expect(diff.isDraftDirty).toBe(true);
editor.requestCloseOthers(1);
expect(editor.pendingClose).toEqual({ kind: "others", keepIndex: 1 });
editor.cancelPendingClose();
editor.requestCloseAll();
expect(editor.pendingClose).toEqual({ kind: "all" });
editor.confirmPendingClose();
expect(editor.tabs).toHaveLength(0);
expect(diff.isDraftDirty).toBe(false);
expect(api.Release).toHaveBeenCalledTimes(1);
});
test("分屏关闭其中一个对比视图不释放仍在使用的草稿", () => {
const editor = useEditorStore();
const diff = useArchiveDiffStore();
editor.openCompareTab();
editor.split("columns");
diff.setDraft("a.equ", "edited", "original");
editor.closeTab(-999, "pane-2");
expect(diff.isDraftDirty).toBe(true);
expect(api.Release).not.toHaveBeenCalled();
editor.closeTab(-999, "pane-1");
expect(api.Release).toHaveBeenCalledTimes(1);
});
test("归档重载不把对比页伪索引传给 GetFile", async () => {
const editor = useEditorStore();
editor.openCompareTab();
await editor.refreshAfterArchiveChange(["__compare__"], true);
expect(api.GetFile).not.toHaveBeenCalled();
expect(editor.tabs[0]?.index).toBe(-999);
});
test("保存全部和活动标签通过对比保存接口写入草稿", async () => {
const editor = useEditorStore();
const diff = useArchiveDiffStore();
editor.openCompareTab();
diff.setDraft("a.equ", "edited", "original");
const save = vi.spyOn(diff, "mergeText").mockImplementation(async (_path, text) => {
diff.originalDraftText = text;
return true;
});
expect(await editor.saveAllDirty()).toBe(1);
expect(save).toHaveBeenCalledWith("a.equ", "edited");
expect(editor.dirtyCount).toBe(0);
diff.updateDraftText("again");
expect(await editor.saveActiveTab()).toBe(true);
expect(save).toHaveBeenLastCalledWith("a.equ", "again");
});
test("对比草稿未能保存时不能继续归档保存或备份", async () => {
const editor = useEditorStore();
const diff = useArchiveDiffStore();
editor.openCompareTab();
diff.setDraft("a.equ", "edited", "original");
vi.spyOn(diff, "mergeText").mockResolvedValue(false);
await expect(editor.saveAllDirty()).rejects.toThrow("对比草稿尚未保存");
expect(editor.dirtyCount).toBe(1);
});
test("自动换行在加载完成前按行长决定,普通文件默认开启", async () => {
api.GetFileBasic.mockImplementation(async (index) =>
meta(index, index === 1 ? "x".repeat(longLineThreshold + 1) : "short\nlines")
);
const store = useEditorStore();
await store.openFile(1);
expect(store.activeTab?.loading).toBe(false);
expect(store.activeTab?.lineWrapping).toBe(false);
await store.openFile(2);
expect(store.activeTab?.lineWrapping).toBe(true);
});
test("换行开关按文件保留,分屏共享,重新打开恢复自动判断", async () => {
api.GetFileBasic.mockImplementation(async (index) => meta(index));
const store = useEditorStore();
await store.openFile(1);
store.activeTab!.lineWrapping = false;
store.split("columns");
expect(store.activeTab?.lineWrapping).toBe(false);
await store.openFile(2);
expect(store.activeTab?.lineWrapping).toBe(true);
store.activateTab("pane-2", 1);
expect(store.activeTab?.lineWrapping).toBe(false);
store.closeTab(1, "pane-2");
store.closeTab(1, "pane-1");
await store.openFile(1);
expect(store.activeTab?.lineWrapping).toBe(true);
});
test("立即创建加载标签;重复打开共享请求", async () => {
const pending = deferred<ReturnType<typeof meta>>();
api.GetFileBasic.mockReturnValue(pending.promise);
const store = useEditorStore();
const opening = store.openFile(1);
expect(store.activeTab?.title).toBe("1.lst");
expect(store.activeTab?.loading).toBe(true);
expect(store.activeTab?.editable).toBe(false);
await store.openFile(1);
expect(api.GetFileBasic).toHaveBeenCalledTimes(1);
pending.resolve(meta(1));
await opening;
expect(store.tabs).toHaveLength(1);
expect(store.activeTab?.loading).toBe(false);
expect(store.activeTab?.text).toBe(meta(1).text);
});
test("请求完成不抢回用户后来选中的标签", async () => {
const first = deferred<ReturnType<typeof meta>>();
api.GetFileBasic.mockImplementation((index) => index === 1 ? first.promise : Promise.resolve(meta(index)));
const store = useEditorStore();
const opening = store.openFile(1);
await store.openFile(2);
first.resolve(meta(1));
await opening;
expect(store.activeKey).toBe(2);
});
test("关闭后重开同一文件,旧请求不能覆盖新标签", async () => {
const first = deferred<ReturnType<typeof meta>>();
api.GetFileBasic.mockReturnValueOnce(first.promise).mockResolvedValueOnce(meta(1, "new"));
const store = useEditorStore();
const opening = store.openFile(1);
await nextTick();
store.closeTab(1);
await store.openFile(1);
first.resolve(meta(1, "stale"));
await opening;
expect(store.tabs).toHaveLength(1);
expect(store.activeTab?.text).toBe("new");
});
test("加载失败保留标签并支持重试", async () => {
api.GetFileBasic.mockRejectedValueOnce(new Error("读取失败")).mockResolvedValueOnce(meta(1));
const store = useEditorStore();
await store.openFile(1);
expect(store.activeTab?.loadError).toBe("读取失败");
expect(store.opening).toBe(false);
await store.retryOpenFile(1);
expect(store.activeTab?.loadError).toBeNull();
expect(store.activeTab?.editable).toBe(true);
});
test("元数据未返回时正文已可编辑;延迟元数据不覆盖草稿", async () => {
const pending = deferred<ReturnType<typeof meta>>();
api.GetFileBasic.mockResolvedValue(meta(1, "original"));
api.GetFile.mockReturnValue(pending.promise);
const store = useEditorStore();
await store.openFile(1);
expect(store.activeTab?.loading).toBe(false);
expect(store.activeTab?.editable).toBe(true);
expect(store.activeTab?.text).toBe("original");
store.updateContent(1, "draft");
const enriched = { ...meta(1, "outdated"), icon: "icon.png" };
pending.resolve(enriched);
await vi.waitFor(() => expect(store.activeTab?.icon).toBe("icon.png"));
expect(store.activeTab?.text).toBe("draft");
expect(store.activeTab?.original).toBe("original");
});
test("元数据加载失败不影响正文与编辑状态", async () => {
api.GetFileBasic.mockResolvedValue(meta(1));
api.GetFile.mockRejectedValue(new Error("标注读取失败"));
const store = useEditorStore();
await store.openFile(1);
await nextTick();
expect(api.GetFile).toHaveBeenCalledTimes(1);
expect(store.activeTab?.loadError).toBeNull();
expect(store.activeTab?.text).toBe(meta(1).text);
expect(store.activeTab?.editable).toBe(true);
expect(store.activeTab?.loading).toBe(false);
});
test("分屏共享加载结果,关闭其中一处不会丢失文件", async () => {
const pending = deferred<ReturnType<typeof meta>>();
api.GetFileBasic.mockReturnValue(pending.promise);
const store = useEditorStore();
const opening = store.openFile(1);
store.split("columns");
store.closeTab(1, "pane-1");
pending.resolve(meta(1));
await opening;
expect(store.activeTab?.text).toBe(meta(1).text);
expect(store.activeTab?.loading).toBe(false);
});
test("临时隐藏标注只作用于当前文件,分屏共享,关闭后重开恢复", async () => {
api.GetFileBasic.mockImplementation((index) => Promise.resolve(meta(index)));
const store = useEditorStore();
await store.openFile(1);
await store.openFile(2);
store.toggleAnnotationsHidden(1);
expect(store.tabs.find((tab) => tab.index === 1)?.annotationsHidden).toBe(true);
expect(store.activeTab?.annotationsHidden).toBe(false);
store.activateTab("pane-1", 1);
store.split("columns");
expect(store.activeTab?.annotationsHidden).toBe(true);
store.closeTab(1, "pane-2");
expect(store.tabs.find((tab) => tab.index === 1)?.annotationsHidden).toBe(true);
store.closeTab(1, "pane-1");
await store.openFile(1);
expect(store.activeTab?.annotationsHidden).toBe(false);
});
test("加载中的标签也受数量上限约束", async () => {
api.GetFileBasic.mockImplementation(async (index) => meta(index));
const store = useEditorStore();
const openings = Array.from({ length: 20 }, (_, index) => store.openFile(index));
await expect(store.openFile(21)).rejects.toThrow("上限 20");
expect(store.tabs).toHaveLength(20);
await Promise.all(openings);
});
test("加载标签立即关闭时不发起读取,也不恢复标签", async () => {
const store = useEditorStore();
const opening = store.openFile(1);
store.closeAllTabs();
await opening;
expect(api.GetFileBasic).not.toHaveBeenCalled();
expect(api.GetFile).not.toHaveBeenCalled();
expect(store.tabs).toHaveLength(0);
expect(store.opening).toBe(false);
});
test("多个文件同时加载时各自维护状态", async () => {
const first = deferred<ReturnType<typeof meta>>();
const second = deferred<ReturnType<typeof meta>>();
api.GetFileBasic.mockImplementation((index) => index === 1 ? first.promise : second.promise);
const store = useEditorStore();
const openingFirst = store.openFile(1);
const openingSecond = store.openFile(2);
second.resolve(meta(2));
await openingSecond;
expect(store.tabs[0].loading).toBe(true);
expect(store.tabs[1].loading).toBe(false);
expect(store.opening).toBe(true);
first.resolve(meta(1));
await openingFirst;
expect(store.opening).toBe(false);
});
test("商店修改携带未保存草稿并同步所有受影响标签,不写磁盘", async () => {
api.GetFileBasic.mockImplementation(async (index) => meta(index, "original"));
const store = useEditorStore();
await store.openFile(1); await store.openFile(2);
store.updateContent(1, "shop draft"); store.updateContent(2, "item draft");
const pending = deferred<any>(); api.ApplyShopEdit.mockReturnValue(pending.promise);
const request = { fileIndex: 1, path: "map/1.lst", text: "shop draft", revision: 1, action: "edit-item" } as any;
const applying = store.applyShopEdit(request); await nextTick();
expect(store.guiApplying).toBe(true);
expect(api.ApplyShopEdit.mock.calls[0][0].drafts).toEqual([
{ fileIndex: 1, path: "map/1.lst", text: "shop draft" }, { fileIndex: 2, path: "map/2.lst", text: "item draft" },
]);
store.updateContent(1, "must not change during apply");
expect(store.tabs.find((tab) => tab.index === 1)?.text).toBe("shop draft");
api.GetFile.mockImplementation(async (index) => ({ ...meta(index, `after ${index}`), modified: true }));
pending.resolve({ revision: 2, files: [1, 2].map((index) => ({ fileIndex: index, path: `map/${index}.lst`, beforeText: index === 1 ? "shop draft" : "item draft", text: `after ${index}` })) });
await applying;
for (const tab of store.tabs) { expect(tab.text).toBe(`after ${tab.index}`); expect(tab.original).toBe(tab.text); expect(tab.modified).toBe(true); }
expect(store.guiApplying).toBe(false);
});
test("商店草稿过期或提交失败保留现有内容并释放锁", async () => {
api.GetFileBasic.mockResolvedValue(meta(1, "new draft"));
const store = useEditorStore(); await store.openFile(1);
const request = { fileIndex: 1, path: "map/1.lst", text: "old draft" } as any;
await expect(store.applyShopEdit(request)).rejects.toThrow("草稿已变化");
expect(api.ApplyShopEdit).not.toHaveBeenCalled();
api.ApplyShopEdit.mockRejectedValue(new Error("已过期"));
await expect(store.applyShopEdit({ ...request, text: "new draft" })).rejects.toThrow("已过期");
expect(store.tabs[0].text).toBe("new draft");
expect(store.guiApplying).toBe(false); expect(store.saving).toBe(false);
});
test("规则刷新按未保存文本重算标注并保留草稿", async () => {
api.GetFileBasic.mockResolvedValue(meta(1, "original"));
const store = useEditorStore();
await store.openFile(1);
store.updateContent(1, "draft\n[name]\n123");
api.GetFile.mockResolvedValue(meta(1, "rendered"));
api.GetAnnotationsForText.mockResolvedValue([{start:6,end:12,title:"new",type:"text"}]);
await store.refreshRenderedText();
expect(store.activeTab?.text).toBe("draft\n[name]\n123");
expect(api.GetAnnotationsForText).toHaveBeenLastCalledWith(1, "draft\n[name]\n123");
expect(store.activeTab?.annotations[0]?.start).toBe(6);
await store.refreshAnnotations();
expect(api.GetAnnotationsForText).toHaveBeenLastCalledWith(1, "draft\n[name]\n123");
});
test("对比 Tab 开启时不调用 GetFile(-999)", async () => {
const store = useEditorStore();
store.openCompareTab();
expect(store.tabs.some((t) => t.index === -999)).toBe(true);
await store.refreshRenderedText();
await store.refreshAnnotations();
expect(api.GetFile).not.toHaveBeenCalledWith(-999);
expect(api.GetAnnotationsForText).not.toHaveBeenCalledWith(-999, expect.anything());
});
test("AI 写入后同步文本基线,保留后端未保存标记,后续保存不被误当作本地草稿", async () => {
api.GetFileBasic.mockResolvedValue(meta(1, "old"));
const store = useEditorStore();
await store.openFile(1);
api.ResolveFiles.mockResolvedValue([{ fileIndex: 1, path: "map/1.lst", size: 3, dataType: 1 }]);
api.GetFile.mockResolvedValue({ ...meta(1, "new"), modified: true });
await store.syncMCPWorkspace();
expect(store.activeTab?.text).toBe("new");
expect(store.activeTab?.original).toBe("new");
expect(store.activeTab?.modified).toBe(true);
expect(store.dirtyCount).toBe(0);
expect(store.archiveDiffDrafts()).toEqual([]);
api.GetFile.mockResolvedValue({ ...meta(1, "new"), modified: false });
await store.syncMCPWorkspace();
expect(store.activeTab?.modified).toBe(false);
});