trueno/src/resource_mirror.jai
2026-08-02 11:42:00 +03:00

163 lines
6.1 KiB
Plaintext

// Packs are built from a throwaway copy of the resource trees, not from the
// trees themselves:
//
// wipe .build/generated -> copy the trees in -> process the copy -> pack it
//
// Processing (currently just the Aseprite export) writes its output into the
// copy, so the real resource trees only ever hold sources. Rebuilding the copy
// from scratch every time is what keeps this simple: there is no stale output to
// detect and nothing to prune, because a deleted source is simply not there.
//
// Used by both the build (meta/pack.jai) and the runtime hot-reloader
// (pack_hotreload.jai), so they process resources identically.
#import "Basic";
#import "String";
#import "Process";
#import "File";
#import "File_Utilities";
// Not ".build/generated/resources": pack.jai derives the pack name by splitting
// on the *first* "/resources/", so a "resources" segment here would shadow the
// real one and yield pack "game" instead of "game_core".
MIRROR_ROOT :: ".build/generated";
ASEPRITE_SOURCE_EXTENSIONS :: string.["aseprite", "ase"];
is_aseprite_source :: (short_name: string) -> bool {
ok, base, extension := split_from_right(short_name, #char ".");
if !ok return false;
for ASEPRITE_SOURCE_EXTENSIONS if extension == it return true;
return false;
}
// Wipes and rebuilds the mirror, runs the processing steps over it, and returns
// the mirrored resource roots to pack in place of the real ones.
prepare_resource_mirror :: (use_test_game: bool) -> [] string {
run_command("rm", "-rf", MIRROR_ROOT);
game_tree := ifx use_test_game then "test_game" else "game";
roots: [..] string;
roots.allocator = temp;
array_add(*roots, copy_tree_into("./resources", MIRROR_ROOT));
array_add(*roots, copy_tree_into(tprint("./%/resources", game_tree), tprint("%/%", MIRROR_ROOT, game_tree)));
export_aseprite_sheets(roots);
return roots;
}
// "cp -r ./game/resources .build/generated/game" -> ".build/generated/game/resources"
copy_tree_into :: (source: string, destination_parent: string) -> string {
make_directory_if_it_does_not_exist(destination_parent, recursive = true);
run_command("cp", "-r", source, destination_parent);
ok, parent, name := split_from_right(source, #char "/");
return tprint("%/%", destination_parent, name);
}
// Aseprite's CLI, reproducing byte for byte what its "Export Sprite Sheet"
// dialog was producing for the sheets that used to be checked in.
// --format json-array and --list-tags are load-bearing: asset_manager.jai parses
// `frames` as an array and drives Animation off `meta.frameTags`.
ASEPRITE_EXPORT_FLAGS :: string.[
"--format", "json-array",
"--list-tags",
"--list-layers",
"--list-slices",
];
// "player.aseprite" -> player.sheet.png / player.sheet.json
// "anim.sheet.aseprite" -> anim.sheet.png / anim.sheet.json
//
// The doubled-up ".sheet" is dropped because asset_manager.jai keys sheets on
// everything before the *first* dot, so "anim.sheet.sheet.png" would load as a
// sheet named "anim" with extension "sheet.sheet.png" and be rejected.
sheet_output_paths :: (source: string) -> (png: string, json: string) {
ok, stem := split_from_right(source, #char ".");
if ends_with(stem, ".sheet") {
stem.count -= ".sheet".count;
}
return tprint("%.sheet.png", stem), tprint("%.sheet.json", stem);
}
find_aseprite_binary :: () -> (path: string, found: bool) {
candidates: [..] string;
candidates.allocator = temp;
#if OS == .LINUX || OS == .MACOS {
POSIX :: #import "POSIX";
read_env :: (name: string) -> string {
value := POSIX.getenv(name.data);
if !value return "";
return to_string(value);
}
// ASEPRITE points the build at a different install without editing this.
override := read_env("ASEPRITE");
if override array_add(*candidates, override);
home := read_env("HOME");
if home array_add(*candidates, tprint("%/bin/aseprite", home));
}
#if OS == .MACOS {
array_add(*candidates, "/Applications/Aseprite.app/Contents/MacOS/aseprite");
}
array_add(*candidates, "aseprite"); // whatever is on PATH
for candidates {
// Probing by running it also catches a path that exists but isn't executable.
result := run_command(it, "--version", capture_and_return_output = true);
if result.type == .EXITED && result.exit_code == 0 return it, true;
}
return "", false;
}
collect_aseprite_sources :: (root: string, sources: *[..] string) {
if !is_directory(root) return;
visit_files(root, true, sources, (info: *File_Visit_Info, sources: *[..] string) {
if info.is_directory return;
if !is_aseprite_source(info.short_name) return;
array_add(sources, copy_string(info.full_name));
});
}
// Exports every Aseprite file in the mirror to a sheet pair beside it. pack.jai
// skips the sources themselves, so only the exports end up in the pack.
export_aseprite_sheets :: (roots: [] string) {
sources: [..] string;
sources.allocator = temp;
for roots collect_aseprite_sources(it, *sources);
if !sources return;
aseprite, found := find_aseprite_binary();
if !found {
log_error("Aseprite export: % source file(s) to export but no aseprite binary was found.", sources.count);
log_error("Looked for $ASEPRITE, $HOME/bin/aseprite, and 'aseprite' on PATH.");
exit(1);
}
for source: sources {
png, json := sheet_output_paths(source);
args: [..] string;
args.allocator = temp;
array_add(*args, aseprite, "-b", source, "--sheet", png, "--data", json);
for flag: ASEPRITE_EXPORT_FLAGS array_add(*args, flag);
result, output, error := run_command(..args, capture_and_return_output = true);
if result.type != .EXITED || result.exit_code != 0 {
log_error("Aseprite export failed for %", source);
if output log_error(output,, logger = runtime_support_default_logger);
if error log_error(error,, logger = runtime_support_default_logger);
exit(1);
}
}
print("Aseprite: exported % sheet(s)\n", sources.count);
}