Skip to content
2 changes: 2 additions & 0 deletions src/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import fromSb3, { fromSb3JSON } from "./io/sb3/fromSb3";
import toSb3 from "./io/sb3/toSb3";
import toLeopard from "./io/leopard/toLeopard";
import toScratchblocks from "./io/scratchblocks/toScratchblocks";
import toPatch from "./io/patch/toPatch";

export type TextToSpeechLanguage =
| "ar"
Expand Down Expand Up @@ -37,6 +38,7 @@ export default class Project {
public toSb3 = toSb3.bind(null, this);
public toLeopard = toLeopard.bind(null, this);
public toScratchblocks = toScratchblocks.bind(null, this);
public toPatch = toPatch.bind(null, this);

public stage: Stage = new Stage();
public sprites: Sprite[] = [];
Expand Down
53 changes: 51 additions & 2 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ program
.requiredOption("-i, --input <path>", "The path to the input project")
.addOption(new Option("-it, --input-type <type>", "The type of input file").choices(["sb3"]))
.requiredOption("-o, --output <path>", "The path to the output project")
.addOption(new Option("-ot, --output-type <type>", "The type of output file").choices(["leopard", "leopard-zip"]))
.addOption(
new Option("-ot, --output-type <type>", "The type of output file").choices(["leopard", "leopard-zip", "patch"])
)
.addOption(new Option("-t, --trace", "Show a detailed error trace"))
.addOption(
new Option("--leopard-url <url>", "The URL to use for Leopard").default("https://unpkg.com/leopard@^1/dist/")
Expand All @@ -28,7 +30,7 @@ const options: {
input: string;
inputType: "sb3";
output: string;
outputType: "leopard" | "leopard-zip";
outputType: "leopard" | "leopard-zip" | "patch";
trace: boolean | undefined;
leopardUrl: string;
} = program.opts();
Expand Down Expand Up @@ -71,6 +73,8 @@ try {
throw new InferTypeError("output", "Scratch 3.0 output projects are not currently supported.");
} else if (path.extname(output) === "") {
outputType = "leopard";
} else if (output.endsWith(".ptch1")) {
outputType = "patch";
} else {
throw new InferTypeError("output", "Could not infer output type.");
}
Expand Down Expand Up @@ -188,6 +192,10 @@ async function run() {
});
}

function toPatch() {
return project.toPatch({});
}

switch (outputType) {
case "leopard": {
const leopard = await writeStep(`${chalk.bold("Converting")} project to ${chalk.white("Leopard")}.`, toLeopard);
Expand Down Expand Up @@ -303,6 +311,47 @@ async function run() {
zip.generateNodeStream({ type: "nodebuffer", streamFiles: true }).pipe(createWriteStream(fullOutputPath));
});

break;
}
case "patch": {
const patch = await writeStep(`${chalk.bold("Converting")} project to ${chalk.white("Patch")}.`, toPatch);

const fullOutputPath = path.resolve(process.cwd(), output);

await writeStep(`${chalk.bold("Exporting")} project to zip file ${chalk.white(fullOutputPath)}.`, async () => {
// First, check if file name is already taken
try {
await fs.access(fullOutputPath);
throw new StepError("Output file already exists.");
} catch (err) {
if (err instanceof Object && "code" in err && err.code === "ENOENT") {
// File does not exist, good
} else {
throw err;
}
}

const zip = new JSZip();

zip.file("project.json", Buffer.from(patch.json));

for (const target of [project.stage, ...project.sprites]) {
for (const costume of target.costumes) {
const filename = `${costume.md5}.${costume.ext}`;
const asset = Buffer.from(costume.asset as ArrayBuffer);
zip.file(filename, asset);
}

for (const sound of target.sounds) {
const filename = `${sound.md5}.${sound.ext}`;
const asset = Buffer.from(sound.asset as ArrayBuffer);
zip.file(filename, asset);
}
}

zip.generateNodeStream({ type: "nodebuffer", streamFiles: true }).pipe(createWriteStream(fullOutputPath));
});

break;
}
}
Expand Down
Loading