diff --git a/implementations/Crafter.Build-Clang.cpp b/implementations/Crafter.Build-Clang.cpp index fc7e636..3880b21 100644 --- a/implementations/Crafter.Build-Clang.cpp +++ b/implementations/Crafter.Build-Clang.cpp @@ -292,36 +292,82 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map std::optional { - std::string ext = asset.extension().string(); - if (ext == ".png") return fs::path(asset.filename()).replace_extension(".ctex"); - if (ext == ".obj") return fs::path(asset.filename()).replace_extension(".cmesh"); + // Asset compilation: each cfg.assets entry is either a single .png/.obj + // file (flat output: outputDir/.ctex/cmesh — preserves the + // original behavior) or a directory (recursed, with the relative tree + // mirrored under outputDir//; .png/.obj are compressed, every + // other file is copied through unchanged). Directory mode lets mod/map + // trees keep their nested layout so mod.json paths like + // "cannon/base.cmesh" resolve correctly at runtime. + // Skipped per-file if the output is newer than the source. Each + // compress runs in its own thread; passthrough copies are bundled into + // a single thread to match the cfg.files pattern. + auto compressedName = [](const fs::path& src) -> std::optional { + std::string ext = src.extension().string(); + if (ext == ".png") return fs::path(src.filename()).replace_extension(".ctex"); + if (ext == ".obj") return fs::path(src.filename()).replace_extension(".cmesh"); return std::nullopt; }; - for (const fs::path& asset : config.assets) { - std::optional outName = assetOutput(asset); - if (!outName) { - buildCancelled.store(true); - buildError = std::format("{}: unsupported asset extension (expected .png or .obj)", asset.string()); - break; - } - fs::path out = outputDir / *outName; - if (fs::exists(out) && fs::exists(asset) && fs::last_write_time(asset) <= fs::last_write_time(out)) continue; - threads.emplace_back([&asset, out = std::move(out), &buildError, &buildCancelled]() { - Progress::Task task(std::format("Compressing asset {}", asset.filename().string())); + auto submitCompress = [&](fs::path sourcePath, fs::path outRelative) { + fs::path out = outputDir / outRelative; + if (fs::exists(out) && fs::exists(sourcePath) && fs::last_write_time(sourcePath) <= fs::last_write_time(out)) return; + threads.emplace_back([sourcePath = std::move(sourcePath), out = std::move(out), &buildError, &buildCancelled]() { + Progress::Task task(std::format("Compressing asset {}", sourcePath.filename().string())); if (buildCancelled.load(std::memory_order_relaxed)) return; - std::string result = CompressAsset(asset, out); + std::error_code ec; + fs::create_directories(out.parent_path(), ec); + std::string result = CompressAsset(sourcePath, out); if (result.empty()) return; bool expected = false; if (buildCancelled.compare_exchange_strong(expected, true)) { buildError = std::move(result); } }); + }; + std::vector> assetPassthroughs; + for (const fs::path& asset : config.assets) { + if (fs::is_directory(asset)) { + fs::path topName = asset.filename(); + for (const auto& entry : fs::recursive_directory_iterator(asset)) { + if (!entry.is_regular_file()) continue; + fs::path rel = fs::relative(entry.path(), asset); + if (std::optional compName = compressedName(entry.path())) { + submitCompress(entry.path(), topName / rel.parent_path() / *compName); + } else { + assetPassthroughs.emplace_back(entry.path(), topName / rel); + } + } + } else { + std::optional outName = compressedName(asset); + if (!outName) { + buildCancelled.store(true); + buildError = std::format("{}: unsupported asset extension (expected .png or .obj, or a directory)", asset.string()); + break; + } + submitCompress(asset, *outName); + } + } + if (!assetPassthroughs.empty()) { + threads.emplace_back([passthroughs = std::move(assetPassthroughs), &outputDir, &buildCancelled, &buildError]() { + Progress::Task task("Copying asset passthrough files"); + if (buildCancelled.load(std::memory_order_relaxed)) return; + try { + for (const auto& [src, rel] : passthroughs) { + fs::path dst = outputDir / rel; + fs::create_directories(dst.parent_path()); + if (!fs::exists(dst)) { + fs::copy_file(src, dst); + } else if (fs::last_write_time(src) > fs::last_write_time(dst)) { + fs::copy_file(src, dst, fs::copy_options::overwrite_existing); + } + } + } catch (const std::exception& e) { + bool expected = false; + if (buildCancelled.compare_exchange_strong(expected, true)) { + buildError = e.what(); + } + } + }); } threads.emplace_back([&config, &outputDir, &buildCancelled, &buildError]() { @@ -679,6 +725,16 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_mapassets) { + // Directory entry: the dep already mirrored the + // (compressed + passthrough) tree under + // depBinDir//. Forward it wholesale + // so our bin dir gets the same layout. + if (fs::is_directory(asset)) { + fs::path src = depBinDir / asset.filename(); + if (!fs::exists(src)) continue; + copyTree(src, outputDir / asset.filename()); + continue; + } std::string ext = asset.extension().string(); fs::path srcName = asset.filename(); if (ext == ".png") srcName.replace_extension(".ctex"); diff --git a/interfaces/Crafter.Build-Clang.cppm b/interfaces/Crafter.Build-Clang.cppm index 64d9403..2fa60bd 100644 --- a/interfaces/Crafter.Build-Clang.cppm +++ b/interfaces/Crafter.Build-Clang.cppm @@ -125,14 +125,23 @@ export namespace Crafter { std::vector buildFiles; std::vector defines; std::vector shaders; - // Source assets (.png, .obj) compressed via Crafter.Asset's - // SaveCompressed → .ctex/.cmesh in this configuration's bin dir. - // Requires Crafter.Asset in cfg.dependencies (transitively): the - // build engine locates it by name and uses its library API to do - // the conversion (an auxiliary host-target executable, generated - // once per build host, links Crafter.Asset and is invoked per - // asset). Forwarded to a consuming executable's bin dir alongside - // .spv shaders and cfg.files entries. + // Source assets compressed via Crafter.Asset's SaveCompressed → + // .ctex/.cmesh in this configuration's bin dir. + // + // Each entry is either: + // - A single .png/.obj file. Output lands flat in the bin dir: + // bin/.ctex or bin/.cmesh. + // - A directory. The build recurses; .png/.obj are compressed + // with the relative tree mirrored under bin//, and + // every other file in the tree is copied through unchanged. + // Lets mod/map trees (mod.json + cannon/base.obj + + // cannon/color.png) keep their nested layout so JSON paths + // like "cannon/base.cmesh" resolve at runtime. + // + // Crafter.Asset must be reachable through cfg.dependencies (the + // build engine links its library API into crafter-build via a + // self-host pass). Forwarded to a consuming executable's bin dir + // alongside .spv shaders and cfg.files entries. std::vector assets; std::vector externalDependencies; std::vector compileFlags;