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,309 @@
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
import { Events } from "@wailsio/runtime";
|
||||
import { ArchiveService } from "../../bindings/pvfine/services";
|
||||
import type {
|
||||
AdvancedSearchDetail,
|
||||
AdvancedSearchHit,
|
||||
AdvancedSearchIndexStatus,
|
||||
} from "../../bindings/pvfine/services/models";
|
||||
import { useArchiveStore } from "./archive";
|
||||
|
||||
export type AdvancedSearchMode = "binary" | "string" | "quick";
|
||||
export type AdvancedStringMatch = "text" | "regex";
|
||||
|
||||
export interface AdvancedSearchItem {
|
||||
key: string;
|
||||
name: string;
|
||||
path: string;
|
||||
size: number;
|
||||
dataType: number;
|
||||
fileIndex: number;
|
||||
details: AdvancedSearchDetail[];
|
||||
}
|
||||
|
||||
/** 高级搜索状态:弹窗隐藏时仍保留当前归档的查询与结果。 */
|
||||
export const useAdvancedSearchStore = defineStore("advancedSearch", () => {
|
||||
const archive = useArchiveStore();
|
||||
const visible = ref(false);
|
||||
const mode = ref<AdvancedSearchMode>("quick");
|
||||
const exactMatch = ref(false);
|
||||
const stringMatch = ref<AdvancedStringMatch>("text");
|
||||
const query = ref("");
|
||||
const scopePath = ref("");
|
||||
const hits = ref<AdvancedSearchItem[]>([]);
|
||||
const nextCursor = ref(-1);
|
||||
const searching = ref(false);
|
||||
const stale = ref(false);
|
||||
const error = ref("");
|
||||
const indexStatus = ref<AdvancedSearchIndexStatus>({
|
||||
state: "idle",
|
||||
stage: "",
|
||||
done: 0,
|
||||
total: 0,
|
||||
error: "",
|
||||
});
|
||||
|
||||
let requestId = 0;
|
||||
let retryAfterIndex = false;
|
||||
|
||||
const regexEnabled = computed(() => mode.value === "string" && stringMatch.value === "regex");
|
||||
const hasResults = computed(() => hits.value.length > 0);
|
||||
|
||||
function eventData(event: any): any {
|
||||
return event?.data ?? event;
|
||||
}
|
||||
|
||||
function readIndexStatus(data: any): AdvancedSearchIndexStatus {
|
||||
return {
|
||||
state: String(data?.state ?? "idle"),
|
||||
stage: String(data?.stage ?? ""),
|
||||
done: Number(data?.done ?? 0),
|
||||
total: Number(data?.total ?? 0),
|
||||
error: String(data?.error ?? ""),
|
||||
};
|
||||
}
|
||||
|
||||
function errorMessage(value: any): string {
|
||||
return String(value?.message ?? value ?? "高级搜索失败");
|
||||
}
|
||||
|
||||
function toItem(hit: AdvancedSearchHit): AdvancedSearchItem {
|
||||
return {
|
||||
key: `${hit.fileIndex}:${hit.path}:${hit.name ?? ""}:${(hit.details ?? []).filter((detail) => detail?.kind === "id").map((detail) => detail?.value).join(",")}`,
|
||||
name: hit.name ?? "",
|
||||
path: hit.path,
|
||||
size: hit.size,
|
||||
dataType: hit.dataType,
|
||||
fileIndex: hit.fileIndex,
|
||||
details: (hit.details ?? []).filter(
|
||||
(detail): detail is AdvancedSearchDetail => !!detail
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function open() {
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function clear() {
|
||||
requestId++;
|
||||
query.value = "";
|
||||
hits.value = [];
|
||||
nextCursor.value = -1;
|
||||
searching.value = false;
|
||||
stale.value = false;
|
||||
error.value = "";
|
||||
retryAfterIndex = false;
|
||||
}
|
||||
|
||||
function resetForArchive() {
|
||||
requestId++;
|
||||
visible.value = false;
|
||||
mode.value = "quick";
|
||||
exactMatch.value = false;
|
||||
stringMatch.value = "text";
|
||||
scopePath.value = "";
|
||||
clear();
|
||||
indexStatus.value = readIndexStatus(null);
|
||||
}
|
||||
|
||||
async function search() {
|
||||
if (searching.value) return;
|
||||
const request = ++requestId;
|
||||
error.value = "";
|
||||
stale.value = false;
|
||||
hits.value = [];
|
||||
nextCursor.value = -1;
|
||||
retryAfterIndex = false;
|
||||
if (!query.value.trim()) {
|
||||
searching.value = false;
|
||||
return;
|
||||
}
|
||||
if (!archive.open) {
|
||||
error.value = "请先打开归档文件";
|
||||
return;
|
||||
}
|
||||
if (regexEnabled.value) {
|
||||
try {
|
||||
// 这是前端即时反馈;最终语法仍由 Go RE2 后端校验。
|
||||
new RegExp(query.value);
|
||||
} catch (value: any) {
|
||||
error.value = `正则表达式无效: ${value?.message ?? value}`;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
searching.value = true;
|
||||
try {
|
||||
const result = await ArchiveService.AdvancedSearch(
|
||||
mode.value === "quick" && exactMatch.value ? "quick-exact" : mode.value,
|
||||
query.value,
|
||||
scopePath.value,
|
||||
regexEnabled.value,
|
||||
0,
|
||||
200
|
||||
);
|
||||
if (request !== requestId) return;
|
||||
const page = (result?.hits ?? [])
|
||||
.filter((hit): hit is AdvancedSearchHit => !!hit)
|
||||
.map(toItem);
|
||||
hits.value = page;
|
||||
nextCursor.value = result?.nextCursor ?? -1;
|
||||
} catch (value) {
|
||||
if (request !== requestId) return;
|
||||
const message = errorMessage(value);
|
||||
error.value = message;
|
||||
retryAfterIndex = message.includes("索引正在构建");
|
||||
} finally {
|
||||
if (request === requestId) searching.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function cancel() {
|
||||
requestId++;
|
||||
retryAfterIndex = false;
|
||||
try {
|
||||
await ArchiveService.CancelAdvancedSearch();
|
||||
nextCursor.value = -1;
|
||||
indexStatus.value = readIndexStatus(null);
|
||||
stale.value = hits.value.length > 0;
|
||||
} catch (value) {
|
||||
error.value = errorMessage(value);
|
||||
} finally {
|
||||
searching.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (searching.value || stale.value || nextCursor.value < 0 || !query.value.trim()) return;
|
||||
const request = requestId;
|
||||
searching.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
const result = await ArchiveService.AdvancedSearch(
|
||||
mode.value === "quick" && exactMatch.value ? "quick-exact" : mode.value,
|
||||
query.value,
|
||||
scopePath.value,
|
||||
regexEnabled.value,
|
||||
nextCursor.value,
|
||||
200
|
||||
);
|
||||
if (request !== requestId) return;
|
||||
const page = (result?.hits ?? [])
|
||||
.filter((hit): hit is AdvancedSearchHit => !!hit)
|
||||
.map(toItem);
|
||||
hits.value.push(...page);
|
||||
nextCursor.value = result?.nextCursor ?? -1;
|
||||
} catch (value) {
|
||||
if (request === requestId) error.value = errorMessage(value);
|
||||
} finally {
|
||||
if (request === requestId) searching.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载剩余分页,调用方在成功后再一次性使用完整结果。 */
|
||||
async function loadAll(): Promise<AdvancedSearchItem[] | null> {
|
||||
if (searching.value || !query.value.trim()) return [...hits.value];
|
||||
if (stale.value) return null;
|
||||
const request = requestId;
|
||||
searching.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
let cursor = nextCursor.value;
|
||||
while (cursor >= 0) {
|
||||
const result = await ArchiveService.AdvancedSearch(
|
||||
mode.value === "quick" && exactMatch.value ? "quick-exact" : mode.value,
|
||||
query.value,
|
||||
scopePath.value,
|
||||
regexEnabled.value,
|
||||
cursor,
|
||||
200
|
||||
);
|
||||
if (request !== requestId || stale.value) return null;
|
||||
const page = (result?.hits ?? [])
|
||||
.filter((hit): hit is AdvancedSearchHit => !!hit)
|
||||
.map(toItem);
|
||||
hits.value.push(...page);
|
||||
const next = result?.nextCursor ?? -1;
|
||||
nextCursor.value = next;
|
||||
if (next === cursor) break;
|
||||
cursor = next;
|
||||
}
|
||||
return [...hits.value];
|
||||
} catch (value) {
|
||||
if (request === requestId) error.value = errorMessage(value);
|
||||
throw value;
|
||||
} finally {
|
||||
if (request === requestId) searching.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function markStale() {
|
||||
requestId++;
|
||||
searching.value = false;
|
||||
nextCursor.value = -1;
|
||||
indexStatus.value = readIndexStatus(null);
|
||||
if (query.value.trim() || hits.value.length > 0) stale.value = true;
|
||||
}
|
||||
|
||||
watch([mode, query, scopePath, stringMatch, exactMatch], () => {
|
||||
requestId++;
|
||||
searching.value = false;
|
||||
retryAfterIndex = false;
|
||||
nextCursor.value = -1;
|
||||
if (hits.value.length > 0) stale.value = true;
|
||||
}, { flush: "sync" });
|
||||
|
||||
Events.On("archive:index-ready", () => {
|
||||
if (mode.value === "quick" && retryAfterIndex && visible.value) {
|
||||
retryAfterIndex = false;
|
||||
void search();
|
||||
}
|
||||
});
|
||||
|
||||
Events.On("archive:opened", () => resetForArchive());
|
||||
Events.On("archive:closed", () => resetForArchive());
|
||||
Events.On("archive:advanced-search-stale", () => markStale());
|
||||
Events.On("archive:advanced-index-progress", (event: any) => {
|
||||
indexStatus.value = readIndexStatus(eventData(event));
|
||||
});
|
||||
Events.On("archive:advanced-index-ready", (event: any) => {
|
||||
indexStatus.value = readIndexStatus(eventData(event));
|
||||
if (retryAfterIndex && visible.value) {
|
||||
retryAfterIndex = false;
|
||||
void search();
|
||||
}
|
||||
});
|
||||
Events.On("archive:advanced-index-error", (event: any) => {
|
||||
indexStatus.value = readIndexStatus(eventData(event));
|
||||
});
|
||||
|
||||
return {
|
||||
visible,
|
||||
mode,
|
||||
stringMatch,
|
||||
exactMatch,
|
||||
query,
|
||||
scopePath,
|
||||
hits,
|
||||
nextCursor,
|
||||
searching,
|
||||
stale,
|
||||
error,
|
||||
indexStatus,
|
||||
regexEnabled,
|
||||
hasResults,
|
||||
open,
|
||||
close,
|
||||
clear,
|
||||
search,
|
||||
cancel,
|
||||
loadMore,
|
||||
loadAll,
|
||||
};
|
||||
});
|
||||
Reference in new issue
Block a user