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,363 @@
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
import { Events } from "@wailsio/runtime";
|
||||
import { BatchService } from "../../bindings/pvfine/services";
|
||||
import type {
|
||||
BatchApplyResult,
|
||||
BatchFilePreview,
|
||||
BatchPreviewPage,
|
||||
StructuredOperation,
|
||||
} from "../../bindings/pvfine/services/models";
|
||||
|
||||
export type BatchMode = "text" | "structured";
|
||||
|
||||
export interface BatchTextForm {
|
||||
find: string;
|
||||
replacement: string;
|
||||
regex: boolean;
|
||||
}
|
||||
|
||||
export interface BatchOperationForm extends StructuredOperation {
|
||||
id: number;
|
||||
}
|
||||
|
||||
/** 批处理弹窗状态与当前会话内的预览计划。 */
|
||||
export const useBatchStore = defineStore("batch", () => {
|
||||
const visible = ref(false);
|
||||
const mode = ref<BatchMode>("text");
|
||||
const scopeLabel = ref("");
|
||||
const scopePaths = ref<string[]>([]);
|
||||
const text = ref<BatchTextForm>({ find: "", replacement: "", regex: false });
|
||||
let operationID = 1;
|
||||
let requestID = 0;
|
||||
let planFingerprint = "";
|
||||
let selectionMode: "all" | "none" | "some" = "all";
|
||||
let excludedIndexes = new Set<number>();
|
||||
const operations = ref<BatchOperationForm[]>([newOperation()]);
|
||||
const rows = ref<BatchFilePreview[]>([]);
|
||||
const planId = ref("");
|
||||
const nextCursor = ref(-1);
|
||||
const requestedFiles = ref(0);
|
||||
const matchedFiles = ref(0);
|
||||
const matchedOccurrences = ref(0);
|
||||
const changedFiles = ref(0);
|
||||
const selectedIndexes = ref<Set<number>>(new Set());
|
||||
const loading = ref(false);
|
||||
const applying = ref(false);
|
||||
const error = ref("");
|
||||
const stale = ref(false);
|
||||
const hasPreview = computed(() => !!planId.value && !stale.value);
|
||||
const selectedCount = computed(() => selectedIndexes.value.size);
|
||||
const canApply = computed(
|
||||
() =>
|
||||
hasPreview.value &&
|
||||
!loading.value &&
|
||||
!applying.value &&
|
||||
(selectedCount.value > 0 || (selectionMode === "all" && changedFiles.value > 0))
|
||||
);
|
||||
const requestFingerprint = computed(() =>
|
||||
JSON.stringify({
|
||||
mode: mode.value,
|
||||
text: text.value,
|
||||
operations: operations.value,
|
||||
paths: scopePaths.value,
|
||||
})
|
||||
);
|
||||
|
||||
watch(requestFingerprint, (fingerprint) => {
|
||||
if (planId.value && planFingerprint && fingerprint !== planFingerprint) stale.value = true;
|
||||
});
|
||||
|
||||
function newOperation(): BatchOperationForm {
|
||||
return {
|
||||
id: operationID++,
|
||||
kind: "set",
|
||||
section: "",
|
||||
tokenIndex: -1,
|
||||
value: "",
|
||||
createIfMissing: false,
|
||||
operator: "+",
|
||||
operand: "",
|
||||
operandEnd: "",
|
||||
anchorSection: "",
|
||||
hasEndTag: false,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePaths(paths: string[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
const result: string[] = [];
|
||||
for (const rawPath of paths) {
|
||||
const path = rawPath.replaceAll("\\", "/").replace(/^\/+|\/+$/g, "").trim();
|
||||
if (!path || seen.has(path)) continue;
|
||||
seen.add(path);
|
||||
result.push(path);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function resetPreview(): void {
|
||||
requestID++;
|
||||
rows.value = [];
|
||||
planId.value = "";
|
||||
nextCursor.value = -1;
|
||||
requestedFiles.value = 0;
|
||||
matchedFiles.value = 0;
|
||||
matchedOccurrences.value = 0;
|
||||
changedFiles.value = 0;
|
||||
selectedIndexes.value = new Set();
|
||||
selectionMode = "all";
|
||||
excludedIndexes = new Set();
|
||||
error.value = "";
|
||||
stale.value = false;
|
||||
planFingerprint = "";
|
||||
}
|
||||
|
||||
function open(paths: string[], label: string): void {
|
||||
scopePaths.value = normalizePaths(paths);
|
||||
scopeLabel.value = label;
|
||||
mode.value = "text";
|
||||
text.value = { find: "", replacement: "", regex: false };
|
||||
operations.value = [newOperation()];
|
||||
resetPreview();
|
||||
visible.value = scopePaths.value.length > 0;
|
||||
if (scopePaths.value.length === 0) error.value = "没有可处理的文件";
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
if (loading.value || applying.value) return;
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function addOperation(initial?: Partial<StructuredOperation>): BatchOperationForm {
|
||||
const base = newOperation();
|
||||
const op: BatchOperationForm = {
|
||||
...base,
|
||||
...initial,
|
||||
id: base.id,
|
||||
};
|
||||
operations.value.push(op);
|
||||
return op;
|
||||
}
|
||||
|
||||
function removeOperation(id: number): void {
|
||||
if (operations.value.length <= 1) return;
|
||||
const index = operations.value.findIndex((operation) => operation.id === id);
|
||||
if (index >= 0) operations.value.splice(index, 1);
|
||||
}
|
||||
|
||||
function moveOperation(id: number, direction: -1 | 1): void {
|
||||
const index = operations.value.findIndex((operation) => operation.id === id);
|
||||
const next = index + direction;
|
||||
if (index < 0 || next < 0 || next >= operations.value.length) return;
|
||||
const [operation] = operations.value.splice(index, 1);
|
||||
operations.value.splice(next, 0, operation);
|
||||
}
|
||||
|
||||
function applyPage(page: BatchPreviewPage, append: boolean): void {
|
||||
const pageRows = (page.rows ?? []).filter(
|
||||
(row): row is BatchFilePreview => !!row
|
||||
);
|
||||
if (append) rows.value.push(...pageRows);
|
||||
else rows.value = pageRows;
|
||||
for (const row of pageRows) {
|
||||
if (
|
||||
row.status === "changed" &&
|
||||
row.fileIndex >= 0 &&
|
||||
selectionMode === "all" &&
|
||||
!excludedIndexes.has(row.fileIndex)
|
||||
) {
|
||||
const next = new Set(selectedIndexes.value);
|
||||
next.add(row.fileIndex);
|
||||
selectedIndexes.value = next;
|
||||
}
|
||||
}
|
||||
planId.value = page.planId;
|
||||
nextCursor.value = page.nextCursor;
|
||||
requestedFiles.value = page.requestedFiles;
|
||||
matchedFiles.value = page.matchedFiles;
|
||||
matchedOccurrences.value = page.matchedOccurrences;
|
||||
changedFiles.value = page.changedFiles;
|
||||
}
|
||||
|
||||
function requestPayload() {
|
||||
return {
|
||||
mode: mode.value,
|
||||
paths: [...scopePaths.value],
|
||||
text:
|
||||
mode.value === "text"
|
||||
? {
|
||||
find: text.value.find,
|
||||
replacement: text.value.replacement,
|
||||
regex: text.value.regex,
|
||||
}
|
||||
: null,
|
||||
operations:
|
||||
mode.value === "structured"
|
||||
? operations.value.map(({ id: _id, ...operation }) => operation)
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
async function preview(): Promise<void> {
|
||||
if (loading.value || applying.value) return;
|
||||
const request = ++requestID;
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
stale.value = false;
|
||||
rows.value = [];
|
||||
planId.value = "";
|
||||
nextCursor.value = -1;
|
||||
planFingerprint = "";
|
||||
selectedIndexes.value = new Set();
|
||||
selectionMode = "all";
|
||||
excludedIndexes = new Set();
|
||||
try {
|
||||
const page = await BatchService.Preview(requestPayload());
|
||||
if (request !== requestID || !page) return;
|
||||
applyPage(page, false);
|
||||
planFingerprint = requestFingerprint.value;
|
||||
} catch (value: any) {
|
||||
if (request === requestID) error.value = errorMessage(value);
|
||||
} finally {
|
||||
if (request === requestID) loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMore(): Promise<boolean> {
|
||||
if (loading.value || !planId.value || nextCursor.value < 0 || stale.value) return false;
|
||||
const request = requestID;
|
||||
const cursor = nextCursor.value;
|
||||
loading.value = true;
|
||||
try {
|
||||
const page = await BatchService.PreviewPage(planId.value, cursor, 100);
|
||||
if (request !== requestID || !page) return false;
|
||||
applyPage(page, true);
|
||||
return nextCursor.value !== cursor;
|
||||
} catch (value: any) {
|
||||
if (request === requestID) error.value = errorMessage(value);
|
||||
return false;
|
||||
} finally {
|
||||
if (request === requestID) loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAll(): Promise<void> {
|
||||
while (nextCursor.value >= 0 && !stale.value) {
|
||||
const progressed = await loadMore();
|
||||
if (!progressed) {
|
||||
if (error.value) throw new Error(error.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelected(fileIndex: number): void {
|
||||
const next = new Set(selectedIndexes.value);
|
||||
if (selectionMode === "all") {
|
||||
if (next.has(fileIndex)) {
|
||||
next.delete(fileIndex);
|
||||
excludedIndexes.add(fileIndex);
|
||||
} else {
|
||||
next.add(fileIndex);
|
||||
excludedIndexes.delete(fileIndex);
|
||||
}
|
||||
} else if (next.has(fileIndex)) {
|
||||
next.delete(fileIndex);
|
||||
} else {
|
||||
next.add(fileIndex);
|
||||
}
|
||||
selectedIndexes.value = next;
|
||||
if (selectionMode === "none") selectionMode = "some";
|
||||
}
|
||||
|
||||
function selectAll(): void {
|
||||
selectionMode = "all";
|
||||
excludedIndexes = new Set();
|
||||
const next = new Set<number>();
|
||||
for (const row of rows.value) {
|
||||
if (row.status === "changed" && row.fileIndex >= 0) next.add(row.fileIndex);
|
||||
}
|
||||
selectedIndexes.value = next;
|
||||
}
|
||||
|
||||
function selectNone(): void {
|
||||
selectionMode = "none";
|
||||
excludedIndexes = new Set();
|
||||
selectedIndexes.value = new Set();
|
||||
}
|
||||
|
||||
async function apply(): Promise<BatchApplyResult> {
|
||||
if (!canApply.value) throw new Error("没有选中的变更文件");
|
||||
applying.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
await loadAll();
|
||||
if (stale.value || !planId.value) throw new Error("批处理预览已过期,请重新预览");
|
||||
return await BatchService.Apply(planId.value, [...selectedIndexes.value]);
|
||||
} catch (value: any) {
|
||||
const message = errorMessage(value);
|
||||
if (message.includes("预览已过期")) stale.value = true;
|
||||
error.value = message;
|
||||
throw value;
|
||||
} finally {
|
||||
applying.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function errorMessage(value: any): string {
|
||||
return String(value?.message ?? value ?? "批处理失败");
|
||||
}
|
||||
|
||||
function markStale(): void {
|
||||
if (planId.value) stale.value = true;
|
||||
}
|
||||
|
||||
Events.On("archive:advanced-search-stale", markStale);
|
||||
Events.On("archive:batch-applied", markStale);
|
||||
Events.On("archive:opened", () => {
|
||||
visible.value = false;
|
||||
resetPreview();
|
||||
});
|
||||
Events.On("archive:closed", () => {
|
||||
visible.value = false;
|
||||
resetPreview();
|
||||
});
|
||||
|
||||
return {
|
||||
visible,
|
||||
mode,
|
||||
scopeLabel,
|
||||
scopePaths,
|
||||
text,
|
||||
operations,
|
||||
rows,
|
||||
planId,
|
||||
nextCursor,
|
||||
requestedFiles,
|
||||
matchedFiles,
|
||||
matchedOccurrences,
|
||||
changedFiles,
|
||||
selectedIndexes,
|
||||
loading,
|
||||
applying,
|
||||
error,
|
||||
stale,
|
||||
hasPreview,
|
||||
selectedCount,
|
||||
canApply,
|
||||
open,
|
||||
close,
|
||||
addOperation,
|
||||
removeOperation,
|
||||
moveOperation,
|
||||
preview,
|
||||
loadMore,
|
||||
loadAll,
|
||||
toggleSelected,
|
||||
selectAll,
|
||||
selectNone,
|
||||
apply,
|
||||
resetPreview,
|
||||
};
|
||||
});
|
||||
Reference in new issue
Block a user