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

95 lines
4.3 KiB
TypeScript

import { expect, test } from "vitest";
import { createGUIModes, createShopSession } from "../src/gui/state";
import { getGUIProvider } from "../src/gui/registry";
import type { ShopDocument } from "../bindings/pvfine/services/models";
const file = { index: 1, path: "itemshop/test.shp", text: "unsaved draft", editable: true };
function document(name = "商店"): ShopDocument {
return {
name, categoryType: "basic job", categories: [{ id: "0", name: "鬼剑士" }, { id: "1", name: "格斗家" }],
tabs: [0, 1].map((sourceStart) => ({ name: `分页${sourceStart}`, sourceStart, groups: [] })), issues: [],
};
}
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 };
}
test("仅支持的文件有 GUI,默认文本,各窗格独立并可清理", () => {
expect(getGUIProvider(file)?.readOnly).toBe(false);
expect(getGUIProvider({ ...file, path: "itemshop/SHOP.SHP" })?.id).toBe("shop");
expect(getGUIProvider({ ...file, path: "skill/atgunner/C4.SKL" })?.id).toBe("skill");
expect(getGUIProvider({ ...file, path: "a.equ" })?.id).toBe("item");
expect(getGUIProvider({ ...file, path: "a.txt" })).toBeNull();
const left = createGUIModes(), right = createGUIModes();
expect(left.entries.get(1)?.mode ?? "text").toBe("text");
left.set(1, "gui");
expect(right.entries.has(1)).toBe(false);
left.set(1, "text");
expect(left.entries.get(1)?.opened).toBe(true);
left.prune([]);
expect(left.entries.size).toBe(0);
right.set(1, "gui"); right.reset();
expect(right.entries.size).toBe(0);
});
test("慢加载立即显示状态,读取草稿,默认第一分类;刷新保留选择", async () => {
const pending = deferred<ShopDocument>();
const calls: unknown[] = [];
const session = createShopSession(async (index, text) => { calls.push([index, text]); return pending.promise; });
const loading = session.load(file, "archive1");
expect(session.loading.value).toBe(true);
expect(session.document.value).toBeNull();
expect(calls).toEqual([[1, "unsaved draft"]]);
pending.resolve(document()); await loading;
expect(session.categoryID.value).toBe("0");
session.tabIndex.value = 1; session.categoryID.value = "1";
await session.load({ ...file, text: "new draft" }, "archive1");
expect(session.tabIndex.value).toBe(1);
expect(session.categoryID.value).toBe("1");
expect(file.text).toBe("unsaved draft");
});
test("失败停止加载,保留上次内容,重试恢复", async () => {
let fail = false;
const session = createShopSession(async () => { if (fail) throw new Error("读取失败"); return document(); });
await session.load(file, "archive1"); fail = true;
await session.load(file, "archive1");
expect(session.loading.value).toBe(false);
expect(session.document.value?.name).toBe("商店");
expect(session.error.value).toBe("读取失败");
fail = false; await session.load(file, "archive1");
expect(session.error.value).toBe("");
});
test("切换文件或同路径重新打开归档时清空旧结果,过期请求不能覆盖", async () => {
const a = deferred<ShopDocument>(), b = deferred<ShopDocument>();
let count = 0;
const session = createShopSession(() => (++count === 1 ? a : b).promise);
const first = session.load(file, "epoch1");
const second = session.load(file, "epoch2");
expect(session.document.value).toBeNull();
b.resolve(document("新归档")); await second;
a.resolve(document("旧归档")); await first;
expect(session.document.value?.name).toBe("新归档");
const third = session.load({ ...file, index: 2 }, "epoch2");
expect(session.document.value).toBeNull();
await third;
});
test("卸载或退出 GUI 后忽略请求;空响应明确失败", async () => {
const pending = deferred<ShopDocument>();
const session = createShopSession(() => pending.promise);
const loading = session.load(file, "epoch1");
session.invalidate(true);
pending.resolve(document()); await loading;
expect(session.document.value).toBeNull();
expect(session.loading.value).toBe(false);
const empty = createShopSession(async () => null);
await empty.load(file, "epoch1");
expect(empty.error.value).toBe("未收到商店数据");
expect(empty.loading.value).toBe(false);
});