Skip to content

Commit

Permalink
Improved Resource Building Greatly!
Browse files Browse the repository at this point in the history
  • Loading branch information
smell-of-curry committed Oct 9, 2024
1 parent 0d1b4cf commit d795d98
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 25 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
130 changes: 106 additions & 24 deletions scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -22,30 +27,47 @@ 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
* @param archive
*/
async function addPathToArchive(
pathToAdd: string,
archive: archiver.Archiver
archive: archiver.Archiver,
progress?: ProgressBar
): Promise<void> {
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);

Expand All @@ -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();

Expand All @@ -70,27 +93,63 @@ 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}`);
}
}

/**
* 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") {
Expand All @@ -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();
}

Expand All @@ -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);
Expand Down
43 changes: 42 additions & 1 deletion scripts/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

0 comments on commit d795d98

Please sign in to comment.