Porytiles
Loading...
Searching...
No Matches
project_tileset_artifact_writer.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <filesystem>
5#include <format>
6#include <fstream>
7#include <iostream>
8#include <map>
9#include <memory>
10#include <optional>
11#include <random>
12#include <ranges>
13#include <sstream>
14#include <string>
15
26
27namespace {
28
29using namespace porytiles;
30
31// Marker directories for artifact categorization
32const std::filesystem::path porytiles_src_marker{"porytiles_src"};
33const std::filesystem::path porytiles_bin_marker{"porytiles_bin"};
34const std::filesystem::path porytiles_generated_marker{"porytiles_generated"};
35
45enum class ArtifactCategory { porytiles_src, porytiles_bin, special };
46
55struct ArtifactPathInfo {
56 ArtifactCategory category;
57 std::filesystem::path directory;
58};
59
71[[nodiscard]] ArtifactPathInfo categorize_artifact_key(const std::filesystem::path &key_path)
72{
73 // Walk through path components to find marker directories
74 std::filesystem::path accumulated;
75 for (const auto &component : key_path) {
76 accumulated /= component;
77
78 if (component == porytiles_src_marker) {
79 return ArtifactPathInfo{ArtifactCategory::porytiles_src, accumulated};
80 }
81 if (component == porytiles_bin_marker) {
82 return ArtifactPathInfo{ArtifactCategory::porytiles_bin, accumulated};
83 }
84 if (component == porytiles_generated_marker) {
85 // For porytiles_generated, the whole path up to the file's parent is special
86 return ArtifactPathInfo{ArtifactCategory::special, key_path.parent_path()};
87 }
88 }
89
90 // Default to special category for unrecognized paths
91 return ArtifactPathInfo{ArtifactCategory::special, key_path.parent_path()};
92}
93
105[[nodiscard]] std::filesystem::path create_project_tmpdir(const std::filesystem::path &project_root)
106{
107 int max_tries = 1000;
108 std::random_device random_device;
109 std::mt19937 mersenne_prng(random_device());
110 std::uniform_int_distribution<uint64_t> uniform_int_distribution(0);
111 std::filesystem::path path;
112
113 for (int i = 0; i <= max_tries; ++i) {
114 std::stringstream string_stream;
115 string_stream << std::hex << uniform_int_distribution(mersenne_prng);
116 path = project_root / (".porytiles_tmp_" + string_stream.str());
117 if (std::filesystem::create_directory(path)) {
118 return path;
119 }
120 if (i == max_tries) {
121 panic("create_project_tmpdir: exceeded maximum retries");
122 }
123 }
124 panic("create_project_tmpdir: unreachable");
125 return {}; // unreachable
126}
127
129save_layer_png(const PngRgbaImageSaver &saver, const Image<Rgba32> &layer_png, const std::filesystem::path &path)
130{
131 auto result = saver.save_to_file(layer_png, path);
132 if (!result.has_value()) {
133 return result;
134 }
135 return {};
136}
137
138ChainableResult<void> save_tiles_png(
139 const PngIndexedImageSaver &saver,
140 const Image<IndexPixel> &tiles_png,
141 const std::filesystem::path &path,
142 TilesPalMode tiles_pal_mode)
143{
144 auto result = saver.save_to_file(tiles_png, path, tiles_pal_mode);
145 if (!result.has_value()) {
146 return result;
147 }
148 return {};
149}
150
151ChainableResult<void> save_metatiles_bin(const std::vector<TilemapEntry> &entries, const std::filesystem::path &path)
152{
153 std::ofstream out{path};
154 for (const auto &entry : entries) {
155 const auto tile_value = static_cast<uint16_t>(
156 (entry.tile_index() & 0x3ff) | ((entry.h_flip() & 1) << 10) | ((entry.v_flip() & 1) << 11) |
157 ((entry.pal_index() & 0xf) << 12));
158 out << static_cast<std::uint8_t>(tile_value);
159 out << static_cast<std::uint8_t>(tile_value >> 8);
160 }
161 out.flush();
162 return {};
163}
164
165ChainableResult<void> save_emerald_metatile_attributes_bin(
166 const std::vector<MetatileAttribute> &attributes, const std::filesystem::path &path)
167{
168 std::ofstream out{path};
169 for (const auto &attribute : attributes) {
170 const std::uint16_t behavior = attribute.behavior();
171 const auto layer_type = static_cast<std::uint8_t>(attribute.layer_type());
172 const auto attribute_value = static_cast<std::uint16_t>((behavior & 0xff) | ((layer_type & 0xf) << 12));
173 out << static_cast<std::uint8_t>(attribute_value);
174 out << static_cast<std::uint8_t>(attribute_value >> 8);
175 }
176 out.flush();
177 return {};
178}
179
180ChainableResult<void> save_firered_metatile_attributes_bin(
181 const std::vector<MetatileAttribute> &attributes, const std::filesystem::path &path)
182{
183 std::ofstream out{path};
184 for (const auto &attribute : attributes) {
185 // FireRed attribute bit layout (from fieldmap.c):
186 // Bits 0-8: behavior (0x000001FF)
187 // Bits 9-13: terrain (0x00003E00)
188 // Bits 14-17: attribute_2 (0x0003C000)
189 // Bits 18-23: attribute_3 (0x00FC0000)
190 // Bits 24-26: encounter_type (0x07000000)
191 // Bits 27-28: attribute_5 (0x18000000)
192 // Bits 29-30: layer_type (0x60000000)
193 // Bit 31: attribute_7 (0x80000000)
194 const auto attribute_value = static_cast<std::uint32_t>(
195 (static_cast<std::uint32_t>(attribute.behavior()) & 0x1FF) |
196 ((static_cast<std::uint32_t>(attribute.terrain()) & 0x1F) << 9) |
197 ((static_cast<std::uint32_t>(attribute.attribute_2()) & 0x0F) << 14) |
198 ((static_cast<std::uint32_t>(attribute.attribute_3()) & 0x3F) << 18) |
199 ((static_cast<std::uint32_t>(attribute.encounter_type()) & 0x07) << 24) |
200 ((static_cast<std::uint32_t>(attribute.attribute_5()) & 0x03) << 27) |
201 ((static_cast<std::uint32_t>(attribute.layer_type()) & 0x03) << 29) |
202 ((static_cast<std::uint32_t>(attribute.attribute_7()) & 0x01) << 31));
203 out << static_cast<std::uint8_t>(attribute_value);
204 out << static_cast<std::uint8_t>(attribute_value >> 8);
205 out << static_cast<std::uint8_t>(attribute_value >> 16);
206 out << static_cast<std::uint8_t>(attribute_value >> 24);
207 }
208 out.flush();
209 return {};
210}
211
213save_palette(const Palette<Rgba32, pal::max_size> &pal, const std::filesystem::path &path, const FilePalSaver &saver)
214{
215 PT_TRY_CALL_CHAIN_ERR(saver.save(pal, path), void, "'{}': Failed to save.", FormatParam(path.c_str()));
216 return {};
217}
218
219ChainableResult<void> save_porymap_anim_frame(
220 const PngIndexedImageSaver &saver,
221 const Image<IndexPixel> &frame,
222 const std::filesystem::path &path,
223 TilesPalMode tiles_pal_mode)
224{
225 auto result = saver.save_to_file(frame, path, tiles_pal_mode);
226 if (!result.has_value()) {
227 return result;
228 }
229 return {};
230}
231
251template <typename StagedDirectory>
252ChainableResult<std::filesystem::path> compute_transaction_dest_path(
253 const std::filesystem::path &transaction_root,
254 const std::filesystem::path &project_root,
255 const ArtifactKey &dest_key,
256 std::map<std::filesystem::path, StagedDirectory> &staged_directories,
257 std::vector<std::pair<std::filesystem::path, std::filesystem::path>> &staged_special_files)
258{
259 if (transaction_root.empty()) {
260 return FormattableError{"No transaction in progress."};
261 }
262
263 const std::filesystem::path key_path{dest_key.key()};
264 const auto path_info = categorize_artifact_key(key_path);
265
266 if (path_info.category == ArtifactCategory::special) {
267 // Special files are handled individually - stage them directly
268 const auto staging_path = transaction_root / key_path;
269 std::filesystem::create_directories(staging_path.parent_path());
270
271 // Register for individual file commit
272 const auto dest_path = project_root / key_path;
273 staged_special_files.emplace_back(staging_path, dest_path);
274
275 return staging_path;
276 }
277
278 // For porytiles_src/porytiles_bin, stage under a directory that will be atomically moved
279 const auto &category_dir = path_info.directory;
280 const auto dest_dir = project_root / category_dir;
281
282 // Register this directory if not already registered
283 if (!staged_directories.contains(dest_dir)) {
284 // Create a unique staging directory for this category
285 const auto staging_dir = transaction_root / category_dir;
286 staged_directories[dest_dir] = StagedDirectory{staging_dir, dest_dir};
287 }
288
289 // Compute the path relative to the category directory
290 const auto relative_within_category = std::filesystem::relative(key_path, category_dir);
291 const auto staging_path = staged_directories[dest_dir].staging_path / relative_within_category;
292
293 // Create parent directories in staging area
294 std::filesystem::create_directories(staging_path.parent_path());
295
296 return staging_path;
297}
298
313template <typename PixelType>
314Image<PixelType> tiles_to_image(
315 const std::vector<PixelTile<PixelType>> &tiles, std::size_t width_tiles = 0, std::size_t height_tiles = 0)
316{
317 if (tiles.empty()) {
318 return Image<PixelType>{};
319 }
320
321 // Determine grid dimensions
322 std::size_t tiles_per_row;
323 std::size_t tiles_per_col;
324
325 if (width_tiles > 0 && height_tiles > 0) {
326 // Use specified dimensions
327 if (width_tiles * height_tiles != tiles.size()) {
328 panic(
329 std::format(
330 "tiles_to_image: width_tiles ({}) * height_tiles ({}) != tiles.size() ({})",
331 width_tiles,
332 height_tiles,
333 tiles.size()));
334 }
335 tiles_per_row = width_tiles;
336 tiles_per_col = height_tiles;
337 }
338 else {
339 // Fall back to single row
340 tiles_per_row = tiles.size();
341 tiles_per_col = 1;
342 }
343
344 const std::size_t image_width = tiles_per_row * tile::side_length_pix;
345 const std::size_t image_height = tiles_per_col * tile::side_length_pix;
346
347 Image<PixelType> img{image_width, image_height};
348
349 for (std::size_t tile_idx = 0; tile_idx < tiles.size(); ++tile_idx) {
350 const auto &tile = tiles[tile_idx];
351 const std::size_t tile_row = tile_idx / tiles_per_row;
352 const std::size_t tile_col = tile_idx % tiles_per_row;
353 const std::size_t pixel_row_offset = tile_row * tile::side_length_pix;
354 const std::size_t pixel_col_offset = tile_col * tile::side_length_pix;
355
356 for (std::size_t pixel_row = 0; pixel_row < tile::side_length_pix; ++pixel_row) {
357 for (std::size_t pixel_col = 0; pixel_col < tile::side_length_pix; ++pixel_col) {
358 const std::size_t dest_row = pixel_row_offset + pixel_row;
359 const std::size_t dest_col = pixel_col_offset + pixel_col;
360 img.set(dest_row, dest_col, tile.at(pixel_row, pixel_col));
361 }
362 }
363 }
364
365 return img;
366}
367
393template <SupportsTransparency PixelType, typename StagedDirectory, typename ComponentGetter, typename SaveFunc>
394ChainableResult<void> write_anim_frame_impl(
395 const ArtifactKey &dest_key,
396 const Tileset &src,
397 const std::string &anim_name,
398 const std::string &frame_name,
399 const std::filesystem::path &transaction_root,
400 const std::filesystem::path &project_root,
401 std::map<std::filesystem::path, StagedDirectory> &staged_directories,
402 std::vector<std::pair<std::filesystem::path, std::filesystem::path>> &staged_special_files,
403 ComponentGetter component_getter,
404 SaveFunc save_func,
405 std::string_view component_name)
406{
407 const auto &component = component_getter(src);
408
409 if (!component.has_anim(anim_name)) {
410 return FormattableError{
411 "animation '{}' not found in {} component",
412 FormatParam{anim_name, Style::bold},
413 FormatParam{std::string{component_name}}};
414 }
415
416 const auto &anim = component.anim_for_name(anim_name);
417
418 // Get the appropriate frame
419 const AnimFrame<PixelType> *frame_ptr = nullptr;
420 if (frame_name != "key") {
421 frame_ptr = &anim.frame_for_name(frame_name);
422 }
423 else {
424 frame_ptr = &anim.key_frame();
425 }
426
427 // Convert tiles to image
428 const auto &params = anim.params();
429 auto img = tiles_to_image(frame_ptr->tiles(), params.width_tiles(), params.height_tiles());
430
431 // Transfer palette from frame to image if present
432 if (frame_ptr->has_palette()) {
433 const auto &pal = frame_ptr->palette();
434 std::vector<Rgba32> pal_vec;
435 pal_vec.reserve(pal.size());
436 for (std::size_t i = 0; i < pal.size(); ++i) {
437 pal_vec.push_back(pal.at(i));
438 }
439 img.palette(std::move(pal_vec));
440 }
441
442 // Compute transaction path (keys are now relative to project_root)
444 transaction_dest_path,
445 compute_transaction_dest_path(
446 transaction_root, project_root, dest_key, staged_directories, staged_special_files),
447 void,
448 "Failed to compute transaction dest path.");
449
450 // Save using provided save function
451 return save_func(img, transaction_dest_path);
452}
453
454} // namespace
455
456namespace porytiles {
457
459{
460 if (!transaction_root_.empty()) {
461 return FormattableError{"Transaction already in progress."};
462 }
463
464 // Create tmpdir inside project root to ensure same-filesystem for atomic moves
465 transaction_root_ = create_project_tmpdir(project_root_);
466
467 // Clear any stale tracking data
468 staged_directories_.clear();
469 staged_special_files_.clear();
470
471 return {};
472}
473
475{
476 if (transaction_root_.empty()) {
477 return FormattableError{"No transaction in progress."};
478 }
479
480 // If nothing was staged, just clean up
481 if (staged_directories_.empty() && staged_special_files_.empty()) {
482 std::filesystem::remove_all(transaction_root_);
483 transaction_root_.clear();
484 return {};
485 }
486
487 // Create backup root inside project for same-filesystem operations
488 const auto backup_root = create_project_tmpdir(project_root_);
489
490 // Track what we've moved for rollback
491 std::vector<std::pair<std::filesystem::path, std::filesystem::path>> moved_directories; // (dest, backup)
492 std::vector<std::pair<std::filesystem::path, std::filesystem::path>> backed_up_special_files;
493 std::vector<std::filesystem::path> new_special_files;
494
495 try {
496 // Phase 1: Backup existing destination directories by moving them to backup
497 for (const auto &[dest_dir, staged_info] : staged_directories_) {
498 if (std::filesystem::exists(dest_dir)) {
499 // Create backup path preserving structure
500 const auto relative = std::filesystem::relative(dest_dir, project_root_);
501 const auto backup_path = backup_root / relative;
502 std::filesystem::create_directories(backup_path.parent_path());
503
504 // Move existing directory to backup (atomic on same filesystem)
505 std::filesystem::rename(dest_dir, backup_path);
506 moved_directories.emplace_back(dest_dir, backup_path);
507 }
508 }
509
510 // Phase 2: Atomic directory moves from staging to destination
511 for (const auto &[dest_dir, staged_info] : staged_directories_) {
512 // Create parent directories if needed
513 std::filesystem::create_directories(dest_dir.parent_path());
514
515 // Atomic move: rename staging directory to final destination
516 std::filesystem::rename(staged_info.staging_path, dest_dir);
517 }
518
519 // Phase 3: Handle special files (like generated_anim_code.h)
520 for (const auto &[staging_path, dest_path] : staged_special_files_) {
521 // Backup existing special file if it exists
522 if (std::filesystem::exists(dest_path)) {
523 const auto relative = std::filesystem::relative(dest_path, project_root_);
524 const auto backup_path = backup_root / relative;
525 std::filesystem::create_directories(backup_path.parent_path());
526 std::filesystem::copy_file(dest_path, backup_path);
527 backed_up_special_files.emplace_back(dest_path, backup_path);
528 }
529 else {
530 new_special_files.push_back(dest_path);
531 }
532
533 // Copy special file to destination (create dirs if needed)
534 std::filesystem::create_directories(dest_path.parent_path());
535 std::filesystem::copy_file(staging_path, dest_path, std::filesystem::copy_options::overwrite_existing);
536 }
537
538 // Phase 4: Success - clean up transaction and backup directories
539 std::filesystem::remove_all(transaction_root_);
540 std::filesystem::remove_all(backup_root);
541 transaction_root_.clear();
542 staged_directories_.clear();
543 staged_special_files_.clear();
544
545 return {};
546 }
547 catch (const std::filesystem::filesystem_error &e) {
548 // Phase 5: Error occurred - rollback
549 try {
550 // Rollback moved directories: move backups back to their original locations
551 for (const auto &[original_path, backup_path] : moved_directories) {
552 if (std::filesystem::exists(backup_path)) {
553 // Remove any partially moved directory at destination
554 if (std::filesystem::exists(original_path)) {
555 std::filesystem::remove_all(original_path);
556 }
557 std::filesystem::rename(backup_path, original_path);
558 }
559 }
560
561 // Rollback special files
562 for (const auto &[original_path, backup_path] : backed_up_special_files) {
563 if (std::filesystem::exists(backup_path)) {
564 std::filesystem::copy_file(
565 backup_path, original_path, std::filesystem::copy_options::overwrite_existing);
566 }
567 }
568
569 // Remove new special files that were created
570 for (const auto &new_file : new_special_files) {
571 if (std::filesystem::exists(new_file)) {
572 std::filesystem::remove(new_file);
573 }
574 }
575 }
576 catch (const std::filesystem::filesystem_error &) {
577 // Critical error during restore - best effort cleanup
578 }
579
580 // Clean up temporary directories
581 if (std::filesystem::exists(backup_root)) {
582 std::filesystem::remove_all(backup_root);
583 }
584 if (std::filesystem::exists(transaction_root_)) {
585 std::filesystem::remove_all(transaction_root_);
586 }
587 transaction_root_.clear();
588 staged_directories_.clear();
589 staged_special_files_.clear();
590
591 return FormattableError{"Failed to commit transaction: {}.", FormatParam{e.what()}};
592 }
593}
594
596{
597 if (transaction_root_.empty()) {
598 return FormattableError{"No transaction in progress."};
599 }
600
601 try {
602 if (std::filesystem::exists(transaction_root_)) {
603 std::filesystem::remove_all(transaction_root_);
604 }
605 transaction_root_.clear();
606 staged_directories_.clear();
607 staged_special_files_.clear();
608 return {};
609 }
610 catch (const std::filesystem::filesystem_error &e) {
611 transaction_root_.clear();
612 staged_directories_.clear();
613 staged_special_files_.clear();
614 return FormattableError{"Failed to rollback transaction: {}.", FormatParam{e.what()}};
615 }
616}
617
618/*
619 * Porymap artifacts
620 */
622{
624 transaction_dest_path,
625 compute_transaction_dest_path(
626 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
627 void,
628 "Failed to compute transaction dest path.");
629 return save_metatiles_bin(src.porymap_component().metatiles_bin(), transaction_dest_path);
630}
631
634{
636 transaction_dest_path,
637 compute_transaction_dest_path(
638 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
639 void,
640 "Failed to compute transaction dest path.");
641 if (metatile_attr_size_ == attr::bytes_per_attr_firered) {
642 return save_firered_metatile_attributes_bin(
643 src.porymap_component().metatile_attributes_bin(), transaction_dest_path);
644 }
645 return save_emerald_metatile_attributes_bin(
646 src.porymap_component().metatile_attributes_bin(), transaction_dest_path);
647}
648
650{
652 transaction_dest_path,
653 compute_transaction_dest_path(
654 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
655 void,
656 "Failed to compute transaction dest path.");
658 tiles_pal_mode_config,
659 domain_config_->tiles_pal_mode(ConfigScopeType::tileset, src.name()),
660 void,
661 "Failed to get tiles_pal_mode config.");
662 return save_tiles_png(
663 *png_indexed_saver_, src.porymap_component().tiles_png(), transaction_dest_path, tiles_pal_mode_config.value());
664}
665
667ProjectTilesetArtifactWriter::write_porymap_pal_n(const ArtifactKey &dest_key, const Tileset &src, std::size_t index)
668{
670 transaction_dest_path,
671 compute_transaction_dest_path(
672 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
673 void,
674 "Failed to compute transaction dest path.");
675 const auto &pal = src.porymap_component().pal_at(index);
676 if (pal.has_any_wildcards()) {
677 panic("attempted to save a Porymap palette containing wildcards");
678 }
679 return save_palette(pal, transaction_dest_path, *pal_saver_);
680}
681
683 const ArtifactKey &dest_key, const Tileset &src, const std::string &anim_name, const std::string &frame_name)
684{
686 tiles_pal_mode_config,
687 domain_config_->tiles_pal_mode(ConfigScopeType::tileset, src.name()),
688 void,
689 "Failed to get tiles_pal_mode config.");
690 return write_anim_frame_impl<IndexPixel>(
691 dest_key,
692 src,
693 anim_name,
694 frame_name,
695 transaction_root_,
696 project_root_,
697 staged_directories_,
698 staged_special_files_,
699 [](const Tileset &t) -> const auto & { return t.porymap_component(); },
700 [this, &tiles_pal_mode_config](const Image<IndexPixel> &img, const std::filesystem::path &path) {
701 return save_porymap_anim_frame(*png_indexed_saver_, img, path, tiles_pal_mode_config.value());
702 },
703 "Porymap");
704}
705
706[[nodiscard]] ChainableResult<void>
708{
709 const auto &porymap_anims = src.porymap_component().anims();
710 if (porymap_anims.empty()) {
711 // If there are no anims, but the params file exists, remove it
712 if (std::filesystem::exists(project_root_ / dest_key.key())) {
713 std::filesystem::remove(project_root_ / dest_key.key());
714 }
715 return {};
716 }
717
718 std::map<DynamicCasedName, AnimParams> anim_params;
719 for (const auto &[anim_name, anim] : porymap_anims) {
720 anim_params[DynamicCasedName{anim_name}] = anim.params();
721 }
722
723 // Determine primary/secondary from metadata
725 is_secondary,
726 metadata_provider_.is_secondary(src.name()),
727 void,
728 "Failed to determine primary/secondary status for '{}'.",
730 const bool is_primary = !is_secondary;
731
732 // Read tileset bin path from config based on primary/secondary status
734 bin_path_base,
735 is_primary ? infra_config_->tileset_paths_primary_bin(ConfigScopeType::tileset, src.name())
737 void,
738 "Failed to get tileset bin path config for '{}'.",
740 const std::filesystem::path tileset_path =
741 std::filesystem::path{bin_path_base.value()} / extract_tileset_cased_name(src.name()).to_snake_case();
742
744 generated_code,
745 anim_code_generator_->generate(src.name(), tileset_path, anim_params, is_primary),
746 void,
747 "Failed to generate animation code for '{}'.",
749
751 transaction_dest_path,
752 compute_transaction_dest_path(
753 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
754 void,
755 "Failed to compute transaction dest path.");
756
757 std::ofstream out{transaction_dest_path};
758 if (!out.is_open()) {
759 return FormattableError{
760 "Failed to open file for writing: '{}'.", FormatParam{transaction_dest_path.string(), Style::bold}};
761 }
762 out << generated_code;
763 out.flush();
764
765 return {};
766}
767
768/*
769 * Porytiles artifacts
770 */
772{
774 transaction_dest_path,
775 compute_transaction_dest_path(
776 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
777 void,
778 "Failed to compute transaction dest path.");
779 return save_layer_png(*png_rgba_saver_, src.porytiles_component().bottom(), transaction_dest_path);
780}
781
783{
785 transaction_dest_path,
786 compute_transaction_dest_path(
787 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
788 void,
789 "Failed to compute transaction dest path.");
790 return save_layer_png(*png_rgba_saver_, src.porytiles_component().middle(), transaction_dest_path);
791}
792
794{
796 transaction_dest_path,
797 compute_transaction_dest_path(
798 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
799 void,
800 "Failed to compute transaction dest path.");
801 return save_layer_png(*png_rgba_saver_, src.porytiles_component().top(), transaction_dest_path);
802}
803
806{
807 const auto &attributes = src.porytiles_component().metatile_attributes();
808
809 constexpr std::uint16_t default_behavior = 0;
810 constexpr std::uint8_t default_terrain = 0;
811 constexpr std::uint8_t default_encounter = 0;
812
813 const bool is_firered = base_game_ == BaseGame::pokefirered;
814
815 // Count non-default attributes
816 std::size_t non_default_count = 0;
817 for (const auto &attribute : attributes | std::views::values) {
818 if (is_firered) {
819 if (attribute.behavior() != default_behavior || attribute.terrain() != default_terrain ||
820 attribute.encounter_type() != default_encounter) {
821 non_default_count++;
822 }
823 }
824 else {
825 if (attribute.behavior() != default_behavior) {
826 non_default_count++;
827 }
828 }
829 }
830
832 transaction_dest_path,
833 compute_transaction_dest_path(
834 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
835 void,
836 "Failed to compute transaction dest path.");
837
838 std::ofstream out{transaction_dest_path};
839 if (!out.is_open()) {
840 return FormattableError{
841 "Failed to open file for writing: '{}'.", FormatParam{transaction_dest_path.string(), Style::bold}};
842 }
843
844 // Write header
845 if (is_firered) {
846 out << "id,behavior,terrainType,encounterType\n";
847 }
848 else {
849 out << "id,behavior\n";
850 }
851
852 if (non_default_count == 0) {
853 // No non-default attributes to write
854 out.flush();
855 return {};
856 }
857
858 // Write each non-default attribute row
859 for (const auto &[metatile_id, attribute] : attributes) {
860 if (is_firered) {
861 if (attribute.behavior() == default_behavior && attribute.terrain() == default_terrain &&
862 attribute.encounter_type() == default_encounter) {
863 continue;
864 }
866 behavior_name,
867 behavior_map_->lookup(attribute.behavior()),
868 void,
869 std::format("Failed to lookup behavior name for metatile {}.", metatile_id));
871 terrain_name,
872 terrain_map_->lookup(attribute.terrain()),
873 void,
874 std::format("Failed to lookup terrain type name for metatile {}.", metatile_id));
876 encounter_name,
877 encounter_map_->lookup(attribute.encounter_type()),
878 void,
879 std::format("Failed to lookup encounter type name for metatile {}.", metatile_id));
880 out << metatile_id << "," << behavior_name << "," << terrain_name << "," << encounter_name << "\n";
881 }
882 else {
883 if (attribute.behavior() == default_behavior) {
884 // Skip default behavior (MB_NORMAL = 0), since it's implicit for missing entries
885 continue;
886 }
888 behavior_name,
889 behavior_map_->lookup(attribute.behavior()),
890 void,
891 std::format("Failed to lookup behavior name for metatile {}.", metatile_id));
892 out << metatile_id << "," << behavior_name << "\n";
893 }
894 }
895
896 out.flush();
897 return {};
898}
899
901ProjectTilesetArtifactWriter::write_porytiles_pal_n(const ArtifactKey &dest_key, const Tileset &src, std::size_t index)
902{
903 if (src.porytiles_component().pal_at(index).has_value()) {
905 transaction_dest_path,
906 compute_transaction_dest_path(
907 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
908 void,
909 "Failed to compute transaction dest path.");
910
911 return save_palette(src.porytiles_component().pal_at(index).value(), transaction_dest_path, *pal_saver_);
912 }
913
914 // No porytiles pal, do nothing
915 return {};
916}
917
919 const ArtifactKey &dest_key, const Tileset &src, const std::string &anim_name, const std::string &frame_name)
920{
921 return write_anim_frame_impl<Rgba32>(
922 dest_key,
923 src,
924 anim_name,
925 frame_name,
926 transaction_root_,
927 project_root_,
928 staged_directories_,
929 staged_special_files_,
930 [](const Tileset &t) -> const auto & { return t.porytiles_component(); },
931 [this](const Image<Rgba32> &img, const std::filesystem::path &path) {
932 return save_layer_png(*png_rgba_saver_, img, path);
933 },
934 "Porytiles");
935}
936
937[[nodiscard]] ChainableResult<void>
939{
940 const auto &porytiles_anims = src.porytiles_component().anims();
941 const auto &primary_overrides = src.porytiles_component().primary_anim_overrides();
942
943 if (porytiles_anims.empty() && primary_overrides.empty()) {
944 /*
945 * Unlike in write_porymap_anim_params, we don't need to delete anything here. That's because anim.json is
946 * within porytiles_src dir, which is written using an atomic move. If the new porytiles_src dir doesn't contain
947 * an anim.json, the old one will get wiped by the commit() call.
948 */
949 return {};
950 }
951
952 // Extract params from animations
953 std::map<DynamicCasedName, AnimParams> anim_params;
954 for (const auto &[anim_name, anim] : porytiles_anims) {
955 anim_params[DynamicCasedName{anim_name}] = anim.params();
956 }
957
958 // Convert primary_anim_overrides keys from std::string to DynamicCasedName
959 std::map<DynamicCasedName, std::vector<AnimOverrideEntry>> primary_refs;
960 for (const auto &[name, entries] : primary_overrides) {
961 primary_refs[DynamicCasedName{name}] = entries;
962 }
963
965 transaction_dest_path,
966 compute_transaction_dest_path(
967 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
968 void,
969 "Failed to compute transaction dest path.");
970
971 return anim_json_parser_->write(transaction_dest_path, anim_params, primary_refs);
972}
973
974} // namespace porytiles
#define PT_TRY_ASSIGN_CHAIN_ERR(var, expr, return_type,...)
Unwraps a ChainableResult, chaining a new error message on failure.
#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
bool has_palette() const
const std::vector< PixelTile< PixelType > > & tiles() const
ChainableResult< void > write(const std::filesystem::path &json_path, const std::map< DynamicCasedName, AnimParams > &params, 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
virtual ChainableResult< std::uint16_t > lookup(const std::string &behavior_name) const =0
Looks up the numeric value for a behavior constant name.
A result type that maintains a chainable sequence of errors for debugging and error reporting.
ChainableResult< ConfigValue< TilesPalMode > > tiles_pal_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.
virtual ChainableResult< std::uint8_t > lookup(const std::string &encounter_name) const =0
Looks up the numeric value for an encounter type constant name.
A service interface that saves a fixed-length Palette to a given file.
virtual ChainableResult< void > save(const Palette< Rgba32, pal::max_size > &pal, const std::filesystem::path &path) const =0
A text parameter with associated styling for formatted output.
General-purpose error implementation with formatted message support.
Definition error.hpp:63
A template for two-dimensional images with arbitrarily typed pixel values.
Definition image.hpp:23
void set(std::size_t i, PixelType pixel)
Sets the pixel value at a given one-dimensional pixel index.
Definition image.hpp:88
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
A generic palette container for colors that support transparency checking.
Definition palette.hpp:47
std::size_t size() const
Returns the number of slots in the palette.
Definition palette.hpp:229
ColorType at(std::size_t index) const
Gets the color at a specific index.
Definition palette.hpp:276
bool has_any_wildcards() const
Checks if the palette contains any wildcard slots.
Definition palette.hpp:211
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, TilesPalMode 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, pal::max_size > & pal_at(std::size_t pal_index) const
const std::vector< MetatileAttribute > & metatile_attributes_bin() const
const Image< IndexPixel > & tiles_png() const
const std::optional< Palette< Rgba32, pal::max_size > > & pal_at(std::size_t pal_index) const
const std::map< std::string, Animation< Rgba32 > > & anims() const
const std::map< std::size_t, MetatileAttribute > & metatile_attributes() 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_porymap_pal_n(const ArtifactKey &dest_key, const Tileset &src, std::size_t index) override
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_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_porytiles_pal_n(const ArtifactKey &dest_key, const Tileset &src, std::size_t index) 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_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
ChainableResult< bool > is_secondary(const std::string &tileset_name) const override
Determines whether a tileset is a secondary tileset.
static const Style bold
Bold text formatting.
virtual ChainableResult< std::uint8_t > lookup(const std::string &terrain_name) const =0
Looks up the numeric value for a terrain type constant name.
A complete tileset containing both Porytiles and Porymap components.
Definition tileset.hpp:14
const PorytilesTilesetComponent & porytiles_component() const
Definition tileset.hpp:37
const PorymapTilesetComponent & porymap_component() const
Definition tileset.hpp:47
const std::string & name() const
Definition tileset.hpp:32
constexpr std::size_t bytes_per_attr_firered
constexpr std::size_t side_length_pix
TilesPalMode
Controls how tiles.png is rendered.
void panic(const StringViewSourceLoc &s)
Unconditionally terminates the program with a panic message.
Definition panic.cpp:43
DynamicCasedName extract_tileset_cased_name(const std::string &tileset_name)
Extracts the tileset short name and wraps it in a DynamicCasedName.
@ tileset
Configuration scoped to a specific tileset.
Utility functions for string manipulation and formatting.