1 Commits
Author SHA1 Message Date
github-actions[bot] 75f9f5f3be Prepare manifest for v2.0.2.3 2026-10-05 07:29:34 +00:00
363 changed files with 1753 additions and 16280 deletions
+4 -4
View File
@@ -16,25 +16,25 @@ tools/*.mjs
*.mjs
*.cjs
!mengsan/tools/check-sts2-relics.mjs
!mengsan/tools/write-sts2-relics-report.mjs
# Local updater downloads and runtime logs
/.update_tmp/
/.planning/
/mengsan/logs/
/logs/
*.log
# Local credentials; keep shareable environment templates
.env
.env.*
!.env.example
!.env.sample
!.env.template
# Dependency and interpreter caches
node_modules/
__pycache__/
*.pyc
# Editor temporary files and OS metadata
*.tmp
*.bak
*.swp
-62
View File
@@ -1,62 +0,0 @@
# 术樱包自动诊断日志
扩展加载后自动记录本扩展的关键业务步骤,发生错误时自动生成独立报告。
无需开启控制台或手动开始录制;不上传任何数据。
## 文件位置
PC 和安卓客户端均写入游戏目录下的 `extension/术樱包/logs/`。
- `runtime-*.jsonl`:按行保存时间、模块、步骤、关联 ID 和上下文。
- `report-*.json`:错误报告,包含首次/最近异常、错误栈、cause、内部异常、
运行版本、错误前操作、当前结算阶段和存档状态。
同一异常在存档层、结算层等多个入口被观察时,共用报告编号并更新修订号;
occurrences 表示被捕获的次数,不表示奖励发放次数。最近报告不会因普通日志
轮转被删除。保存回执绑定对应修订和真实通道,旧版本写入成功不会误标新版本。
扩展设置中的“诊断日志位置”显示实际位置和最近报告保存状态。
只需要保留相应报告和附近的 runtime 文件;如何发送由用户自行处理。
## 采集范围
- 主动记录术樱包加载、更新、梦三战斗、结算、存档、手牌、地图及菜单错误。
- 全局错误仅接收来源文件或异常栈属于本扩展的情况,包含编码后的中文路径。
- 不因为当前开启梦三就收集所有错误,也不根据错误消息包含“术樱包”归因。
- 原引擎错误处理仍会执行;不替换引擎事件、Promise 或 console 的原型。
## 存档诊断
报告分别记录 storageStatus、memoryPublished 和 commitReceipt:
- storageStatus=committed 表示存档事务完成,或 localStorage 写入已返回。
- memoryPublished=true 表示运行内存已发布成功存档。
- commitReceipt=true 表示结算协调器收到了提交成功结果。
资源清理、技能释放或地图展示随后失败时,不能据此判断存档失败。
重试沿用现有去重和奖励提交规则,日志不参与存档事务,也不触发自动重试。
## 容量与兼容
默认每秒批量保存普通日志;错误报告立即排入写入队列。运行日志按约
256 KiB 分段;内存保留最近约 512 KiB / 400 条记录。日志目录自动清理
系统自己生成的旧文件,保留总量约 5 MiB、最多 64 个文件。报告包含截断和
丢弃计数;“完整诊断”指受限的错误链与现场记录,不是无限保留整个游戏过程。
待写队列默认最多 12 项、合计 2 MiB,优先保存错误报告;另有最多一项正在写入。
错误报告复用已脱敏的记录文本,按字节预算一次裁剪,保留首次和最近异常。
PC 使用异步文件操作,安卓使用 Cordova 目录和 FileWriter 回调,写入前截断旧
内容,使用 UTF-8 Blob。单次保存操作设置 5 秒观察超时;超时后停止使用该
文件通道,避免迟到写入与后续写入重叠。写入异常不会中断游戏业务。
原生文件不可用时,降级到独立的 `shuying-diagnostics` IndexedDB;它不与
游戏存档共用数据库。若两种持久化方式都不可用,仅保留本次运行的内存
记录,菜单会明确显示未持久化,不能宣称文件已生成。
切入后台或页面隐藏时尝试保存缓冲;被系统强制杀死或断电时,最后尚未完成的
异步写入可能丢失。结算内部等待超过 15 秒会记录观察日志,选择奖励、地图节点
和剧情等待不会被当作超时错误;观察器不会结束业务事件。
字段过滤会隐藏凭据字段、常见 URL 凭据参数和 Bearer 内容。报告不会复制完整
玩家对象、DOM、引擎状态或整个存档。日志文件不进入发布清单和 Git。
-348
View File
@@ -1,348 +0,0 @@
import {
byteLength, cleanText, extensionError, extensionSource, serializeError, snapshot,
} from "./serialize.js";
import { createDiagnosticStorage, isDiagnosticFile, LOG_DIRECTORY } from "./storage.js";
const noop = () => null;
const inactive = Object.freeze({
info: noop, warn: noop, error: noop, scope: () => inactive,
registerContext: () => noop, flush: async () => {},
step: async (_event, action) => action(),
status: () => ({ enabled: false }), dispose: noop,
});
let current = inactive;
export function getDiagnostics() { return current; }
export function createDiagnostics(options = {}) {
const env = options.env || {};
const now = options.now || Date.now;
const monotonic = () => {
try { return env.performance?.now?.() ?? now(); } catch { return now(); }
};
const launchId = `${now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
const maxMemoryBytes = options.maxMemoryBytes ?? 512 * 1024;
const maxJournalBytes = options.maxJournalBytes ?? 256 * 1024;
const maxReportBytes = options.maxReportBytes ?? 512 * 1024;
const maxDiskBytes = options.maxDiskBytes ?? 5 * 1024 * 1024;
const maxFiles = options.maxFiles ?? 64;
const maxPending = options.maxPending ?? 12;
const maxPendingBytes = options.maxPendingBytes ?? 2 * 1024 * 1024;
const flushMs = options.flushMs ?? 1000;
const recent = [], contexts = new Map(), pending = new Map();
const errors = new WeakMap(), fingerprints = new Map(), listeners = [];
let sequence = 0, reportSequence = 0, segment = 0, memoryBytes = 0;
let journal = [], journalBytes = 0, revision = 0, queuedRevision = -1;
let flushTimer = null, worker = null, stopped = false, unavailable = false;
let latestReport = null, droppedRecords = 0, droppedWrites = 0;
let inventory = null, inventoryKind = null;
let pendingBytes = 0;
const metadata = snapshot({
extension: "术樱包", extensionVersion: options.version || "unknown",
engineVersion: env.lib?.version || "unknown", launchId,
platform: env.lib?.node ? "pc" : env.lib?.device ? "android" : "browser",
userAgent: env.window?.navigator?.userAgent || "unknown",
modules: { mengsan: env.lib?.config?.shuYing_mengsan_installed_version || null },
});
const roots = [];
try {
const root = new URL("../", import.meta.url);
if (["file:", "http:", "https:"].includes(root.protocol)) roots.push(root.href);
} catch {}
if (options.extensionRoot) {
try {
roots.push(new URL(options.extensionRoot.replace(/\/?$/, "/"),
env.document?.baseURI || env.window?.location?.href).href);
} catch {}
}
const storage = options.storage || createDiagnosticStorage(env, {
timeoutMs: options.storageTimeoutMs,
onFailure(error, phase) {
record("WARN", "diagnostics.storage.failure", { phase }, error, "diagnostics");
},
});
const storageStatus = () => storage.status?.() || { kind: "file", location: LOG_DIRECTORY };
const runtimeName = () => `runtime-${launchId}-${segment}.jsonl`;
function contextFor(extra) {
const values = {};
for (const [name, provider] of contexts) {
try { values[name] = snapshot(provider()); }
catch { values[name] = { diagnosticContextUnavailable: true }; }
}
return { ...values, ...snapshot(extra || {}) };
}
function schedule() {
if (flushTimer || stopped || unavailable) return;
flushTimer = setTimeout(() => { flushTimer = null; void flush(); }, flushMs);
flushTimer?.unref?.();
}
function record(level, event, extra, error, module = "extension") {
if (stopped) return null;
try {
const entry = {
schemaVersion: 1, sequence: ++sequence, time: new Date(now()).toISOString(),
launchId, level, module: cleanText(module, 128), event: cleanText(event, 256),
context: contextFor(extra),
};
if (error !== undefined) entry.error = serializeError(error);
const item = { entry, bytes: 0, text: "" };
recent.push(item);
if (level === "ERROR") createReport(entry, error);
const text = JSON.stringify(entry);
const bytes = byteLength(text) + 1;
item.text = text;
delete item.entry;
item.bytes = bytes; memoryBytes += bytes;
if (journalBytes + bytes > maxJournalBytes && journal.length) {
enqueue(runtimeName(), journal.join(""));
segment++; journal = []; journalBytes = 0; queuedRevision = -1;
}
journal.push(text + "\n"); journalBytes += bytes; revision++;
while (recent.length > 400 || memoryBytes > maxMemoryBytes) {
memoryBytes -= recent.shift().bytes; droppedRecords++;
}
schedule();
if (level === "ERROR") void flush();
return entry;
} catch {
return null;
}
}
function createReport(entry, rawError) {
let known;
if (rawError && typeof rawError === "object") known = errors.get(rawError);
const fingerprint = `${entry.module}:${entry.event}:${entry.error?.name}:${entry.error?.message}`;
if (!known) {
const prior = fingerprints.get(fingerprint);
if (prior && now() - prior.time < 5000) known = prior;
}
if (!known) known = { id: `${launchId}-${++reportSequence}`, count: 0,
first: entry.time, time: now(), firstFailure: entry };
known.count++; known.time = now();
if (rawError && typeof rawError === "object") errors.set(rawError, known);
fingerprints.set(fingerprint, known);
while (fingerprints.size > 64) fingerprints.delete(fingerprints.keys().next().value);
entry.reportId = known.id;
const file = `report-${known.id}.json`;
const report = {
schemaVersion: 1, reportId: known.id, firstSeen: known.first,
lastSeen: entry.time, occurrences: known.count, environment: metadata,
storageAtCapture: snapshot(storageStatus()), firstFailure: known.firstFailure,
failure: entry,
droppedRecords, droppedWrites,
};
const records = recent.map(item => {
const text = item.text || JSON.stringify(item.entry);
return { text, bytes: item.text ? item.bytes - 1 : byteLength(text) };
});
let start = 0;
let bytes = byteLength(JSON.stringify(report)) + 14
+ records.reduce((total, item) => total + item.bytes + 1, 0);
while (bytes + 32 > maxReportBytes && start < records.length - 1) {
bytes -= records[start++].bytes + 1;
}
report.droppedRecords += start;
const text = JSON.stringify(report).slice(0, -1) + ',"records":['
+ records.slice(start).map(item => item.text).join(",") + "]}";
latestReport = { id: known.id, file, revision: known.count,
persisted: false, event: entry.event };
enqueue(file, text, true, known.count);
}
function enqueue(name, text, priority = false, reportRevision = null) {
if (unavailable || stopped) return;
const bytes = byteLength(text);
const previous = pending.get(name);
if (previous) { pendingBytes -= previous.bytes; pending.delete(name); }
if (bytes > maxPendingBytes) { droppedWrites++; return; }
while (pending.size >= maxPending || pendingBytes + bytes > maxPendingBytes) {
const victim = [...pending].find(([, item]) => !item.priority)?.[0]
|| (priority ? pending.keys().next().value : null);
if (!victim) { droppedWrites++; return; }
pendingBytes -= pending.get(victim).bytes;
pending.delete(victim); droppedWrites++;
}
pending.set(name, { text, bytes, priority, reportRevision });
pendingBytes += bytes;
startWorker();
}
async function prune(protect, text) {
const kind = storageStatus().kind;
if (!inventory || inventoryKind !== kind) {
inventory = new Map((await storage.list()).filter(file =>
isDiagnosticFile(file.name)).map(file => [file.name, file]));
inventoryKind = kind;
}
inventory.set(protect, { name: protect, bytes: byteLength(text), time: now() });
const files = [...inventory.values()];
files.sort((a, b) => (a.time || 0) - (b.time || 0));
let bytes = files.reduce((sum, file) => sum + (file.bytes || 0), 0);
let count = files.length;
for (const file of files) {
if (bytes <= maxDiskBytes && count <= maxFiles) break;
if (file.name === protect || file.name === runtimeName()
|| file.name === latestReport?.file) continue;
await storage.remove(file.name);
inventory.delete(file.name);
bytes -= file.bytes || 0; count--;
}
}
function startWorker() {
if (worker || !pending.size) return;
worker = Promise.resolve().then(async () => {
while (pending.size && !unavailable) {
const name = [...pending].find(([, item]) => item.priority)?.[0]
|| pending.keys().next().value;
const item = pending.get(name);
pending.delete(name);
pendingBytes -= item.bytes;
try {
await storage.write(name, item.text);
if (latestReport?.file === name && latestReport.revision === item.reportRevision) {
latestReport.persisted = true;
latestReport.savedStorage = snapshot(storageStatus());
}
try { await prune(name, item.text); } catch { inventory = null; }
} catch {
unavailable = true; pending.clear(); pendingBytes = 0;
}
}
}).catch(() => { unavailable = true; pending.clear(); pendingBytes = 0; }).finally(() => {
worker = null;
if (pending.size && !unavailable) startWorker();
});
}
async function flush() {
try {
clearTimeout(flushTimer); flushTimer = null;
if (journal.length && queuedRevision !== revision) {
queuedRevision = revision;
enqueue(runtimeName(), journal.join(""));
}
while (worker) await worker;
} catch {}
}
function scope(module, provider = () => ({})) {
const details = extra => {
try {
const context = typeof provider === "function" ? provider() : provider;
return { ...snapshot(context), ...snapshot(extra || {}) };
} catch { return { diagnosticContextUnavailable: true }; }
};
return Object.freeze({
info: (event, extra) => record("INFO", event, details(extra), undefined, module),
warn: (event, extra, error) => record("WARN", event, details(extra), error, module),
error: (event, error, extra) => record("ERROR", event, details(extra), error, module),
async step(event, action, extra = {}, options = {}) {
const started = monotonic();
record("INFO", `${event}.start`, details(extra), undefined, module);
let timer;
if (options.observeWait !== false) {
timer = setTimeout(() => {
if (env.document?.hidden) return;
record("WARN", `${event}.waiting`, details({ ...extra,
elapsedMs: monotonic() - started }), undefined, module);
}, options.waitMs ?? 15000);
timer?.unref?.();
}
try {
const result = await action();
record("INFO", `${event}.complete`, details({ ...extra,
elapsedMs: monotonic() - started }), undefined, module);
return result;
} catch (error) {
record("ERROR", `${event}.failed`, details({ ...extra,
elapsedMs: monotonic() - started }), error, module);
throw error;
} finally { clearTimeout(timer); }
},
});
}
function listen(target, event, handler, capture = false) {
if (typeof target?.addEventListener !== "function") return;
target.addEventListener(event, handler, capture);
listeners.push(() => target.removeEventListener(event, handler, capture));
}
const globalLogger = scope("extension.global");
const globalSeen = new WeakMap();
function captureGlobal(error, source, extra = {}) {
if (!extensionSource(source, roots) && !extensionError(error, roots)) return;
if (error && typeof error === "object") {
const last = globalSeen.get(error);
if (last !== undefined && now() - last < 20) return;
globalSeen.set(error, now());
}
globalLogger.error("uncaught", error, { source, ...extra });
}
listen(env.window, "error", event => {
try {
if (extensionSource(event.filename, roots) || extensionError(event.error, roots)) {
captureGlobal(event.error || event.message, event.filename,
{ line: event.lineno, column: event.colno });
} else {
const source = event.target?.src || event.target?.href;
if (extensionSource(source, roots)) globalLogger.error("resource.load",
new Error("术樱包资源加载失败"), { source });
}
} catch {}
}, true);
listen(env.window, "unhandledrejection", event => {
try {
if (extensionError(event.reason, roots)) captureGlobal(event.reason, "",
{ origin: "unhandledrejection" });
} catch {}
});
if (env.window) {
const previous = env.window.onerror;
const chained = function(message, source, line, column, error) {
try { captureGlobal(error || message, source, { line, column }); } catch {}
return typeof previous === "function" ? previous.apply(this, arguments) : undefined;
};
env.window.onerror = chained;
listeners.push(() => {
if (env.window.onerror === chained) env.window.onerror = previous;
});
}
listen(env.document, "visibilitychange", () => {
if (env.document.hidden) void flush();
});
listen(env.window, "pagehide", () => { void flush(); });
const api = Object.freeze({
...scope("extension"), scope,
registerContext(name, provider) {
contexts.set(name, provider);
return () => contexts.delete(name);
},
flush,
status: () => snapshot({ enabled: true, ...storageStatus(),
unavailable, latestReport, droppedRecords, droppedWrites,
pendingFiles: pending.size, pendingBytes,
runtimeFile: runtimeName(), launchId }),
dispose() {
void flush(); stopped = true; clearTimeout(flushTimer);
for (const remove of listeners) remove();
if (current === api) current = inactive;
},
});
api.info("diagnostics.started", { environment: metadata });
return api;
}
export function configureDiagnostics({ lib, game, window, document, version, extensionRoot }) {
if (current !== inactive) return current;
try {
current = createDiagnostics({ version, extensionRoot,
env: { lib, game, window, document, performance: window?.performance,
localStorage: window?.localStorage, indexedDB: window?.indexedDB } });
} catch {}
return current;
}
-127
View File
@@ -1,127 +0,0 @@
const secretKey = /password|passwd|secret|token|cookie|authorization|api[_-]?key|credential/i;
export function cleanText(value, limit = 8192) {
let text;
try { text = String(value); } catch { return "[unreadable]"; }
const truncated = text.length > limit;
text = text.slice(0, limit);
text = text.replace(/([?&](?:token|key|api[_-]?key|access_token|password|secret|auth)[a-z0-9_-]*=)[^&#\s]*/gi,
"$1[redacted]");
text = text.replace(/((?:Bearer|Basic)\s+)[\w.+\/=-]+/gi, "$1[redacted]");
text = text.replace(/\b(?:https?|ftp):\/\/[^\s/?#]*/gi, (url, offset, source) => {
const schemeEnd = url.indexOf("://") + 3;
const at = url.lastIndexOf("@");
if (at >= schemeEnd) return url.slice(0, schemeEnd) + "[redacted]@" + url.slice(at + 1);
if (truncated && offset + url.length === source.length
&& url.slice(schemeEnd).includes(":")) return url.slice(0, schemeEnd) + "[redacted]";
return url;
});
return !truncated && text.length <= limit ? text : text.slice(0, limit) + "[truncated]";
}
function read(object, key) {
try { return object[key]; } catch { return "[unreadable]"; }
}
export function snapshot(value, depth = 0, seen = new WeakSet()) {
if (value === null || value === undefined) return value ?? null;
if (typeof value === "string") return cleanText(value, 2048);
if (typeof value === "boolean") return value;
if (typeof value === "number") return Number.isFinite(value) ? value : String(value);
if (typeof value !== "object") return cleanText(value, 256);
if (seen.has(value)) return "[circular]";
if (depth >= 5) return "[depth limit]";
seen.add(value);
if (Array.isArray(value)) {
const result = value.slice(0, 32).map(item => snapshot(item, depth + 1, seen));
if (value.length > 32) result.push(`[${value.length - 32} more items]`);
return result;
}
let keys;
try { keys = Object.keys(value); } catch { return "[unreadable]"; }
const result = {};
for (const key of keys.slice(0, 40)) {
if (key === "__proto__" || key === "constructor") continue;
result[key] = secretKey.test(key) ? "[redacted]"
: snapshot(read(value, key), depth + 1, seen);
}
if (keys.length > 40) result.truncatedKeys = keys.length - 40;
return result;
}
export function serializeError(error, depth = 0, seen = new WeakSet(),
budget = { nodes: 0, characters: 32768 }) {
if (++budget.nodes > 32 || budget.characters <= 0) {
return { name: "TruncatedError", message: "[error size limit]" };
}
const text = (value, limit = 8192) => {
const result = cleanText(value, Math.max(0, Math.min(limit, budget.characters)));
budget.characters -= result.length;
return result;
};
if (!error || typeof error !== "object") {
return { name: "ThrownValue", message: text(error) };
}
if (seen.has(error)) return { name: "CircularError", message: "[circular]" };
if (depth >= 5) return { name: "TruncatedError", message: "[depth limit]" };
seen.add(error);
const result = {
name: text(read(error, "name") || "Error", 128),
message: text(read(error, "message") || error),
stack: text(read(error, "stack") || "", 16384),
};
for (const key of ["code", "operationUncertain"]) {
const value = read(error, key);
if (value !== undefined) result[key] = snapshot(value);
}
const cause = read(error, "cause");
if (cause !== undefined) result.cause = serializeError(cause, depth + 1, seen, budget);
const errors = read(error, "errors");
if (Array.isArray(errors)) {
result.errors = errors.slice(0, 16).map(item =>
serializeError(item, depth + 1, seen, budget));
if (errors.length > 16) result.truncatedErrors = errors.length - 16;
}
return result;
}
function decode(value) {
let text = cleanText(value, 32768).replace(/\\/g, "/");
for (let count = 0; count < 2; count++) {
try { text = decodeURIComponent(text); } catch { break; }
}
return text;
}
export function extensionSource(value, roots = []) {
if (typeof value !== "string" || !value) return false;
const source = cleanText(value, 32768).replace(/\\/g, "/").trim();
const url = source.match(/(?:file|https?):\/\/[^\n]+/i)?.[0];
const native = source.match(/[a-z]:\/[^\n]+/i)?.[0];
const candidate = decode((url || native || source)
.replace(/\)\s*$/, "").split(/[?#]/, 1)[0]);
if (roots.some(root => {
const base = decode(root).split(/[?#]/, 1)[0];
return base && (candidate.startsWith(base)
|| (base.startsWith("file:") && candidate.startsWith(base.replace(/^file:\/+/, ""))));
})) return true;
if (/^(?:\.?\/)?extension\/术樱包\//.test(candidate)) return true;
return !roots.length && /\/extension\/术樱包\//.test(candidate);
}
export function extensionError(error, roots = [], depth = 0) {
if (!error || typeof error !== "object" || depth >= 5) return false;
const stack = read(error, "stack");
if (typeof stack === "string" && stack.split("\n").slice(1).some(line =>
/(?:\bat\s|@)/.test(line) && extensionSource(line, roots))) return true;
if (extensionSource(read(error, "fileName"), roots)) return true;
if (extensionError(read(error, "cause"), roots, depth + 1)) return true;
const errors = read(error, "errors");
return Array.isArray(errors) && errors.slice(0, 16).some(item =>
extensionError(item, roots, depth + 1));
}
export function byteLength(text) {
if (typeof TextEncoder === "function") return new TextEncoder().encode(text).length;
return new Blob([text]).size;
}
-206
View File
@@ -1,206 +0,0 @@
import { byteLength } from "./serialize.js";
export const LOG_DIRECTORY = "extension/术樱包/logs";
const ownedName = /^(?:runtime|report)-[a-zA-Z0-9-]+\.(?:jsonl|json)$/;
export function isDiagnosticFile(name) {
return typeof name === "string" && ownedName.test(name);
}
function checkedName(name) {
if (!isDiagnosticFile(name)) throw new Error("Invalid diagnostic filename");
return name;
}
function callback(start) {
return new Promise((resolve, reject) => {
try { start(resolve, reject); } catch (error) { reject(error); }
});
}
function nodeStorage(env) {
const fs = env.lib?.node?.fs;
const root = env.window?.__dirname;
if (!fs || typeof root !== "string" || !root) return null;
const directory = `${root.replace(/[\\/]$/, "")}/${LOG_DIRECTORY}`;
const invoke = (method, ...args) => callback((resolve, reject) => {
fs[method](...args, (error, value) => error ? reject(error) : resolve(value));
});
const ready = () => invoke("mkdir", directory, { recursive: true });
return {
kind: "file", location: directory,
async write(name, text) {
await ready();
await invoke("writeFile", `${directory}/${checkedName(name)}`, text, "utf8");
},
async list() {
await ready();
const names = (await invoke("readdir", directory)).filter(isDiagnosticFile);
const entries = [];
for (const name of names) {
const stat = await invoke("lstat", `${directory}/${name}`);
if (stat.isFile()) entries.push({ name, bytes: stat.size, time: stat.mtimeMs });
}
return entries;
},
remove: name => invoke("unlink", `${directory}/${checkedName(name)}`),
};
}
function cordovaStorage(env) {
const host = env.window;
if (typeof host?.resolveLocalFileSystemURL !== "function") return null;
const root = env.localStorage?.getItem("noname_inited");
if (!root) return null;
let directoryPromise;
const directory = () => {
if (!directoryPromise) directoryPromise = callback((resolve, reject) => {
host.resolveLocalFileSystemURL(root, resolve, reject);
}).then(async entry => {
for (const part of LOG_DIRECTORY.split("/")) {
entry = await callback((resolve, reject) =>
entry.getDirectory(part, { create: true }, resolve, reject));
}
return entry;
});
return directoryPromise;
};
const file = async name => {
const entry = await directory();
return callback((resolve, reject) =>
entry.getFile(checkedName(name), { create: true }, resolve, reject));
};
return {
kind: "file", location: `${root.replace(/\/$/, "")}/${LOG_DIRECTORY}`,
async write(name, text) {
const entry = await file(name);
await callback((resolve, reject) => entry.createWriter(writer => {
let truncated = false;
writer.onerror = () => reject(writer.error || new Error("Diagnostic write failed"));
writer.onwriteend = () => {
if (writer.error) { reject(writer.error); return; }
if (truncated) { resolve(); return; }
truncated = true;
try { writer.write(new Blob([text], { type: "text/plain;charset=utf-8" })); }
catch (error) { reject(error); }
};
try { writer.truncate(0); } catch (error) { reject(error); }
}, reject));
},
async list() {
const entry = await directory();
const reader = entry.createReader();
const entries = [];
while (true) {
const batch = await callback((resolve, reject) =>
reader.readEntries(resolve, reject));
if (!batch.length) break;
for (const item of batch) {
if (!item.isFile || !isDiagnosticFile(item.name)) continue;
const metadata = await callback((resolve, reject) =>
item.getMetadata(resolve, reject));
entries.push({ name: item.name, bytes: metadata.size,
time: new Date(metadata.modificationTime).getTime() });
}
}
return entries;
},
async remove(name) {
const entry = await file(name);
return callback((resolve, reject) => entry.remove(resolve, reject));
},
};
}
function databaseStorage(env) {
const indexedDB = env.indexedDB;
if (!indexedDB) return null;
let database;
const open = () => database ||= callback((resolve, reject) => {
const request = indexedDB.open("shuying-diagnostics", 1);
let failed = false;
request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains("files")) db.createObjectStore("files", { keyPath: "name" });
};
request.onsuccess = () => {
if (failed) { request.result.close(); return; }
request.result.onversionchange = () => request.result.close();
resolve(request.result);
};
request.onerror = () => { failed = true; reject(request.error); };
request.onblocked = () => { failed = true; reject(new Error("Diagnostic database blocked")); };
});
const transaction = async action => {
const db = await open();
return callback((resolve, reject) => {
const tx = db.transaction(["files"], "readwrite");
let value, failure;
tx.oncomplete = () => resolve(value);
tx.onerror = event => { failure = event.target?.error; };
tx.onabort = () => reject(tx.error || failure || new Error("Diagnostic transaction aborted"));
try { action(tx.objectStore("files"), result => { value = result; }); }
catch (error) { try { tx.abort(); } catch {} reject(error); }
});
};
return {
kind: "indexeddb", location: "本机独立诊断数据库",
write(name, text) {
return transaction(store => store.put({ name: checkedName(name), text,
bytes: byteLength(text), time: Date.now() }));
},
list() {
return transaction((store, done) => {
const entries = [];
const cursor = store.openCursor();
cursor.onsuccess = () => {
const item = cursor.result;
if (!item) { done(entries); return; }
if (isDiagnosticFile(item.value.name)) entries.push({
name: item.value.name, bytes: item.value.bytes, time: item.value.time,
});
item.continue();
};
});
},
remove: name => transaction(store => store.delete(checkedName(name))),
};
}
export function createDiagnosticStorage(env, options = {}) {
const timeoutMs = options.timeoutMs ?? 5000;
const onFailure = options.onFailure || (() => {});
const candidates = [];
try {
const native = nodeStorage(env) || cordovaStorage(env);
if (native) candidates.push(native);
} catch (error) { onFailure(error, "file.init"); }
try {
const database = databaseStorage(env);
if (database) candidates.push(database);
} catch (error) { onFailure(error, "database.init"); }
let index = 0;
const timed = action => new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error("Diagnostic storage timed out")), timeoutMs);
Promise.resolve().then(action).then(resolve, reject).finally(() => clearTimeout(timer));
});
const execute = async (method, ...args) => {
while (index < candidates.length) {
const backend = candidates[index];
try { return await timed(() => backend[method](...args)); }
catch (error) {
try { onFailure(error, `${backend.kind}.${method}`); } catch {}
if (method !== "write") throw error;
index++;
}
}
throw new Error("Diagnostic persistence unavailable; records kept in memory");
};
return {
write: (name, text) => execute("write", name, text),
list: () => execute("list"),
remove: name => execute("remove", name),
status: () => ({ kind: candidates[index]?.kind || "memory",
location: candidates[index]?.location || "仅当前运行内存,未能持久化" }),
};
}
+384 -419
View File
File diff suppressed because it is too large Load Diff
+666 -3731
View File
File diff suppressed because it is too large Load Diff
+1 -307
View File
@@ -1,5 +1,5 @@
{
"version": "2.0.2.3-rc.3",
"version": "2.0.2.3",
"module_id": "mengsan",
"snapshot": "legacy",
"files": [
@@ -302,305 +302,6 @@
"mengsan/assets/portraits/wriggler-forest-portrait.png",
"mengsan/assets/portraits/wriggler-original.webp",
"mengsan/assets/portraits/zhangfei-portrait.jpg",
"mengsan/assets/relics/akabeko.png",
"mengsan/assets/relics/alchemical_coffer.png",
"mengsan/assets/relics/amethyst_aubergine.png",
"mengsan/assets/relics/anchor.png",
"mengsan/assets/relics/arcane_scroll.png",
"mengsan/assets/relics/archaic_tooth.png",
"mengsan/assets/relics/art_of_war.png",
"mengsan/assets/relics/astrolabe.png",
"mengsan/assets/relics/bag_of_marbles.png",
"mengsan/assets/relics/bag_of_preparation.png",
"mengsan/assets/relics/beating_remnant.png",
"mengsan/assets/relics/beautiful_bracelet.png",
"mengsan/assets/relics/bellows.png",
"mengsan/assets/relics/belt_buckle.png",
"mengsan/assets/relics/big_hat.png",
"mengsan/assets/relics/big_mushroom.png",
"mengsan/assets/relics/biiig_hug.png",
"mengsan/assets/relics/bing_bong.png",
"mengsan/assets/relics/black_blood.png",
"mengsan/assets/relics/black_star.png",
"mengsan/assets/relics/blessed_antler.png",
"mengsan/assets/relics/blood_soaked_rose.png",
"mengsan/assets/relics/blood_vial.png",
"mengsan/assets/relics/bone_flute.png",
"mengsan/assets/relics/bone_tea.png",
"mengsan/assets/relics/book_of_five_rings.png",
"mengsan/assets/relics/book_repair_knife.png",
"mengsan/assets/relics/bookmark.png",
"mengsan/assets/relics/booming_conch.png",
"mengsan/assets/relics/bound_phylactery.png",
"mengsan/assets/relics/bowler_hat.png",
"mengsan/assets/relics/bread.png",
"mengsan/assets/relics/brilliant_scarf.png",
"mengsan/assets/relics/brimstone.png",
"mengsan/assets/relics/bronze_scales.png",
"mengsan/assets/relics/burning_blood.png",
"mengsan/assets/relics/burning_sticks.png",
"mengsan/assets/relics/byrdpip.png",
"mengsan/assets/relics/calling_bell.png",
"mengsan/assets/relics/candelabra.png",
"mengsan/assets/relics/captains_wheel.png",
"mengsan/assets/relics/cauldron.png",
"mengsan/assets/relics/centennial_puzzle.png",
"mengsan/assets/relics/chandelier.png",
"mengsan/assets/relics/charons_ashes.png",
"mengsan/assets/relics/chemical_x.png",
"mengsan/assets/relics/choices_paradox.png",
"mengsan/assets/relics/chosen_cheese.png",
"mengsan/assets/relics/circlet.png",
"mengsan/assets/relics/claws.png",
"mengsan/assets/relics/cloak_clasp.png",
"mengsan/assets/relics/cracked_core.png",
"mengsan/assets/relics/crossbow.png",
"mengsan/assets/relics/cursed_pearl.png",
"mengsan/assets/relics/darkstone_periapt.png",
"mengsan/assets/relics/data_disk.png",
"mengsan/assets/relics/daughter_of_the_wind.png",
"mengsan/assets/relics/delicate_frond.png",
"mengsan/assets/relics/demon_tongue.png",
"mengsan/assets/relics/diamond_diadem.png",
"mengsan/assets/relics/dingy_rug.png",
"mengsan/assets/relics/distinguished_cape.png",
"mengsan/assets/relics/divine_destiny.png",
"mengsan/assets/relics/divine_right.png",
"mengsan/assets/relics/dollys_mirror.png",
"mengsan/assets/relics/dowsing_rod.png",
"mengsan/assets/relics/dragon_fruit.png",
"mengsan/assets/relics/dream_catcher.png",
"mengsan/assets/relics/driftwood.png",
"mengsan/assets/relics/dusty_tome.png",
"mengsan/assets/relics/ectoplasm.png",
"mengsan/assets/relics/electric_shrymp.png",
"mengsan/assets/relics/ember_tea.png",
"mengsan/assets/relics/emotion_chip.png",
"mengsan/assets/relics/empty_cage.png",
"mengsan/assets/relics/eternal_feather.png",
"mengsan/assets/relics/fake_anchor.png",
"mengsan/assets/relics/fake_blood_vial.png",
"mengsan/assets/relics/fake_happy_flower.png",
"mengsan/assets/relics/fake_lees_waffle.png",
"mengsan/assets/relics/fake_mango.png",
"mengsan/assets/relics/fake_merchants_rug.png",
"mengsan/assets/relics/fake_orichalcum.png",
"mengsan/assets/relics/fake_snecko_eye.png",
"mengsan/assets/relics/fake_strike_dummy.png",
"mengsan/assets/relics/fake_venerable_tea_set.png",
"mengsan/assets/relics/fencing_manual.png",
"mengsan/assets/relics/festive_popper.png",
"mengsan/assets/relics/fiddle.png",
"mengsan/assets/relics/fishing_rod.png",
"mengsan/assets/relics/forgotten_soul.png",
"mengsan/assets/relics/fragrant_mushroom.png",
"mengsan/assets/relics/fresnel_lens.png",
"mengsan/assets/relics/frozen_egg.png",
"mengsan/assets/relics/funerary_mask.png",
"mengsan/assets/relics/fur_coat.png",
"mengsan/assets/relics/galactic_dust.png",
"mengsan/assets/relics/gambling_chip.png",
"mengsan/assets/relics/game_piece.png",
"mengsan/assets/relics/ghost_seed.png",
"mengsan/assets/relics/girya.png",
"mengsan/assets/relics/glass_eye.png",
"mengsan/assets/relics/glitter.png",
"mengsan/assets/relics/gnarled_hammer.png",
"mengsan/assets/relics/gold_plated_cables.png",
"mengsan/assets/relics/golden_compass.png",
"mengsan/assets/relics/golden_pearl.png",
"mengsan/assets/relics/gorget.png",
"mengsan/assets/relics/gremlin_horn.png",
"mengsan/assets/relics/hand_drill.png",
"mengsan/assets/relics/happy_flower.png",
"mengsan/assets/relics/hefty_tablet.png",
"mengsan/assets/relics/helical_dart.png",
"mengsan/assets/relics/history_course.png",
"mengsan/assets/relics/horn_cleat.png",
"mengsan/assets/relics/ice_cream.png",
"mengsan/assets/relics/infused_core.png",
"mengsan/assets/relics/intimidating_helmet.png",
"mengsan/assets/relics/iron_club.png",
"mengsan/assets/relics/ivory_tile.png",
"mengsan/assets/relics/jeweled_mask.png",
"mengsan/assets/relics/jewelry_box.png",
"mengsan/assets/relics/joss_paper.png",
"mengsan/assets/relics/juzu_bracelet.png",
"mengsan/assets/relics/kaleidoscope.png",
"mengsan/assets/relics/kifuda.png",
"mengsan/assets/relics/kunai.png",
"mengsan/assets/relics/kusarigama.png",
"mengsan/assets/relics/lantern.png",
"mengsan/assets/relics/large_capsule.png",
"mengsan/assets/relics/lasting_candy.png",
"mengsan/assets/relics/lava_lamp.png",
"mengsan/assets/relics/lava_rock.png",
"mengsan/assets/relics/lead_paperweight.png",
"mengsan/assets/relics/leafy_poultice.png",
"mengsan/assets/relics/lees_waffle.png",
"mengsan/assets/relics/letter_opener.png",
"mengsan/assets/relics/lizard_tail.png",
"mengsan/assets/relics/looming_fruit.png",
"mengsan/assets/relics/lords_parasol.png",
"mengsan/assets/relics/lost_coffer.png",
"mengsan/assets/relics/lost_wisp.png",
"mengsan/assets/relics/lucky_fysh.png",
"mengsan/assets/relics/lunar_pastry.png",
"mengsan/assets/relics/mango.png",
"mengsan/assets/relics/manifest.json",
"mengsan/assets/relics/massive_scroll.png",
"mengsan/assets/relics/maw_bank.png",
"mengsan/assets/relics/meal_ticket.png",
"mengsan/assets/relics/meat_cleaver.png",
"mengsan/assets/relics/meat_on_the_bone.png",
"mengsan/assets/relics/membership_card.png",
"mengsan/assets/relics/mercury_hourglass.png",
"mengsan/assets/relics/metronome.png",
"mengsan/assets/relics/mini_regent.png",
"mengsan/assets/relics/miniature_cannon.png",
"mengsan/assets/relics/miniature_tent.png",
"mengsan/assets/relics/molten_egg.png",
"mengsan/assets/relics/mr_struggles.png",
"mengsan/assets/relics/mummified_hand.png",
"mengsan/assets/relics/music_box.png",
"mengsan/assets/relics/mystic_lighter.png",
"mengsan/assets/relics/neows_bones.png",
"mengsan/assets/relics/neows_sacrifice.png",
"mengsan/assets/relics/neows_talisman.png",
"mengsan/assets/relics/neows_torment.png",
"mengsan/assets/relics/new_leaf.png",
"mengsan/assets/relics/ninja_scroll.png",
"mengsan/assets/relics/nunchaku.png",
"mengsan/assets/relics/nutritious_oyster.png",
"mengsan/assets/relics/nutritious_soup.png",
"mengsan/assets/relics/oddly_smooth_stone.png",
"mengsan/assets/relics/old_coin.png",
"mengsan/assets/relics/orange_dough.png",
"mengsan/assets/relics/orichalcum.png",
"mengsan/assets/relics/ornamental_fan.png",
"mengsan/assets/relics/orrery.png",
"mengsan/assets/relics/paels_blood.png",
"mengsan/assets/relics/paels_claw.png",
"mengsan/assets/relics/paels_eye.png",
"mengsan/assets/relics/paels_flesh.png",
"mengsan/assets/relics/paels_growth.png",
"mengsan/assets/relics/paels_horn.png",
"mengsan/assets/relics/paels_legion.png",
"mengsan/assets/relics/paels_tears.png",
"mengsan/assets/relics/paels_tooth.png",
"mengsan/assets/relics/paels_wing.png",
"mengsan/assets/relics/pandoras_box.png",
"mengsan/assets/relics/pantograph.png",
"mengsan/assets/relics/paper_krane.png",
"mengsan/assets/relics/paper_phrog.png",
"mengsan/assets/relics/parrying_shield.png",
"mengsan/assets/relics/pear.png",
"mengsan/assets/relics/pen_nib.png",
"mengsan/assets/relics/pendulum.png",
"mengsan/assets/relics/permafrost.png",
"mengsan/assets/relics/petrified_toad.png",
"mengsan/assets/relics/phial_holster.png",
"mengsan/assets/relics/philosophers_stone.png",
"mengsan/assets/relics/phylactery_unbound.png",
"mengsan/assets/relics/planisphere.png",
"mengsan/assets/relics/pocketwatch.png",
"mengsan/assets/relics/pollinous_core.png",
"mengsan/assets/relics/pomander.png",
"mengsan/assets/relics/potion_belt.png",
"mengsan/assets/relics/power_cell.png",
"mengsan/assets/relics/prayer_wheel.png",
"mengsan/assets/relics/precarious_shears.png",
"mengsan/assets/relics/precise_scissors.png",
"mengsan/assets/relics/preserved_fog.png",
"mengsan/assets/relics/prismatic_gem.png",
"mengsan/assets/relics/pumpkin_candle.png",
"mengsan/assets/relics/punch_dagger.png",
"mengsan/assets/relics/radiant_pearl.png",
"mengsan/assets/relics/rainbow_ring.png",
"mengsan/assets/relics/razor_tooth.png",
"mengsan/assets/relics/red_mask.png",
"mengsan/assets/relics/red_skull.png",
"mengsan/assets/relics/regal_pillow.png",
"mengsan/assets/relics/regalite.png",
"mengsan/assets/relics/reptile_trinket.png",
"mengsan/assets/relics/ring_of_the_drake.png",
"mengsan/assets/relics/ring_of_the_snake.png",
"mengsan/assets/relics/ringing_triangle.png",
"mengsan/assets/relics/ripple_basin.png",
"mengsan/assets/relics/royal_poison.png",
"mengsan/assets/relics/royal_stamp.png",
"mengsan/assets/relics/ruined_helmet.png",
"mengsan/assets/relics/runic_capacitor.png",
"mengsan/assets/relics/runic_pyramid.png",
"mengsan/assets/relics/sai.png",
"mengsan/assets/relics/sand_castle.png",
"mengsan/assets/relics/screaming_flagon.png",
"mengsan/assets/relics/scroll_boxes.png",
"mengsan/assets/relics/sea_glass.png",
"mengsan/assets/relics/seal_of_gold.png",
"mengsan/assets/relics/self_forming_clay.png",
"mengsan/assets/relics/sere_talon.png",
"mengsan/assets/relics/shovel.png",
"mengsan/assets/relics/shuriken.png",
"mengsan/assets/relics/signet_ring.png",
"mengsan/assets/relics/silken_tress.png",
"mengsan/assets/relics/silver_crucible.png",
"mengsan/assets/relics/sling_of_courage.png",
"mengsan/assets/relics/small_capsule.png",
"mengsan/assets/relics/snecko_eye.png",
"mengsan/assets/relics/snecko_skull.png",
"mengsan/assets/relics/sozu.png",
"mengsan/assets/relics/sparkling_rouge.png",
"mengsan/assets/relics/spiked_gauntlets.png",
"mengsan/assets/relics/stone_calendar.png",
"mengsan/assets/relics/stone_cracker.png",
"mengsan/assets/relics/stone_humidifier.png",
"mengsan/assets/relics/storybook.png",
"mengsan/assets/relics/strawberry.png",
"mengsan/assets/relics/strike_dummy.png",
"mengsan/assets/relics/sturdy_clamp.png",
"mengsan/assets/relics/sword_of_jade.png",
"mengsan/assets/relics/sword_of_stone.png",
"mengsan/assets/relics/symbiotic_virus.png",
"mengsan/assets/relics/tanxs_whistle.png",
"mengsan/assets/relics/tea_of_discourtesy.png",
"mengsan/assets/relics/the_abacus.png",
"mengsan/assets/relics/the_boot.png",
"mengsan/assets/relics/the_courier.png",
"mengsan/assets/relics/throwing_axe.png",
"mengsan/assets/relics/tingsha.png",
"mengsan/assets/relics/tiny_mailbox.png",
"mengsan/assets/relics/toasty_mittens.png",
"mengsan/assets/relics/toolbox.png",
"mengsan/assets/relics/touch_of_orobas.png",
"mengsan/assets/relics/tough_bandages.png",
"mengsan/assets/relics/toxic_egg.png",
"mengsan/assets/relics/toy_box.png",
"mengsan/assets/relics/tri_boomerang.png",
"mengsan/assets/relics/tungsten_rod.png",
"mengsan/assets/relics/tuning_fork.png",
"mengsan/assets/relics/twisted_funnel.png",
"mengsan/assets/relics/unceasing_top.png",
"mengsan/assets/relics/undying_sigil.png",
"mengsan/assets/relics/unsettling_lamp.png",
"mengsan/assets/relics/vajra.png",
"mengsan/assets/relics/vambrace.png",
"mengsan/assets/relics/velvet_choker.png",
"mengsan/assets/relics/venerable_tea_set.png",
"mengsan/assets/relics/very_hot_cocoa.png",
"mengsan/assets/relics/vexing_puzzlebox.png",
"mengsan/assets/relics/vitruvian_minion.png",
"mengsan/assets/relics/war_hammer.png",
"mengsan/assets/relics/war_paint.png",
"mengsan/assets/relics/whetstone.png",
"mengsan/assets/relics/whispering_earring.png",
"mengsan/assets/relics/white_beast_statue.png",
"mengsan/assets/relics/white_star.png",
"mengsan/assets/relics/wing_charm.png",
"mengsan/assets/relics/winged_boots.png",
"mengsan/assets/relics/wongo_customer_appreciation_badge.png",
"mengsan/assets/relics/wongos_mystery_ticket.png",
"mengsan/assets/relics/yummy_cookie.png",
"mengsan/assets/sushuang-zhangshiping.png",
"mengsan/battle/battle-director.js",
"mengsan/battle/battle-flow.js",
@@ -608,7 +309,6 @@
"mengsan/battle/battle-resources.js",
"mengsan/battle/battle-session.js",
"mengsan/battle/battle-settlement.js",
"mengsan/battle/camps.js",
"mengsan/battle/combat-rules.js",
"mengsan/battle/death-blow.js",
"mengsan/battle/engine-session.js",
@@ -752,14 +452,9 @@
"mengsan/progression/story.js",
"mengsan/register.js",
"mengsan/relics/battle.js",
"mengsan/relics/catalog.js",
"mengsan/relics/combat.js",
"mengsan/relics/definitions.js",
"mengsan/relics/hooks.js",
"mengsan/relics/progression.js",
"mengsan/relics/random.js",
"mengsan/relics/rooms.js",
"mengsan/relics/rules.js",
"mengsan/runtime/battle-director.js",
"mengsan/runtime/battle-flow.js",
"mengsan/runtime/battle-loop.js",
@@ -803,7 +498,6 @@
"mengsan/ui/map/index.js",
"mengsan/ui/map/renderer.js",
"mengsan/ui/navigation.js",
"mengsan/ui/relic-icon.js",
"mengsan/ui/relics.js",
"mengsan/ui/reward-package.js",
"mengsan/ui/reward-ui.js",
-103
View File
@@ -1096,109 +1096,6 @@
}
}
/* 更新设置:沿用选择窗口材质,两个选项与取消/确认按钮适配窄屏。 */
.shuYing-option-dialog-mask[data-variant="settings"] {
--setting-muted: #d1c9bc;
--setting-focus: #ffe2a0;
padding: max(12px, env(safe-area-inset-top)) max(12px, env(safe-area-inset-right))
max(12px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left)) !important;
}
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog {
width: min(600px, 100%) !important;
max-height: calc(100vh - 32px) !important;
max-height: calc(100dvh - max(12px, env(safe-area-inset-top)) - max(12px, env(safe-area-inset-bottom))) !important;
}
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-title,
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-intro,
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-controls {
flex: none !important;
}
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-intro,
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-tag,
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-description {
color: var(--setting-muted) !important;
font-size: 14px !important;
}
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-options {
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
min-height: 0 !important;
gap: 12px !important;
padding: 18px !important;
overflow-x: hidden !important;
}
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-card {
min-height: 170px !important;
padding: 38px 18px 18px !important;
transform: none !important;
touch-action: manipulation;
}
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-order {
display: none !important;
}
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-name {
margin-top: 0 !important;
font-size: 25px !important;
}
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-description {
margin-top: 12px !important;
}
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-controls {
gap: 12px !important;
padding: 0 18px 18px !important;
}
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-confirm {
flex: 1 !important;
min-width: 0 !important;
min-height: 48px !important;
height: auto !important;
padding: 10px 8px !important;
font-size: 16px !important;
line-height: 1.4 !important;
touch-action: manipulation;
}
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-cancel {
background: rgba(255, 255, 255, 0.04) !important;
border-color: rgba(232, 190, 99, 0.4) !important;
color: var(--setting-muted) !important;
}
.shuYing-option-dialog-mask[data-variant="settings"] button:focus-visible {
outline: 2px solid var(--setting-focus) !important;
outline-offset: 2px !important;
}
.shuYing-option-dialog-mask[data-variant="settings"] button:active:not(:disabled) {
opacity: 0.85 !important;
}
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-confirm:disabled {
opacity: 0.5 !important;
cursor: default !important;
box-shadow: none !important;
}
@media screen and (max-width: 520px) {
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-options {
grid-template-columns: minmax(0, 1fr) !important;
padding: 12px !important;
}
.shuYing-option-dialog-mask[data-variant="settings"] .shuYing-option-dialog-card {
min-height: 140px !important;
}
}
/* 天书对局羁绊入口与概览:不依赖十周年 UI 节点,原版和扩展 UI 共用。 */
.shuYing-bond-overview-button {
all: unset !important;
+3 -11
View File
@@ -2,11 +2,10 @@ import { lib, game, ui, get, ai, _status } from "../../noname.js";
import updater from "./update.js"
import func from "./function.js"
import initShuYingMenu from "./menu.js";
import { configureDiagnostics } from "./diagnostics/index.js";
export const type = "extension";
export let shuYing = new Object();
const shuYingLocalVersion = "2.0.2.3-rc.3";
const shuYingLocalVersion = "2.0.2.3";
shuYing.name = "术樱包";
shuYing.editable = false;
@@ -17,11 +16,6 @@ shuYing.text = {}; //中上方文本
shuYing.text_style = {}; //中上方文本样式
shuYing.moduleCache = {}; //动态加载模块缓存,避免同一文件重复import
shuYing.voices = {}; //语音台词表,voices.js缺失时保持为空
shuYing.diagnostics = configureDiagnostics({
lib, game, window, document, version: shuYingLocalVersion,
extensionRoot: shuYing.url,
});
const diagnostics = shuYing.diagnostics.scope("extension");
//按需加载扩展内部模块。optional为true时,文件缺失只关闭对应功能,不中断整个扩展入口。
shuYing.loadModule = async (path, label, optional = true) => {
@@ -30,7 +24,6 @@ shuYing.loadModule = async (path, label, optional = true) => {
.then(module => module.default ?? module)
.catch(error => {
const message = `术樱包:${label || path}加载失败`;
diagnostics.error("module.load.failed", error, { path, label, optional });
if (!optional) throw error;
console.warn(message, error);
return null;
@@ -132,7 +125,6 @@ shuYing.getVersion = (callback) => {
.catch(error => {
if (requestId != shuYing.versionRequestId) return null;
console.error("术樱包获取在线版本失败:", error);
diagnostics.error("version.lookup.failed", error);
shuYing.updateOnlineVersionMenu(null, "获取失败");
return null;
});
@@ -253,7 +245,7 @@ shuYing.initCharacter = async () => {
}
//初始化游戏时
shuYing.precontent = () => diagnostics.step("precontent", async () => {
shuYing.precontent = async () => {
await shuYing.loadVoices();
shuYing.initCSS();
shuYing.initFunction();
@@ -281,7 +273,7 @@ shuYing.precontent = () => diagnostics.step("precontent", async () => {
initShuYingMenu({ lib, game, ui, shuYing, updater });
shuYing.getVersion();
await shuYing.initCharacter();
});
};
shuYing.config = () => {
+4 -44
View File
@@ -225,8 +225,7 @@ export default function initFunction(lib, game, ui, get, ai, _status, shuYing =
};
// 创建可复用的对局单选界面;单击选中卡片,确认后返回对应键值。
shuYing.chooseSingleOptionDialog = ({ title = "请选择", intro = "", options = [], defaultKey = null,
currentKey = null, cancelable = false, variant = "default", confirmText = "确定" } = {}) => {
shuYing.chooseSingleOptionDialog = ({ title = "请选择", intro = "", options = [], defaultKey = null } = {}) => {
return new Promise(resolve => {
const items = options.map(option => typeof option == "string" ? { key: option, name: option } : option)
.filter(option => option?.key);
@@ -241,26 +240,18 @@ export default function initFunction(lib, game, ui, get, ai, _status, shuYing =
if (parent) parent.appendChild(node);
return node;
};
const previousFocus = document.activeElement;
const mask = createNode("div", "shuYing-option-dialog-mask", null, document.body);
mask.dataset.variant = variant;
const dialog = createNode("div", "shuYing-option-dialog", null, mask);
dialog.setAttribute("role", "dialog");
dialog.setAttribute("aria-modal", "true");
dialog.setAttribute("aria-label", title);
createNode("div", "shuYing-option-dialog-title", title, dialog);
if (intro) createNode("div", "shuYing-option-dialog-intro", intro, dialog);
const optionArea = createNode("div", "shuYing-option-dialog-options", null, dialog);
const controls = createNode("div", "shuYing-option-dialog-controls", null, dialog);
let closed = false;
let onKeydown = null;
let selectedKey = defaultKey && items.some(item => item.key == defaultKey) ? defaultKey : items[0].key;
const finish = key => {
if (closed) return;
closed = true;
if (onKeydown) document.removeEventListener("keydown", onKeydown, true);
mask.remove();
if (cancelable && previousFocus?.isConnected) previousFocus.focus?.({ preventScroll: true });
resolve(key);
};
const refreshSelection = () => {
@@ -269,7 +260,6 @@ export default function initFunction(lib, game, ui, get, ai, _status, shuYing =
card.classList.toggle("selected", selected);
card.setAttribute("aria-pressed", String(selected));
});
confirm.disabled = currentKey != null && selectedKey == currentKey;
};
items.forEach((option, index) => {
const card = createNode("button", "shuYing-option-dialog-card", null, optionArea);
@@ -286,44 +276,14 @@ export default function initFunction(lib, game, ui, get, ai, _status, shuYing =
refreshSelection();
});
});
if (cancelable) {
const cancel = createNode("button", "shuYing-option-dialog-confirm shuYing-option-dialog-cancel", "取消", controls);
cancel.type = "button";
cancel.addEventListener("click", event => { event.stopPropagation(); finish(null); });
}
const confirm = createNode("button", "shuYing-option-dialog-confirm", confirmText, controls);
const confirm = createNode("button", "shuYing-option-dialog-confirm", "确定", controls);
confirm.type = "button";
confirm.addEventListener("click", event => {
event.stopPropagation();
if (!confirm.disabled) finish(selectedKey);
});
mask.addEventListener("click", event => {
event.stopPropagation();
if (cancelable && event.target == mask) finish(null);
finish(selectedKey);
});
mask.addEventListener("click", event => event.stopPropagation());
dialog.addEventListener("click", event => event.stopPropagation());
if (cancelable) {
dialog.addEventListener("keydown", event => event.stopPropagation());
onKeydown = event => {
if (event.key == "Escape") {
event.preventDefault();
event.stopPropagation();
finish(null);
}
else if (event.key == "Tab") {
const buttons = Array.from(dialog.querySelectorAll("button:not(:disabled)"));
const first = buttons[0], last = buttons[buttons.length - 1];
if (!dialog.contains(document.activeElement)
|| event.shiftKey && document.activeElement == first
|| !event.shiftKey && document.activeElement == last) {
event.preventDefault();
(event.shiftKey ? last : first)?.focus();
}
event.stopPropagation();
}
};
document.addEventListener("keydown", onKeydown, true);
}
refreshSelection();
const initial = Array.from(optionArea.querySelectorAll(".shuYing-option-dialog-card")).find(node => node.dataset.key == selectedKey)
|| optionArea.querySelector(".shuYing-option-dialog-card");
-16
View File
@@ -2,22 +2,6 @@
`register.js` 是扩展加载入口,`mode.js` 负责组装模式、注册技能并连接各领域模块,`config.js` 提供模式配置。发布器和外部扩展应继续从 `register.js` 加载,避免依赖内部文件路径。
梦三独立组装模式,不加载或继承身份模式。敌我关系统一由
`battle/camps.js` 读取角色的 `storage.mengsanCamp_shuying`:主角、支援和
羁绊助战为 `ally`,怪物及敌方援军为 `enemy`。`side` 与梦三专属的
`identity` 标记只用于核心及界面兼容,不赋予主公、忠臣或反贼规则。
技能应使用 `isFriendOf` / `isEnemyOf`、`getFriends` / `getEnemies` 或
`get.attitude` 判断敌我。基础态度为同阵营及自身 `+8`、对立阵营 `-8`,
未设置有效阵营的其他对象为 `0`;剧情切换阵营后按新阵营判断。
`get.effect`、`get.effect_use`、`get.damageEffect`、`get.recoverEffect` 和
牌价值、技能效果修正继续使用核心算法,并通过上述态度计算行动收益。
有益效果不一律视为正收益,伤害也不一律视为负收益,仍遵循技能自己的 AI 配置。
梦三不启用主公技,初始技能及动态授予技能均按此限制处理。阵营始终公开,
出牌或技能的 `logAi` / `addExpose` 调用不推断隐藏身份;击杀不附加身份奖惩,
胜负与奖励沿用梦三关卡和结算流程。
| 目录 | 职责 |
| --- | --- |
| `battle/` | 战斗会话、回合、结算、费用与通用战斗效果 |
+1 -1
View File
@@ -9,7 +9,7 @@
- 批量优化只处理 `mengsan_*.png` 运行时卡牌,参考原件如 `wound-original.png` 不参与。
- 画风:泛黄宣纸、黑褐水墨、飞白与淡赭色、中国古代人物或兵器;根据卡牌效果独立安排竖幅构图。拆包图只作题材参考,不修改拆包原件。
- 立绘不绘制名称、费用、花色、强化角标或边框。左上花色点数、左侧中段费用、右下名称由游戏原生节点和梦三样式叠加;左侧信息栏和右下名称需留空间。
- 卡牌定义使用 `fullimage: true` 与明确的 `image` 路径,不使用横图 `fullskin`。手牌叠层按实际注册定义的 `fullimage: true` 及 `ext:术樱包/mengsan/assets/cards/牌名.png` 自动设置 `data-mengsan-local-art="full"`;四字及以上的翻译名称自动标记为长名,常规和窄屏样式统一,不再维护逐牌 CSS 名单。保持 `cover`,不使用 `100% 100%` 拉伸。
- 卡牌定义使用 `fullimage: true` 与明确的 `image` 路径,不使用横图 `fullskin`。`mengsan_ic_`、`mengsan_colorless_`、`mengsan_event_`、`mengsan_status_`、`mengsan_curse_` 已按前缀接入 `ui/hand/style.css` 的本地完整卡面选择器;其他命名的新增卡牌须单独接入,长牌名同时加入长名称窄屏规则。保持 `cover`,不使用 `100% 100%` 拉伸。
- 费用和强化角标继续使用现有 `data-*` 与伪元素,避免向卡牌子节点写入额外文字而触发十周年 UI 扫描。
- 不改变卡牌费用、效果、强化、奖励与存档数据;不修改全局标准/军争牌或十周年 UI。
- 接入后检查完整立绘、花色/费用/名称、扇形叠牌、悬停放大和窄屏。文件检查、隔离行为测试及预览不代表实机验收。
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Some files were not shown because too many files have changed in this diff Show More