DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,211 @@
|
||||
import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
|
||||
const api = vi.hoisted(() => ({
|
||||
Open: vi.fn(),
|
||||
OpenDialog: vi.fn(),
|
||||
Close: vi.fn(),
|
||||
IndexStatus: vi.fn(),
|
||||
RebuildSearchIndex: vi.fn(),
|
||||
SetTextEncoding: vi.fn(),
|
||||
}));
|
||||
const events = vi.hoisted(() => new Map<string, (event: unknown) => void>());
|
||||
vi.mock("@wailsio/runtime", () => ({
|
||||
Events: { On: (name: string, callback: (event: unknown) => void) => events.set(name, callback) },
|
||||
}));
|
||||
vi.mock("../bindings/pvfine/services", () => ({ ArchiveService: api, EditorService: {} }));
|
||||
|
||||
import { useArchiveStore } from "../src/stores/archive";
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
let reject!: (reason: Error) => void;
|
||||
const promise = new Promise<T>((yes, no) => { resolve = yes; reject = no; });
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
function emit(name: string, data?: unknown) {
|
||||
events.get(name)?.({ data });
|
||||
}
|
||||
|
||||
async function flush() {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.resetAllMocks();
|
||||
events.clear();
|
||||
setActivePinia(createPinia());
|
||||
vi.stubGlobal("window", {
|
||||
setInterval: (...args: Parameters<typeof setInterval>) => globalThis.setInterval(...args),
|
||||
clearInterval: (handle: number) => globalThis.clearInterval(handle),
|
||||
localStorage: { getItem: () => null, setItem: vi.fn() },
|
||||
});
|
||||
api.IndexStatus.mockResolvedValue({ state: "building", stage: "sqlite" });
|
||||
api.Close.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
test("string table events retain timings without restarting semantic indexing", () => {
|
||||
const store = useArchiveStore();
|
||||
store.info = { path: "/game/Script.pvf" } as NonNullable<typeof store.info>;
|
||||
emit("archive:string-table-index", {
|
||||
archivePath: "/game/Script.pvf",
|
||||
stats: { revision: 4, state: "ready", tableBuilds: 1, entries: 100, mappingDurationMs: 2, tableDurationMs: 12, buildDurationMs: 14 },
|
||||
});
|
||||
expect(store.stringTableIndex.buildDurationMs).toBe(14);
|
||||
expect(store.stringTableIndex.tableBuilds).toBe(1);
|
||||
expect(store.stringTableIndex.entries).toBe(100);
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
emit("archive:string-table-index", { archivePath: "/game/Script.pvf", stats: { revision: 3, buildDurationMs: 10 } });
|
||||
expect(store.stringTableIndex.buildDurationMs).toBe(14);
|
||||
emit("archive:string-table-index", { archivePath: "/game/old.pvf", stats: { revision: 10, buildDurationMs: 200 } });
|
||||
expect(store.stringTableIndex.buildDurationMs).toBe(14);
|
||||
emit("archive:string-table-index", {
|
||||
archivePath: "/game/Script.pvf", kind: "snapshot", stats: { revision: 0, state: "idle" },
|
||||
});
|
||||
expect(store.stringTableIndex.state).toBe("idle");
|
||||
expect(store.stringTableIndex.buildDurationMs).toBe(0);
|
||||
emit("archive:closed");
|
||||
expect(store.stringTableIndex.state).toBe("idle");
|
||||
expect(store.stringTableIndex.buildDurationMs).toBe(0);
|
||||
});
|
||||
|
||||
test("index polling restores missing string table statistics", async () => {
|
||||
api.IndexStatus.mockResolvedValue({
|
||||
state: "ready", stringTableIndex: { revision: 4, state: "ready", tableBuilds: 2, buildDurationMs: 30 },
|
||||
});
|
||||
const store = useArchiveStore();
|
||||
emit("archive:opened", { path: "/game/Script.pvf" });
|
||||
await flush();
|
||||
expect(store.stringTableIndex.buildDurationMs).toBe(30);
|
||||
expect(store.stringTableIndex.tableBuilds).toBe(2);
|
||||
expect(store.indexReady).toBe(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllTimers();
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
test("a stale poll cannot overwrite the ready event or restart indexing", async () => {
|
||||
const pending = deferred<{ state: string }>();
|
||||
api.IndexStatus.mockReturnValueOnce(pending.promise);
|
||||
const store = useArchiveStore();
|
||||
emit("archive:opened", { path: "/game/Script.pvf" });
|
||||
emit("archive:index-ready", { state: "ready", stage: "ready-sqlite", done: 10, total: 10 });
|
||||
pending.resolve({ state: "building" });
|
||||
await flush();
|
||||
expect(store.indexReady).toBe(true);
|
||||
expect(store.indexing).toBe(false);
|
||||
expect(store.indexStatus.stage).toBe("ready-sqlite");
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
});
|
||||
|
||||
test("a stale poll cannot overwrite an error event", async () => {
|
||||
const pending = deferred<{ state: string }>();
|
||||
api.IndexStatus.mockReturnValueOnce(pending.promise);
|
||||
const store = useArchiveStore();
|
||||
emit("archive:opened", { path: "/game/Script.pvf" });
|
||||
emit("archive:index-error", { state: "error", error: "build failed" });
|
||||
pending.resolve({ state: "building" });
|
||||
await flush();
|
||||
expect(store.indexStatus.state).toBe("error");
|
||||
expect(store.indexStatus.error).toBe("build failed");
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
});
|
||||
|
||||
test("a stale poll cannot overwrite newer progress", async () => {
|
||||
const pending = deferred<{ state: string; done: number }>();
|
||||
api.IndexStatus.mockReturnValueOnce(pending.promise);
|
||||
const store = useArchiveStore();
|
||||
emit("archive:opened", { path: "/game/Script.pvf" });
|
||||
emit("archive:index-progress", { state: "building", done: 20, total: 100 });
|
||||
pending.resolve({ state: "building", done: 5 });
|
||||
await flush();
|
||||
expect(store.indexStatus.done).toBe(20);
|
||||
expect(vi.getTimerCount()).toBe(1);
|
||||
});
|
||||
|
||||
test("a poll from a closed archive cannot change the next archive's state", async () => {
|
||||
const pending = deferred<{ state: string }>();
|
||||
api.IndexStatus.mockReturnValueOnce(pending.promise);
|
||||
const store = useArchiveStore();
|
||||
emit("archive:opened", { path: "/game/first.pvf" });
|
||||
emit("archive:closed");
|
||||
emit("archive:opened", { path: "/game/second.pvf" });
|
||||
pending.resolve({ state: "ready" });
|
||||
await flush();
|
||||
expect(store.info?.path).toBe("/game/second.pvf");
|
||||
expect(store.indexing).toBe(true);
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
expect(api.IndexStatus).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test("polling retries transient errors and recovers a missed ready event", async () => {
|
||||
api.IndexStatus.mockRejectedValueOnce(new Error("temporary error"))
|
||||
.mockResolvedValueOnce({ state: "ready", stage: "ready-sqlite" });
|
||||
const store = useArchiveStore();
|
||||
emit("archive:opened", { path: "/game/Script.pvf" });
|
||||
await flush();
|
||||
expect(store.indexing).toBe(true);
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
expect(store.indexReady).toBe(true);
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
});
|
||||
|
||||
test("background refresh keeps polling until refreshing is false", async () => {
|
||||
const store = useArchiveStore();
|
||||
emit("archive:opened", { path: "/game/Script.pvf" });
|
||||
await flush();
|
||||
emit("archive:index-ready", { state: "ready", refreshing: true });
|
||||
api.IndexStatus.mockResolvedValueOnce({ state: "ready", refreshing: false });
|
||||
expect(vi.getTimerCount()).toBe(1);
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
expect(store.refreshingIndex).toBe(false);
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
});
|
||||
|
||||
test("rebuild starts fallback polling when progress and ready events are missed", async () => {
|
||||
const store = useArchiveStore();
|
||||
store.info = { path: "/game/Script.pvf" } as NonNullable<typeof store.info>;
|
||||
api.RebuildSearchIndex.mockResolvedValue({ state: "building" });
|
||||
api.IndexStatus.mockResolvedValueOnce({ state: "ready" });
|
||||
await store.rebuildSearchIndex();
|
||||
await flush();
|
||||
expect(store.indexReady).toBe(true);
|
||||
expect(api.IndexStatus).toHaveBeenCalledTimes(1);
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
});
|
||||
|
||||
test("a stale rebuild response cannot overwrite the ready event", async () => {
|
||||
const pending = deferred<{ state: string }>();
|
||||
const store = useArchiveStore();
|
||||
store.info = { path: "/game/Script.pvf" } as NonNullable<typeof store.info>;
|
||||
api.RebuildSearchIndex.mockReturnValueOnce(pending.promise);
|
||||
const rebuilding = store.rebuildSearchIndex();
|
||||
emit("archive:index-ready", { state: "ready", stage: "ready-sqlite" });
|
||||
pending.resolve({ state: "building" });
|
||||
await rebuilding;
|
||||
expect(store.indexReady).toBe(true);
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
});
|
||||
|
||||
|
||||
test("encoding switch updates archive info and reload resets string table stats", async () => {
|
||||
const store = useArchiveStore();
|
||||
const info = { path: "/game/Script.pvf", textEncoding: "GBK", textEncodingLabel: "GBK" };
|
||||
store.info = { ...info, textEncoding: "auto" } as NonNullable<typeof store.info>;
|
||||
api.SetTextEncoding.mockResolvedValue(info);
|
||||
await store.setTextEncoding("GBK");
|
||||
expect(api.SetTextEncoding).toHaveBeenCalledWith("GBK");
|
||||
expect(store.info?.textEncoding).toBe("GBK");
|
||||
store.stringTableIndex.revision = 20;
|
||||
emit("archive:reloaded", info);
|
||||
expect(store.stringTableIndex.revision).toBe(0);
|
||||
api.SetTextEncoding.mockRejectedValue(new Error("请先保存"));
|
||||
await expect(store.setTextEncoding("auto")).rejects.toThrow("请先保存");
|
||||
expect(store.info?.textEncoding).toBe("GBK");
|
||||
});
|
||||
Reference in new issue
Block a user