35const std::filesystem::path porytiles_src_marker{
"porytiles_src"};
36const std::filesystem::path porytiles_bin_marker{
"porytiles_bin"};
37const std::filesystem::path porytiles_generated_marker{
"porytiles_generated"};
46enum class ArtifactCategory { porytiles_src, porytiles_bin, special };
54struct ArtifactPathInfo {
55 ArtifactCategory category;
56 std::filesystem::path directory;
68[[nodiscard]] ArtifactPathInfo categorize_artifact_key(
const std::filesystem::path &key_path)
71 std::filesystem::path accumulated;
72 for (
const auto &component : key_path) {
73 accumulated /= component;
75 if (component == porytiles_src_marker) {
76 return ArtifactPathInfo{ArtifactCategory::porytiles_src, accumulated};
78 if (component == porytiles_bin_marker) {
79 return ArtifactPathInfo{ArtifactCategory::porytiles_bin, accumulated};
81 if (component == porytiles_generated_marker) {
83 return ArtifactPathInfo{ArtifactCategory::special, key_path.parent_path()};
88 return ArtifactPathInfo{ArtifactCategory::special, key_path.parent_path()};
100[[nodiscard]] std::filesystem::path create_project_tmpdir(
const std::filesystem::path &project_root)
102 int max_tries = 1000;
103 std::random_device random_device;
104 std::mt19937 mersenne_prng(random_device());
105 std::uniform_int_distribution<uint64_t> uniform_int_distribution(0);
106 std::filesystem::path path;
108 for (
int i = 0; i <= max_tries; ++i) {
109 std::stringstream string_stream;
110 string_stream << std::hex << uniform_int_distribution(mersenne_prng);
111 path = project_root / (
".porytiles_tmp_" + string_stream.str());
112 if (std::filesystem::create_directory(path)) {
115 if (i == max_tries) {
116 panic(
"create_project_tmpdir: exceeded maximum retries");
119 panic(
"create_project_tmpdir: unreachable");
127 if (!result.has_value()) {
136 const std::filesystem::path &path,
139 auto result = saver.
save_to_file(tiles_png, path, tiles_palette_mode);
140 if (!result.has_value()) {
146ChainableResult<void> save_metatiles_bin(
const std::vector<TilemapEntry> &entries,
const std::filesystem::path &path)
148 std::ofstream out{path};
149 for (
const auto &entry : entries) {
151 const auto tile_value =
static_cast<uint16_t
>(
152 (entry.tile_index() & 0x3ffu) | ((entry.h_flip() & 1u) << 10u) | ((entry.v_flip() & 1u) << 11u) |
153 ((entry.palette_index() & 0xfu) << 12u));
154 out << static_cast<std::uint8_t>(tile_value);
155 out << static_cast<std::uint8_t>(tile_value >> 8u);
171 const std::filesystem::path &path,
174 auto result = saver.
save_to_file(frame, path, tiles_palette_mode);
175 if (!result.has_value()) {
198template <
typename StagedDirectory>
200 const std::filesystem::path &transaction_root,
201 const std::filesystem::path &project_root,
203 std::map<std::filesystem::path, StagedDirectory> &staged_directories,
204 std::vector<std::pair<std::filesystem::path, std::filesystem::path>> &staged_special_files)
206 if (transaction_root.empty()) {
210 const std::filesystem::path key_path{dest_key.
key()};
211 const auto path_info = categorize_artifact_key(key_path);
213 if (path_info.category == ArtifactCategory::special) {
215 const auto staging_path = transaction_root / key_path;
216 std::filesystem::create_directories(staging_path.parent_path());
219 const auto dest_path = project_root / key_path;
220 staged_special_files.emplace_back(staging_path, dest_path);
226 const auto &category_dir = path_info.directory;
227 const auto dest_dir = project_root / category_dir;
230 if (!staged_directories.contains(dest_dir)) {
232 const auto staging_dir = transaction_root / category_dir;
233 staged_directories[dest_dir] = StagedDirectory{staging_dir, dest_dir};
237 const auto relative_within_category = std::filesystem::relative(key_path, category_dir);
238 const auto staging_path = staged_directories[dest_dir].staging_path / relative_within_category;
241 std::filesystem::create_directories(staging_path.parent_path());
258template <
typename PixelType>
260 const std::vector<
PixelTile<PixelType>> &tiles, std::size_t width_tiles = 0, std::size_t height_tiles = 0)
267 std::size_t tiles_per_row;
268 std::size_t tiles_per_col;
270 if (width_tiles > 0 && height_tiles > 0) {
272 if (width_tiles * height_tiles != tiles.size()) {
275 "tiles_to_image: width_tiles ({}) * height_tiles ({}) != tiles.size() ({})",
280 tiles_per_row = width_tiles;
281 tiles_per_col = height_tiles;
285 tiles_per_row = tiles.size();
294 for (std::size_t tile_idx = 0; tile_idx < tiles.size(); ++tile_idx) {
295 const auto &tile = tiles[tile_idx];
296 const std::size_t tile_row = tile_idx / tiles_per_row;
297 const std::size_t tile_col = tile_idx % tiles_per_row;
303 const std::size_t dest_row = pixel_row_offset + pixel_row;
304 const std::size_t dest_col = pixel_col_offset + pixel_col;
305 img.
set(dest_row, dest_col, tile.at(pixel_row, pixel_col));
336template <SupportsTransparency PixelType,
typename StagedDirectory,
typename ComponentGetter,
typename SaveFunc>
340 const std::string &anim_name,
341 const std::string &frame_name,
342 const std::filesystem::path &transaction_root,
343 const std::filesystem::path &project_root,
344 std::map<std::filesystem::path, StagedDirectory> &staged_directories,
345 std::vector<std::pair<std::filesystem::path, std::filesystem::path>> &staged_special_files,
346 ComponentGetter component_getter,
348 std::string_view component_name)
350 const auto &component = component_getter(src);
352 if (!component.has_anim(anim_name)) {
354 "animation '{}' not found in {} component",
359 const auto &anim = component.anim_for_name(anim_name);
363 if (frame_name !=
"key") {
364 frame_ptr = &anim.frame_for_name(frame_name);
367 frame_ptr = &anim.key_frame();
371 const auto ¶ms = anim.params();
372 auto img = tiles_to_image(frame_ptr->
tiles(), params.width_tiles(), params.height_tiles());
376 const auto &palette = frame_ptr->
palette();
377 std::vector<Rgba32> palette_vec;
378 palette_vec.reserve(palette.
size());
379 for (std::size_t i = 0; i < palette.
size(); ++i) {
380 palette_vec.push_back(palette.
at(i));
382 img.palette(std::move(palette_vec));
387 transaction_dest_path,
388 compute_transaction_dest_path(
389 transaction_root, project_root, dest_key, staged_directories, staged_special_files),
391 "Failed to compute transaction dest path.");
394 return save_func(img, transaction_dest_path);
403 if (!transaction_root_.empty()) {
408 transaction_root_ = create_project_tmpdir(project_root_);
411 staged_directories_.clear();
412 staged_special_files_.clear();
419 if (transaction_root_.empty()) {
424 if (staged_directories_.empty() && staged_special_files_.empty()) {
425 std::filesystem::remove_all(transaction_root_);
426 transaction_root_.clear();
431 const auto backup_root = create_project_tmpdir(project_root_);
434 std::vector<std::pair<std::filesystem::path, std::filesystem::path>> moved_directories;
435 std::vector<std::pair<std::filesystem::path, std::filesystem::path>> backed_up_special_files;
436 std::vector<std::filesystem::path> new_special_files;
440 for (
const auto &[dest_dir, staged_info] : staged_directories_) {
441 if (std::filesystem::exists(dest_dir)) {
443 const auto relative = std::filesystem::relative(dest_dir, project_root_);
444 const auto backup_path = backup_root / relative;
445 std::filesystem::create_directories(backup_path.parent_path());
448 std::filesystem::rename(dest_dir, backup_path);
449 moved_directories.emplace_back(dest_dir, backup_path);
454 for (
const auto &[dest_dir, staged_info] : staged_directories_) {
456 std::filesystem::create_directories(dest_dir.parent_path());
459 std::filesystem::rename(staged_info.staging_path, dest_dir);
463 for (
const auto &[staging_path, dest_path] : staged_special_files_) {
465 if (std::filesystem::exists(dest_path)) {
466 const auto relative = std::filesystem::relative(dest_path, project_root_);
467 const auto backup_path = backup_root / relative;
468 std::filesystem::create_directories(backup_path.parent_path());
469 std::filesystem::copy_file(dest_path, backup_path);
470 backed_up_special_files.emplace_back(dest_path, backup_path);
473 new_special_files.push_back(dest_path);
477 std::filesystem::create_directories(dest_path.parent_path());
478 std::filesystem::copy_file(staging_path, dest_path, std::filesystem::copy_options::overwrite_existing);
482 std::filesystem::remove_all(transaction_root_);
483 std::filesystem::remove_all(backup_root);
484 transaction_root_.clear();
485 staged_directories_.clear();
486 staged_special_files_.clear();
490 catch (
const std::filesystem::filesystem_error &e) {
494 for (
const auto &[original_path, backup_path] : moved_directories) {
495 if (std::filesystem::exists(backup_path)) {
497 if (std::filesystem::exists(original_path)) {
498 std::filesystem::remove_all(original_path);
500 std::filesystem::rename(backup_path, original_path);
505 for (
const auto &[original_path, backup_path] : backed_up_special_files) {
506 if (std::filesystem::exists(backup_path)) {
507 std::filesystem::copy_file(
508 backup_path, original_path, std::filesystem::copy_options::overwrite_existing);
513 for (
const auto &new_file : new_special_files) {
514 if (std::filesystem::exists(new_file)) {
515 std::filesystem::remove(new_file);
519 catch (
const std::filesystem::filesystem_error &) {
524 if (std::filesystem::exists(backup_root)) {
525 std::filesystem::remove_all(backup_root);
527 if (std::filesystem::exists(transaction_root_)) {
528 std::filesystem::remove_all(transaction_root_);
530 transaction_root_.clear();
531 staged_directories_.clear();
532 staged_special_files_.clear();
540 if (transaction_root_.empty()) {
545 if (std::filesystem::exists(transaction_root_)) {
546 std::filesystem::remove_all(transaction_root_);
548 transaction_root_.clear();
549 staged_directories_.clear();
550 staged_special_files_.clear();
553 catch (
const std::filesystem::filesystem_error &e) {
554 transaction_root_.clear();
555 staged_directories_.clear();
556 staged_special_files_.clear();
565 transaction_dest_path,
566 compute_transaction_dest_path(
567 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
569 "Failed to compute transaction dest path.");
577 transaction_dest_path,
578 compute_transaction_dest_path(
579 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
581 "Failed to compute transaction dest path.");
589 transaction_dest_path,
590 compute_transaction_dest_path(
591 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
593 "Failed to compute transaction dest path.");
595 tiles_palette_mode_config,
598 "Failed to get tiles_palette_mode config.");
599 return save_tiles_png(
602 transaction_dest_path,
603 tiles_palette_mode_config.value());
610 transaction_dest_path,
611 compute_transaction_dest_path(
612 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
614 "Failed to compute transaction dest path.");
617 panic(
"attempted to save a Porymap palette containing wildcards");
619 return save_palette(palette, transaction_dest_path, *palette_saver_);
623 const ArtifactKey &dest_key,
const Tileset &src,
const std::string &anim_name,
const std::string &frame_name)
626 tiles_palette_mode_config,
629 "Failed to get tiles_palette_mode config.");
630 return write_anim_frame_impl<IndexPixel>(
638 staged_special_files_,
640 [
this, &tiles_palette_mode_config](
const Image<IndexPixel> &img,
const std::filesystem::path &path) {
641 return save_porymap_anim_frame(*png_indexed_saver_, img, path, tiles_palette_mode_config.value());
650 if (porymap_anims.empty()) {
652 if (std::filesystem::exists(project_root_ / dest_key.
key())) {
653 std::filesystem::remove(project_root_ / dest_key.
key());
658 std::map<DynamicCasedName, AnimParams> anim_params;
659 for (
const auto &[anim_name, anim] : porymap_anims) {
668 "Failed to determine primary/secondary status for '{}'.",
670 const bool is_primary = !is_secondary;
678 "Failed to get tileset bin path config for '{}'.",
680 const std::filesystem::path tileset_path =
685 anim_code_generator_->
generate(src.
name(), tileset_path, anim_params, is_primary),
687 "Failed to generate animation code for '{}'.",
691 transaction_dest_path,
692 compute_transaction_dest_path(
693 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
695 "Failed to compute transaction dest path.");
697 std::ofstream out{transaction_dest_path};
698 if (!out.is_open()) {
702 out << generated_code;
712 transaction_dest_path,
713 compute_transaction_dest_path(
714 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
716 "Failed to compute transaction dest path.");
723 transaction_dest_path,
724 compute_transaction_dest_path(
725 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
727 "Failed to compute transaction dest path.");
734 transaction_dest_path,
735 compute_transaction_dest_path(
736 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
738 "Failed to compute transaction dest path.");
751 "Failed to resolve role_pins.");
755 transaction_dest_path,
756 compute_transaction_dest_path(
757 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
759 "Failed to compute transaction dest path.");
761 std::ofstream out{transaction_dest_path};
762 if (!out.is_open()) {
771 std::string header =
"id";
773 header +=
"," + field.name();
778 out << header <<
"\n";
784 return attribute.
fields().contains(field.name()) ? attribute.
field(field.name()) : field.default_value();
794 if (!cells.empty()) {
797 const std::uint32_t value = effective_value(attribute, field);
798 if (!field.has_provider()) {
799 cells += std::to_string(value);
802 const auto provider_it = providers_->find(field.name());
803 if (provider_it == providers_->end()) {
806 "write_attributes_csv: field '{}' has a provider spec but no provider was built for it",
811 provider_it->second->lookup(value),
813 std::format(
"Failed to lookup {} name for metatile {}.", field.name(), metatile_id));
822 if (effective_value(attribute, field) != field.default_value()) {
832 auto omit_row = [&](
const MetatileAttribute &attribute) ->
bool {
return is_all_default(attribute); };
834 if (role_pins.empty()) {
837 std::size_t non_default_count = 0;
838 for (
const auto &attribute : attributes | std::views::values) {
839 if (!omit_row(attribute)) {
844 if (non_default_count == 0) {
849 for (
const auto &[metatile_id, attribute] : attributes) {
850 if (omit_row(attribute)) {
854 out << metatile_id <<
"," << fields_str <<
"\n";
872 panic(
"write_attributes_csv: unhandled FieldRole in role pin cell rendering");
879 cells +=
"," + render_pin_cell(pin.role, attribute);
890 default_attribute.
field(field.name(), field.default_value());
895 auto emit_row = [&](std::size_t metatile_id,
const std::string &fields_str,
const MetatileAttribute &attribute) {
897 if (!fields_str.empty()) {
898 out <<
"," << fields_str;
900 out << render_pin_cells(attribute) <<
"\n";
903 for (std::size_t metatile_id = 0; metatile_id < layer_metatile_count; ++metatile_id) {
904 const auto it = attributes.find(metatile_id);
905 const MetatileAttribute &attribute = it != attributes.end() ? it->second : default_attribute;
908 emit_row(metatile_id, fields_str, attribute);
913 for (
const auto &[metatile_id, attribute] : attributes) {
914 if (metatile_id < layer_metatile_count) {
918 emit_row(metatile_id, fields_str, attribute);
930 transaction_dest_path,
931 compute_transaction_dest_path(
932 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
934 "Failed to compute transaction dest path.");
945 const ArtifactKey &dest_key,
const Tileset &src,
const std::string &anim_name,
const std::string &frame_name)
947 return write_anim_frame_impl<Rgba32>(
955 staged_special_files_,
957 [
this](
const Image<Rgba32> &img,
const std::filesystem::path &path) {
958 return save_layer_png(*png_rgba_saver_, img, path);
969 if (porytiles_anims.empty() && primary_overrides.empty()) {
977 std::map<DynamicCasedName, AnimParams> anim_params;
978 for (
const auto &[anim_name, anim] : porytiles_anims) {
983 std::map<DynamicCasedName, std::vector<AnimOverrideEntry>> primary_refs;
984 for (
const auto &[
name, entries] : primary_overrides) {
989 transaction_dest_path,
990 compute_transaction_dest_path(
991 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
993 "Failed to compute transaction dest path.");
995 return anim_json_parser_->
write(transaction_dest_path, anim_params, primary_refs);
#define PT_TRY_ASSIGN_CHAIN_ERR(var, expr, return_type,...)
Unwraps a ChainableResult, chaining a new error message on failure.
#define PT_TRY_ASSIGN_PASS_ERR(var, expr, return_type)
Unwraps a ChainableResult, passing through the error chain with an empty FormattableError when types ...
#define PT_TRY_CALL_CHAIN_ERR(expr, return_type,...)
Unwraps a void ChainableResult, chaining a new error message on failure.
ChainableResult< std::string > generate(const std::string &tileset_name, const std::filesystem::path &tileset_path_from_project_root, const std::map< DynamicCasedName, AnimParams > &animations, bool is_primary) const
Generates the complete generated_anim_code.h content.
Represents a single frame of an animation, containing tiles and a frame name.
const Palette< Rgba32 > & palette() const
const std::vector< PixelTile< PixelType > > & tiles() const
ChainableResult< void > write(const std::filesystem::path &json_path, const std::map< DynamicCasedName, AnimParams > ¶ms, const std::map< DynamicCasedName, std::vector< AnimOverrideEntry > > &primary_references={}) const
Writes animation parameters to an anim.json file.
A type-safe wrapper for artifact keys.
const std::string & key() const
A result type that maintains a chainable sequence of errors for debugging and error reporting.
ChainableResult< ConfigValue< TilesPaletteMode > > tiles_palette_mode(ConfigScopeType type, const std::string &scope) const
A smart string wrapper that preserves word structure for lossless case format conversion.
std::string to_snake_case() const
Outputs all words flattened and joined with underscores.
One named bit-field within a metatile attribute layout.
A service interface that saves a fixed-length Palette to a given file.
virtual ChainableResult< void > save(const Palette< Rgba32, palette::max_size > &palette, const std::filesystem::path &path) const =0
A template for two-dimensional images with arbitrarily typed pixel values.
void set(std::size_t i, PixelType pixel)
Sets the pixel value at a given one-dimensional pixel index.
ChainableResult< ConfigValue< std::string > > tileset_paths_secondary_bin(ConfigScopeType type, const std::string &scope) const
ChainableResult< ConfigValue< std::string > > tileset_paths_primary_bin(ConfigScopeType type, const std::string &scope) const
ChainableResult< ConfigValue< RolePinDefinitions > > role_pins(ConfigScopeType type, const std::string &scope) const
A generic palette container for colors that support transparency checking.
std::size_t size() const
Returns the number of slots in the palette.
ColorType at(std::size_t index) const
Gets the color at a specific index.
bool has_any_wildcards() const
Checks if the palette contains any wildcard slots.
An 8x8 tile backed by literal-array-based per-pixel storage of an arbitrary pixel type.
An image saver that saves PNG files from an Image with an index pixel type.
virtual ChainableResult< void > save_to_file(const Image< IndexPixel > &image, const std::filesystem::path &path, TilesPaletteMode mode) const
An image saver that saves PNG files from an Image with an Rgba32 pixel type.
virtual ChainableResult< void > save_to_file(const Image< Rgba32 > &image, const std::filesystem::path &path) const
const std::map< std::string, Animation< IndexPixel > > & anims() const
const std::vector< TilemapEntry > & metatiles_bin() const
const Palette< Rgba32, palette::max_size > & palette_at(std::size_t palette_index) const
const std::vector< MetatileAttribute > & metatile_attributes_bin() const
const Image< IndexPixel > & tiles_png() const
const std::optional< Palette< Rgba32, palette::max_size > > & palette_at(std::size_t palette_index) const
const Image< Rgba32 > & bottom() const
const Image< Rgba32 > & middle() const
const std::map< std::string, Animation< Rgba32 > > & anims() const
const std::map< std::size_t, MetatileAttribute > & metatile_attributes() const
const Image< Rgba32 > & top() const
const std::map< std::string, std::vector< AnimOverrideEntry > > & primary_anim_overrides() const
ChainableResult< void > write_porymap_anim_params(const ArtifactKey &dest_key, const Tileset &src) override
Writes the animation parameters to the Porymap component backing store.
ChainableResult< void > write_metatiles_bin(const ArtifactKey &dest_key, const Tileset &src) override
ChainableResult< void > write_top_png(const ArtifactKey &dest_key, const Tileset &src) override
ChainableResult< void > begin_transaction() override
Begins a new transaction for atomic write operations.
ChainableResult< void > write_porytiles_anim_params(const ArtifactKey &dest_key, const Tileset &src) override
Writes animation parameters to the Porytiles component backing store.
ChainableResult< void > write_tiles_png(const ArtifactKey &dest_key, const Tileset &src) override
ChainableResult< void > write_porytiles_palette_n(const ArtifactKey &dest_key, const Tileset &src, std::size_t index) override
ChainableResult< void > write_bottom_png(const ArtifactKey &dest_key, const Tileset &src) override
ChainableResult< void > commit() override
Commits all buffered write operations in the current transaction.
ChainableResult< void > write_middle_png(const ArtifactKey &dest_key, const Tileset &src) override
ChainableResult< void > write_porymap_anim_frame(const ArtifactKey &dest_key, const Tileset &src, const std::string &anim_name, const std::string &frame_name) override
ChainableResult< void > write_metatile_attributes_bin(const ArtifactKey &dest_key, const Tileset &src) override
ChainableResult< void > rollback() override
Rolls back all buffered write operations in the current transaction.
ChainableResult< void > write_porymap_palette_n(const ArtifactKey &dest_key, const Tileset &src, std::size_t index) override
ChainableResult< void > write_porytiles_anim_frame(const ArtifactKey &dest_key, const Tileset &src, const std::string &anim_name, const std::string &frame_name) override
ChainableResult< void > write_attributes_csv(const ArtifactKey &dest_key, const Tileset &src) override
const std::vector< Field > & value_fields() const
Returns the fields that hold plain per-metatile values, excluding the layer_type-role field.
static const Style bold
Bold text formatting.
A complete tileset containing both Porytiles and Porymap components.
const PorytilesTilesetComponent & porytiles_component() const
const PorymapTilesetComponent & porymap_component() const
const std::string & name() const
constexpr std::size_t side_length_pix
std::string pin_column_name(FieldRole role)
Returns the one legal attributes.csv column name for a role's pin column.
ChainableResult< void > save_metatile_attributes_bin(const std::vector< MetatileAttribute > &attributes, const std::filesystem::path &path, const Schema &schema)
Writes metatile attributes to a metatile_attributes.bin file according to a schema.
std::vector< RolePinDefinition > RolePinDefinitions
An ordered list of role pin definitions; order is display/declaration order (also CSV column order).
void panic(const StringViewSourceLoc &s)
Unconditionally terminates the program with a panic message.
std::string layer_type_csv_token(LayerType layer_type)
Converts a LayerType to its lowercase CSV token.
TilesPaletteMode
Controls how tiles.png is rendered.
DynamicCasedName extract_tileset_cased_name(const std::string &tileset_name)
Extracts the tileset short name and wraps it in a DynamicCasedName.
FieldRole
Semantic roles a schema field can carry beyond holding a plain per-metatile value.
@ tileset
Configuration scoped to a specific tileset.
Utility functions for string manipulation and formatting.
A user request to emit a trailing pin column for one schema role in attributes.csv.