@sfmc-bds/bds-tools 0.2.0-beta.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +663 -0
- package/README.md +1 -1
- package/dist/bds-manager.d.ts +16 -6
- package/dist/bds-manager.d.ts.map +1 -1
- package/dist/bds-manager.js +59 -66
- package/dist/bds-manager.js.map +2 -2
- package/dist/changelog.d.ts +11 -7
- package/dist/changelog.d.ts.map +1 -1
- package/dist/changelog.js +60 -34
- package/dist/changelog.js.map +2 -2
- package/dist/check-update.d.ts.map +1 -1
- package/dist/check-update.js +59 -23
- package/dist/check-update.js.map +2 -2
- package/dist/cli-pack-manager.js +11 -1
- package/dist/cli-pack-manager.js.map +2 -2
- package/dist/dependency-negotiator.d.ts +73 -0
- package/dist/dependency-negotiator.d.ts.map +1 -0
- package/dist/dependency-negotiator.js +151 -0
- package/dist/dependency-negotiator.js.map +7 -0
- package/dist/fsx.d.ts +17 -14
- package/dist/fsx.d.ts.map +1 -1
- package/dist/fsx.js +67 -19
- package/dist/fsx.js.map +2 -2
- package/dist/host-platform.d.ts +25 -0
- package/dist/host-platform.d.ts.map +1 -0
- package/dist/host-platform.js +50 -0
- package/dist/host-platform.js.map +7 -0
- package/dist/http.d.ts +49 -11
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js.map +2 -2
- package/dist/level-dat.d.ts +105 -0
- package/dist/level-dat.d.ts.map +1 -0
- package/dist/level-dat.js +490 -0
- package/dist/level-dat.js.map +7 -0
- package/dist/pack-manager.d.ts +41 -34
- package/dist/pack-manager.d.ts.map +1 -1
- package/dist/pack-manager.js +79 -25
- package/dist/pack-manager.js.map +2 -2
- package/dist/pack-update/bindings.d.ts +16 -0
- package/dist/pack-update/bindings.d.ts.map +1 -0
- package/dist/pack-update/bindings.js +69 -0
- package/dist/pack-update/bindings.js.map +7 -0
- package/dist/pack-update/config.d.ts +16 -0
- package/dist/pack-update/config.d.ts.map +1 -0
- package/dist/pack-update/config.js +146 -0
- package/dist/pack-update/config.js.map +7 -0
- package/dist/pack-update/deps.d.ts +28 -0
- package/dist/pack-update/deps.d.ts.map +1 -0
- package/dist/pack-update/deps.js +20 -0
- package/dist/pack-update/deps.js.map +7 -0
- package/dist/pack-update/index.d.ts +55 -0
- package/dist/pack-update/index.d.ts.map +1 -0
- package/dist/pack-update/index.js +72 -0
- package/dist/pack-update/index.js.map +7 -0
- package/dist/pack-update/providers/curseforge.d.ts +23 -0
- package/dist/pack-update/providers/curseforge.d.ts.map +1 -0
- package/dist/pack-update/providers/curseforge.js +229 -0
- package/dist/pack-update/providers/curseforge.js.map +7 -0
- package/dist/pack-update/providers/index.d.ts +16 -0
- package/dist/pack-update/providers/index.d.ts.map +1 -0
- package/dist/pack-update/providers/index.js +37 -0
- package/dist/pack-update/providers/index.js.map +7 -0
- package/dist/pack-update/service.d.ts +28 -0
- package/dist/pack-update/service.d.ts.map +1 -0
- package/dist/pack-update/service.js +570 -0
- package/dist/pack-update/service.js.map +7 -0
- package/dist/pack-update/types.d.ts +126 -0
- package/dist/pack-update/types.d.ts.map +1 -0
- package/dist/pack-update/types.js +8 -0
- package/dist/pack-update/types.js.map +7 -0
- package/dist/pack-update/version-policy.d.ts +81 -0
- package/dist/pack-update/version-policy.d.ts.map +1 -0
- package/dist/pack-update/version-policy.js +210 -0
- package/dist/pack-update/version-policy.js.map +7 -0
- package/dist/paths.d.ts +1 -1
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +3 -5
- package/dist/paths.js.map +2 -2
- package/dist/process-probe.d.ts +34 -0
- package/dist/process-probe.d.ts.map +1 -0
- package/dist/process-probe.js +152 -0
- package/dist/process-probe.js.map +7 -0
- package/dist/qq-events-notify.d.ts +16 -0
- package/dist/qq-events-notify.d.ts.map +1 -0
- package/dist/qq-events-notify.js +70 -0
- package/dist/qq-events-notify.js.map +7 -0
- package/dist/qqutil.d.ts +10 -5
- package/dist/qqutil.d.ts.map +1 -1
- package/dist/qqutil.js +53 -10
- package/dist/qqutil.js.map +2 -2
- package/dist/rollback.d.ts.map +1 -1
- package/dist/rollback.js +2 -2
- package/dist/rollback.js.map +2 -2
- package/dist/server-properties-i18n.d.ts +17 -0
- package/dist/server-properties-i18n.d.ts.map +1 -0
- package/dist/server-properties-i18n.js +201 -0
- package/dist/server-properties-i18n.js.map +7 -0
- package/dist/server-properties.d.ts +25 -0
- package/dist/server-properties.d.ts.map +1 -1
- package/dist/server-properties.js +141 -1
- package/dist/server-properties.js.map +2 -2
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/upstream.d.ts +18 -9
- package/dist/upstream.d.ts.map +1 -1
- package/dist/upstream.js +8 -7
- package/dist/upstream.js.map +2 -2
- package/dist/version.js +2 -2
- package/dist/version.js.map +2 -2
- package/dist/world-packs.d.ts +108 -8
- package/dist/world-packs.d.ts.map +1 -1
- package/dist/world-packs.js +245 -12
- package/dist/world-packs.js.map +3 -3
- package/package.json +24 -17
package/dist/http.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/http.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * http.ts \u2014 \u7A33\u5B9A HTTP \u5DE5\u5177 (\u5206\u9636\u6BB5\u8D85\u65F6 + \u81EA\u52A8\u91CD\u5B9A\u5411 + \u6D41\u5F0F\u4E0B\u8F7D)\n *\n * \u6539\u8FDB:\n * - connect timeout vs total timeout \u5206\u5F00\n * - \u4E0B\u8F7D\u901A\u8FC7\u6D41\u7BA1\u9053\uFF0C\u907F\u514D\u5927\u6587\u4EF6\u52A0\u8F7D\u5230\u5185\u5B58\n * - \u5931\u8D25\u540E\u5DF2\u5199\u5165\u7684\u4E34\u65F6\u6587\u4EF6\u53EF\u88AB\u6E05\u7406\n */\n\nimport http from \"node:http\";\nimport https from \"node:https\";\nimport type { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport { createWriteStream, statSync } from \"node:fs\";\nimport { log } from \"./log.js\";\n\ninterface HttpOptions {\n method?: \"GET\" | \"HEAD\" | \"POST\";\n headers?: Record<string, string>;\n body?: string;\n connectTimeoutMs?: number;\n totalTimeoutMs?: number;\n redirects?: number;\n}\n\nconst MAX_REDIRECTS = 5;\nconst DEFAULT_CONNECT_TIMEOUT = 15_000;\nconst DEFAULT_TOTAL_TIMEOUT = 600_000;\n\nexport async function httpRequest(\n url: string,\n opts: HttpOptions = {}\n): Promise<{ statusCode: number; body: Buffer; headers: Record<string, string | string[] | undefined> }> {\n const redirects = opts.redirects ?? 0;\n const totalTimeoutMs = opts.totalTimeoutMs ?? DEFAULT_TOTAL_TIMEOUT;\n const connectTimeoutMs = opts.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT;\n\n const u = new URL(url);\n const isHttps = u.protocol === \"https:\";\n const mod = isHttps ? https : http;\n\n return new Promise((resolve, reject) => {\n const req = mod.request(\n {\n hostname: u.hostname,\n port: u.port || (isHttps ? 443 : 80),\n path: u.pathname + u.search,\n method: opts.method ?? \"GET\",\n headers: { \"User-Agent\": \"BDSUpdater/2.0\", ...(opts.headers ?? {}) },\n },\n (res) => {\n const chunks: Buffer[] = [];\n res.on(\"data\", (c: Buffer) => chunks.push(c));\n res.on(\"end\", () => {\n const body = Buffer.concat(chunks);\n const status = res.statusCode ?? 0;\n // 3xx \u2192 \u8DDF\u968F Location\n if (status >= 300 && status < 400 && redirects < MAX_REDIRECTS) {\n const loc = res.headers.location;\n if (!loc) return reject(new Error(`HTTP ${status} \u4F46\u7F3A\u5C11 Location`));\n const next = loc.startsWith(\"http\") ? loc : new URL(loc, url).href;\n httpRequest(next, { ...opts, redirects: redirects + 1 })\n .then(resolve)\n .catch(reject);\n return;\n }\n if (status >= 400) {\n return reject(new Error(`HTTP ${status} for ${url}`));\n }\n resolve({ statusCode: status, body, headers: res.headers });\n });\n }\n );\n\n const totalTimer = setTimeout(() => {\n req.destroy(new Error(`HTTP \u603B\u8D85\u65F6 ${totalTimeoutMs}ms: ${url}`));\n }, totalTimeoutMs);\n const connectTimer = setTimeout(() => {\n req.destroy(new Error(`HTTP \u8FDE\u63A5\u8D85\u65F6 ${connectTimeoutMs}ms: ${url}`));\n }, connectTimeoutMs);\n\n req.on(\"socket\", () => clearTimeout(connectTimer));\n req.on(\"error\", (err) => {\n clearTimeout(totalTimer);\n clearTimeout(connectTimer);\n reject(err);\n });\n\n if (opts.body) req.write(opts.body);\n req.end();\n });\n}\n\nexport async function httpGetJson<T = unknown>(\n url: string,\n opts: HttpOptions = {}\n): Promise<T> {\n const res = await httpRequest(url, opts);\n try {\n return JSON.parse(res.body.toString(\"utf-8\")) as T;\n } catch (e) {\n throw new Error(`JSON \u89E3\u6790\u5931\u8D25 ${url}: ${(e as Error).message}`);\n }\n}\n\nexport async function httpGetText(url: string, opts: HttpOptions = {}): Promise<string> {\n const res = await httpRequest(url, opts);\n return res.body.toString(\"utf-8\");\n}\n\n/** \u591A\u4E2A\u6E90\u5E76\u53D1\u8BF7\u6C42\uFF0C\u8C01\u5148\u6210\u529F\u8FD4\u56DE\uFF08\u6BCF\u4E2A\u6E90\u90FD\u7528 JSON \u89E3\u6790\uFF0C\u89E3\u6790\u5931\u8D25\u4E5F\u5931\u8D25\uFF09 */\nexport async function fetchJsonWithFallback<T = unknown>(\n sources: string[],\n timeoutMs = 15_000\n): Promise<T> {\n const tasks = sources.map(async (url) => {\n const res = await httpGetJson<T>(url, { totalTimeoutMs: timeoutMs });\n return { url, value: res };\n });\n const settled = await Promise.allSettled(tasks);\n for (const r of settled) {\n if (r.status === \"fulfilled\") return r.value.value;\n }\n const errors = settled\n .filter((r): r is PromiseRejectedResult => r.status === \"rejected\")\n .map((r) => r.reason?.message ?? \"unknown\")\n .join(\"; \");\n throw new Error(`\u6240\u6709\u6E90\u5747\u4E0D\u53EF\u7528: ${errors}`);\n}\n\n/**\n * \u6D41\u5F0F\u4E0B\u8F7D\u5230\u6307\u5B9A\u8DEF\u5F84\uFF0C\u8FD4\u56DE\u5B57\u8282\u6570\u3002\n * - \u5931\u8D25\u65F6\u81EA\u52A8\u6E05\u7406 partial \u6587\u4EF6\n * - \u652F\u6301 progress \u56DE\u8C03\n * - \u652F\u6301 connect/total \u9636\u6BB5\u8D85\u65F6\n */\nexport interface DownloadOptions extends HttpOptions {\n onProgress?: (downloaded: number, total: number) => void;\n}\n\nexport async function httpDownload(\n url: string,\n destPath: string,\n opts: DownloadOptions = {}\n): Promise<number> {\n const totalTimeoutMs = opts.totalTimeoutMs ?? DEFAULT_TOTAL_TIMEOUT;\n const connectTimeoutMs = opts.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT;\n\n const u = new URL(url);\n const isHttps = u.protocol === \"https:\";\n const mod = isHttps ? https : http;\n let redirects = opts.redirects ?? 0;\n\n return new Promise((resolve, reject) => {\n const req = mod.request(\n {\n hostname: u.hostname,\n port: u.port || (isHttps ? 443 : 80),\n path: u.pathname + u.search,\n method: \"GET\",\n headers: { \"User-Agent\": \"BDSUpdater/2.0\", ...(opts.headers ?? {}) },\n },\n (res) => {\n const status = res.statusCode ?? 0;\n if (status >= 300 && status < 400 && redirects < MAX_REDIRECTS) {\n const loc = res.headers.location;\n if (!loc) return reject(new Error(`HTTP ${status} \u7F3A\u5C11 Location`));\n redirects++;\n const next = loc.startsWith(\"http\") ? loc : new URL(loc, url).href;\n res.resume();\n httpDownload(next, destPath, { ...opts, redirects })\n .then(resolve)\n .catch(reject);\n return;\n }\n if (status >= 400) return reject(new Error(`HTTP ${status} for ${url}`));\n\n const total = parseInt(String(res.headers[\"content-length\"] ?? 0), 10) || 0;\n const file = createWriteStream(destPath);\n let downloaded = 0;\n let failed = false;\n // \u8282\u6D41\u8FDB\u5EA6\u56DE\u8C03:\u6BCF\u4E2A tick \u6700\u591A 10 \u6B21/\u79D2(100ms \u95F4\u9694),\n // \u907F\u514D cli-progress \u9891\u7E41\u91CD\u7ED8\u62D6\u6162\u4E0B\u8F7D\n let lastProgressAt = 0;\n const PROGRESS_INTERVAL_MS = 100;\n\n const handleError = (e: Error): void => {\n if (failed) return;\n failed = true;\n file.close();\n // \u6E05\u7406 partial \u6587\u4EF6\n import(\"node:fs\").then((fs) => {\n try { fs.unlinkSync(destPath); } catch {}\n });\n reject(e);\n };\n\n const stream: Readable = res;\n stream.on(\"data\", (chunk: Buffer) => {\n downloaded += chunk.length;\n if (opts.onProgress) {\n const now = Date.now();\n if (now - lastProgressAt >= PROGRESS_INTERVAL_MS) {\n lastProgressAt = now;\n /* total=0\uFF08\u65E0 Content-Length\uFF09\u65F6\u4ECD\u56DE\u8C03\uFF0C\u8C03\u7528\u65B9\u53EF\u7528\u5DF2\u4E0B\u8F7D\u5B57\u8282\u66F4\u65B0 UI */\n opts.onProgress(downloaded, total);\n }\n }\n });\n stream.on(\"error\", (e) => handleError(e as Error));\n\n file.on(\"error\", (e) => handleError(e as Error));\n\n pipeline(stream, file).catch((e) => {\n if (!failed) handleError(e as Error);\n });\n\n // \u4E0D\u80FD\u76F4\u63A5\u76D1\u542C file 'finish' \u56E0\u4E3A\u6211\u4EEC\u5728\u9519\u8BEF\u65F6\u624B\u52A8 handleError\n file.on(\"finish\", () => {\n if (failed) return;\n try {\n const finalBytes = statSync(destPath).size;\n // \u6536\u5C3E:\u786E\u4FDD\u6700\u540E\u4E00\u6B21\u56DE\u8C03\u8BA9\u8FDB\u5EA6\u6761\u8D70\u5230 100%(\u5373\u4FBF\u6700\u8FD1\n // \u4E00\u4E2A tick \u7684\u8282\u6D41\u6CA1\u89E6\u53D1,\u4E5F\u8865\u4E00\u6B21)\u3002\n // \u65E0 Content-Length\uFF08total=0\uFF09\u65F6\u7528 finalBytes \u4F5C\u603B\u91CF\uFF0C\u4E0E\u4E2D\u9014\u300C\u4ECD\u56DE\u8C03\u300D\u5951\u7EA6\u4E00\u81F4\uFF08LSP\uFF09\u3002\n if (opts.onProgress) {\n opts.onProgress(finalBytes, total > 0 ? total : finalBytes);\n }\n resolve(finalBytes);\n } catch (e) {\n reject(e as Error);\n }\n });\n }\n );\n\n const totalTimer = setTimeout(() => {\n req.destroy(new Error(`\u4E0B\u8F7D\u603B\u8D85\u65F6 ${totalTimeoutMs}ms: ${url}`));\n }, totalTimeoutMs);\n const connectTimer = setTimeout(() => {\n req.destroy(new Error(`\u4E0B\u8F7D\u8FDE\u63A5\u8D85\u65F6 ${connectTimeoutMs}ms: ${url}`));\n }, connectTimeoutMs);\n\n req.on(\"socket\", () => clearTimeout(connectTimer));\n req.on(\"error\", (e) => {\n clearTimeout(totalTimer);\n clearTimeout(connectTimer);\n log.warn(`HTTP \u9519\u8BEF ${url}: ${(e as Error).message}`);\n reject(e);\n });\n req.end();\n });\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["/**\n * http.ts \u2014 \u9488\u5BF9\u5927\u6587\u4EF6\u4E0E\u5916\u90E8\u4E0A\u6E38\u8BBE\u8BA1\u7684 HTTP \u7F51\u7EDC\u8BF7\u6C42\u5DE5\u5177\n *\n * \u6838\u5FC3\u673A\u5236\uFF1A\n * - \u5206\u9636\u6BB5\u8D85\u65F6\u63A7\u5236\uFF1A\u533A\u5206\u8FDE\u63A5\u5EFA\u7ACB\u8D85\u65F6\uFF08connectTimeout\uFF09\u4E0E\u5168\u91CF\u4F20\u8F93\u8D85\u65F6\uFF08totalTimeout\uFF09\n * - \u81EA\u52A8\u91CD\u5B9A\u5411\u5904\u7406\uFF1A\u5B89\u5168\u8DDF\u8E2A 3xx \u8DF3\u8F6C\uFF08\u6700\u591A 5 \u6B21\uFF09\n * - \u6D41\u5F0F\u6587\u4EF6\u4E0B\u8F7D\uFF1A\u901A\u8FC7 pipeline \u7BA1\u9053\u76F4\u63A5\u843D\u76D8\uFF0C\u907F\u514D\u5927\u6587\u4EF6\u4E00\u6B21\u6027\u52A0\u8F7D\u81F3\u5185\u5B58\n * - \u5F02\u5E38\u5BB9\u9519\u6E05\u7406\uFF1A\u4E0B\u8F7D\u5931\u8D25\u65F6\u81EA\u52A8\u6E05\u7406\u6B8B\u7559\u7684\u4E34\u65F6\u6587\u4EF6\u788E\u7247\n */\n\nimport http from \"node:http\";\nimport https from \"node:https\";\nimport type { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport { createWriteStream, statSync } from \"node:fs\";\nimport { log } from \"./log.js\";\n\ninterface HttpOptions {\n method?: \"GET\" | \"HEAD\" | \"POST\";\n headers?: Record<string, string>;\n body?: string;\n connectTimeoutMs?: number;\n totalTimeoutMs?: number;\n redirects?: number;\n}\n\nconst MAX_REDIRECTS = 5;\nconst DEFAULT_CONNECT_TIMEOUT = 15_000;\nconst DEFAULT_TOTAL_TIMEOUT = 600_000;\n\n/**\n * \u53D1\u8D77\u5E95\u5C42 HTTP/HTTPS \u8BF7\u6C42\u3002\n *\n * @param url \u76EE\u6807\u8BF7\u6C42\u5730\u5740\u3002\n * @param opts \u8BF7\u6C42\u53C2\u6570\uFF08\u5305\u542B\u8BF7\u6C42\u65B9\u6CD5\u3001\u8BF7\u6C42\u5934\u3001\u4E3B\u4F53\u3001\u9636\u6BB5\u8D85\u65F6\u65F6\u95F4\u53CA\u91CD\u5B9A\u5411\u6B21\u6570\u7B49\uFF09\u3002\n * @returns \u5305\u542B\u72B6\u6001\u7801\u3001\u54CD\u5E94\u4F53 Buffer \u53CA\u54CD\u5E94\u5934\u5B57\u5178\u7684\u7ED3\u679C\u5BF9\u8C61\u3002\n */\nexport async function httpRequest(\n\n url: string,\n opts: HttpOptions = {}\n): Promise<{ statusCode: number; body: Buffer; headers: Record<string, string | string[] | undefined> }> {\n const redirects = opts.redirects ?? 0;\n const totalTimeoutMs = opts.totalTimeoutMs ?? DEFAULT_TOTAL_TIMEOUT;\n const connectTimeoutMs = opts.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT;\n\n const u = new URL(url);\n const isHttps = u.protocol === \"https:\";\n const mod = isHttps ? https : http;\n\n return new Promise((resolve, reject) => {\n const req = mod.request(\n {\n hostname: u.hostname,\n port: u.port || (isHttps ? 443 : 80),\n path: u.pathname + u.search,\n method: opts.method ?? \"GET\",\n headers: { \"User-Agent\": \"BDSUpdater/2.0\", ...(opts.headers ?? {}) },\n },\n (res) => {\n const chunks: Buffer[] = [];\n res.on(\"data\", (c: Buffer) => chunks.push(c));\n res.on(\"end\", () => {\n const body = Buffer.concat(chunks);\n const status = res.statusCode ?? 0;\n // 3xx \u2192 \u8DDF\u968F Location\n if (status >= 300 && status < 400 && redirects < MAX_REDIRECTS) {\n const loc = res.headers.location;\n if (!loc) return reject(new Error(`HTTP ${status} \u4F46\u7F3A\u5C11 Location`));\n const next = loc.startsWith(\"http\") ? loc : new URL(loc, url).href;\n httpRequest(next, { ...opts, redirects: redirects + 1 })\n .then(resolve)\n .catch(reject);\n return;\n }\n if (status >= 400) {\n return reject(new Error(`HTTP ${status} for ${url}`));\n }\n resolve({ statusCode: status, body, headers: res.headers });\n });\n }\n );\n\n const totalTimer = setTimeout(() => {\n req.destroy(new Error(`HTTP \u603B\u8D85\u65F6 ${totalTimeoutMs}ms: ${url}`));\n }, totalTimeoutMs);\n const connectTimer = setTimeout(() => {\n req.destroy(new Error(`HTTP \u8FDE\u63A5\u8D85\u65F6 ${connectTimeoutMs}ms: ${url}`));\n }, connectTimeoutMs);\n\n req.on(\"socket\", () => clearTimeout(connectTimer));\n req.on(\"error\", (err) => {\n clearTimeout(totalTimer);\n clearTimeout(connectTimer);\n reject(err);\n });\n\n if (opts.body) req.write(opts.body);\n req.end();\n });\n}\n\n/**\n * \u53D1\u8D77 GET \u8BF7\u6C42\u5E76\u81EA\u52A8\u5C06\u54CD\u5E94\u4F53\u89E3\u6790\u4E3A JSON \u6570\u636E\u5BF9\u8C61\u3002\n *\n * @template T \u9884\u671F\u7684 JSON \u6570\u636E\u7C7B\u578B\u3002\n * @param url \u8BF7\u6C42\u76EE\u6807 URL\u3002\n * @param opts \u8BF7\u6C42\u914D\u7F6E\u9009\u9879\u3002\n * @returns \u89E3\u6790\u540E\u7684 JSON \u5BF9\u8C61\u3002\n */\nexport async function httpGetJson<T = unknown>(\n url: string,\n opts: HttpOptions = {}\n): Promise<T> {\n const res = await httpRequest(url, opts);\n try {\n return JSON.parse(res.body.toString(\"utf-8\")) as T;\n } catch (e) {\n throw new Error(`JSON \u89E3\u6790\u5931\u8D25 ${url}: ${(e as Error).message}`);\n }\n}\n\n/**\n * \u53D1\u8D77 GET \u8BF7\u6C42\u5E76\u83B7\u53D6 UTF-8 \u7EAF\u6587\u672C\u54CD\u5E94\u5185\u5BB9\u3002\n *\n * @param url \u8BF7\u6C42\u76EE\u6807 URL\u3002\n * @param opts \u8BF7\u6C42\u914D\u7F6E\u9009\u9879\u3002\n * @returns \u54CD\u5E94\u6587\u672C\u5B57\u7B26\u4E32\u3002\n */\nexport async function httpGetText(url: string, opts: HttpOptions = {}): Promise<string> {\n const res = await httpRequest(url, opts);\n return res.body.toString(\"utf-8\");\n}\n\n/**\n * \u9488\u5BF9\u591A\u4E2A\u5907\u9009\u6E90\u5E76\u53D1\u53D1\u8D77\u8BF7\u6C42\uFF0C\u7531\u6700\u5148\u6210\u529F\u8FD4\u56DE\u4E14\u5408\u6CD5\u89E3\u6790 JSON \u7684\u6E90\u63D0\u4F9B\u6570\u636E\uFF08Happy Eyeballs \u673A\u5236\uFF09\u3002\n *\n * @template T \u8FD4\u56DE\u7684 JSON \u6570\u636E\u7C7B\u578B\u3002\n * @param sources \u5019\u9009 URL \u5217\u8868\u3002\n * @param timeoutMs \u8D85\u65F6\u65F6\u95F4\uFF08\u6BEB\u79D2\uFF0C\u9ED8\u8BA4\u4E3A 15000\uFF09\u3002\n * @returns \u6700\u5148\u6210\u529F\u54CD\u5E94\u7684\u6570\u636E\u3002\n * @throws \u5F53\u6240\u6709\u5907\u9009\u6E90\u5747\u5931\u8D25\u65F6\u629B\u51FA\u805A\u5408\u9519\u8BEF\u3002\n */\nexport async function fetchJsonWithFallback<T = unknown>(\n sources: string[],\n timeoutMs = 15_000\n): Promise<T> {\n const tasks = sources.map(async (url) => {\n const res = await httpGetJson<T>(url, { totalTimeoutMs: timeoutMs });\n return { url, value: res };\n });\n const settled = await Promise.allSettled(tasks);\n for (const r of settled) {\n if (r.status === \"fulfilled\") return r.value.value;\n }\n const errors = settled\n .filter((r): r is PromiseRejectedResult => r.status === \"rejected\")\n .map((r) => r.reason?.message ?? \"unknown\")\n .join(\"; \");\n throw new Error(`\u6240\u6709\u6E90\u5747\u4E0D\u53EF\u7528: ${errors}`);\n}\n\n/** \u4E0B\u8F7D\u914D\u7F6E\u9009\u9879\u3002 */\nexport interface DownloadOptions extends HttpOptions {\n /** \u4E0B\u8F7D\u8FDB\u5EA6\u53D8\u5316\u56DE\u8C03\u51FD\u6570\u3002 */\n onProgress?: (downloaded: number, total: number) => void;\n}\n\n/**\n * \u6D41\u5F0F\u4E0B\u8F7D\u7F51\u7EDC\u8D44\u6E90\u81F3\u672C\u5730\u6307\u5B9A\u8DEF\u5F84\u3002\n * - \u4F20\u8F93\u5931\u8D25\u65F6\u81EA\u52A8\u5220\u9664\u6B8B\u7559\u7684\u4E34\u65F6\u6587\u4EF6\u788E\u7247\n * - \u652F\u6301\u8282\u6D41\u7684\u8FDB\u5EA6\u901A\u77E5\u56DE\u8C03\n * - \u652F\u6301\u8FDE\u63A5\u5EFA\u7ACB\u4E0E\u5168\u91CF\u4F20\u8F93\u53CC\u9636\u6BB5\u72EC\u7ACB\u8D85\u65F6\n *\n * @param url \u4E0B\u8F7D\u76EE\u6807\u5730\u5740\u3002\n * @param destPath \u672C\u5730\u843D\u76D8\u76EE\u6807\u7EDD\u5BF9\u8DEF\u5F84\u3002\n * @param opts \u4E0B\u8F7D\u53C2\u6570\u9009\u9879\u3002\n * @returns \u6210\u529F\u4E0B\u8F7D\u7684\u603B\u5B57\u8282\u6570\u3002\n */\n\n\nexport async function httpDownload(\n url: string,\n destPath: string,\n opts: DownloadOptions = {}\n): Promise<number> {\n const totalTimeoutMs = opts.totalTimeoutMs ?? DEFAULT_TOTAL_TIMEOUT;\n const connectTimeoutMs = opts.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT;\n\n const u = new URL(url);\n const isHttps = u.protocol === \"https:\";\n const mod = isHttps ? https : http;\n let redirects = opts.redirects ?? 0;\n\n return new Promise((resolve, reject) => {\n const req = mod.request(\n {\n hostname: u.hostname,\n port: u.port || (isHttps ? 443 : 80),\n path: u.pathname + u.search,\n method: \"GET\",\n headers: { \"User-Agent\": \"BDSUpdater/2.0\", ...(opts.headers ?? {}) },\n },\n (res) => {\n const status = res.statusCode ?? 0;\n if (status >= 300 && status < 400 && redirects < MAX_REDIRECTS) {\n const loc = res.headers.location;\n if (!loc) return reject(new Error(`HTTP ${status} \u7F3A\u5C11 Location`));\n redirects++;\n const next = loc.startsWith(\"http\") ? loc : new URL(loc, url).href;\n res.resume();\n httpDownload(next, destPath, { ...opts, redirects })\n .then(resolve)\n .catch(reject);\n return;\n }\n if (status >= 400) return reject(new Error(`HTTP ${status} for ${url}`));\n\n const total = parseInt(String(res.headers[\"content-length\"] ?? 0), 10) || 0;\n const file = createWriteStream(destPath);\n let downloaded = 0;\n let failed = false;\n // \u8282\u6D41\u8FDB\u5EA6\u56DE\u8C03:\u6BCF\u4E2A tick \u6700\u591A 10 \u6B21/\u79D2(100ms \u95F4\u9694),\n // \u907F\u514D cli-progress \u9891\u7E41\u91CD\u7ED8\u62D6\u6162\u4E0B\u8F7D\n let lastProgressAt = 0;\n const PROGRESS_INTERVAL_MS = 100;\n\n const handleError = (e: Error): void => {\n if (failed) return;\n failed = true;\n file.close();\n // \u6E05\u7406 partial \u6587\u4EF6\n import(\"node:fs\").then((fs) => {\n try { fs.unlinkSync(destPath); } catch {}\n });\n reject(e);\n };\n\n const stream: Readable = res;\n stream.on(\"data\", (chunk: Buffer) => {\n downloaded += chunk.length;\n if (opts.onProgress) {\n const now = Date.now();\n if (now - lastProgressAt >= PROGRESS_INTERVAL_MS) {\n lastProgressAt = now;\n /* total=0\uFF08\u65E0 Content-Length\uFF09\u65F6\u4ECD\u56DE\u8C03\uFF0C\u8C03\u7528\u65B9\u53EF\u7528\u5DF2\u4E0B\u8F7D\u5B57\u8282\u66F4\u65B0 UI */\n opts.onProgress(downloaded, total);\n }\n }\n });\n stream.on(\"error\", (e) => handleError(e as Error));\n\n file.on(\"error\", (e) => handleError(e as Error));\n\n pipeline(stream, file).catch((e) => {\n if (!failed) handleError(e as Error);\n });\n\n // \u4E0D\u80FD\u76F4\u63A5\u76D1\u542C file 'finish' \u56E0\u4E3A\u6211\u4EEC\u5728\u9519\u8BEF\u65F6\u624B\u52A8 handleError\n file.on(\"finish\", () => {\n if (failed) return;\n try {\n const finalBytes = statSync(destPath).size;\n // \u6536\u5C3E:\u786E\u4FDD\u6700\u540E\u4E00\u6B21\u56DE\u8C03\u8BA9\u8FDB\u5EA6\u6761\u8D70\u5230 100%(\u5373\u4FBF\u6700\u8FD1\n // \u4E00\u4E2A tick \u7684\u8282\u6D41\u6CA1\u89E6\u53D1,\u4E5F\u8865\u4E00\u6B21)\u3002\n // \u65E0 Content-Length\uFF08total=0\uFF09\u65F6\u7528 finalBytes \u4F5C\u603B\u91CF\uFF0C\u4E0E\u4E2D\u9014\u300C\u4ECD\u56DE\u8C03\u300D\u5951\u7EA6\u4E00\u81F4\uFF08LSP\uFF09\u3002\n if (opts.onProgress) {\n opts.onProgress(finalBytes, total > 0 ? total : finalBytes);\n }\n resolve(finalBytes);\n } catch (e) {\n reject(e as Error);\n }\n });\n }\n );\n\n const totalTimer = setTimeout(() => {\n req.destroy(new Error(`\u4E0B\u8F7D\u603B\u8D85\u65F6 ${totalTimeoutMs}ms: ${url}`));\n }, totalTimeoutMs);\n const connectTimer = setTimeout(() => {\n req.destroy(new Error(`\u4E0B\u8F7D\u8FDE\u63A5\u8D85\u65F6 ${connectTimeoutMs}ms: ${url}`));\n }, connectTimeoutMs);\n\n req.on(\"socket\", () => clearTimeout(connectTimer));\n req.on(\"error\", (e) => {\n clearTimeout(totalTimer);\n clearTimeout(connectTimer);\n log.warn(`HTTP \u9519\u8BEF ${url}: ${(e as Error).message}`);\n reject(e);\n });\n req.end();\n });\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* level-dat.ts — Minecraft 基岩版世界存档 level.dat 小端序 NBT 解析与实验性玩法安全修改器
|
|
3
|
+
*
|
|
4
|
+
* 基岩版 level.dat 规范:
|
|
5
|
+
* - Header(8 字节):
|
|
6
|
+
* - [0..3] uint32 LE:存储格式版本(当前版本通常为 10)
|
|
7
|
+
* - [4..7] uint32 LE:有效 NBT 数据字节长度(data_length)
|
|
8
|
+
* - Payload:未压缩的小端序(Little-Endian)标准 NBT 复合标签(TAG_Compound)
|
|
9
|
+
*
|
|
10
|
+
* 核心能力:
|
|
11
|
+
* - 原生无外部黑盒依赖,Buffer 级解析与精确字节位移
|
|
12
|
+
* - 读取世界实验性玩法的开启状态(特别是 Beta APIs / gametest)
|
|
13
|
+
* - 安全注入或切换实验性开关并自动备份原文件(.bak)与校准 Header data_length
|
|
14
|
+
*/
|
|
15
|
+
export declare const TAG_END = 0;
|
|
16
|
+
export declare const TAG_BYTE = 1;
|
|
17
|
+
export declare const TAG_SHORT = 2;
|
|
18
|
+
export declare const TAG_INT = 3;
|
|
19
|
+
export declare const TAG_LONG = 4;
|
|
20
|
+
export declare const TAG_FLOAT = 5;
|
|
21
|
+
export declare const TAG_DOUBLE = 6;
|
|
22
|
+
export declare const TAG_BYTE_ARRAY = 7;
|
|
23
|
+
export declare const TAG_STRING = 8;
|
|
24
|
+
export declare const TAG_LIST = 9;
|
|
25
|
+
export declare const TAG_COMPOUND = 10;
|
|
26
|
+
export declare const TAG_INT_ARRAY = 11;
|
|
27
|
+
export declare const TAG_LONG_ARRAY = 12;
|
|
28
|
+
export interface NbtTagMeta {
|
|
29
|
+
type: number;
|
|
30
|
+
name: string;
|
|
31
|
+
startOffset: number;
|
|
32
|
+
nameOffset: number;
|
|
33
|
+
valueOffset: number;
|
|
34
|
+
endOffset: number;
|
|
35
|
+
value?: unknown;
|
|
36
|
+
}
|
|
37
|
+
export interface KnownExperimentDefinition {
|
|
38
|
+
id: string;
|
|
39
|
+
nbtKey: string;
|
|
40
|
+
nameZh: string;
|
|
41
|
+
nameEn: string;
|
|
42
|
+
aliases?: string[];
|
|
43
|
+
isRootTag?: boolean;
|
|
44
|
+
extraExpTags?: string[];
|
|
45
|
+
}
|
|
46
|
+
export declare const KNOWN_EXPERIMENTS: readonly KnownExperimentDefinition[];
|
|
47
|
+
export declare function resolveExperimentDefinition(query: string): KnownExperimentDefinition | null;
|
|
48
|
+
export interface LevelDatExperimentsInfo {
|
|
49
|
+
experimentsEverUsed: boolean;
|
|
50
|
+
savedWithToggledExperiments: boolean;
|
|
51
|
+
experiments: Record<string, number | boolean>;
|
|
52
|
+
hasBetaApis: boolean;
|
|
53
|
+
knownExperiments: Record<string, boolean>;
|
|
54
|
+
}
|
|
55
|
+
export interface LevelDatMutateResult {
|
|
56
|
+
success: boolean;
|
|
57
|
+
changed: boolean;
|
|
58
|
+
backupPath?: string;
|
|
59
|
+
error?: string;
|
|
60
|
+
previousBetaApisState?: boolean;
|
|
61
|
+
enabledExperiments?: string[];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 遍历并跳过指定类型的 NBT 载荷,返回下一个 tag 的 offset。
|
|
65
|
+
*/
|
|
66
|
+
export declare function skipNbtPayload(buf: Buffer, offset: number, type: number): number;
|
|
67
|
+
/**
|
|
68
|
+
* 解析复合标签内部所有直接子标签的元数据映射。
|
|
69
|
+
*/
|
|
70
|
+
export declare function parseCompoundChildren(buf: Buffer, compoundValueOffset: number): {
|
|
71
|
+
children: Map<string, NbtTagMeta>;
|
|
72
|
+
compoundEndOffset: number;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* 解析基岩版 level.dat 的完整文件结构并定位根 Compound。
|
|
76
|
+
*/
|
|
77
|
+
export declare function parseBedrockLevelDat(buf: Buffer): {
|
|
78
|
+
version: number;
|
|
79
|
+
dataLength: number;
|
|
80
|
+
rootCompoundName: string;
|
|
81
|
+
rootChildren: Map<string, NbtTagMeta>;
|
|
82
|
+
rootEndOffset: number;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* 读取世界存档 level.dat 的实验性配置。若解析失败或文件不存在返回 null。
|
|
86
|
+
*/
|
|
87
|
+
export declare function readLevelDatExperiments(levelDatPath: string): LevelDatExperimentsInfo | null;
|
|
88
|
+
/**
|
|
89
|
+
* 为指定 level.dat 开启指定的实验性开关(若未指定或传 "all" 则开启全部已知实验性开关)。
|
|
90
|
+
*
|
|
91
|
+
* 保证:
|
|
92
|
+
* - 自动备份原文件至 `${levelDatPath}.bak`;
|
|
93
|
+
* - 自动更新 root 与 experiments 复合标签中的目标 byte tags;
|
|
94
|
+
* - 自动更新 experiments_ever_used = 1b 与 saved_with_toggled_experiments = 1b;
|
|
95
|
+
* - 精确校准 8 字节文件头部的 dataLength;
|
|
96
|
+
* - 原子写操作(.tmp -> rename);
|
|
97
|
+
* - 若所请求开关已全部处于开启状态,直接返回 changed: false,不破坏文件。
|
|
98
|
+
*/
|
|
99
|
+
export declare function enableExperimentsInLevelDat(levelDatPath: string, targetExperimentIds?: string[] | "all"): Promise<LevelDatMutateResult>;
|
|
100
|
+
/**
|
|
101
|
+
* 为指定 level.dat 开启 Beta APIs(gametest)。
|
|
102
|
+
* 保持向后兼容,委托给 enableExperimentsInLevelDat。
|
|
103
|
+
*/
|
|
104
|
+
export declare function enableBetaApisInLevelDat(levelDatPath: string): Promise<LevelDatMutateResult>;
|
|
105
|
+
//# sourceMappingURL=level-dat.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"level-dat.d.ts","sourceRoot":"","sources":["../src/level-dat.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
const TAG_END = 0;
|
|
3
|
+
const TAG_BYTE = 1;
|
|
4
|
+
const TAG_SHORT = 2;
|
|
5
|
+
const TAG_INT = 3;
|
|
6
|
+
const TAG_LONG = 4;
|
|
7
|
+
const TAG_FLOAT = 5;
|
|
8
|
+
const TAG_DOUBLE = 6;
|
|
9
|
+
const TAG_BYTE_ARRAY = 7;
|
|
10
|
+
const TAG_STRING = 8;
|
|
11
|
+
const TAG_LIST = 9;
|
|
12
|
+
const TAG_COMPOUND = 10;
|
|
13
|
+
const TAG_INT_ARRAY = 11;
|
|
14
|
+
const TAG_LONG_ARRAY = 12;
|
|
15
|
+
const KNOWN_EXPERIMENTS = [
|
|
16
|
+
{
|
|
17
|
+
id: "gametest",
|
|
18
|
+
nbtKey: "gametest",
|
|
19
|
+
nameZh: "\u6D4B\u8BD5\u7248 API",
|
|
20
|
+
nameEn: "Beta APIs (gametest)",
|
|
21
|
+
aliases: ["beta", "beta_apis", "betapis", "gametest"]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "upcoming_creator_features",
|
|
25
|
+
nbtKey: "upcoming_creator_features",
|
|
26
|
+
nameZh: "\u5373\u5C06\u63A8\u51FA\u7684\u521B\u4F5C\u8005\u529F\u80FD",
|
|
27
|
+
nameEn: "Upcoming Creator Features",
|
|
28
|
+
aliases: ["upcoming", "creator_features", "upcoming_features"]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "experimental_creator_cameras",
|
|
32
|
+
nbtKey: "experimental_creator_cameras",
|
|
33
|
+
nameZh: "\u521B\u5EFA\u8005\u7167\u76F8\u673A\u7684\u5B9E\u9A8C\u6027\u529F\u80FD",
|
|
34
|
+
nameEn: "Experimental Creator Cameras",
|
|
35
|
+
aliases: ["camera", "cameras", "creator_cameras", "creator_camera"]
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "voxel_shapes",
|
|
39
|
+
nbtKey: "voxel_shapes",
|
|
40
|
+
nameZh: "\u5B9E\u9A8C\u6027Voxel\u5F62\u72B6\u7279\u5F81",
|
|
41
|
+
nameEn: "Experimental Voxel Shapes",
|
|
42
|
+
aliases: ["voxel", "voxel_shape", "experimental_voxel_shapes", "voxelshapes"]
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "villager_trades_rebalance",
|
|
46
|
+
nbtKey: "villager_trades_rebalance",
|
|
47
|
+
nameZh: "\u6751\u6C11\u8D38\u6613\u518D\u5E73\u8861",
|
|
48
|
+
nameEn: "Villager Trade Rebalance",
|
|
49
|
+
aliases: ["villager", "trades", "villager_trades", "trade_rebalance"]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "drop_3_2026",
|
|
53
|
+
nbtKey: "drop_3_2026",
|
|
54
|
+
nameZh: "2026\u5E74\u7B2C3\u6B21\u66F4\u65B0",
|
|
55
|
+
nameEn: "Drop 3 of 2026",
|
|
56
|
+
aliases: [
|
|
57
|
+
"drop3",
|
|
58
|
+
"drop_3",
|
|
59
|
+
"drop_3_2026",
|
|
60
|
+
"wilderness",
|
|
61
|
+
"wilderness_bound",
|
|
62
|
+
"update_2026_drop_3",
|
|
63
|
+
"update_26_3",
|
|
64
|
+
"update_drop_3"
|
|
65
|
+
],
|
|
66
|
+
extraExpTags: ["update_2026_drop_3"]
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "education",
|
|
70
|
+
nbtKey: "educationFeaturesEnabled",
|
|
71
|
+
nameZh: "Minecraft Education \u529F\u80FD",
|
|
72
|
+
nameEn: "Minecraft Education Features",
|
|
73
|
+
isRootTag: true,
|
|
74
|
+
aliases: ["edu", "education", "education_edition", "educationfeatures"],
|
|
75
|
+
extraExpTags: ["education"]
|
|
76
|
+
}
|
|
77
|
+
];
|
|
78
|
+
function resolveExperimentDefinition(query) {
|
|
79
|
+
const q = query.trim().toLowerCase().replace(/[-_]/g, "");
|
|
80
|
+
for (const def of KNOWN_EXPERIMENTS) {
|
|
81
|
+
if (def.id.replace(/[-_]/g, "") === q) return def;
|
|
82
|
+
if (def.nbtKey.replace(/[-_]/g, "") === q) return def;
|
|
83
|
+
if (def.aliases) {
|
|
84
|
+
for (const a of def.aliases) {
|
|
85
|
+
if (a.replace(/[-_]/g, "") === q) return def;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
function skipNbtPayload(buf, offset, type) {
|
|
92
|
+
switch (type) {
|
|
93
|
+
case TAG_BYTE:
|
|
94
|
+
return offset + 1;
|
|
95
|
+
case TAG_SHORT:
|
|
96
|
+
return offset + 2;
|
|
97
|
+
case TAG_INT:
|
|
98
|
+
case TAG_FLOAT:
|
|
99
|
+
return offset + 4;
|
|
100
|
+
case TAG_LONG:
|
|
101
|
+
case TAG_DOUBLE:
|
|
102
|
+
return offset + 8;
|
|
103
|
+
case TAG_BYTE_ARRAY: {
|
|
104
|
+
if (offset + 4 > buf.length) throw new Error("Unexpected EOF reading TAG_Byte_Array length");
|
|
105
|
+
const len = buf.readInt32LE(offset);
|
|
106
|
+
return offset + 4 + Math.max(0, len);
|
|
107
|
+
}
|
|
108
|
+
case TAG_STRING: {
|
|
109
|
+
if (offset + 2 > buf.length) throw new Error("Unexpected EOF reading TAG_String length");
|
|
110
|
+
const len = buf.readUInt16LE(offset);
|
|
111
|
+
return offset + 2 + len;
|
|
112
|
+
}
|
|
113
|
+
case TAG_LIST: {
|
|
114
|
+
if (offset + 5 > buf.length) throw new Error("Unexpected EOF reading TAG_List header");
|
|
115
|
+
const elemType = buf.readUInt8(offset);
|
|
116
|
+
const count = buf.readInt32LE(offset + 1);
|
|
117
|
+
let cur = offset + 5;
|
|
118
|
+
for (let i = 0; i < count; i++) {
|
|
119
|
+
cur = skipNbtPayload(buf, cur, elemType);
|
|
120
|
+
}
|
|
121
|
+
return cur;
|
|
122
|
+
}
|
|
123
|
+
case TAG_COMPOUND: {
|
|
124
|
+
let cur = offset;
|
|
125
|
+
while (cur < buf.length) {
|
|
126
|
+
const t = buf.readUInt8(cur);
|
|
127
|
+
if (t === TAG_END) {
|
|
128
|
+
return cur + 1;
|
|
129
|
+
}
|
|
130
|
+
if (cur + 3 > buf.length) throw new Error("Unexpected EOF reading compound child header");
|
|
131
|
+
const nameLen = buf.readUInt16LE(cur + 1);
|
|
132
|
+
const childValOffset = cur + 3 + nameLen;
|
|
133
|
+
cur = skipNbtPayload(buf, childValOffset, t);
|
|
134
|
+
}
|
|
135
|
+
return cur;
|
|
136
|
+
}
|
|
137
|
+
case TAG_INT_ARRAY: {
|
|
138
|
+
if (offset + 4 > buf.length) throw new Error("Unexpected EOF reading TAG_Int_Array length");
|
|
139
|
+
const len = buf.readInt32LE(offset);
|
|
140
|
+
return offset + 4 + Math.max(0, len) * 4;
|
|
141
|
+
}
|
|
142
|
+
case TAG_LONG_ARRAY: {
|
|
143
|
+
if (offset + 4 > buf.length) throw new Error("Unexpected EOF reading TAG_Long_Array length");
|
|
144
|
+
const len = buf.readInt32LE(offset);
|
|
145
|
+
return offset + 4 + Math.max(0, len) * 8;
|
|
146
|
+
}
|
|
147
|
+
default:
|
|
148
|
+
throw new Error(`Unsupported or unknown NBT tag type: ${type} at offset ${offset}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function parseCompoundChildren(buf, compoundValueOffset) {
|
|
152
|
+
const children = /* @__PURE__ */ new Map();
|
|
153
|
+
let cur = compoundValueOffset;
|
|
154
|
+
while (cur < buf.length) {
|
|
155
|
+
const tagType = buf.readUInt8(cur);
|
|
156
|
+
if (tagType === TAG_END) {
|
|
157
|
+
return { children, compoundEndOffset: cur + 1 };
|
|
158
|
+
}
|
|
159
|
+
if (cur + 3 > buf.length) {
|
|
160
|
+
throw new Error(`Unexpected EOF reading named tag header at offset ${cur}`);
|
|
161
|
+
}
|
|
162
|
+
const nameLen = buf.readUInt16LE(cur + 1);
|
|
163
|
+
const nameOffset = cur + 3;
|
|
164
|
+
if (nameOffset + nameLen > buf.length) {
|
|
165
|
+
throw new Error(`Unexpected EOF reading tag name at offset ${cur}`);
|
|
166
|
+
}
|
|
167
|
+
const name = buf.toString("utf8", nameOffset, nameOffset + nameLen);
|
|
168
|
+
const valOffset = nameOffset + nameLen;
|
|
169
|
+
const endOffset = skipNbtPayload(buf, valOffset, tagType);
|
|
170
|
+
let val = void 0;
|
|
171
|
+
if (tagType === TAG_BYTE) {
|
|
172
|
+
val = buf.readInt8(valOffset);
|
|
173
|
+
} else if (tagType === TAG_INT) {
|
|
174
|
+
val = buf.readInt32LE(valOffset);
|
|
175
|
+
} else if (tagType === TAG_STRING) {
|
|
176
|
+
const sLen = buf.readUInt16LE(valOffset);
|
|
177
|
+
val = buf.toString("utf8", valOffset + 2, valOffset + 2 + sLen);
|
|
178
|
+
}
|
|
179
|
+
children.set(name, {
|
|
180
|
+
type: tagType,
|
|
181
|
+
name,
|
|
182
|
+
startOffset: cur,
|
|
183
|
+
nameOffset,
|
|
184
|
+
valueOffset: valOffset,
|
|
185
|
+
endOffset,
|
|
186
|
+
value: val
|
|
187
|
+
});
|
|
188
|
+
cur = endOffset;
|
|
189
|
+
}
|
|
190
|
+
return { children, compoundEndOffset: cur };
|
|
191
|
+
}
|
|
192
|
+
function parseBedrockLevelDat(buf) {
|
|
193
|
+
if (buf.length < 8) {
|
|
194
|
+
throw new Error("Invalid level.dat: buffer smaller than 8-byte header");
|
|
195
|
+
}
|
|
196
|
+
const version = buf.readUInt32LE(0);
|
|
197
|
+
const dataLength = buf.readUInt32LE(4);
|
|
198
|
+
if (buf.length < 9 || buf.readUInt8(8) !== TAG_COMPOUND) {
|
|
199
|
+
throw new Error("Invalid level.dat: root tag is not TAG_Compound");
|
|
200
|
+
}
|
|
201
|
+
const rootNameLen = buf.readUInt16LE(9);
|
|
202
|
+
const rootCompoundName = buf.toString("utf8", 11, 11 + rootNameLen);
|
|
203
|
+
const rootValueOffset = 11 + rootNameLen;
|
|
204
|
+
const { children: rootChildren, compoundEndOffset: rootEndOffset } = parseCompoundChildren(buf, rootValueOffset);
|
|
205
|
+
return {
|
|
206
|
+
version,
|
|
207
|
+
dataLength,
|
|
208
|
+
rootCompoundName,
|
|
209
|
+
rootChildren,
|
|
210
|
+
rootEndOffset
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function readLevelDatExperiments(levelDatPath) {
|
|
214
|
+
if (!fs.existsSync(levelDatPath)) return null;
|
|
215
|
+
try {
|
|
216
|
+
const buf = fs.readFileSync(levelDatPath);
|
|
217
|
+
const parsed = parseBedrockLevelDat(buf);
|
|
218
|
+
let experimentsEverUsed = false;
|
|
219
|
+
const everUsedTag = parsed.rootChildren.get("experiments_ever_used");
|
|
220
|
+
if (everUsedTag && everUsedTag.type === TAG_BYTE && Number(everUsedTag.value) === 1) {
|
|
221
|
+
experimentsEverUsed = true;
|
|
222
|
+
}
|
|
223
|
+
let savedWithToggledExperiments = false;
|
|
224
|
+
const savedTag = parsed.rootChildren.get("saved_with_toggled_experiments");
|
|
225
|
+
if (savedTag && savedTag.type === TAG_BYTE && Number(savedTag.value) === 1) {
|
|
226
|
+
savedWithToggledExperiments = true;
|
|
227
|
+
}
|
|
228
|
+
const experiments = {};
|
|
229
|
+
let hasBetaApis = false;
|
|
230
|
+
const expCompoundTag = parsed.rootChildren.get("experiments");
|
|
231
|
+
if (expCompoundTag && expCompoundTag.type === TAG_COMPOUND) {
|
|
232
|
+
const expChildren = parseCompoundChildren(buf, expCompoundTag.valueOffset).children;
|
|
233
|
+
const innerEverUsed = expChildren.get("experiments_ever_used");
|
|
234
|
+
if (innerEverUsed && Number(innerEverUsed.value) === 1) {
|
|
235
|
+
experimentsEverUsed = true;
|
|
236
|
+
}
|
|
237
|
+
const innerSaved = expChildren.get("saved_with_toggled_experiments");
|
|
238
|
+
if (innerSaved && Number(innerSaved.value) === 1) {
|
|
239
|
+
savedWithToggledExperiments = true;
|
|
240
|
+
}
|
|
241
|
+
for (const [name, meta] of expChildren.entries()) {
|
|
242
|
+
if (meta.type === TAG_BYTE) {
|
|
243
|
+
const num = Number(meta.value);
|
|
244
|
+
experiments[name] = num;
|
|
245
|
+
if (name === "gametest" && num === 1) {
|
|
246
|
+
hasBetaApis = true;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const knownExperiments = {};
|
|
252
|
+
for (const def of KNOWN_EXPERIMENTS) {
|
|
253
|
+
let enabled = false;
|
|
254
|
+
if (def.isRootTag) {
|
|
255
|
+
const rootTag = parsed.rootChildren.get(def.nbtKey);
|
|
256
|
+
if (rootTag && Number(rootTag.value) === 1) {
|
|
257
|
+
enabled = true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const keysToCheck = [def.nbtKey, ...def.aliases ?? []];
|
|
261
|
+
for (const k of keysToCheck) {
|
|
262
|
+
if (experiments[k] === 1) {
|
|
263
|
+
enabled = true;
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
knownExperiments[def.id] = enabled;
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
experimentsEverUsed,
|
|
271
|
+
savedWithToggledExperiments,
|
|
272
|
+
experiments,
|
|
273
|
+
hasBetaApis,
|
|
274
|
+
knownExperiments
|
|
275
|
+
};
|
|
276
|
+
} catch {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function createByteTagBuffer(name, value) {
|
|
281
|
+
const nameBuf = Buffer.from(name, "utf8");
|
|
282
|
+
const buf = Buffer.alloc(1 + 2 + nameBuf.length + 1);
|
|
283
|
+
buf.writeUInt8(TAG_BYTE, 0);
|
|
284
|
+
buf.writeUInt16LE(nameBuf.length, 1);
|
|
285
|
+
nameBuf.copy(buf, 3);
|
|
286
|
+
buf.writeInt8(value, 3 + nameBuf.length);
|
|
287
|
+
return buf;
|
|
288
|
+
}
|
|
289
|
+
function createCompoundTagBuffer(name, byteTags) {
|
|
290
|
+
const nameBuf = Buffer.from(name, "utf8");
|
|
291
|
+
const childBuffers = [];
|
|
292
|
+
for (const [k, v] of Object.entries(byteTags)) {
|
|
293
|
+
childBuffers.push(createByteTagBuffer(k, v));
|
|
294
|
+
}
|
|
295
|
+
childBuffers.push(Buffer.from([TAG_END]));
|
|
296
|
+
const header = Buffer.alloc(1 + 2 + nameBuf.length);
|
|
297
|
+
header.writeUInt8(TAG_COMPOUND, 0);
|
|
298
|
+
header.writeUInt16LE(nameBuf.length, 1);
|
|
299
|
+
nameBuf.copy(header, 3);
|
|
300
|
+
return Buffer.concat([header, ...childBuffers]);
|
|
301
|
+
}
|
|
302
|
+
async function enableExperimentsInLevelDat(levelDatPath, targetExperimentIds) {
|
|
303
|
+
if (!fs.existsSync(levelDatPath)) {
|
|
304
|
+
return { success: false, changed: false, error: `File not found: ${levelDatPath}` };
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
const raw = await fs.promises.readFile(levelDatPath);
|
|
308
|
+
const parsed = parseBedrockLevelDat(raw);
|
|
309
|
+
let targetDefs = [];
|
|
310
|
+
if (!targetExperimentIds || targetExperimentIds === "all") {
|
|
311
|
+
targetDefs = [...KNOWN_EXPERIMENTS];
|
|
312
|
+
} else {
|
|
313
|
+
const seen = /* @__PURE__ */ new Set();
|
|
314
|
+
for (const id of targetExperimentIds) {
|
|
315
|
+
const def = resolveExperimentDefinition(id);
|
|
316
|
+
if (def && !seen.has(def.id)) {
|
|
317
|
+
seen.add(def.id);
|
|
318
|
+
targetDefs.push(def);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (targetDefs.length === 0) {
|
|
322
|
+
return { success: true, changed: false, enabledExperiments: [] };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const expCompoundTag = parsed.rootChildren.get("experiments");
|
|
326
|
+
const existingExpChildren = expCompoundTag && expCompoundTag.type === TAG_COMPOUND ? parseCompoundChildren(raw, expCompoundTag.valueOffset).children : /* @__PURE__ */ new Map();
|
|
327
|
+
let needsChange = false;
|
|
328
|
+
const enabledIds = [];
|
|
329
|
+
if (!expCompoundTag) {
|
|
330
|
+
needsChange = true;
|
|
331
|
+
} else {
|
|
332
|
+
const expEverUsed = existingExpChildren.get("experiments_ever_used");
|
|
333
|
+
if (!expEverUsed || Number(expEverUsed.value) !== 1) needsChange = true;
|
|
334
|
+
const expSaved = existingExpChildren.get("saved_with_toggled_experiments");
|
|
335
|
+
if (!expSaved || Number(expSaved.value) !== 1) needsChange = true;
|
|
336
|
+
}
|
|
337
|
+
const rootEverUsed = parsed.rootChildren.get("experiments_ever_used");
|
|
338
|
+
if (rootEverUsed && Number(rootEverUsed.value) !== 1) needsChange = true;
|
|
339
|
+
const rootSaved = parsed.rootChildren.get("saved_with_toggled_experiments");
|
|
340
|
+
if (rootSaved && Number(rootSaved.value) !== 1) needsChange = true;
|
|
341
|
+
let previousBetaApisState = false;
|
|
342
|
+
const currentGametest = existingExpChildren.get("gametest");
|
|
343
|
+
if (currentGametest && Number(currentGametest.value) === 1) {
|
|
344
|
+
previousBetaApisState = true;
|
|
345
|
+
}
|
|
346
|
+
for (const def of targetDefs) {
|
|
347
|
+
enabledIds.push(def.id);
|
|
348
|
+
if (def.isRootTag) {
|
|
349
|
+
const rootTag = parsed.rootChildren.get(def.nbtKey);
|
|
350
|
+
if (!rootTag || Number(rootTag.value) !== 1) {
|
|
351
|
+
needsChange = true;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (!def.isRootTag) {
|
|
355
|
+
const tag = existingExpChildren.get(def.nbtKey);
|
|
356
|
+
if (!tag || Number(tag.value) !== 1) {
|
|
357
|
+
needsChange = true;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (def.extraExpTags) {
|
|
361
|
+
for (const extra of def.extraExpTags) {
|
|
362
|
+
const tag = existingExpChildren.get(extra);
|
|
363
|
+
if (!tag || Number(tag.value) !== 1) {
|
|
364
|
+
needsChange = true;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (!needsChange) {
|
|
370
|
+
return {
|
|
371
|
+
success: true,
|
|
372
|
+
changed: false,
|
|
373
|
+
previousBetaApisState,
|
|
374
|
+
enabledExperiments: enabledIds
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
const backupPath = `${levelDatPath}.bak`;
|
|
378
|
+
await fs.promises.copyFile(levelDatPath, backupPath);
|
|
379
|
+
let mutatedRaw = Buffer.from(raw);
|
|
380
|
+
let nbtBody;
|
|
381
|
+
const requiredExpTags = {
|
|
382
|
+
experiments_ever_used: 1,
|
|
383
|
+
saved_with_toggled_experiments: 1
|
|
384
|
+
};
|
|
385
|
+
for (const def of targetDefs) {
|
|
386
|
+
if (!def.isRootTag) {
|
|
387
|
+
requiredExpTags[def.nbtKey] = 1;
|
|
388
|
+
}
|
|
389
|
+
if (def.extraExpTags) {
|
|
390
|
+
for (const extra of def.extraExpTags) {
|
|
391
|
+
requiredExpTags[extra] = 1;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const rootNeedsEducation = targetDefs.some((d) => d.id === "education");
|
|
396
|
+
if (rootEverUsed) {
|
|
397
|
+
mutatedRaw.writeInt8(1, rootEverUsed.valueOffset);
|
|
398
|
+
}
|
|
399
|
+
if (rootSaved) {
|
|
400
|
+
mutatedRaw.writeInt8(1, rootSaved.valueOffset);
|
|
401
|
+
}
|
|
402
|
+
if (rootNeedsEducation) {
|
|
403
|
+
const eduTag = parsed.rootChildren.get("educationFeaturesEnabled");
|
|
404
|
+
if (eduTag) {
|
|
405
|
+
mutatedRaw.writeInt8(1, eduTag.valueOffset);
|
|
406
|
+
}
|
|
407
|
+
const optOut = parsed.rootChildren.get("has_opted_out_of_education");
|
|
408
|
+
if (optOut) {
|
|
409
|
+
mutatedRaw.writeInt8(0, optOut.valueOffset);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const rootTagsToAppend = [];
|
|
413
|
+
if (rootNeedsEducation && !parsed.rootChildren.has("educationFeaturesEnabled")) {
|
|
414
|
+
rootTagsToAppend.push(createByteTagBuffer("educationFeaturesEnabled", 1));
|
|
415
|
+
}
|
|
416
|
+
if (!expCompoundTag) {
|
|
417
|
+
const newExpCompoundBuf = createCompoundTagBuffer("experiments", requiredExpTags);
|
|
418
|
+
const rootEndTagIndex = parsed.rootEndOffset - 1;
|
|
419
|
+
const beforeEnd = mutatedRaw.subarray(8, rootEndTagIndex);
|
|
420
|
+
const afterEnd = mutatedRaw.subarray(rootEndTagIndex);
|
|
421
|
+
nbtBody = Buffer.concat([beforeEnd, ...rootTagsToAppend, newExpCompoundBuf, afterEnd]);
|
|
422
|
+
} else {
|
|
423
|
+
const { compoundEndOffset: expEndOffset } = parseCompoundChildren(raw, expCompoundTag.valueOffset);
|
|
424
|
+
const expEndTagIndex = expEndOffset - 1;
|
|
425
|
+
const rootEndTagIndex = parsed.rootEndOffset - 1;
|
|
426
|
+
const expTagsToAppend = [];
|
|
427
|
+
for (const [name, val] of Object.entries(requiredExpTags)) {
|
|
428
|
+
const existing = existingExpChildren.get(name);
|
|
429
|
+
if (existing) {
|
|
430
|
+
mutatedRaw.writeInt8(val, existing.valueOffset);
|
|
431
|
+
} else {
|
|
432
|
+
expTagsToAppend.push(createByteTagBuffer(name, val));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const part1 = mutatedRaw.subarray(8, expEndTagIndex);
|
|
436
|
+
const part2 = Buffer.concat(expTagsToAppend);
|
|
437
|
+
const part3 = mutatedRaw.subarray(expEndTagIndex, rootEndTagIndex);
|
|
438
|
+
const part4 = Buffer.concat(rootTagsToAppend);
|
|
439
|
+
const part5 = mutatedRaw.subarray(rootEndTagIndex);
|
|
440
|
+
nbtBody = Buffer.concat([part1, part2, part3, part4, part5]);
|
|
441
|
+
}
|
|
442
|
+
const newHeader = Buffer.alloc(8);
|
|
443
|
+
newHeader.writeUInt32LE(parsed.version, 0);
|
|
444
|
+
newHeader.writeUInt32LE(nbtBody.length, 4);
|
|
445
|
+
const finalFileBuffer = Buffer.concat([newHeader, nbtBody]);
|
|
446
|
+
const tmpFile = `${levelDatPath}.${process.pid}.tmp`;
|
|
447
|
+
await fs.promises.writeFile(tmpFile, finalFileBuffer);
|
|
448
|
+
await fs.promises.rename(tmpFile, levelDatPath);
|
|
449
|
+
return {
|
|
450
|
+
success: true,
|
|
451
|
+
changed: true,
|
|
452
|
+
backupPath,
|
|
453
|
+
previousBetaApisState,
|
|
454
|
+
enabledExperiments: enabledIds
|
|
455
|
+
};
|
|
456
|
+
} catch (err) {
|
|
457
|
+
return {
|
|
458
|
+
success: false,
|
|
459
|
+
changed: false,
|
|
460
|
+
error: err instanceof Error ? err.message : String(err)
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
async function enableBetaApisInLevelDat(levelDatPath) {
|
|
465
|
+
return await enableExperimentsInLevelDat(levelDatPath, ["gametest"]);
|
|
466
|
+
}
|
|
467
|
+
export {
|
|
468
|
+
KNOWN_EXPERIMENTS,
|
|
469
|
+
TAG_BYTE,
|
|
470
|
+
TAG_BYTE_ARRAY,
|
|
471
|
+
TAG_COMPOUND,
|
|
472
|
+
TAG_DOUBLE,
|
|
473
|
+
TAG_END,
|
|
474
|
+
TAG_FLOAT,
|
|
475
|
+
TAG_INT,
|
|
476
|
+
TAG_INT_ARRAY,
|
|
477
|
+
TAG_LIST,
|
|
478
|
+
TAG_LONG,
|
|
479
|
+
TAG_LONG_ARRAY,
|
|
480
|
+
TAG_SHORT,
|
|
481
|
+
TAG_STRING,
|
|
482
|
+
enableBetaApisInLevelDat,
|
|
483
|
+
enableExperimentsInLevelDat,
|
|
484
|
+
parseBedrockLevelDat,
|
|
485
|
+
parseCompoundChildren,
|
|
486
|
+
readLevelDatExperiments,
|
|
487
|
+
resolveExperimentDefinition,
|
|
488
|
+
skipNbtPayload
|
|
489
|
+
};
|
|
490
|
+
//# sourceMappingURL=level-dat.js.map
|