mirror of
https://github.com/Shu-Ying/noname-shuying.git
synced 2026-11-02 18:50:03 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
552576b222 |
Vendored
+18
-18
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"version": "2.0.2.3",
|
"version": "2.0.2.1",
|
||||||
"generated_at": "2026-10-05T07:29:31.854561+00:00",
|
"generated_at": "2026-10-04T20:26:18.324852+00:00",
|
||||||
"algorithm": "sha256",
|
"algorithm": "sha256",
|
||||||
"update_channels": true,
|
"update_channels": true,
|
||||||
"file_count": 278,
|
"file_count": 278,
|
||||||
"size_bytes": 55486956,
|
"size_bytes": 55472293,
|
||||||
"folder_sha256": "78276895cfb9deee4864dbad40b96dfb40155424f9c981895c69474ecbada478",
|
"folder_sha256": "874bf6414e4d4f761bc9ccb25a6a8f46e2efdf35609887ea856b9b68b3c8c9a3",
|
||||||
"core": [
|
"core": [
|
||||||
"extension.js",
|
"extension.js",
|
||||||
"menu.js",
|
"menu.js",
|
||||||
@@ -401,15 +401,15 @@
|
|||||||
},
|
},
|
||||||
"extension.js": {
|
"extension.js": {
|
||||||
"size": 15459,
|
"size": 15459,
|
||||||
"sha256": "353d4415f1da0faf57f6d8f05f476bf2946f8d309fd3f89240b24acbfcde6d02"
|
"sha256": "9656c118dfc744e89fef826d75fc1d58515507021ad1e710d7fd6d479cc631ee"
|
||||||
},
|
},
|
||||||
"function.js": {
|
"function.js": {
|
||||||
"size": 33746,
|
"size": 33746,
|
||||||
"sha256": "df1aa3102a214986723af365da1029485680519d355a1ff88973eb36f8a6f909"
|
"sha256": "df1aa3102a214986723af365da1029485680519d355a1ff88973eb36f8a6f909"
|
||||||
},
|
},
|
||||||
"menu.js": {
|
"menu.js": {
|
||||||
"size": 24539,
|
"size": 24566,
|
||||||
"sha256": "30fe584d24cc583c2ac982d5e1177cf5ce698ed09252d9027b70569b912195f3"
|
"sha256": "d6f1fb89707de3558ca9a00814a1d79f12f5799cd85328e764e02a66e4ffba5e"
|
||||||
},
|
},
|
||||||
"pve/audio/dead/十二生肖/chenlong.mp3": {
|
"pve/audio/dead/十二生肖/chenlong.mp3": {
|
||||||
"size": 68757,
|
"size": 68757,
|
||||||
@@ -1300,12 +1300,12 @@
|
|||||||
"sha256": "41914d7d542ddfb93c89aefffc245869d63a9fb2f52293df8e6667009832afd8"
|
"sha256": "41914d7d542ddfb93c89aefffc245869d63a9fb2f52293df8e6667009832afd8"
|
||||||
},
|
},
|
||||||
"update.js": {
|
"update.js": {
|
||||||
"size": 73811,
|
"size": 60005,
|
||||||
"sha256": "8369d59752f9eb7109f248d60b25601d0a489df891c8e31af2dfe0478b67fdad"
|
"sha256": "f809d6e3ea5114f78a0773314c23b4120ccbd763b1f2f0f1f400753287af76f7"
|
||||||
},
|
},
|
||||||
"update.txt": {
|
"update.txt": {
|
||||||
"size": 4787,
|
"size": 3903,
|
||||||
"sha256": "cba4f2eb6c04eee700423c0336311a86019666232763100d0a33cca5fe09c824"
|
"sha256": "8ef4a71189bdfc4068e0ebab27dad13f7551354f72a0c6ed51d77b62c8c06fb2"
|
||||||
},
|
},
|
||||||
"voices.js": {
|
"voices.js": {
|
||||||
"size": 1711,
|
"size": 1711,
|
||||||
@@ -1571,7 +1571,7 @@
|
|||||||
"extension.js": {
|
"extension.js": {
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"size": 15459,
|
"size": 15459,
|
||||||
"sha256": "353d4415f1da0faf57f6d8f05f476bf2946f8d309fd3f89240b24acbfcde6d02"
|
"sha256": "9656c118dfc744e89fef826d75fc1d58515507021ad1e710d7fd6d479cc631ee"
|
||||||
},
|
},
|
||||||
"function.js": {
|
"function.js": {
|
||||||
"type": "file",
|
"type": "file",
|
||||||
@@ -1580,8 +1580,8 @@
|
|||||||
},
|
},
|
||||||
"menu.js": {
|
"menu.js": {
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"size": 24539,
|
"size": 24566,
|
||||||
"sha256": "30fe584d24cc583c2ac982d5e1177cf5ce698ed09252d9027b70569b912195f3"
|
"sha256": "d6f1fb89707de3558ca9a00814a1d79f12f5799cd85328e764e02a66e4ffba5e"
|
||||||
},
|
},
|
||||||
"pve": {
|
"pve": {
|
||||||
"type": "directory",
|
"type": "directory",
|
||||||
@@ -2825,13 +2825,13 @@
|
|||||||
},
|
},
|
||||||
"update.js": {
|
"update.js": {
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"size": 73811,
|
"size": 60005,
|
||||||
"sha256": "8369d59752f9eb7109f248d60b25601d0a489df891c8e31af2dfe0478b67fdad"
|
"sha256": "f809d6e3ea5114f78a0773314c23b4120ccbd763b1f2f0f1f400753287af76f7"
|
||||||
},
|
},
|
||||||
"update.txt": {
|
"update.txt": {
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"size": 4787,
|
"size": 3903,
|
||||||
"sha256": "cba4f2eb6c04eee700423c0336311a86019666232763100d0a33cca5fe09c824"
|
"sha256": "8ef4a71189bdfc4068e0ebab27dad13f7551354f72a0c6ed51d77b62c8c06fb2"
|
||||||
},
|
},
|
||||||
"voices.js": {
|
"voices.js": {
|
||||||
"type": "file",
|
"type": "file",
|
||||||
|
|||||||
Vendored
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "2.0.2.3",
|
"version": "2.0.2.1",
|
||||||
"generated_at": "2026-10-05T07:29:31.855773+00:00",
|
"generated_at": "2026-10-04T20:26:18.326606+00:00",
|
||||||
"algorithm": "sha256",
|
"algorithm": "sha256",
|
||||||
"update_channels": true,
|
"update_channels": true,
|
||||||
"file_count": 457,
|
"file_count": 457,
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "2.0.2.3",
|
"version": "2.0.2.1",
|
||||||
"module_id": "mengsan",
|
"module_id": "mengsan",
|
||||||
"snapshot": "legacy",
|
"snapshot": "legacy",
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ import initShuYingMenu from "./menu.js";
|
|||||||
|
|
||||||
export const type = "extension";
|
export const type = "extension";
|
||||||
export let shuYing = new Object();
|
export let shuYing = new Object();
|
||||||
const shuYingLocalVersion = "2.0.2.3";
|
const shuYingLocalVersion = "2.0.2.1";
|
||||||
|
|
||||||
shuYing.name = "术樱包";
|
shuYing.name = "术樱包";
|
||||||
shuYing.editable = false;
|
shuYing.editable = false;
|
||||||
|
|||||||
@@ -273,25 +273,21 @@ export default function initShuYingMenu({ lib, game, ui, shuYing, updater }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const logs = [
|
const logs = [
|
||||||
"-----< 2.0.2.3 改动 >-----",
|
"-----< 2.0.1.1 改动 >-----",
|
||||||
"新增正式版、测试版更新通道切换。",
|
"新增“旧轮次”选项,可在过关后按照旧规则将行动权交给玩家方1号位上家的Boss",
|
||||||
"支持选择 Gitea 或 GitHub 下载源。",
|
"新增“羁绊技能倾向”选项,已激活羁绊的相关技能将按羁绊等级获得温和的出现概率提升",
|
||||||
"更新系统采用并发下载:安卓最多 4 个、PC 最多 8 个,大文件受内存限制。",
|
"羁绊技能倾向权重现可在天书配置中按等级独立调整",
|
||||||
"新增梦三模式安装入口",
|
"新增“虚拟偶像参与奖励”选项,开启后虚拟偶像可以选择过关技能并激活、升级羁绊效果",
|
||||||
"-----< 2.0.2.3 修复 >-----",
|
"-----< 2.0.1.1 修复 >-----",
|
||||||
"修复天书乱斗跨关后,按轮数冷却的技能未正确重置,以及旧关卡历史影响新关卡计数的问题。",
|
"修复多次触发青龙偃月刀追加【杀】并击杀Boss后,过关奖励结束时无法正确跳过当前角色剩余回合的问题",
|
||||||
"修复【清剿】旧关卡的延迟弃牌结算可能弃掉新关卡奖励的问题。",
|
"修复旧轮次在新Boss召唤前提前判断座位,导致行动权交接条件无法正常触发的问题",
|
||||||
"修复天书乱斗应战时的座位重叠、动态 Boss 控制切换、代理头像显示及胜负判断问题。",
|
"修复当前行动角色为虚拟偶像时,旧轮次无法将行动权交给玩家方1号位上家Boss的问题",
|
||||||
"修正“神武再世”三级羁绊的体力流失统计,按目标本回合累计流失的体力判定。",
|
"修复“南蛮入侵”和“群雄逐鹿”羁绊虽然对友方无效,但AI仍会将友方计入用牌收益而拒绝使用相关牌的问题",
|
||||||
"修复切换下载源后,旧版本查询结果覆盖新结果的问题。",
|
|
||||||
"核心文件修复增加大小和 SHA-256 校验。",
|
|
||||||
];
|
];
|
||||||
const keywordColors = {
|
const keywordColors = {
|
||||||
BOSS: "#ff6b6b",
|
BOSS: "#ff6b6b",
|
||||||
PVE: "#ff6b6b",
|
PVE: "#ff6b6b",
|
||||||
PVP: "#ff6b6b",
|
PVP: "#ff6b6b",
|
||||||
正式版: "#ff6b6b",
|
|
||||||
测试版: "#ff99cc",
|
|
||||||
天书乱斗: "#66ccff",
|
天书乱斗: "#66ccff",
|
||||||
羁绊: "#d77cff",
|
羁绊: "#d77cff",
|
||||||
动态难度: "#d77cff",
|
动态难度: "#d77cff",
|
||||||
|
|||||||
@@ -5,156 +5,6 @@ const githubRepositoryApi = "https://api.github.com/repos/Shu-Ying/noname-shuyin
|
|||||||
const githubRawBase = "https://raw.githubusercontent.com/Shu-Ying/noname-shuying";
|
const githubRawBase = "https://raw.githubusercontent.com/Shu-Ying/noname-shuying";
|
||||||
const coreTempDir = "extension/术樱包/.update_tmp";
|
const coreTempDir = "extension/术樱包/.update_tmp";
|
||||||
let fileMutationUncertain = false;
|
let fileMutationUncertain = false;
|
||||||
const verifiedCacheKey = "shuYing_update_verified_files";
|
|
||||||
let verifiedFiles = new Map();
|
|
||||||
let sessionVerified = new Set();
|
|
||||||
let verificationContext = null;
|
|
||||||
let verificationDirty = false;
|
|
||||||
let fullVerification = false;
|
|
||||||
|
|
||||||
function cacheContext() {
|
|
||||||
if (lib.node?.fs && typeof window.__dirname == "string") return `node:${window.__dirname}`;
|
|
||||||
if (hasCordovaFiles()) return `cordova:${localStorage.getItem("noname_inited")}`;
|
|
||||||
return null; // The browser file service exposes no reliable modification timestamp.
|
|
||||||
}
|
|
||||||
|
|
||||||
function beginUpdate(shuYing, full = false) {
|
|
||||||
fullVerification = full;
|
|
||||||
verificationContext = cacheContext();
|
|
||||||
verifiedFiles = new Map();
|
|
||||||
sessionVerified = new Set();
|
|
||||||
verificationDirty = false;
|
|
||||||
const saved = lib.config[verifiedCacheKey];
|
|
||||||
if (verificationContext && saved?.schema == 1 && saved.context == verificationContext
|
|
||||||
&& saved.files && typeof saved.files == "object" && !Array.isArray(saved.files)) {
|
|
||||||
for (const [file, entry] of Object.entries(saved.files)) {
|
|
||||||
if (isSafeManifestPath(file) && entry && typeof entry.sha256 == "string" && /^[a-f0-9]{64}$/.test(entry.sha256)
|
|
||||||
&& Number.isSafeInteger(entry.size) && entry.size >= 0 && typeof entry.fingerprint == "string") {
|
|
||||||
verifiedFiles.set(file, { ...entry });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
shuYing._updateLogs = [];
|
|
||||||
shuYing._updateLogDirty = false;
|
|
||||||
shuYing._updateLogDisabled = false;
|
|
||||||
shuYing._progressRendered = 0;
|
|
||||||
shuYing._updateStarted = Date.now();
|
|
||||||
shuYing._updateStats = { cached: 0, hashed: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
function relativeTarget(target) {
|
|
||||||
const prefix = "extension/术樱包/";
|
|
||||||
if (!target.startsWith(prefix)) return null;
|
|
||||||
const file = target.slice(prefix.length);
|
|
||||||
return isSafeManifestPath(file) && !file.startsWith(".update_tmp/") && file != "log.txt" ? file : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function invalidateVerifiedFile(target) {
|
|
||||||
const file = relativeTarget(target);
|
|
||||||
if (!file) return;
|
|
||||||
if (verifiedFiles.delete(file)) verificationDirty = true;
|
|
||||||
sessionVerified.delete(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function persistVerificationCache() {
|
|
||||||
if (!verificationContext || !verificationDirty) return;
|
|
||||||
await saveUpdaterConfig({ [verifiedCacheKey]: {
|
|
||||||
schema: 1, context: verificationContext, files: Object.fromEntries(verifiedFiles),
|
|
||||||
} });
|
|
||||||
verificationDirty = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function invalidateBeforeChanges(files) {
|
|
||||||
for (const file of files) invalidateVerifiedFile(`extension/术樱包/${file}`);
|
|
||||||
// Commit invalidation before mutation, including same-size writes on coarse timestamps.
|
|
||||||
await persistVerificationCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
function sameMetadata(left, right) {
|
|
||||||
return !!left && !!right && left.size == right.size && left.fingerprint == right.fingerprint;
|
|
||||||
}
|
|
||||||
|
|
||||||
function rememberVerified(target, info, metadata) {
|
|
||||||
const file = relativeTarget(target);
|
|
||||||
if (!verificationContext || !file || !metadata || metadata.size != info.size) return;
|
|
||||||
verifiedFiles.set(file, { sha256: info.sha256, size: info.size, fingerprint: metadata.fingerprint });
|
|
||||||
sessionVerified.add(file);
|
|
||||||
verificationDirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function verifyLocalFile(file, manifest, shuYing, full = fullVerification) {
|
|
||||||
const target = `extension/术樱包/${file}`;
|
|
||||||
const info = manifest.files[file];
|
|
||||||
try {
|
|
||||||
const before = await fileMetadata(target);
|
|
||||||
const cached = verifiedFiles.get(file);
|
|
||||||
if ((!full || sessionVerified.has(file)) && before && cached
|
|
||||||
&& cached.sha256 == info.sha256 && cached.size == info.size && sameMetadata(cached, before)) {
|
|
||||||
if (shuYing?._updateStats) shuYing._updateStats.cached++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
invalidateVerifiedFile(target);
|
|
||||||
await verifyFile(file, await readFile(target), manifest);
|
|
||||||
const after = await fileMetadata(target);
|
|
||||||
if (before && !sameMetadata(before, after)) throw new Error(`校验期间文件发生变化:${file}`);
|
|
||||||
rememberVerified(target, info, sameMetadata(before, after) ? after : null);
|
|
||||||
if (shuYing?._updateStats) shuYing._updateStats.hashed++;
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
invalidateVerifiedFile(target);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runFilePool(items, handler, options = {}) {
|
|
||||||
const concurrency = options.concurrency || (lib.device ? 4 : 8);
|
|
||||||
const budget = lib.device ? 24 * 1024 * 1024 : 64 * 1024 * 1024;
|
|
||||||
const controller = new AbortController();
|
|
||||||
const results = new Array(items.length);
|
|
||||||
let next = 0, active = 0, reserved = 0, failure = null;
|
|
||||||
return await new Promise((resolve, reject) => {
|
|
||||||
const fail = error => {
|
|
||||||
const cause = operationError(error) || new Error("资源任务失败");
|
|
||||||
if (!failure) { failure = cause; controller.abort(cause); }
|
|
||||||
if (cause.operationUncertain) failure.operationUncertain = true;
|
|
||||||
};
|
|
||||||
const pump = () => {
|
|
||||||
while (!failure && active < concurrency && next < items.length) {
|
|
||||||
// Bound estimated working memory; a file exceeding the budget runs alone.
|
|
||||||
let weight;
|
|
||||||
try { weight = Math.min(budget, Math.max(1, options.weight?.(items[next]) || 1)); }
|
|
||||||
catch (error) { fail(error); break; }
|
|
||||||
if (active && reserved + weight > budget) break;
|
|
||||||
const index = next++;
|
|
||||||
active++;
|
|
||||||
reserved += weight;
|
|
||||||
Promise.resolve().then(() => handler(items[index], index, controller.signal))
|
|
||||||
.then(value => { results[index] = value; })
|
|
||||||
.catch(fail).finally(() => { active--; reserved -= weight; pump(); });
|
|
||||||
}
|
|
||||||
if (!active && (failure || next == items.length)) {
|
|
||||||
if (failure) {
|
|
||||||
if (fileMutationUncertain) failure.operationUncertain = true;
|
|
||||||
reject(failure);
|
|
||||||
}
|
|
||||||
else resolve(results);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
pump();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function finishUpdate(shuYing) {
|
|
||||||
try {
|
|
||||||
await persistVerificationCache();
|
|
||||||
}
|
|
||||||
catch (error) { addLog(shuYing, `校验缓存保存失败,下次重新检查:${getErrorMessage(error)}`); }
|
|
||||||
if (shuYing._updateStarted) {
|
|
||||||
const stats = shuYing._updateStats;
|
|
||||||
addLog(shuYing, `本次耗时 ${((Date.now() - shuYing._updateStarted) / 1000).toFixed(1)} 秒;缓存通过 ${stats?.cached || 0} 次,完整校验通过 ${stats?.hashed || 0} 次`);
|
|
||||||
}
|
|
||||||
await flushLog(shuYing);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseVersion(version) {
|
function parseVersion(version) {
|
||||||
const match = /^(\d+)\.(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?$/.exec(version);
|
const match = /^(\d+)\.(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?$/.exec(version);
|
||||||
@@ -285,10 +135,10 @@ function getLogText(shuYing) {
|
|||||||
|
|
||||||
async function writeLog(shuYing) {
|
async function writeLog(shuYing) {
|
||||||
if (!shuYing || typeof game.writeFile != "function") return;
|
if (!shuYing || typeof game.writeFile != "function") return;
|
||||||
await writeFile(getLogText(shuYing), "extension/术樱包", "log.txt", { diagnostic: true });
|
await writeFile(getLogText(shuYing), "extension/术樱包", "log.txt");
|
||||||
}
|
}
|
||||||
|
|
||||||
function addLog(shuYing, message) {
|
async function addLog(shuYing, message) {
|
||||||
if (!shuYing) return;
|
if (!shuYing) return;
|
||||||
|
|
||||||
if (!Array.isArray(shuYing._updateLogs)) {
|
if (!Array.isArray(shuYing._updateLogs)) {
|
||||||
@@ -297,27 +147,13 @@ function addLog(shuYing, message) {
|
|||||||
|
|
||||||
const time = new Date().toLocaleString();
|
const time = new Date().toLocaleString();
|
||||||
shuYing._updateLogs.push(`[${time}] ${message}`);
|
shuYing._updateLogs.push(`[${time}] ${message}`);
|
||||||
shuYing._updateLogDirty = true;
|
|
||||||
if (!shuYing._updateLogTimer && !shuYing._updateLogDisabled) {
|
|
||||||
shuYing._updateLogTimer = setTimeout(() => { void flushLog(shuYing); }, 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function flushLog(shuYing) {
|
try {
|
||||||
clearTimeout(shuYing._updateLogTimer);
|
await writeLog(shuYing);
|
||||||
shuYing._updateLogTimer = null;
|
}
|
||||||
if (shuYing._updateLogWrite) await shuYing._updateLogWrite;
|
catch (error) {
|
||||||
if (!shuYing._updateLogDirty || shuYing._updateLogDisabled) return;
|
|
||||||
shuYing._updateLogDirty = false;
|
|
||||||
const operation = writeLog(shuYing).catch(error => {
|
|
||||||
shuYing._updateLogDisabled = true;
|
|
||||||
clearTimeout(shuYing._updateLogTimer);
|
|
||||||
shuYing._updateLogTimer = null;
|
|
||||||
console.error("写入术樱包日志失败:", error);
|
console.error("写入术樱包日志失败:", error);
|
||||||
});
|
}
|
||||||
shuYing._updateLogWrite = operation;
|
|
||||||
await operation;
|
|
||||||
if (shuYing._updateLogWrite == operation) shuYing._updateLogWrite = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHTML(text) {
|
function escapeHTML(text) {
|
||||||
@@ -339,26 +175,17 @@ function formatBytes(bytes) {
|
|||||||
function setProgress(shuYing, options = {}) {
|
function setProgress(shuYing, options = {}) {
|
||||||
if (!shuYing?.text) return;
|
if (!shuYing?.text) return;
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
if (!options.force && now - (shuYing._progressRendered || 0) < 120) return;
|
|
||||||
shuYing._progressRendered = now;
|
|
||||||
|
|
||||||
const current = Math.max(0, Number(options.current) || 0);
|
const current = Math.max(0, Number(options.current) || 0);
|
||||||
const total = Math.max(0, Number(options.total) || 0);
|
const total = Math.max(0, Number(options.total) || 0);
|
||||||
const percent = options.totalBytes > 0
|
const percent = total > 0 ? Math.min(100, Math.round(current / total * 100)) : 0;
|
||||||
? Math.min(100, Math.round((options.bytes || 0) / options.totalBytes * 100))
|
|
||||||
: total > 0 ? Math.min(100, Math.round(current / total * 100)) : 0;
|
|
||||||
const title = options.title || "资源更新";
|
const title = options.title || "资源更新";
|
||||||
const status = options.status || "准备中";
|
const status = options.status || "准备中";
|
||||||
const file = options.file || "";
|
const file = options.file || "";
|
||||||
const transfer = options.totalBytes > 0
|
|
||||||
? `${formatBytes(options.bytes || 0)} / ${formatBytes(options.totalBytes)} · ${formatBytes(options.speed || 0)}/秒`
|
|
||||||
: `${current}/${total}`;
|
|
||||||
|
|
||||||
shuYing.text.innerHTML = `
|
shuYing.text.innerHTML = `
|
||||||
<div style="position:static;box-sizing:border-box;writing-mode:horizontal-tb;text-orientation:mixed;display:flex;align-items:center;justify-content:space-between;gap:12px;min-width:0;width:100%;">
|
<div style="position:static;box-sizing:border-box;writing-mode:horizontal-tb;text-orientation:mixed;display:flex;align-items:center;justify-content:space-between;gap:12px;min-width:0;width:100%;">
|
||||||
<div style="position:static;box-sizing:border-box;font-size:16px;font-weight:700;color:#fff;white-space:nowrap;word-break:keep-all;overflow:hidden;text-overflow:ellipsis;min-width:0;">${escapeHTML(title)}</div>
|
<div style="position:static;box-sizing:border-box;font-size:16px;font-weight:700;color:#fff;white-space:nowrap;word-break:keep-all;overflow:hidden;text-overflow:ellipsis;min-width:0;">${escapeHTML(title)}</div>
|
||||||
<div style="position:static;box-sizing:border-box;font-size:12px;color:rgba(245,241,232,0.72);white-space:nowrap;word-break:keep-all;flex:0 0 auto;">${escapeHTML(transfer)}</div>
|
<div style="position:static;box-sizing:border-box;font-size:12px;color:rgba(245,241,232,0.72);white-space:nowrap;word-break:keep-all;flex:0 0 auto;">${current}/${total}</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="position:static;box-sizing:border-box;display:block;width:100%;height:8px;min-height:8px;border-radius:999px;background:linear-gradient(90deg,#6ee7b7 0%,#60a5fa ${percent}%,rgba(255,255,255,0.14) ${percent}%,rgba(255,255,255,0.14) 100%);overflow:hidden;"></div>
|
<div style="position:static;box-sizing:border-box;display:block;width:100%;height:8px;min-height:8px;border-radius:999px;background:linear-gradient(90deg,#6ee7b7 0%,#60a5fa ${percent}%,rgba(255,255,255,0.14) ${percent}%,rgba(255,255,255,0.14) 100%);overflow:hidden;"></div>
|
||||||
<div style="position:static;box-sizing:border-box;writing-mode:horizontal-tb;text-orientation:mixed;display:flex;justify-content:space-between;gap:10px;font-size:12px;color:rgba(245,241,232,0.82);min-width:0;width:100%;">
|
<div style="position:static;box-sizing:border-box;writing-mode:horizontal-tb;text-orientation:mixed;display:flex;justify-content:space-between;gap:10px;font-size:12px;color:rgba(245,241,232,0.82);min-width:0;width:100%;">
|
||||||
@@ -384,120 +211,27 @@ function splitPath(path) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForRetry(delay, signal) {
|
async function fetchFile(path, tag, source) {
|
||||||
if (signal?.aborted) throw signal.reason || new Error("下载已停止");
|
const response = await fetch(getRemoteUrl(tag, path, source));
|
||||||
await new Promise((resolve, reject) => {
|
if (!response.ok) throw new Error(`${path} 下载失败:${response.status}`);
|
||||||
const cancel = () => {
|
|
||||||
clearTimeout(timer);
|
return await response.arrayBuffer();
|
||||||
signal.removeEventListener("abort", cancel);
|
|
||||||
reject(signal.reason || new Error("下载已停止"));
|
|
||||||
};
|
|
||||||
const timer = setTimeout(() => { signal?.removeEventListener("abort", cancel); resolve(); }, delay);
|
|
||||||
signal?.addEventListener("abort", cancel, { once: true });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchFile(path, tag, source, options = {}) {
|
async function writeTargetFile(data, target) {
|
||||||
for (let attempt = 0; ; attempt++) {
|
|
||||||
if (options.signal?.aborted) throw options.signal.reason || new Error("下载已停止");
|
|
||||||
const controller = new AbortController();
|
|
||||||
const cancel = () => controller.abort(options.signal.reason);
|
|
||||||
options.signal?.addEventListener("abort", cancel, { once: true });
|
|
||||||
let timer, reader, timedOut = false;
|
|
||||||
const resetTimeout = () => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
timer = setTimeout(() => { timedOut = true; controller.abort(new Error(`${path} 下载超时`)); }, 60000);
|
|
||||||
};
|
|
||||||
options.onProgress?.(0, 0);
|
|
||||||
try {
|
|
||||||
resetTimeout();
|
|
||||||
const response = await fetch(getRemoteUrl(tag, path, source), { signal: controller.signal });
|
|
||||||
reader = response.body?.getReader?.();
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = new Error(`${path} 下载失败:${response.status}`);
|
|
||||||
error.retryable = [408, 429, 500, 502, 503, 504].includes(response.status);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
if (!reader) {
|
|
||||||
const data = await response.arrayBuffer();
|
|
||||||
if (Number.isSafeInteger(options.size) && data.byteLength > options.size) {
|
|
||||||
throw new Error(`${path} 下载数据超过清单大小`);
|
|
||||||
}
|
|
||||||
options.onProgress?.(data.byteLength, data.byteLength);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
const chunks = [];
|
|
||||||
let loaded = 0;
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
if (done) break;
|
|
||||||
loaded += value.byteLength;
|
|
||||||
if (Number.isSafeInteger(options.size) && loaded > options.size) {
|
|
||||||
throw new Error(`${path} 下载数据超过清单大小`);
|
|
||||||
}
|
|
||||||
chunks.push(value);
|
|
||||||
options.onProgress?.(loaded, value.byteLength);
|
|
||||||
resetTimeout();
|
|
||||||
}
|
|
||||||
const data = new Uint8Array(loaded);
|
|
||||||
let offset = 0;
|
|
||||||
for (const chunk of chunks) { data.set(chunk, offset); offset += chunk.byteLength; }
|
|
||||||
return data.buffer;
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
if (options.signal?.aborted || attempt >= 2
|
|
||||||
|| !(timedOut || error?.retryable || error instanceof TypeError)) throw error;
|
|
||||||
await addLog(options.shuYing, `下载重试 ${attempt + 1}/2:${path};${getErrorMessage(error)}`);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
clearTimeout(timer);
|
|
||||||
options.signal?.removeEventListener("abort", cancel);
|
|
||||||
if (reader) await reader.cancel().catch(() => {});
|
|
||||||
}
|
|
||||||
await waitForRetry(600 * (attempt + 1), options.signal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadProgress(shuYing, files, manifest) {
|
|
||||||
const received = new Map();
|
|
||||||
const totalBytes = files.reduce((sum, file) => sum + manifest.files[file].size, 0);
|
|
||||||
const started = Date.now();
|
|
||||||
let transferred = 0, completed = 0, bytes = 0;
|
|
||||||
const render = (file, force = false) => {
|
|
||||||
setProgress(shuYing, { title: "下载更新", status: `下载并校验(${completed}/${files.length})`,
|
|
||||||
file, current: completed, total: files.length, bytes, totalBytes,
|
|
||||||
speed: transferred / Math.max(0.1, (Date.now() - started) / 1000), force });
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
receive(file, loaded, delta) {
|
|
||||||
const value = Math.min(loaded, manifest.files[file].size);
|
|
||||||
bytes += value - (received.get(file) || 0);
|
|
||||||
received.set(file, value);
|
|
||||||
transferred += delta;
|
|
||||||
render(file);
|
|
||||||
},
|
|
||||||
complete(file) { completed++; render(file, completed == files.length); },
|
|
||||||
finish() { addLog(shuYing, `下载及准备完成:${files.length} 个文件,${formatBytes(totalBytes)},用时 ${((Date.now() - started) / 1000).toFixed(1)} 秒`); },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function writeTargetFile(data, target, verifiedInfo = null) {
|
|
||||||
if (typeof game.writeFile != "function") {
|
if (typeof game.writeFile != "function") {
|
||||||
throw new Error("当前环境不支持写入文件");
|
throw new Error("当前环境不支持写入文件");
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetInfo = splitPath(target);
|
const targetInfo = splitPath(target);
|
||||||
const metadata = await writeFile(data, targetInfo.dir, targetInfo.name);
|
await writeFile(data, targetInfo.dir, targetInfo.name);
|
||||||
if (verifiedInfo) rememberVerified(target, verifiedInfo, metadata);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadFile(path, target, manifest, options = {}) {
|
async function downloadFile(path, target, manifest) {
|
||||||
const data = await fetchFile(path, manifest.tag_name, manifest.source,
|
const data = await fetchFile(path, manifest.tag_name, manifest.source);
|
||||||
{ ...options, size: manifest.files[path]?.size });
|
|
||||||
if (manifest?.files?.[path]) {
|
if (manifest?.files?.[path]) {
|
||||||
await verifyFile(path, data, manifest);
|
await verifyFile(path, data, manifest);
|
||||||
}
|
}
|
||||||
if (options.signal?.aborted) throw options.signal.reason || new Error("下载已停止");
|
|
||||||
await writeTargetFile(data, target);
|
await writeTargetFile(data, target);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -737,34 +471,6 @@ function checkFile(path) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function fileMetadata(path) {
|
|
||||||
assertLocalPath(path);
|
|
||||||
return fileOperation(`读取文件属性:${path}`, (resolve, reject) => {
|
|
||||||
if (lib.node?.fs && typeof window.__dirname == "string") {
|
|
||||||
lib.node.fs.stat(lib.node.path.join(window.__dirname, path), (error, stat) => {
|
|
||||||
if (error?.code == "ENOENT") { resolve(null); return; }
|
|
||||||
if (error) { reject(error); return; }
|
|
||||||
if (!stat.isFile() || !Number.isFinite(stat.mtimeMs) || !Number.isFinite(stat.ctimeMs)) {
|
|
||||||
resolve(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve({ size: stat.size, fingerprint: `node:${stat.dev}:${stat.ino}:${stat.mtimeMs}:${stat.ctimeMs}` });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (hasCordovaFiles()) {
|
|
||||||
cordovaEntry(path).then(entry => entry.file(file => {
|
|
||||||
try {
|
|
||||||
const modified = Number(file.lastModified) || new Date(file.lastModifiedDate).getTime();
|
|
||||||
resolve(Number.isFinite(modified) && modified > 0
|
|
||||||
? { size: file.size, fingerprint: `cordova:${modified}` } : null);
|
|
||||||
}
|
|
||||||
catch { resolve(null); }
|
|
||||||
}, reject)).catch(error => error?.code == 1 ? resolve(null) : reject(error));
|
|
||||||
}
|
|
||||||
else resolve(null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function safeCheckFile(path) {
|
async function safeCheckFile(path) {
|
||||||
// An unreadable file must never be treated as a missing file during replacement.
|
// An unreadable file must never be treated as a missing file during replacement.
|
||||||
return await checkFile(path);
|
return await checkFile(path);
|
||||||
@@ -830,7 +536,7 @@ function hasCordovaFiles() {
|
|||||||
return !!lib.device && typeof window.resolveLocalFileSystemURL == "function";
|
return !!lib.device && typeof window.resolveLocalFileSystemURL == "function";
|
||||||
}
|
}
|
||||||
|
|
||||||
function cordovaEntry(path, create = false, mutation = create) {
|
function cordovaEntry(path, create = false) {
|
||||||
return fileOperation(`打开文件:${path}`, (resolve, reject) => {
|
return fileOperation(`打开文件:${path}`, (resolve, reject) => {
|
||||||
const root = localStorage.getItem("noname_inited");
|
const root = localStorage.getItem("noname_inited");
|
||||||
if (!root) throw new Error("无法确定游戏文件目录");
|
if (!root) throw new Error("无法确定游戏文件目录");
|
||||||
@@ -838,7 +544,7 @@ function cordovaEntry(path, create = false, mutation = create) {
|
|||||||
try { entry.getFile(path, { create }, resolve, reject); }
|
try { entry.getFile(path, { create }, resolve, reject); }
|
||||||
catch (error) { reject(error); }
|
catch (error) { reject(error); }
|
||||||
}, reject);
|
}, reject);
|
||||||
}, mutation);
|
}, create);
|
||||||
}
|
}
|
||||||
|
|
||||||
function readFile(path) {
|
function readFile(path) {
|
||||||
@@ -859,13 +565,12 @@ function readFile(path) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function writeFile(data, path, name, options = {}) {
|
async function writeFile(data, path, name) {
|
||||||
const target = `${path.replace(/\/$/, "")}/${name}`;
|
const target = `${path.replace(/\/$/, "")}/${name}`;
|
||||||
assertLocalPath(target);
|
assertLocalPath(target);
|
||||||
invalidateVerifiedFile(target);
|
|
||||||
await fileOperation(`写入文件:${target}`, (resolve, reject) => {
|
await fileOperation(`写入文件:${target}`, (resolve, reject) => {
|
||||||
if (hasCordovaFiles()) {
|
if (hasCordovaFiles()) {
|
||||||
cordovaEntry(target, true, !options.diagnostic).then(entry => entry.createWriter(writer => {
|
cordovaEntry(target, true).then(entry => entry.createWriter(writer => {
|
||||||
let truncated = false;
|
let truncated = false;
|
||||||
writer.onerror = () => reject(writer.error || new Error("写入失败"));
|
writer.onerror = () => reject(writer.error || new Error("写入失败"));
|
||||||
writer.onwriteend = () => {
|
writer.onwriteend = () => {
|
||||||
@@ -901,9 +606,7 @@ async function writeFile(data, path, name, options = {}) {
|
|||||||
if (error) reject(error);
|
if (error) reject(error);
|
||||||
else resolve();
|
else resolve();
|
||||||
});
|
});
|
||||||
}, !options.diagnostic);
|
}, true);
|
||||||
if (options.diagnostic) return null;
|
|
||||||
const before = await fileMetadata(target);
|
|
||||||
const actual = toUint8Array(await readFile(target));
|
const actual = toUint8Array(await readFile(target));
|
||||||
const expected = toUint8Array(data);
|
const expected = toUint8Array(data);
|
||||||
let equal = actual.length == expected.length;
|
let equal = actual.length == expected.length;
|
||||||
@@ -911,14 +614,11 @@ async function writeFile(data, path, name, options = {}) {
|
|||||||
if (!equal) {
|
if (!equal) {
|
||||||
throw new Error(`文件落盘校验失败:${target}`);
|
throw new Error(`文件落盘校验失败:${target}`);
|
||||||
}
|
}
|
||||||
const after = await fileMetadata(target);
|
|
||||||
return sameMetadata(before, after) ? after : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeFile(path) {
|
async function removeFile(path) {
|
||||||
assertMutationReady();
|
assertMutationReady();
|
||||||
assertLocalPath(path);
|
assertLocalPath(path);
|
||||||
invalidateVerifiedFile(path);
|
|
||||||
const exists = await checkFile(path);
|
const exists = await checkFile(path);
|
||||||
if (exists == -1) return;
|
if (exists == -1) return;
|
||||||
if (exists != 1) throw new Error(`待删除路径不是文件:${path}`);
|
if (exists != 1) throw new Error(`待删除路径不是文件:${path}`);
|
||||||
@@ -1006,24 +706,21 @@ async function downloadCoreFiles(shuYing, manifest) {
|
|||||||
await updateAllFiles(shuYing, manifest, files);
|
await updateAllFiles(shuYing, manifest, files);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getChangedFiles(manifest, shuYing) {
|
async function getChangedFiles(manifest) {
|
||||||
if (typeof game.readFile != "function") throw new Error("当前环境不支持校验本地文件");
|
if (typeof game.readFile != "function") {
|
||||||
const files = getManifestFileKeys(manifest);
|
throw new Error("当前环境不支持校验本地文件");
|
||||||
let checked = 0;
|
}
|
||||||
const started = Date.now();
|
const changed = [];
|
||||||
const results = await runFilePool(files, async file => {
|
for (const file of getManifestFileKeys(manifest)) {
|
||||||
let changed = null;
|
try {
|
||||||
try { await verifyLocalFile(file, manifest, shuYing); }
|
const data = await readFile(`extension/术樱包/${file}`);
|
||||||
catch (error) { changed = file; }
|
await verifyFile(file, data, manifest);
|
||||||
checked++;
|
}
|
||||||
if (shuYing) setProgress(shuYing, {
|
catch (error) {
|
||||||
title: "检查更新", status: fullVerification ? "完整校验资源" : "检查资源变化",
|
changed.push(file);
|
||||||
file, current: checked, total: files.length, force: checked == files.length,
|
}
|
||||||
});
|
}
|
||||||
return changed;
|
return changed;
|
||||||
}, { concurrency: lib.device ? 1 : 2, weight: file => manifest.files[file].size * 3 });
|
|
||||||
await addLog(shuYing, `资源检查完成:${files.length} 个文件,用时 ${((Date.now() - started) / 1000).toFixed(1)} 秒`);
|
|
||||||
return results.filter(Boolean);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeLegacyModuleManifest(manifest, moduleId) {
|
function makeLegacyModuleManifest(manifest, moduleId) {
|
||||||
@@ -1094,11 +791,7 @@ function getMengsanRecord() {
|
|||||||
|
|
||||||
async function saveMengsanRecord(record) {
|
async function saveMengsanRecord(record) {
|
||||||
const version = record.status == "disabled" ? "disabled" : record.version || "legacy";
|
const version = record.status == "disabled" ? "disabled" : record.version || "legacy";
|
||||||
await saveUpdaterConfig({ [mengsanRecordKey]: record, [mengsanVersionKey]: version });
|
// Both keys commit together; saveConfig/putDB request success is too early.
|
||||||
}
|
|
||||||
|
|
||||||
async function saveUpdaterConfig(values) {
|
|
||||||
// Publish only after both the IndexedDB transaction and its requests commit.
|
|
||||||
lib.status.reload++;
|
lib.status.reload++;
|
||||||
try {
|
try {
|
||||||
if (lib.db) {
|
if (lib.db) {
|
||||||
@@ -1109,9 +802,10 @@ async function saveUpdaterConfig(values) {
|
|||||||
transaction = lib.db.transaction(["config"], "readwrite");
|
transaction = lib.db.transaction(["config"], "readwrite");
|
||||||
transaction.oncomplete = () => resolve();
|
transaction.oncomplete = () => resolve();
|
||||||
transaction.onerror = event => { requestError = event.target?.error; };
|
transaction.onerror = event => { requestError = event.target?.error; };
|
||||||
transaction.onabort = () => reject(transaction.error || requestError || new Error("更新记录未能保存"));
|
transaction.onabort = () => reject(transaction.error || requestError || new Error("梦三安装记录未能保存"));
|
||||||
const store = transaction.objectStore("config");
|
const store = transaction.objectStore("config");
|
||||||
for (const [key, value] of Object.entries(values)) store.put(value, key);
|
store.put(record, mengsanRecordKey);
|
||||||
|
store.put(version, mengsanVersionKey);
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
try { transaction?.abort(); } catch {}
|
try { transaction?.abort(); } catch {}
|
||||||
@@ -1123,10 +817,12 @@ async function saveUpdaterConfig(values) {
|
|||||||
const key = `${lib.configprefix}config`;
|
const key = `${lib.configprefix}config`;
|
||||||
const config = JSON.parse(localStorage.getItem(key) || "{}");
|
const config = JSON.parse(localStorage.getItem(key) || "{}");
|
||||||
if (!config || typeof config != "object" || Array.isArray(config)) throw new Error("本地配置格式无效");
|
if (!config || typeof config != "object" || Array.isArray(config)) throw new Error("本地配置格式无效");
|
||||||
Object.assign(config, values);
|
config[mengsanRecordKey] = record;
|
||||||
|
config[mengsanVersionKey] = version;
|
||||||
localStorage.setItem(key, JSON.stringify(config));
|
localStorage.setItem(key, JSON.stringify(config));
|
||||||
}
|
}
|
||||||
Object.assign(lib.config, values);
|
lib.config[mengsanRecordKey] = record;
|
||||||
|
lib.config[mengsanVersionKey] = version;
|
||||||
}
|
}
|
||||||
finally { game.reload2(); }
|
finally { game.reload2(); }
|
||||||
}
|
}
|
||||||
@@ -1189,8 +885,9 @@ async function installMengsanFiles(shuYing, manifest, files, previousManifest =
|
|||||||
try {
|
try {
|
||||||
await updateAllFiles(shuYing, manifest, files, operationRoot);
|
await updateAllFiles(shuYing, manifest, files, operationRoot);
|
||||||
filesApplied = true;
|
filesApplied = true;
|
||||||
const invalid = await getChangedFiles(manifest, shuYing);
|
for (const file of currentFiles) {
|
||||||
if (invalid.length) throw new Error(`梦三模块校验失败:${invalid.slice(0, 5).join("、")}`);
|
await verifyFile(file, await readFile(`extension/术樱包/${file}`), manifest);
|
||||||
|
}
|
||||||
await saveMengsanRecord({ status: "installed", version: manifest.version,
|
await saveMengsanRecord({ status: "installed", version: manifest.version,
|
||||||
source: manifest.source, files: currentFiles });
|
source: manifest.source, files: currentFiles });
|
||||||
}
|
}
|
||||||
@@ -1281,23 +978,21 @@ async function updateAllFiles(shuYing, manifest, files, operationRoot = newOpera
|
|||||||
let retainRecovery = false;
|
let retainRecovery = false;
|
||||||
try {
|
try {
|
||||||
ensureProgressNode(shuYing);
|
ensureProgressNode(shuYing);
|
||||||
await invalidateBeforeChanges([...files, ...obsoleteFiles]);
|
|
||||||
await createDir(`${newRoot}/`);
|
await createDir(`${newRoot}/`);
|
||||||
await createDir(`${oldRoot}/`);
|
await createDir(`${oldRoot}/`);
|
||||||
await createFolders(`${newRoot}/`, files);
|
await createFolders(`${newRoot}/`, files);
|
||||||
await createFolders(`${oldRoot}/`, [...files, ...obsoleteFiles]);
|
await createFolders(`${oldRoot}/`, [...files, ...obsoleteFiles]);
|
||||||
|
|
||||||
const progress = downloadProgress(shuYing, files, manifest);
|
for (let i = 0; i < files.length; i++) {
|
||||||
await runFilePool(files, async (file, index, signal) => {
|
const file = files[i];
|
||||||
staged.add(file);
|
setProgress(shuYing, {
|
||||||
await downloadFile(file, `${newRoot}/${file}`, manifest, {
|
title: "版本更新", status: "下载并校验", file,
|
||||||
signal, shuYing, onProgress: (loaded, delta) => progress.receive(file, loaded, delta),
|
current: i, total: files.length * 2,
|
||||||
});
|
});
|
||||||
progress.complete(file);
|
staged.add(file);
|
||||||
}, { weight: file => manifest.files[file].size * 4 });
|
await downloadFile(file, `${newRoot}/${file}`, manifest);
|
||||||
progress.finish();
|
}
|
||||||
|
|
||||||
const installStarted = Date.now();
|
|
||||||
await createFolders("extension/术樱包/", files);
|
await createFolders("extension/术樱包/", files);
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
const file = files[i];
|
const file = files[i];
|
||||||
@@ -1312,14 +1007,11 @@ async function updateAllFiles(shuYing, manifest, files, operationRoot = newOpera
|
|||||||
}
|
}
|
||||||
|
|
||||||
setProgress(shuYing, {
|
setProgress(shuYing, {
|
||||||
title: "安装更新", status: "写入并检查", file,
|
title: "版本更新", status: "写入中", file,
|
||||||
current: i, total: files.length, force: i == 0,
|
current: files.length + i, total: files.length * 2,
|
||||||
});
|
});
|
||||||
const prepared = await readFile(`${newRoot}/${file}`);
|
|
||||||
// Recheck staged bytes before publishing a verification receipt.
|
|
||||||
await verifyFile(file, prepared, manifest);
|
|
||||||
installed.push(file);
|
installed.push(file);
|
||||||
await writeTargetFile(prepared, target, manifest.files[file]);
|
await writeTargetFile(await readFile(`${newRoot}/${file}`), target);
|
||||||
}
|
}
|
||||||
for (const file of obsoleteFiles) {
|
for (const file of obsoleteFiles) {
|
||||||
const target = `extension/术樱包/${file}`;
|
const target = `extension/术樱包/${file}`;
|
||||||
@@ -1333,8 +1025,6 @@ async function updateAllFiles(shuYing, manifest, files, operationRoot = newOpera
|
|||||||
}
|
}
|
||||||
await removeManifestOldPaths(shuYing, manifest);
|
await removeManifestOldPaths(shuYing, manifest);
|
||||||
filesInstalled = true;
|
filesInstalled = true;
|
||||||
setProgress(shuYing, { title: "安装更新", status: "文件处理完成", current: files.length, total: files.length, force: true });
|
|
||||||
await addLog(shuYing, `文件替换完成,用时 ${((Date.now() - installStarted) / 1000).toFixed(1)} 秒`);
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
const rollbackFiles = filesInstalled || error.operationUncertain ? []
|
const rollbackFiles = filesInstalled || error.operationUncertain ? []
|
||||||
@@ -1389,10 +1079,44 @@ async function updateAllFiles(shuYing, manifest, files, operationRoot = newOpera
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function downloadList(shuYing, files, manifest) {
|
async function downloadList(shuYing, files, manifest) {
|
||||||
// Repair also stages downloads before sequential replacement and rollback.
|
let finished = 0;
|
||||||
const changed = await getChangedFiles(manifest, shuYing);
|
const total = files.length;
|
||||||
const allowed = new Set(files);
|
|
||||||
await updateAllFiles(shuYing, manifest, changed.filter(file => allowed.has(file)));
|
for (const file of files) {
|
||||||
|
const target = `extension/术樱包/${file}`;
|
||||||
|
try {
|
||||||
|
await addLog(shuYing, `检查:${file}`);
|
||||||
|
setProgress(shuYing, { title: "查漏补缺", status: "检查本地文件", file: target, current: finished, total });
|
||||||
|
const exists = typeof game.checkFile == "function" ? await safeCheckFile(target) : 0;
|
||||||
|
|
||||||
|
if (exists == 1 && typeof game.readFile == "function") {
|
||||||
|
try {
|
||||||
|
await verifyFile(file, await readFile(target), manifest);
|
||||||
|
finished++;
|
||||||
|
await addLog(shuYing, `校验通过,跳过:${file}`);
|
||||||
|
setProgress(shuYing, { title: "查漏补缺", status: "校验通过", file: target, current: finished, total });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
await addLog(shuYing, `本地文件需修复:${file};${getErrorMessage(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await addLog(shuYing, `下载:${file}`);
|
||||||
|
setProgress(shuYing, { title: "查漏补缺", status: "下载中", file: target, current: finished, total });
|
||||||
|
await downloadFile(file, target, manifest);
|
||||||
|
if (manifest?.files?.[file]) {
|
||||||
|
await addLog(shuYing, `校验通过:${file}`);
|
||||||
|
}
|
||||||
|
finished++;
|
||||||
|
await addLog(shuYing, `完成:${file}`);
|
||||||
|
setProgress(shuYing, { title: "查漏补缺", status: "已完成", file: target, current: finished, total });
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
await addLog(shuYing, `失败:${file};${getErrorMessage(error)}`);
|
||||||
|
throw new Error(`${file} 处理失败:${getErrorMessage(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getOnlineVersion(channel = getUpdateChannel(),
|
async function getOnlineVersion(channel = getUpdateChannel(),
|
||||||
@@ -1403,8 +1127,7 @@ async function getOnlineVersion(channel = getUpdateChannel(),
|
|||||||
|
|
||||||
async function checkVersion(shuYing, options = {}) {
|
async function checkVersion(shuYing, options = {}) {
|
||||||
try {
|
try {
|
||||||
beginUpdate(shuYing);
|
shuYing._updateLogs = [];
|
||||||
ensureProgressNode(shuYing);
|
|
||||||
await addLog(shuYing, "版本检查开始");
|
await addLog(shuYing, "版本检查开始");
|
||||||
const onlineVersion = await getOnlineVersion(options.channel, options.source);
|
const onlineVersion = await getOnlineVersion(options.channel, options.source);
|
||||||
const localVersion = lib.config.shuYing_local_version || "0.0.0.0";
|
const localVersion = lib.config.shuYing_local_version || "0.0.0.0";
|
||||||
@@ -1438,7 +1161,7 @@ async function checkVersion(shuYing, options = {}) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const changed = await getChangedFiles(manifest, shuYing);
|
const changed = await getChangedFiles(manifest);
|
||||||
const obsolete = await getObsoleteFiles(manifest);
|
const obsolete = await getObsoleteFiles(manifest);
|
||||||
let mengsanManifest = null;
|
let mengsanManifest = null;
|
||||||
let mengsanChanged = [];
|
let mengsanChanged = [];
|
||||||
@@ -1457,7 +1180,7 @@ async function checkVersion(shuYing, options = {}) {
|
|||||||
getManifestFileKeys(previousMengsan)
|
getManifestFileKeys(previousMengsan)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
mengsanChanged = await getChangedFiles(mengsanManifest, shuYing);
|
mengsanChanged = await getChangedFiles(mengsanManifest);
|
||||||
mengsanObsolete = await getObsoleteFiles(mengsanManifest);
|
mengsanObsolete = await getObsoleteFiles(mengsanManifest);
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
@@ -1511,11 +1234,8 @@ async function checkVersion(shuYing, options = {}) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
try { await finishUpdate(shuYing); }
|
removeProgressNode(shuYing);
|
||||||
finally {
|
setBusy(shuYing, false);
|
||||||
removeProgressNode(shuYing);
|
|
||||||
setBusy(shuYing, false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1527,7 +1247,7 @@ async function repairMissingFiles(shuYing) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
beginUpdate(shuYing, true);
|
shuYing._updateLogs = [];
|
||||||
await addLog(shuYing, "查漏补缺开始");
|
await addLog(shuYing, "查漏补缺开始");
|
||||||
ensureProgressNode(shuYing);
|
ensureProgressNode(shuYing);
|
||||||
setProgress(shuYing, { title: "查漏补缺", status: "获取校验清单", current: 0, total: 1 });
|
setProgress(shuYing, { title: "查漏补缺", status: "获取校验清单", current: 0, total: 1 });
|
||||||
@@ -1576,8 +1296,9 @@ async function repairMissingFiles(shuYing) {
|
|||||||
await createFolders("extension/术樱包/", files, shuYing);
|
await createFolders("extension/术樱包/", files, shuYing);
|
||||||
|
|
||||||
await downloadList(shuYing, files, manifest);
|
await downloadList(shuYing, files, manifest);
|
||||||
|
await removeManifestOldPaths(shuYing, manifest);
|
||||||
if (mengsanManifest) {
|
if (mengsanManifest) {
|
||||||
const moduleChanged = await getChangedFiles(mengsanManifest, shuYing);
|
const moduleChanged = await getChangedFiles(mengsanManifest);
|
||||||
await installMengsanFiles(shuYing, mengsanManifest, moduleChanged, previousMengsan);
|
await installMengsanFiles(shuYing, mengsanManifest, moduleChanged, previousMengsan);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1591,18 +1312,15 @@ async function repairMissingFiles(shuYing) {
|
|||||||
alert("查漏补缺失败,请查看 extension/术樱包/log.txt");
|
alert("查漏补缺失败,请查看 extension/术樱包/log.txt");
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
try { await finishUpdate(shuYing); }
|
removeProgressNode(shuYing);
|
||||||
finally {
|
setBusy(shuYing, false);
|
||||||
removeProgressNode(shuYing);
|
|
||||||
setBusy(shuYing, false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function manageMengsanModule(shuYing) {
|
async function manageMengsanModule(shuYing) {
|
||||||
setBusy(shuYing, true);
|
setBusy(shuYing, true);
|
||||||
try {
|
try {
|
||||||
beginUpdate(shuYing);
|
shuYing._updateLogs = [];
|
||||||
assertMutationReady();
|
assertMutationReady();
|
||||||
const record = getMengsanRecord();
|
const record = getMengsanRecord();
|
||||||
const localVersion = lib.config.shuYing_local_version || "0.0.0.0";
|
const localVersion = lib.config.shuYing_local_version || "0.0.0.0";
|
||||||
@@ -1623,7 +1341,6 @@ async function manageMengsanModule(shuYing) {
|
|||||||
source: record?.source || getUpdateSource(), files,
|
source: record?.source || getUpdateSource(), files,
|
||||||
};
|
};
|
||||||
await saveMengsanRecord(pending);
|
await saveMengsanRecord(pending);
|
||||||
await invalidateBeforeChanges(files);
|
|
||||||
ensureProgressNode(shuYing);
|
ensureProgressNode(shuYing);
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
const file = files[i];
|
const file = files[i];
|
||||||
@@ -1665,42 +1382,59 @@ async function manageMengsanModule(shuYing) {
|
|||||||
alert(`${hint}\n${getErrorMessage(error)}\n请查看 extension/术樱包/log.txt。`);
|
alert(`${hint}\n${getErrorMessage(error)}\n请查看 extension/术樱包/log.txt。`);
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
try { await finishUpdate(shuYing); }
|
removeProgressNode(shuYing);
|
||||||
finally {
|
setBusy(shuYing, false);
|
||||||
removeProgressNode(shuYing);
|
|
||||||
setBusy(shuYing, false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function repairCoreFiles(shuYing) {
|
async function repairCoreFiles(shuYing) {
|
||||||
|
let shouldDownload = false;
|
||||||
|
let manifest;
|
||||||
|
let coreFiles;
|
||||||
|
|
||||||
|
try {
|
||||||
|
manifest = await getManifest();
|
||||||
|
coreFiles = getManifestCoreFiles(manifest);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
alert("获取服务器校验清单失败,请检查网络或 manifest.json。");
|
||||||
|
setBusy(shuYing, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof game.readFile != "function") {
|
||||||
|
shouldDownload = confirm("此端暂不支持本地资源检查。如有问题,点击确定重新下载核心文件。");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
try {
|
||||||
|
for (const file of coreFiles) {
|
||||||
|
await readFile(`extension/术樱包/${file}`);
|
||||||
|
}
|
||||||
|
shouldDownload = confirm("本地资源文件检测无误,若有问题点击确定重新下载主要js文件(包含关键js文件, 请勿关机、断网等操作,注意备份!)");
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
shouldDownload = confirm("本地资源不完整!点击确认重新获取!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldDownload) {
|
||||||
|
setBusy(shuYing, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
beginUpdate(shuYing, true);
|
|
||||||
ensureProgressNode(shuYing);
|
|
||||||
await addLog(shuYing, "核心文件修复开始");
|
|
||||||
const manifest = await getManifest();
|
|
||||||
const files = getManifestCoreFiles(manifest);
|
|
||||||
if (!files.length) throw new Error("校验清单缺少核心文件");
|
|
||||||
const coreManifest = { ...manifest, files: Object.fromEntries(files.map(file => [file, manifest.files[file]])) };
|
|
||||||
const changed = await getChangedFiles(coreManifest, shuYing);
|
|
||||||
const message = changed.length ? `有 ${changed.length} 个核心文件需修复,是否重新下载核心文件?`
|
|
||||||
: "核心文件校验通过,是否仍重新下载核心文件?";
|
|
||||||
if (!confirm(message)) return;
|
|
||||||
await downloadCoreFiles(shuYing, manifest);
|
await downloadCoreFiles(shuYing, manifest);
|
||||||
await addLog(shuYing, "核心文件修复完成");
|
|
||||||
alert("下载完成,重启生效");
|
alert("下载完成,重启生效");
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
await addLog(shuYing, `核心文件修复失败:${getErrorMessage(error)}`);
|
|
||||||
alert("核心文件下载或校验失败,请检查服务器文件与 manifest.json 是否一致。");
|
alert("核心文件下载或校验失败,请检查服务器文件与 manifest.json 是否一致。");
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
try { await finishUpdate(shuYing); }
|
removeProgressNode(shuYing);
|
||||||
finally {
|
setBusy(shuYing, false);
|
||||||
removeProgressNode(shuYing);
|
|
||||||
setBusy(shuYing, false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-12
@@ -1,15 +1,3 @@
|
|||||||
"-----< 2.0.2.3 改动 >-----",
|
|
||||||
"新增正式版、测试版更新通道切换。",
|
|
||||||
"支持选择 Gitea 或 GitHub 下载源。",
|
|
||||||
"更新系统采用并发下载:安卓最多 4 个、PC 最多 8 个,大文件受内存限制。",
|
|
||||||
"新增梦三模式安装入口",
|
|
||||||
"-----< 2.0.2.3 修复 >-----",
|
|
||||||
"修复天书乱斗跨关后,按轮数冷却的技能未正确重置,以及旧关卡历史影响新关卡计数的问题。",
|
|
||||||
"修复【清剿】旧关卡的延迟弃牌结算可能弃掉新关卡奖励的问题。",
|
|
||||||
"修复天书乱斗应战时的座位重叠、动态 Boss 控制切换、代理头像显示及胜负判断问题。",
|
|
||||||
"修正“神武再世”三级羁绊的体力流失统计,按目标本回合累计流失的体力判定。",
|
|
||||||
"修复切换下载源后,旧版本查询结果覆盖新结果的问题。",
|
|
||||||
"核心文件修复增加大小和 SHA-256 校验。",
|
|
||||||
"-----< 2.0.1.1 改动 >-----",
|
"-----< 2.0.1.1 改动 >-----",
|
||||||
"新增“旧轮次”选项,可在过关后按照旧规则将行动权交给玩家方1号位上家的Boss",
|
"新增“旧轮次”选项,可在过关后按照旧规则将行动权交给玩家方1号位上家的Boss",
|
||||||
"新增“羁绊技能倾向”选项,已激活羁绊的相关技能将按羁绊等级获得温和的出现概率提升",
|
"新增“羁绊技能倾向”选项,已激活羁绊的相关技能将按羁绊等级获得温和的出现概率提升",
|
||||||
|
|||||||
Reference in New Issue
Block a user