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

113 lines
5.2 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { createPinia, setActivePinia } from "pinia";
import { nextTick } from "vue";
const api = vi.hoisted(() => ({
GetFileBasic: vi.fn(), GetFile: vi.fn(), SetText: vi.fn(), GetAnnotations: vi.fn(),
Save: vi.fn(), ResolveFiles: vi.fn(), refreshInfo: vi.fn(), showArchiveEditor: vi.fn(),
events: new Map<string, Array<() => void>>(),
}));
vi.mock("@wailsio/runtime", () => ({ Events: { On: (name: string, fn: () => void) => {
api.events.set(name, [...(api.events.get(name) ?? []), fn]);
} } }));
vi.mock("../bindings/pvfine/services", () => ({ EditorService: api, ArchiveService: api, FileGUIService: {} }));
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) => `skill/test/${index}.skl`, refreshTreeTags: vi.fn() }) }));
import { useEditorStore } from "../src/stores/editor";
function meta(index: number, text = "original", path = `skill/test/${index}.skl`) {
return { index, path, text, editable: true, dataType: 1, size: text.length, tags: [], annotations: [], modified: false };
}
beforeEach(() => {
setActivePinia(createPinia());
vi.resetAllMocks();
api.events.clear();
api.GetFileBasic.mockImplementation(async (index) => meta(index));
api.GetFile.mockResolvedValue(null);
api.GetAnnotations.mockResolvedValue([]);
api.SetText.mockResolvedValue(undefined);
});
describe("通用技能 GUI 独立草稿", () => {
it("浏览超过 20 个技能也不创建标签,切换和关闭 GUI 后仍保留草稿", async () => {
const editor = useEditorStore();
for (let index = 1; index <= 25; index++) await editor.openGUIFile(index);
editor.updateContent(1, "modified");
editor.closeAllTabs();
expect((await editor.openGUIFile(1)).text).toBe("modified");
expect(editor.tabs).toHaveLength(0);
expect(editor.activeKey).toBeNull();
expect(api.showArchiveEditor).not.toHaveBeenCalled();
expect(editor.dirtyCount).toBe(1);
expect(await editor.saveFileTab(1)).toBe(true);
expect(api.SetText).toHaveBeenCalledExactlyOnceWith(1, "modified");
expect(editor.tabs).toHaveLength(0);
expect(editor.dirtyCount).toBe(0);
});
it("显式打开编辑器才创建标签,共用原草稿并避免重复保存", async () => {
const editor = useEditorStore();
const draft = await editor.openGUIFile(1);
editor.updateContent(1, "GUI draft");
await editor.openFile(1);
expect(editor.tabs).toHaveLength(1);
expect(editor.tabs[0]).toBe(draft);
expect(editor.activeTab?.text).toBe("GUI draft");
editor.updateContent(1, "text draft");
expect(await editor.openGUIFile(1)).toBe(draft);
expect(draft.text).toBe("text draft");
expect(editor.dirtyCount).toBe(1);
expect(await editor.saveAllDirty()).toBe(1);
expect(api.SetText).toHaveBeenCalledTimes(1);
expect(api.GetFileBasic).toHaveBeenCalledTimes(1);
});
it("主工具栏保存包含独立草稿,写入失败时保留草稿与脏状态", async () => {
const editor = useEditorStore();
await editor.openGUIFile(1);
await editor.openGUIFile(2);
editor.updateContent(1, "first");
editor.updateContent(2, "second");
api.SetText.mockRejectedValueOnce(new Error("write failed"));
await expect(editor.saveFileTab(1)).rejects.toThrow("write failed");
expect(editor.getFileDraft(1)?.original).toBe("original");
expect(editor.dirtyCount).toBe(2);
expect(editor.saving).toBe(false);
api.Save.mockResolvedValue({ modifiedCount: 0 });
await editor.save();
expect(api.SetText).toHaveBeenCalledWith(1, "first");
expect(api.SetText).toHaveBeenCalledWith(2, "second");
expect(api.Save).toHaveBeenCalledOnce();
expect(editor.dirtyCount).toBe(0);
expect(editor.tabs).toHaveLength(0);
});
it("文件索引变化时按路径重绑定,保存使用新索引且保留草稿", async () => {
const editor = useEditorStore();
await editor.openGUIFile(1);
editor.updateContent(1, "draft");
api.ResolveFiles.mockResolvedValue([{ path: "skill/test/1.skl", fileIndex: 100, size: 8, dataType: 1, tags: [], isDir: false }]);
await editor.refreshAfterArchiveChange();
expect(editor.getFileDraft("skill/test/1.skl")?.index).toBe(100);
expect(editor.getFileDraft(1)).toBeUndefined();
expect(editor.tabs).toHaveLength(0);
await editor.saveFileTab(100);
expect(api.SetText).toHaveBeenCalledExactlyOnceWith(100, "draft");
});
it("关闭归档清空草稿,并拒绝跨归档的慢请求", async () => {
let resolve!: (value: ReturnType<typeof meta>) => void;
api.GetFileBasic.mockReturnValue(new Promise((done) => { resolve = done; }));
const editor = useEditorStore();
const pending = editor.openGUIFile(1);
await nextTick();
for (const fn of api.events.get("archive:closed") ?? []) fn();
resolve(meta(1));
await expect(pending).rejects.toThrow("归档已切换");
expect(editor.getFileDraft(1)).toBeUndefined();
expect(editor.dirtyCount).toBe(0);
expect(editor.tabs).toHaveLength(0);
});
});