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

+45
View File
@@ -0,0 +1,45 @@
import type { TreeOption } from "naive-ui";
import type { ExportPreviewFile } from "../../bindings/pvfine/services/models";
export function exportFileKey(path: string): string {
return `file:${path}`;
}
export function buildExportTree(files: ExportPreviewFile[]): TreeOption[] {
const roots: TreeOption[] = [];
const directories = new Map<string, TreeOption>();
for (const file of files) {
const parts = file.path.split("/");
let children = roots;
let parent = "";
for (const part of parts.slice(0, -1)) {
parent = parent ? `${parent}/${part}` : part;
let directory = directories.get(parent);
if (!directory) {
directory = { key: `dir:${parent}`, label: part, children: [], isLeaf: false };
directories.set(parent, directory);
children.push(directory);
}
children = directory.children!;
}
children.push({
key: exportFileKey(file.path),
label: parts[parts.length - 1],
isLeaf: true,
checkboxDisabled: file.required,
path: file.path,
required: file.required,
});
}
function sort(nodes: TreeOption[]): void {
nodes.sort((a, b) => {
if (a.isLeaf !== b.isLeaf) return a.isLeaf ? 1 : -1;
const sa = String(a.label);
const sb = String(b.label);
return sa < sb ? -1 : (sa > sb ? 1 : 0);
});
for (const node of nodes) if (node.children) sort(node.children);
}
sort(roots);
return roots;
}
+105
View File
@@ -0,0 +1,105 @@
import type { ImageBatchResult, ImageData, ImageReference } from "../../bindings/pvfine/services/models";
const batchSize = 128;
const maxConcurrentBatches = 2;
const batchDelayMs = 8;
const cacheEntries = 512;
type ImageJob = {
key: string;
generation: number;
epoch: number;
reference: ImageReference;
promise: Promise<ImageData | null>;
resolve: (data: ImageData | null) => void;
};
// Coalesce only requested images, including visible tree rows and previews.
// Reset settles old callers immediately and prevents old responses filling the cache.
export function createImageLoader(
getGeneration: () => number,
loadBatch: (references: ImageReference[], generation: number) => Promise<ImageBatchResult[]>,
onError: (error: unknown) => void = () => {},
) {
const pending = new Map<string, ImageJob>();
const resolved = new Map<string, ImageData | null>();
let queue: ImageJob[] = [];
let timer: ReturnType<typeof setTimeout> | null = null;
let active = 0;
let epoch = 0;
function schedule() {
if (timer !== null || queue.length === 0 || active >= maxConcurrentBatches) return;
timer = setTimeout(() => {
timer = null;
pump();
}, batchDelayMs);
}
function pump() {
while (queue.length > 0 && active < maxConcurrentBatches) {
const jobs = queue.splice(0, batchSize);
active++;
void run(jobs);
}
}
async function run(jobs: ImageJob[]) {
try {
const results = await loadBatch(jobs.map((job) => job.reference), jobs[0]!.generation);
for (let index = 0; index < jobs.length; index++) {
const job = jobs[index]!;
if (job.epoch !== epoch || job.generation !== getGeneration()) continue;
const result = results[index];
const data = result?.data ?? null;
if (result?.error) {
onError(result.error);
} else if (result) {
resolved.delete(job.key);
resolved.set(job.key, data);
while (resolved.size > cacheEntries) resolved.delete(resolved.keys().next().value!);
}
job.resolve(data);
}
} catch (error) {
if (jobs[0]!.epoch === epoch) onError(error);
} finally {
for (const job of jobs) {
job.resolve(null); // also settles errors or stale/incomplete responses
if (pending.get(job.key) === job) pending.delete(job.key);
}
active--;
pump();
}
}
function load(reference: ImageReference | null | undefined): Promise<ImageData | null> {
if (!reference?.path || !Number.isInteger(reference.index) || reference.index < 0) return Promise.resolve(null);
const path = reference.path.replaceAll("\\", "/").replace(/^\/+|\/+$/g, "").toLowerCase();
if (!path) return Promise.resolve(null);
const generation = getGeneration();
const key = `${generation}\u0000${path}\u0000${reference.index}`;
if (resolved.has(key)) return Promise.resolve(resolved.get(key) ?? null);
const existing = pending.get(key);
if (existing) return existing.promise;
let resolve!: ImageJob["resolve"];
const promise = new Promise<ImageData | null>((done) => { resolve = done; });
const job: ImageJob = { key, generation, epoch, reference: { path, index: reference.index }, promise, resolve };
pending.set(key, job);
queue.push(job);
schedule();
return promise;
}
function reset() {
epoch++;
if (timer !== null) clearTimeout(timer);
timer = null;
queue = [];
for (const job of pending.values()) job.resolve(null);
pending.clear();
resolved.clear();
}
return { load, reset };
}