Verify and restore the complete Minecraft asset cache
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 8m10s
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 8m10s
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import { unzipSync } from "fflate";
|
||||
export const assetsRoot = ".cache/minecraft/1.21.11";
|
||||
@@ -24,13 +24,38 @@ async function assetsReady(minecraftVersion) {
|
||||
try {
|
||||
const marker = JSON.parse(await readFile(completionMarker, "utf8"));
|
||||
if (
|
||||
marker.format !== "minegeo-assets/v2" ||
|
||||
marker.format !== "minegeo-assets/v3" ||
|
||||
marker.minecraftVersion !== minecraftVersion ||
|
||||
!marker.files
|
||||
!marker.files ||
|
||||
!Array.isArray(marker.entries) ||
|
||||
marker.entries.length !== marker.files
|
||||
)
|
||||
return false;
|
||||
// A blockstate alone is insufficient: interrupted setup can leave models
|
||||
// and textures absent. The marker is only written after all extraction.
|
||||
// Verify the complete extracted inventory, not just common blocks. A
|
||||
// partial cache otherwise silently drops newer or less common textures.
|
||||
let next = 0;
|
||||
await Promise.all(
|
||||
Array.from({ length: 16 }, async () => {
|
||||
while (next < marker.entries.length) {
|
||||
const entry = marker.entries[next++];
|
||||
if (
|
||||
!Array.isArray(entry) ||
|
||||
entry.length !== 2 ||
|
||||
typeof entry[0] !== "string" ||
|
||||
!/^assets\/minecraft\/(models|blockstates|textures)\/[a-z0-9_./-]+\.(json|png|mcmeta)$/.test(
|
||||
entry[0],
|
||||
) ||
|
||||
entry[0].includes("..") ||
|
||||
!Number.isSafeInteger(entry[1]) ||
|
||||
entry[1] < 1
|
||||
)
|
||||
throw new Error("Invalid asset inventory");
|
||||
const info = await stat(`${assetsRoot}/${entry[0]}`);
|
||||
if (!info.isFile() || info.size !== entry[1])
|
||||
throw new Error("Missing asset");
|
||||
}
|
||||
}),
|
||||
);
|
||||
for (const asset of requiredAssets) {
|
||||
const bytes = await readFile(`${assetsRoot}/assets/minecraft/${asset}`);
|
||||
if (asset.endsWith(".json")) JSON.parse(bytes.toString());
|
||||
@@ -96,23 +121,41 @@ export async function prepareAssets(minecraftVersion = "1.21.11") {
|
||||
}
|
||||
const entries = unzipSync(jar, {
|
||||
filter: (f) =>
|
||||
/^assets\/minecraft\/(models|blockstates|textures\/block)\//.test(
|
||||
f.name,
|
||||
) && /\.(json|png|mcmeta)$/.test(f.name),
|
||||
/^assets\/minecraft\/(models|blockstates|textures)\//.test(f.name) &&
|
||||
/\.(json|png|mcmeta)$/.test(f.name),
|
||||
});
|
||||
for (const [name, bytes] of Object.entries(entries)) {
|
||||
const path = `${assetsRoot}/${name}`;
|
||||
await mkdir(path.slice(0, path.lastIndexOf("/")), { recursive: true });
|
||||
await writeFile(path, bytes);
|
||||
}
|
||||
const files = Object.entries(entries);
|
||||
const directories = new Set(
|
||||
files.map(
|
||||
([name]) => `${assetsRoot}/${name.slice(0, name.lastIndexOf("/"))}`,
|
||||
),
|
||||
);
|
||||
await Promise.all(
|
||||
[...directories].map((path) => mkdir(path, { recursive: true })),
|
||||
);
|
||||
let nextFile = 0;
|
||||
const writes = await Promise.allSettled(
|
||||
Array.from({ length: Math.min(16, files.length) }, async () => {
|
||||
while (nextFile < files.length) {
|
||||
const [name, bytes] = files[nextFile++];
|
||||
await writeFile(`${assetsRoot}/${name}`, bytes);
|
||||
}
|
||||
}),
|
||||
);
|
||||
const failedWrite = writes.find((result) => result.status === "rejected");
|
||||
if (failedWrite?.status === "rejected") throw failedWrite.reason;
|
||||
// Completion is recorded last, so a killed extraction is repaired next time.
|
||||
await writeFile(
|
||||
completionMarker,
|
||||
JSON.stringify({
|
||||
format: "minegeo-assets/v2",
|
||||
format: "minegeo-assets/v3",
|
||||
minecraftVersion,
|
||||
clientSha1: version.downloads.client.sha1,
|
||||
files: Object.keys(entries).length,
|
||||
entries: Object.entries(entries).map(([name, bytes]) => [
|
||||
name,
|
||||
bytes.length,
|
||||
]),
|
||||
}),
|
||||
);
|
||||
if (!(await assetsReady(minecraftVersion)))
|
||||
|
||||
@@ -23,6 +23,8 @@ const files = {
|
||||
"textures/block/dirt.png": png,
|
||||
"textures/block/stone.png": png,
|
||||
"textures/block/water_still.png": png,
|
||||
"textures/block/newer_fixture.png": png,
|
||||
"textures/entity/newer_fixture.png": png,
|
||||
"textures/block/water_still.png.mcmeta": Buffer.from(
|
||||
'{"animation":{"frametime":2}}',
|
||||
),
|
||||
@@ -145,6 +147,17 @@ test("a complete marker with a missing texture is repaired from the verified cac
|
||||
assert.equal(requests.filter((url) => url.endsWith("client.jar")).length, 0);
|
||||
});
|
||||
|
||||
test("missing non-core 26.2 textures are restored from the verified client", async (t) => {
|
||||
const requests = await isolatedCache(t, false, "26.2");
|
||||
const directory = await prepareAssets("26.2");
|
||||
const path = `${directory}/assets/minecraft/textures/block/newer_fixture.png`;
|
||||
await rm(path);
|
||||
requests.length = 0;
|
||||
await prepareAssets("26.2");
|
||||
assert.deepEqual(await readFile(path), png);
|
||||
assert.equal(requests.filter((url) => url.endsWith("client.jar")).length, 0);
|
||||
});
|
||||
|
||||
test("a corrupted cached client is downloaded again", async (t) => {
|
||||
const requests = await isolatedCache(t);
|
||||
await mkdir(".cache/minecraft", { recursive: true });
|
||||
|
||||
Reference in New Issue
Block a user