服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
43 lines
2.0 KiB
TypeScript
43 lines
2.0 KiB
TypeScript
import type { SearchHit, SearchResult } from "../../bindings/pvfine/services/models";
|
|
import type { GUIProvider } from "./types";
|
|
|
|
export interface GUISearchCursor { queryIndex: number; offset: number }
|
|
|
|
export function guiPathMatches(provider: GUIProvider, path: string): boolean {
|
|
return provider.matches({ index: 0, path, text: "", editable: true });
|
|
}
|
|
|
|
export function newGUIFilePath(provider: GUIProvider, raw: string): string {
|
|
const path = raw.trim().replaceAll("\\", "/").replace(/^\/+|\/+$/g, "");
|
|
if (!path || path.split("/").some(part => !part || part === "." || part === "..")) {
|
|
throw new Error("路径无效:不能为空,且不能包含空段、. 或 ..");
|
|
}
|
|
if (!guiPathMatches(provider, path)) throw new Error(`请选择支持的文件类型或路径:${provider.launcher.fileTypes}`);
|
|
return path;
|
|
}
|
|
|
|
/** 先按类型搜索;关键词搜索过滤后继续翻页,避免无关结果遮住目标文件。 */
|
|
export async function searchGUIFiles(
|
|
provider: GUIProvider,
|
|
query: string,
|
|
cursor: GUISearchCursor,
|
|
search: (query: string, cursor: number, limit: number) => Promise<SearchResult | null>,
|
|
isCurrent: () => boolean,
|
|
): Promise<{ hits: SearchHit[]; next: GUISearchCursor | null }> {
|
|
const queries = query.trim() ? [query.trim()] : provider.launcher.queries;
|
|
const next = { ...cursor };
|
|
const hits = new Map<number, SearchHit>();
|
|
// 限制单次扫描量,仍可通过“继续搜索”访问剩余结果。
|
|
for (let page = 0; page < 10 && next.queryIndex < queries.length; page++) {
|
|
const result = await search(queries[next.queryIndex]!, next.offset, 100);
|
|
if (!isCurrent()) return { hits: [], next: null };
|
|
for (const hit of result?.hits ?? []) {
|
|
if (hit && guiPathMatches(provider, hit.path)) hits.set(hit.fileIndex, hit);
|
|
}
|
|
if ((result?.nextCursor ?? -1) < 0) { next.queryIndex++; next.offset = 0; }
|
|
else next.offset = result!.nextCursor;
|
|
if (hits.size) break;
|
|
}
|
|
return { hits: [...hits.values()], next: next.queryIndex < queries.length ? next : null };
|
|
}
|