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

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