New modding system
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
// deno task pack-mod mods/copper_tools [copper_tools.bmod]
|
||||
// builds a mod folder into a .bmod: its data merged into data.json, each script bundled into one module, and its
|
||||
// textures and credits. deno task build does this for every mod in mods/, see "Mod files" in MODS.md
|
||||
import { basename, dirname, resolve } from "@std/path";
|
||||
import { type Bmod, BMOD_EXTENSION, SCRIPT_SIDES, type ScriptSide, write_bmod } from "$/common/bmod.ts";
|
||||
import type { ManifestJson } from "$/common/mod_data.ts";
|
||||
import { load_and_check, type LoadedMod } from "./check_mods.ts";
|
||||
|
||||
// what each script is bundled for: the server runs in deno, clients in browsers, worldgen in both
|
||||
const PLATFORMS: Record<ScriptSide, "deno" | "browser"> = { server: "deno", client: "browser", worldgen: "browser" };
|
||||
|
||||
// a mod that passed check-mods, as .bmod bytes
|
||||
export async function pack_mod(mod: LoadedMod): Promise<Uint8Array<ArrayBuffer>> {
|
||||
const manifest = mod.manifest as unknown as ManifestJson;
|
||||
const scripts: Bmod["scripts"] = {};
|
||||
for (const side of SCRIPT_SIDES) {
|
||||
const entry = manifest.scripts?.[side];
|
||||
if (entry) scripts[side] = await bundle_script(mod.dir, `${mod.dir}/${entry}`, PLATFORMS[side]);
|
||||
}
|
||||
|
||||
const textures = new Map<string, Uint8Array>();
|
||||
for (const [id, path] of [...mod.texture_files].sort(([a], [b]) => a.localeCompare(b))) {
|
||||
textures.set(id, Deno.readFileSync(path));
|
||||
}
|
||||
|
||||
return write_bmod({
|
||||
manifest,
|
||||
data: {
|
||||
blocks: mod.blocks.map((b) => b.json),
|
||||
models: mod.models.map((m) => m.json),
|
||||
items: mod.items.map((i) => i.json),
|
||||
recipes: mod.recipes.map((r) => r.json),
|
||||
ores: mod.ores.map((o) => o.json),
|
||||
},
|
||||
scripts,
|
||||
textures,
|
||||
credits: manifest.credits ? Deno.readTextFileSync(`${mod.dir}/${manifest.credits}`) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
// a mod is a deno project: with its own deno.json its scripts are bundled with it (its imports, npm packages and
|
||||
// so on), otherwise with the deno.json of the folder the build runs in
|
||||
async function bundle_script(mod_dir: string, entry: string, platform: "deno" | "browser"): Promise<string> {
|
||||
const config = ["deno.json", "deno.jsonc"].map((name) => `${mod_dir}/${name}`).find(exists);
|
||||
if (!config) {
|
||||
const result = await Deno.bundle({ entrypoints: [entry], platform, write: false, minify: false });
|
||||
if (!result.success || !result.outputFiles?.length) {
|
||||
throw new Error(`Couldn't bundle ${entry}:\n${result.errors.map((e) => e.text).join("\n")}`);
|
||||
}
|
||||
return result.outputFiles[0].text();
|
||||
}
|
||||
|
||||
const output = await Deno.makeTempFile({ suffix: ".js" });
|
||||
try {
|
||||
const result = await new Deno.Command(Deno.execPath(), {
|
||||
args: ["bundle", "--config", config, "--platform", platform, "--output", output, entry],
|
||||
stdout: "piped",
|
||||
stderr: "piped",
|
||||
env: { NO_COLOR: "1" },
|
||||
}).output();
|
||||
if (!result.success) {
|
||||
throw new Error(`Couldn't bundle ${entry}:\n${new TextDecoder().decode(result.stderr).trim()}`);
|
||||
}
|
||||
return Deno.readTextFileSync(output);
|
||||
} finally {
|
||||
Deno.removeSync(output);
|
||||
}
|
||||
}
|
||||
|
||||
function exists(path: string) {
|
||||
try {
|
||||
Deno.statSync(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
const [dir, out] = Deno.args;
|
||||
if (!dir) {
|
||||
console.error("usage: deno task pack-mod <mod folder> [output.bmod]");
|
||||
Deno.exit(1);
|
||||
}
|
||||
// its siblings are checked too, so references to mods it depends on can be checked
|
||||
const folder = resolve(dir);
|
||||
const mod = load_and_check(dirname(folder)).find((m) => resolve(m.dir) === folder);
|
||||
if (!mod) {
|
||||
console.error(`${dir} isn't a mod folder`);
|
||||
Deno.exit(1);
|
||||
}
|
||||
if (mod.report.errors.length > 0) {
|
||||
console.error(`${mod.id} has errors (deno task check-mods for details):`);
|
||||
for (const error of mod.report.errors) console.error(` ${error}`);
|
||||
Deno.exit(1);
|
||||
}
|
||||
const file = out ?? `${basename(folder)}${BMOD_EXTENSION}`;
|
||||
const bytes = await pack_mod(mod);
|
||||
Deno.writeFileSync(file, bytes);
|
||||
console.log(`Packed ${mod.id} into ${file} (${(bytes.length / 1024).toFixed(1)} KB)`);
|
||||
}
|
||||
Reference in New Issue
Block a user