DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

+309
View File
@@ -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,
};
});