diff --git a/package.json b/package.json index 079fcfb36..f30124719 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,11 @@ }, "homepage": "https://github.com/smell-of-curry/pokebedrock-res#readme", "dependencies": { + "@types/progress": "^2.0.7", "archiver": "^6.0.0", "fs-extra": "^11.2.0", "inquirer": "^10.1.7", + "progress": "^2.0.3", "sharp": "^0.33.5" }, "devDependencies": { diff --git a/scripts/build.ts b/scripts/build.ts index 7acbccfd2..13dbd591e 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -3,7 +3,12 @@ import fsExtra from "fs-extra"; import archiver from "archiver"; import path from "path"; import sharp from "sharp"; -import { removeComments } from "./utils"; +import ProgressBar from "progress"; +import { + countFilesRecursively, + removeCommentsFromJSON, + removeCommentsFromLang, +} from "./utils"; /** * Files/Directories to exclude from build. @@ -22,8 +27,20 @@ const exclude = [ "package-lock.json", "package.json", "tsconfig.json", + "pokemon.json", ]; +/** + * Will be the resulting `contents.json` file. + */ +const contents: { content: { path: string }[] } = { content: [] }; + +/** + * A path to all the textures inside `./textures` directory. + * Used to generate `textures_list.json` + */ +const textures: string[] = []; + /** * Adds a path to a archive, and compressing files. * @param pathToAdd @@ -31,21 +48,26 @@ const exclude = [ */ async function addPathToArchive( pathToAdd: string, - archive: archiver.Archiver + archive: archiver.Archiver, + progress?: ProgressBar ): Promise { - if (fs.lstatSync(pathToAdd).isDirectory()) { - const items = fs.readdirSync(pathToAdd); + const pathStat = await fs.promises.lstat(pathToAdd); + const parsedPath = pathToAdd.replace(/\\/g, "/"); + + if (pathStat.isDirectory()) { + const items = await fs.promises.readdir(pathToAdd); for (const item of items) { - await addPathToArchive(path.join(pathToAdd, item), archive); + await addPathToArchive(path.join(pathToAdd, item), archive, progress); } - } else { + } else if (pathStat.isFile()) { + contents.content.push({ path: parsedPath }); const ext = pathToAdd.split(".").pop(); if (ext === "json") { // Compress JSON file try { const fileContents = await fsExtra.readFile(pathToAdd, "utf-8"); - const commentsRemoved = removeComments(fileContents); + const commentsRemoved = removeCommentsFromJSON(fileContents); const parsedJson = JSON.parse(commentsRemoved); const compressedContents = JSON.stringify(parsedJson); @@ -55,13 +77,14 @@ async function addPathToArchive( console.error(`Error compressing JSON: ${pathToAdd}:`, error); } } else if (ext === "png") { + if (pathToAdd.startsWith("textures")) textures.push(parsedPath); + // Compress PNG file try { const compressedBuffer = await sharp(pathToAdd) .png({ effort: 10, compressionLevel: 9, - quality: 100, }) .toBuffer(); @@ -70,9 +93,40 @@ async function addPathToArchive( } catch (error) { console.error(`Error compressing PNG: ${pathToAdd}:`, error); } + } else if (ext === "material") { + try { + const fileContents = await fsExtra.readFile(pathToAdd, "utf-8"); + const commentsRemoved = removeCommentsFromJSON(fileContents); + const parsedJson = JSON.parse(commentsRemoved); + let compressedContents = JSON.stringify(parsedJson, null, 2); + + // Replace Unix line endings with CRLF (Windows-style) + compressedContents = compressedContents.replace(/\n/g, "\r\n"); + + // Add the parsed Material content as a temporary file + archive.append(compressedContents, { name: pathToAdd }); + } catch (error) { + console.error(`Error parsing JSON (material): ${pathToAdd}:`, error); + } + } else if (ext === "lang") { + // Lang can have comments, and extra spaces, we want to remove those + try { + const fileContents = await fsExtra.readFile(pathToAdd, "utf-8"); + const commentsRemoved = removeCommentsFromLang(fileContents); + const compressedContents = commentsRemoved; + + // Add the compressed Lang content as a temporary file + archive.append(compressedContents, { name: pathToAdd }); + } catch (error) { + console.error(`Error compressing Lang: ${pathToAdd}:`, error); + } } else { archive.file(pathToAdd, { name: pathToAdd }); } + + progress?.tick(); + } else { + console.warn(`[WARN] Unknown path type: ${pathToAdd}`); } } @@ -80,17 +134,22 @@ async function addPathToArchive( * Pipes all files in the current directory to a zip file. * @param fileName */ -async function pipeToFile(outputFileName: string) { - if (fs.existsSync(outputFileName)) fs.unlinkSync(outputFileName); - - const output = fs.createWriteStream(outputFileName); +async function pipeToFiles(outputFileNames: string[]) { const archive = archiver("zip", { zlib: { level: 9 } }); - output.on("close", () => { - console.log(archive.pointer() + " total bytes"); - console.log((archive.pointer() / 1024 ** 2).toFixed(2) + "MB"); - console.log(`Pack Archive created for ${outputFileName}!`); - }); + const outputs: fs.WriteStream[] = []; + for (const outputFileName of outputFileNames) { + if (fs.existsSync(outputFileName)) fs.unlinkSync(outputFileName); + const output = fs.createWriteStream(outputFileName); + + output.on("close", () => { + console.log(`Pack Archive created for ${outputFileName}!`); + console.log(" -" + archive.pointer() + " total bytes"); + console.log(" -" + (archive.pointer() / 1024 ** 2).toFixed(2) + "MB"); + }); + + outputs.push(output); + } archive.on("warning", (err) => { if (err.code === "ENOENT") { @@ -108,13 +167,37 @@ async function pipeToFile(outputFileName: string) { for (const path of paths) { if (exclude.includes(path)) continue; - if (path.startsWith(outputFileName.split(".")[0])) continue; - console.log(`Adding ${path} to '${outputFileName}'...`); - - await addPathToArchive(path, archive); + if ( + outputFileNames.some((outputFileName) => + path.startsWith(outputFileName.split(".")[0]) + ) + ) + continue; + + if (fs.lstatSync(path).isDirectory()) { + const totalFiles = countFilesRecursively(path); + console.log(`Adding ${totalFiles}x files from '${path}' ...`); + + // Initialize the progress bar + const progress = new ProgressBar("Archiving [:bar] :percent :etas", { + total: totalFiles, + width: 40, + complete: "=", + incomplete: " ", + renderThrottle: 16, + }); + + await addPathToArchive(path, archive, progress); + } else { + await addPathToArchive(path, archive); + } } - archive.pipe(output); + archive.append(JSON.stringify(contents), { name: "contents.json" }); + archive.append(JSON.stringify(textures), { + name: "textures/textures_list.json", + }); + for (const output of outputs) archive.pipe(output); await archive.finalize(); } @@ -129,8 +212,7 @@ async function pipeToFile(outputFileName: string) { const version = manifest.header.version.join("."); const fileName = `PokeBedrock RES ${version}`; - await pipeToFile(`${fileName}.mcpack`); - await pipeToFile(`${fileName}.zip`); + await pipeToFiles([`${fileName}.zip`, `${fileName}.mcpack`]); } catch (error) { console.error("Error:", error); process.exit(1); diff --git a/scripts/utils.ts b/scripts/utils.ts index 5b7a66995..97be18e76 100644 --- a/scripts/utils.ts +++ b/scripts/utils.ts @@ -108,7 +108,7 @@ type LogType = * @param jsonData * @returns */ -export function removeComments(jsonData: string): string { +export function removeCommentsFromJSON(jsonData: string): string { let result = ""; let inString = false; let inSingleLineComment = false; @@ -157,3 +157,44 @@ export function removeComments(jsonData: string): string { return result; } +/** + * Removes comments & spaces from a `.lang` file, returning a clean result in CRLF format. + * Supports comments starting with two or more `#` and in-line comments. + * @param langData + */ +export function removeCommentsFromLang(langData: string): string { + // Split the data into lines + const lines = langData.split(/\r?\n/); + + const cleanedLines = lines + .map(line => { + // Remove any in-line comments that have two or more # (e.g., ##, ###, etc.) + const noInlineComments = line.split(/#{2,}/)[0].trim(); + + // Return the line only if it's not empty and not a full-line comment (starting with ## or more #) + return noInlineComments.length > 0 && !noInlineComments.match(/^#{2,}/) + ? noInlineComments + : null; + }) + .filter(Boolean); // Remove null values + + // Join the cleaned lines with CRLF (\r\n) to maintain Windows-style line breaks + return cleanedLines.join("\r\n"); +} + +/** + * Function to count the total files and folders recursively. + */ +export function countFilesRecursively(directory: string): number { + let count = 0; + const items = fs.readdirSync(directory); + for (const item of items) { + const fullPath = path.join(directory, item); + if (fs.lstatSync(fullPath).isDirectory()) { + count += countFilesRecursively(fullPath); + } else { + count += 1; + } + } + return count; +} \ No newline at end of file