Porytiles
Loading...
Searching...
No Matches
tileset_compile_validators.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <map>
4#include <set>
5#include <string>
6#include <vector>
7
8#include "gsl/pointers"
9
21
32namespace porytiles {
33
48};
49
50namespace details {
51
71 const TilesetCompileValidatorServices &services,
72 const Metatile<Rgba32> &metatile,
73 std::size_t metatile_index,
74 std::size_t internal_tile_index,
75 std::size_t row,
76 std::size_t col,
77 const std::string &diagnostic_code,
78 const std::string &error_message,
79 const Rgba32 &extrinsic_transparency)
80{
81 auto [layer, subtile] = metatile::from_internal_tile_index(internal_tile_index);
82 std::vector errors = {services.diag.formatter().format(
83 "{}: {}",
85 metatile::message_header(services.diag.formatter(), metatile_index, layer, subtile, row, col), Style::bold},
86 FormatParam{error_message})};
87 std::vector highlight = services.tile_printer.print_metatile_pixel_highlight(
88 metatile, layer, subtile, row, col, extrinsic_transparency);
89 errors.append_range(highlight);
90 services.diag.error(diagnostic_code, errors);
91}
92
113 const TilesetCompileValidatorServices &services,
114 const PixelTile<Rgba32> &tile,
115 const std::string &anim_name,
116 const std::string &frame_name,
117 std::size_t internal_tile_index,
118 std::size_t row,
119 std::size_t col,
120 const std::string &diagnostic_code,
121 const std::string &error_message,
122 const Rgba32 &extrinsic_transparency)
123{
124 std::vector errors = {services.diag.formatter().format(
125 "{}: {}",
127 anim::message_header(services.diag.formatter(), anim_name, frame_name, internal_tile_index, row, col),
129 FormatParam{error_message})};
130 std::vector highlight = services.tile_printer.print_tile_pixel_highlight(tile, row, col, extrinsic_transparency);
131 errors.append_range(highlight);
132 services.diag.error(diagnostic_code, errors);
133}
134
153 const TilesetCompileValidatorServices &services,
154 const PixelTile<Rgba32> &tile,
155 const std::string &anim_name,
156 const std::string &frame_name,
157 std::size_t internal_tile_index,
158 const std::string &diagnostic_code,
159 const std::string &error_message,
160 const Rgba32 &extrinsic_transparency)
161{
162 std::vector errors = {services.diag.formatter().format(
163 "{}: {}",
165 anim::message_header(services.diag.formatter(), anim_name, frame_name, internal_tile_index), Style::bold},
166 FormatParam{error_message})};
167 std::vector tile_visual = services.tile_printer.print_tile(tile, extrinsic_transparency);
168 errors.append_range(tile_visual);
169 services.diag.error(diagnostic_code, errors);
170}
171
184 const std::string &tag,
185 const TilesetCompileValidatorServices &services,
186 const std::map<Rgba32, unsigned int> &color_counts)
187{
188 std::vector<std::string> color_lines;
189 color_lines.emplace_back("color counts:");
190 auto counts = services.pal_printer.print_rgba_pal_counts(color_counts);
191 color_lines.append_range(counts);
192 services.diag.error_note(tag, color_lines);
193}
194
195} // namespace details
196
215 const TilesetCompileValidatorServices &services,
216 const std::string &tileset_name,
217 bool is_secondary,
218 const std::vector<Metatile<Rgba32>> &metatiles)
219{
220 PT_UNWRAP_TILESET_CONFIG_REF(services.config, num_metatiles_in_primary, tileset_name, void);
221 PT_UNWRAP_TILESET_CONFIG_REF(services.config, num_metatiles_total, tileset_name, void);
222
223 std::size_t metatile_limit = is_secondary ? (num_metatiles_total.value() - num_metatiles_in_primary.value())
224 : num_metatiles_in_primary.value();
225
226 if (metatiles.size() > metatile_limit) {
227 services.diag.error(
228 "metatile-limit-exceeded",
229 "Too many metatiles ({}) in Porytiles component for tileset '{}'.",
230 FormatParam{metatiles.size(), Style::bold},
231 FormatParam{tileset_name, Style::bold});
232
233 std::vector<std::string> note_text;
234 if (is_secondary) {
235 note_text.append_range(build_subtraction_limit_lines(
236 services.diag.formatter(),
237 "Metatile limit",
238 metatile_limit,
239 num_metatiles_total,
240 num_metatiles_in_primary));
241 }
242 else {
243 note_text.push_back(
244 services.diag.formatter().format("Metatile limit is '{}'.", FormatParam{metatile_limit, Style::bold}));
245 note_text.emplace_back("");
246 note_text.append_range(format_config_note(services.diag.formatter(), num_metatiles_in_primary));
247 }
248 services.diag.error_note("metatile-limit-exceeded", note_text);
249
250 return FormattableError{
251 "Found '{}' metatiles, limit is '{}'.",
252 FormatParam{metatiles.size(), Style::bold},
253 FormatParam{metatile_limit, Style::bold}};
254 }
255
256 return {};
257}
258
279 const TilesetCompileValidatorServices &services,
280 const std::string &tileset_name,
282 std::size_t pal_index)
283{
284 PT_UNWRAP_TILESET_CONFIG_REF(services.config, extrinsic_transparency, tileset_name, void);
285
286 bool hit_error = false;
287 const std::string filename = pal_filename(pal_index);
288 std::vector<std::size_t> violating_slots{};
289
290 if (pal.is_wildcard(0)) {
291 panic("unexpected wildcard in Porymap palette slot 0");
292 }
293
294 // Check 1: Slot 0 should match extrinsic transparency (warning only)
295 const Rgba32 slot0_color = pal.slot_zero_color();
296 if (!slot0_color.is_extrinsically_transparent(extrinsic_transparency)) {
297 std::vector<std::string> warning_lines;
298 warning_lines.emplace_back(services.diag.formatter().format(
299 "Porymap palette '{}' slot 0 color '{}' does not match extrinsic transparency '{}'",
300 FormatParam{filename, Style::bold},
301 FormatParam{slot0_color.to_jasc_str(), Style::bold},
302 FormatParam{extrinsic_transparency.value().to_jasc_str(), Style::bold}));
303 warning_lines.emplace_back("Slot 0 is typically reserved for the transparency color.");
304 warning_lines.emplace_back("If you are using slot 0 for a .pla blend color, you can ignore this warning.");
305 services.diag.warning("porymap-palette-slot-0", warning_lines);
306
307 services.diag.warning_note(
308 "porymap-palette-slot-0",
310 services.diag.formatter(),
311 services.pal_printer,
312 "reserved transparency slot",
313 pal,
314 filename,
315 std::vector<std::size_t>{0}));
316 services.diag.warning_note(
317 "porymap-palette-slot-0", format_config_note(services.diag.formatter(), extrinsic_transparency));
318 }
319
320 // Check 2: Non-slot-0 positions cannot contain extrinsic transparency
321 for (std::size_t slot = 1; slot < pal.size(); ++slot) {
322 if (pal.is_wildcard(slot)) {
323 panic("unexpected wildcard in Porymap palette");
324 }
325 const Rgba32 color = pal.at(slot);
326 if (color.is_extrinsically_transparent(extrinsic_transparency)) {
327 hit_error = true;
328 violating_slots.push_back(slot);
329 std::vector<std::string> error_lines;
330 error_lines.emplace_back(services.diag.formatter().format(
331 "Porymap palette '{}' slot '{}' contains extrinsic transparency color '{}'",
332 FormatParam{filename, Style::bold},
333 FormatParam{slot, Style::bold},
334 FormatParam{color.to_jasc_str(), Style::bold}));
335 error_lines.emplace_back("Extrinsic transparency is not allowed in non-slot-0 positions.");
336 services.diag.error("porymap-palette-transparency", error_lines);
337 }
338 }
339
340 if (hit_error) {
341 // Print the palette with violating slots highlighted
342 services.diag.error_note(
343 "porymap-palette-transparency",
345 services.diag.formatter(),
346 services.pal_printer,
347 "slots with invalid extrinsic transparency",
348 pal,
349 filename,
350 violating_slots));
351 services.diag.error_note(
352 "porymap-palette-transparency", format_config_note(services.diag.formatter(), extrinsic_transparency));
353
354 return FormattableError{"Validation failed for Porymap palette '{}'.", FormatParam{filename, Style::bold}};
355 }
356
357 return {};
358}
359
382 const TilesetCompileValidatorServices &services,
383 const std::string &tileset_name,
385 std::size_t pal_index)
386{
387 PT_UNWRAP_TILESET_CONFIG_REF(services.config, extrinsic_transparency, tileset_name, void);
388
389 bool hit_error = false;
390 const std::string filename = pal_filename(pal_index);
391 std::vector<std::size_t> violating_slots;
392
393 // Check 1: Slot 0 should match extrinsic transparency (warning only)
394 if (!pal.is_wildcard(0)) {
395 const Rgba32 slot0_color = pal.slot_zero_color();
396 if (!slot0_color.is_extrinsically_transparent(extrinsic_transparency)) {
397 // Build the warning text
398 std::vector<std::string> warning_text;
399 warning_text.emplace_back(services.diag.formatter().format(
400 "Porytiles palette '{}' slot 0 color '{}' does not match extrinsic transparency '{}'",
401 FormatParam{filename, Style::bold},
402 FormatParam{slot0_color.to_jasc_str(), Style::bold},
403 FormatParam{extrinsic_transparency.value().to_jasc_str(), Style::bold}));
404 warning_text.emplace_back("Slot 0 is typically reserved for the transparency color.");
405 warning_text.emplace_back("If you are using slot 0 for a .pla blend color, you can ignore this warning.");
406 services.diag.warning("porytiles-palette-slot-0", warning_text);
407
408 // Print the palette and config notes.
409 services.diag.warning_note(
410 "porytiles-palette-slot-0",
412 services.diag.formatter(),
413 services.pal_printer,
414 "reserved transparency slot",
415 pal,
416 filename,
417 std::vector<std::size_t>{0}));
418 services.diag.warning_note(
419 "porytiles-palette-slot-0", format_config_note(services.diag.formatter(), extrinsic_transparency));
420 }
421 }
422
423 // Check 2: Non-slot-0 positions cannot contain extrinsic transparency
424 for (std::size_t slot = 1; slot < pal.size(); ++slot) {
425 if (pal.is_wildcard(slot)) {
426 continue;
427 }
428 const Rgba32 color = pal.at(slot);
429 if (color.is_extrinsically_transparent(extrinsic_transparency)) {
430 hit_error = true;
431 violating_slots.push_back(slot);
432 std::vector<std::string> error_lines;
433 error_lines.emplace_back(services.diag.formatter().format(
434 "Porytiles palette '{}' slot '{}' contains extrinsic transparency color '{}'",
435 FormatParam{filename, Style::bold},
436 FormatParam{slot, Style::bold},
437 FormatParam{color.to_jasc_str(), Style::bold}));
438 error_lines.emplace_back("Extrinsic transparency is not allowed in non-slot-0 positions.");
439 services.diag.error("porytiles-palette-transparency", error_lines);
440 }
441 }
442
443 if (hit_error) {
444 // Print the palette and config notes
445 services.diag.error_note(
446 "porytiles-palette-transparency",
448 services.diag.formatter(),
449 services.pal_printer,
450 "slots with invalid extrinsic transparency",
451 pal,
452 filename,
453 violating_slots));
454 services.diag.error_note(
455 "porytiles-palette-transparency", format_config_note(services.diag.formatter(), extrinsic_transparency));
456
457 return FormattableError{"Validation failed for Porytiles palette '{}'.", FormatParam{filename, Style::bold}};
458 }
459
460 return {};
461}
462
487 const TilesetCompileValidatorServices &services, const std::string &tileset_name, const PaletteHint &hint)
488{
489 PT_UNWRAP_TILESET_CONFIG_REF(services.config, extrinsic_transparency, tileset_name, void);
490
491 bool hit_any_error = false;
492 const std::string &hint_name = hint.name();
493
494 if (hint.pal().size() >= pal::max_size) {
495 services.diag.error(
496 "palette-hint-size-violation",
497 services.diag.formatter().format(
498 "palette hint '{}' has size '{}', max allowed size is '{}'",
499 FormatParam{hint_name, Style::bold},
500 FormatParam{hint.pal().size(), Style::bold},
501 FormatParam{pal::max_size - 1, Style::bold}));
502 services.diag.error_note(
503 "palette-hint-size-violation",
505 services.diag.formatter(),
506 services.pal_printer,
507 "invalid extra slots start here",
508 hint,
509 hint_name,
510 std::vector{pal::max_size}));
511 return FormattableError{"Validation failed for palette hint '{}'.", FormatParam{hint_name, Style::bold}};
512 }
513
514 std::set<Rgba32> seen_colors{};
515 std::vector<std::size_t> violating_slots{};
516
517 // Check 1: Extrinsic transparency not allowed in hints
518 for (std::size_t slot = 0; slot < hint.pal().size(); ++slot) {
519 if (hint.pal().is_wildcard(slot)) {
520 continue;
521 }
522 const Rgba32 color = hint.pal().at(slot);
523
524 if (color.is_extrinsically_transparent(extrinsic_transparency)) {
525 hit_any_error = true;
526 violating_slots.push_back(slot);
527 std::vector<std::string> error_lines;
528 error_lines.emplace_back(services.diag.formatter().format(
529 "palette hint '{}' slot '{}' contains extrinsic transparency color '{}'",
530 FormatParam{hint_name, Style::bold},
531 FormatParam{slot, Style::bold},
532 FormatParam{color.to_jasc_str(), Style::bold}));
533 error_lines.emplace_back("Extrinsic transparency is not allowed in palette hints.");
534 services.diag.error("palette-hint-transparency", error_lines);
535 }
536 }
537 if (!violating_slots.empty()) {
538 services.diag.error_note(
539 "palette-hint-transparency",
541 services.diag.formatter(),
542 services.pal_printer,
543 "slots with invalid extrinsic transparency",
544 hint,
545 hint_name,
546 violating_slots));
547 services.diag.error_note(
548 "palette-hint-transparency", format_config_note(services.diag.formatter(), extrinsic_transparency));
549 }
550 violating_slots.clear();
551
552 // Check 2: No duplicate colors
553 for (std::size_t slot = 0; slot < hint.pal().size(); ++slot) {
554 if (hint.pal().is_wildcard(slot)) {
555 continue;
556 }
557 const Rgba32 color = hint.pal().at(slot);
558
559 if (seen_colors.contains(color)) {
560 hit_any_error = true;
561 violating_slots.push_back(slot);
562 std::vector<std::string> error_lines;
563 error_lines.emplace_back(services.diag.formatter().format(
564 "palette hint '{}' contains duplicate color '{}' at slot '{}'",
565 FormatParam{hint_name, Style::bold},
566 FormatParam{color.to_jasc_str(), Style::bold},
567 FormatParam{slot, Style::bold}));
568 error_lines.emplace_back("Duplicate colors are not allowed in palette hints.");
569 services.diag.error("palette-hint-duplicate-color", error_lines);
570 }
571 seen_colors.insert(color);
572 }
573 if (!violating_slots.empty()) {
574 services.diag.error_note(
575 "palette-hint-duplicate-color",
577 services.diag.formatter(),
578 services.pal_printer,
579 "slots with invalid duplicate colors",
580 hint,
581 hint_name,
582 violating_slots));
583 }
584
585 if (hit_any_error) {
586 return FormattableError{"Validation failed for palette hint '{}'.", FormatParam{hint_name, Style::bold}};
587 }
588 return {};
589}
590
607 const TilesetCompileValidatorServices &services,
608 const std::string &tileset_name,
609 const std::vector<Metatile<Rgba32>> &metatiles,
610 const std::map<std::string, Animation<Rgba32>> &anims)
611{
612 PT_UNWRAP_TILESET_CONFIG_REF(services.config, extrinsic_transparency, tileset_name, void);
613
614 bool hit_error = false;
615
616 // Validate metatiles
617 std::size_t metatile_index = 0;
618 for (const auto &metatile : metatiles) {
619 const auto decomposed_metatile = metatile.decompose();
620
621 // Iterate over each internal tile
622 for (std::size_t internal_tile_index = 0; internal_tile_index < decomposed_metatile.size();
623 ++internal_tile_index) {
624 const auto &tile = decomposed_metatile[internal_tile_index];
625
626 // Iterate over each pixel in the current internal tile
627 for (std::size_t row = 0; row < tile::side_length_pix; ++row) {
628 for (std::size_t col = 0; col < tile::side_length_pix; ++col) {
629 const auto &pixel = tile.at(row, col);
630 if (pixel.alpha() != Rgba32::alpha_opaque && pixel.alpha() != Rgba32::alpha_transparent) {
631 hit_error = true;
632 std::string error_message = services.diag.formatter().format(
633 "invalid alpha channel: {}", FormatParam{std::to_string(pixel.alpha()), Style::bold});
635 services,
636 metatile,
637 metatile_index,
638 internal_tile_index,
639 row,
640 col,
641 "alpha-channel-violation",
642 error_message,
643 extrinsic_transparency);
644 }
645 }
646 }
647 }
648 metatile_index++;
649 }
650
651 // Lambda to avoid duplicating logic
652 auto validate_anim_frame = [&services, &hit_error, &extrinsic_transparency](
653 const std::string &anim_name, const AnimFrame<Rgba32> &frame) -> void {
654 const auto &frame_tiles = frame.tiles();
655
656 // Loop over each internal frame tile
657 for (std::size_t internal_tile_index = 0; internal_tile_index < frame_tiles.size(); ++internal_tile_index) {
658 const auto &tile = frame_tiles.at(internal_tile_index);
659
660 // loop over tile pixels, row-major
661 for (std::size_t row = 0; row < tile::side_length_pix; ++row) {
662 for (std::size_t col = 0; col < tile::side_length_pix; ++col) {
663 const auto &pixel = tile.at(row, col);
664
665 // validate
666 if (pixel.alpha() != Rgba32::alpha_opaque && pixel.alpha() != Rgba32::alpha_transparent) {
667 hit_error = true;
668 std::string error_message = services.diag.formatter().format(
669 "invalid alpha channel: {}", FormatParam{std::to_string(pixel.alpha()), Style::bold});
671 services,
672 tile,
673 anim_name,
674 frame.frame_name(),
675 internal_tile_index,
676 row,
677 col,
678 "alpha-channel-violation",
679 error_message,
680 extrinsic_transparency);
681 }
682 }
683 }
684 }
685 };
686
687 // Validate animations
688 for (const auto &[anim_name, anim] : anims) {
689 if (anim.has_key_frame()) {
690 const auto &key_frame = anim.key_frame();
691 validate_anim_frame(anim_name, key_frame);
692 }
693 for (const AnimFrame<Rgba32> &frame : anim.frames() | std::views::values) {
694 validate_anim_frame(anim_name, frame);
695 }
696 }
697
698 if (hit_error) {
699 return FormattableError{"Found input pixel(s) with invalid alpha channel value(s)."};
700 }
701
702 return {};
703}
704
726 const TilesetCompileValidatorServices &services,
727 const std::string &tileset_name,
728 const std::vector<Metatile<Rgba32>> &metatiles)
729{
730 PT_UNWRAP_TILESET_CONFIG_REF(services.config, extrinsic_transparency, tileset_name, void);
731 PT_UNWRAP_TILESET_CONFIG_REF(services.config, num_tiles_per_metatile, tileset_name, void);
732 auto configured_layer_mode = layer_mode_from_val(num_tiles_per_metatile);
733
734 // If layer mode is triple, just return
735 if (configured_layer_mode == LayerMode::triple) {
736 return {};
737 }
738
739 bool hit_error = false;
740 std::size_t metatile_index = 0;
741
742 for (const auto &metatile : metatiles) {
743 // Determine the implied layer mode for this metatile
744 // Check each of the 4 tile positions (northwest, northeast, southwest, southeast)
745 bool found_triple_layer_region = false;
746
747 for (std::size_t subtile_idx = 0; subtile_idx < metatile::tiles_per_metatile_layer; ++subtile_idx) {
748 const auto &bottom_tile = metatile.bottom(subtile_idx);
749 const auto &middle_tile = metatile.middle(subtile_idx);
750 const auto &top_tile = metatile.top(subtile_idx);
751 const auto subtile = metatile::subtile_from_index(subtile_idx);
752
753 // Check if each layer has at least one non-transparent pixel in this tile position
754 const bool bottom_has_opaque = !bottom_tile.is_transparent(extrinsic_transparency.value());
755 const bool middle_has_opaque = !middle_tile.is_transparent(extrinsic_transparency.value());
756 const bool top_has_opaque = !top_tile.is_transparent(extrinsic_transparency.value());
757
758 // If all three layers have opaque pixels in this tile position, this is a triple-layer region
759 if (bottom_has_opaque && middle_has_opaque && top_has_opaque) {
760 found_triple_layer_region = true;
761 }
762
763 const LayerMode implied_mode = found_triple_layer_region ? LayerMode::triple : LayerMode::dual;
764
765 // Error condition if implied mode is triple for a dual-layer compilation
766 if (implied_mode == LayerMode::triple && configured_layer_mode == LayerMode::dual) {
767 hit_error = true;
768 std::vector errors = {services.diag.formatter().format(
769 "{}: {}",
771 metatile::message_header(services.diag.formatter(), metatile_index, subtile), Style::bold},
772 FormatParam{"non-transparent content on all three layers"})};
773 std::vector bottom_highlight = services.tile_printer.print_metatile_tile_highlight(
774 metatile, metatile::Layer::bottom, subtile, extrinsic_transparency);
775 std::vector middle_highlight = services.tile_printer.print_metatile_tile_highlight(
776 metatile, metatile::Layer::middle, subtile, extrinsic_transparency);
777 std::vector top_highlight = services.tile_printer.print_metatile_tile_highlight(
778 metatile, metatile::Layer::top, subtile, extrinsic_transparency);
779 errors.append_range(bottom_highlight);
780 errors.append_range(middle_highlight);
781 errors.append_range(top_highlight);
782 services.diag.error("layer-mode-violation", errors);
783 }
784 }
785
786 metatile_index++;
787 }
788
789 if (hit_error) {
790 std::vector<std::string> note_text;
791 note_text.push_back(
792 services.diag.formatter().format("Implied layer mode is '{}'.", FormatParam{LayerMode::dual, Style::bold}));
793 note_text.emplace_back("");
794 note_text.append_range(format_config_note(services.diag.formatter(), num_tiles_per_metatile));
795 note_text.emplace_back("");
796 note_text.emplace_back("Consider enabling triple-layer metatiles.");
797 note_text.push_back(services.diag.formatter().format(
798 "To enable layer mode '{}' for your project:", FormatParam{LayerMode::triple, Style::bold}));
799 note_text.push_back(services.diag.formatter().format(
800 " - set '{}' = '{}'",
801 FormatParam{num_tiles_per_metatile.canonical_name(), Style::bold},
803 note_text.push_back(services.diag.formatter().format(
804 " - follow the steps here: {}",
805 FormatParam{"https://github.com/pret/pokeemerald/wiki/Triple-layer-metatiles", Style::underline}));
806
807 // Emit note
808 services.diag.error_note("layer-mode-violation", note_text);
809
810 return FormattableError{"Found metatile(s) with mismatched implied layer mode."};
811 }
812
813 return {};
814}
815
834 const TilesetCompileValidatorServices &services,
835 const std::string &tileset_name,
836 const std::vector<Metatile<Rgba32>> &metatiles,
837 const std::map<std::string, Animation<Rgba32>> &anims)
838{
839 PT_UNWRAP_TILESET_CONFIG_REF(services.config, extrinsic_transparency, tileset_name, void);
840
841 bool hit_error = false;
842 std::size_t metatile_index = 0;
843 for (const auto &metatile : metatiles) {
844 const auto decomposed_metatile = metatile.decompose();
845
846 // Iterate over each internal tile
847 for (std::size_t internal_tile_index = 0; internal_tile_index < decomposed_metatile.size();
848 ++internal_tile_index) {
849 const auto &tile = decomposed_metatile[internal_tile_index];
850 std::map<Rgba32, unsigned int> color_counts;
851
852 // Iterate over each pixel in the current internal tile
853 for (std::size_t row = 0; row < tile::side_length_pix; ++row) {
854 for (std::size_t col = 0; col < tile::side_length_pix; ++col) {
855 const auto &pixel = tile.at(row, col);
856 if (pixel.alpha() != Rgba32::alpha_transparent && pixel != extrinsic_transparency.value()) {
857 color_counts[pixel]++;
858 }
859
860 if (color_counts.size() > pal::max_size - 1) {
861 hit_error = true;
862 std::string error_message = services.diag.formatter().format(
863 "found {}th unique tile color: {}",
865 FormatParam{pixel.to_jasc_str(), Style::bold});
867 services,
868 metatile,
869 metatile_index,
870 internal_tile_index,
871 row,
872 col,
873 "tile-color-count-violation",
874 error_message,
875 extrinsic_transparency);
876 details::report_color_counts("tile-color-count-violation", services, color_counts);
877 goto next_tile;
878 }
879 }
880 }
881 next_tile:;
882 }
883 metatile_index++;
884 }
885
886 auto validate_anim_frame = [&services, &hit_error, &extrinsic_transparency](
887 const std::string &anim_name, const AnimFrame<Rgba32> &frame) -> void {
888 const auto &frame_tiles = frame.tiles();
889 // Iterate over each internal tile
890 for (std::size_t internal_tile_index = 0; internal_tile_index < frame_tiles.size(); ++internal_tile_index) {
891 const auto &tile = frame_tiles[internal_tile_index];
892 std::map<Rgba32, unsigned int> color_counts;
893
894 // Iterate over each pixel in the current internal tile
895 for (std::size_t row = 0; row < tile::side_length_pix; ++row) {
896 for (std::size_t col = 0; col < tile::side_length_pix; ++col) {
897 const auto &pixel = tile.at(row, col);
898 if (pixel.alpha() != Rgba32::alpha_transparent && pixel != extrinsic_transparency.value()) {
899 color_counts[pixel]++;
900 }
901
902 if (color_counts.size() > pal::max_size - 1) {
903 hit_error = true;
904 std::string error_message = services.diag.formatter().format(
905 "found {}th unique frame tile color: {}",
907 FormatParam{pixel.to_jasc_str(), Style::bold});
909 services,
910 tile,
911 anim_name,
912 frame.frame_name(),
913 internal_tile_index,
914 row,
915 col,
916 "tile-color-count-violation",
917 error_message,
918 extrinsic_transparency);
919 details::report_color_counts("tile-color-count-violation", services, color_counts);
920 goto next_tile;
921 }
922 }
923 }
924 next_tile:;
925 }
926 };
927
928 // Validate animations
929 for (const auto &[anim_name, anim] : anims) {
930 if (anim.has_key_frame()) {
931 const auto &key_frame = anim.key_frame();
932 validate_anim_frame(anim_name, key_frame);
933 }
934 for (const AnimFrame<Rgba32> &frame : anim.frames() | std::views::values) {
935 validate_anim_frame(anim_name, frame);
936 }
937 }
938
939 if (hit_error) {
940 return FormattableError{
941 "Found tile(s) with more than {} unique non-transparent pixels.", FormatParam{pal::max_size - 1}};
942 }
943
944 return {};
945}
946
971 const TilesetCompileValidatorServices &services,
972 const std::string &tileset_name,
973 bool is_secondary,
974 const std::vector<Metatile<Rgba32>> &metatiles,
975 const std::map<std::string, Animation<Rgba32>> &anims,
976 const std::array<std::optional<Palette<Rgba32, pal::max_size>>, pal::num_pals> &porytiles_pals,
977 const std::vector<PaletteHint> &hints)
978{
979 PT_UNWRAP_TILESET_CONFIG_REF(services.config, extrinsic_transparency, tileset_name, void);
980 PT_UNWRAP_TILESET_CONFIG_REF(services.config, num_pals_in_primary, tileset_name, void);
981 PT_UNWRAP_TILESET_CONFIG_REF(services.config, num_pals_total, tileset_name, void);
982
983 ConfigValue<std::size_t> num_pals_cfg = is_secondary ? num_pals_total : num_pals_in_primary;
984 std::size_t count_max = num_pals_cfg.value() * (pal::max_size - 1);
985
986 std::map<Rgba32, unsigned int> color_counts{};
987 bool hit_first_violation = false;
988
989 // Count colors in the input metatiles
990 std::size_t metatile_index = 0;
991 for (const auto &metatile : metatiles) {
992 const auto decomposed_metatile = metatile.decompose();
993 for (std::size_t internal_tile_index = 0; internal_tile_index < decomposed_metatile.size();
994 ++internal_tile_index) {
995 const auto &tile = decomposed_metatile[internal_tile_index];
996 for (std::size_t row = 0; row < tile::side_length_pix; ++row) {
997 for (std::size_t col = 0; col < tile::side_length_pix; ++col) {
998 const auto &pixel = tile.at(row, col);
999 if (!pixel.is_transparent(extrinsic_transparency)) {
1000 color_counts[pixel]++;
1001 }
1002
1003 /*
1004 * Print the first violation only, but we'll keep counting across the rest of the metatiles so we
1005 * can give a final tally.
1006 */
1007 if (color_counts.size() > count_max && !hit_first_violation) {
1008 hit_first_violation = true;
1009 std::string error_message = services.diag.formatter().format(
1010 "found {}th globally unique color: {}",
1012 FormatParam{pixel.to_jasc_str(), Style::bold});
1014 services,
1015 metatile,
1016 metatile_index,
1017 internal_tile_index,
1018 row,
1019 col,
1020 "global-color-count-violation",
1021 error_message,
1022 extrinsic_transparency);
1023 }
1024 }
1025 }
1026 }
1027 metatile_index++;
1028 }
1029
1030 // Count colors in the input anims
1031 auto validate_anim_frame = [&services, &hit_first_violation, &color_counts, &count_max, &extrinsic_transparency](
1032 const std::string &anim_name, const AnimFrame<Rgba32> &frame) -> void {
1033 const auto &frame_tiles = frame.tiles();
1034 // Loop over each internal frame tile
1035 for (std::size_t internal_tile_index = 0; internal_tile_index < frame_tiles.size(); ++internal_tile_index) {
1036 const auto &tile = frame_tiles.at(internal_tile_index);
1037
1038 // loop over tile pixels, row-major
1039 for (std::size_t row = 0; row < tile::side_length_pix; ++row) {
1040 for (std::size_t col = 0; col < tile::side_length_pix; ++col) {
1041 const auto &pixel = tile.at(row, col);
1042
1043 if (!pixel.is_transparent(extrinsic_transparency)) {
1044 color_counts[pixel]++;
1045 }
1046
1047 /*
1048 * Print the first violation only, but we'll keep counting across the rest of the anims so we can
1049 * give a final tally.
1050 */
1051 if (color_counts.size() > count_max && !hit_first_violation) {
1052 hit_first_violation = true;
1053 std::string error_message = services.diag.formatter().format(
1054 "found {}th globally unique color: {}",
1056 FormatParam{pixel.to_jasc_str(), Style::bold});
1058 services,
1059 tile,
1060 anim_name,
1061 frame.frame_name(),
1062 internal_tile_index,
1063 row,
1064 col,
1065 "global-color-count-violation",
1066 error_message,
1067 extrinsic_transparency);
1068 }
1069 }
1070 }
1071 }
1072 };
1073
1074 for (const auto &[anim_name, anim] : anims) {
1075 if (anim.has_key_frame()) {
1076 const auto &key_frame = anim.key_frame();
1077 validate_anim_frame(anim_name, key_frame);
1078 }
1079 for (const AnimFrame<Rgba32> &frame : anim.frames() | std::views::values) {
1080 validate_anim_frame(anim_name, frame);
1081 }
1082 }
1083
1084 /*
1085 * Finally, check Porytiles override pals and hints. We check these last, since we've now seen all colors in the
1086 * actual input assets. This allows us to warn the user if they've specified manual colors that never actually
1087 * appear in the input assets.
1088 */
1089 std::set<Rgba32> unused_manual_colors{};
1090 for (std::size_t pal_index = 0; pal_index < num_pals_cfg; ++pal_index) {
1091 const std::string filename = pal_filename(pal_index);
1092
1093 if (porytiles_pals.at(pal_index).has_value()) {
1094 const auto &pal = porytiles_pals.at(pal_index).value();
1095 std::vector<std::size_t> unused_slots{};
1096
1097 for (std::size_t slot_index = 0; slot_index < pal.size(); ++slot_index) {
1098 if (pal.is_wildcard(slot_index)) {
1099 continue;
1100 }
1101
1102 const auto &pixel = pal.at(slot_index);
1103 /*
1104 * Warn user for nontransparent pal colors that satisfy either:
1105 *
1106 * 1. The pal color is not present in color_counts, i.e. it wasn't seen in any input assets.
1107 * 2. It's present in our unused_manual_colors set, which we update as we iterate over the pals.
1108 *
1109 * Condition 2) is vital because it allows us to detect and report all instances of an unused color,
1110 * even if it was duplicated multiple times in the manual palettes.
1111 */
1112 if ((!color_counts.contains(pixel) || unused_manual_colors.contains(pixel)) &&
1113 !pixel.is_transparent(extrinsic_transparency)) {
1114 services.diag.warning(
1115 "unused-manual-color",
1116 services.diag.formatter().format(
1117 "color '{}' in slot '{}' of Porytiles palette '{}' is unused",
1118 FormatParam{pixel, Style::bold},
1119 FormatParam{slot_index, Style::bold},
1120 FormatParam{filename, Style::bold}));
1121 unused_slots.push_back(slot_index);
1122 unused_manual_colors.insert(pixel);
1123 }
1124
1125 // Increment count for color.
1126 if (!pixel.is_transparent(extrinsic_transparency)) {
1127 color_counts[pixel]++;
1128 }
1129
1130 /*
1131 * Print the first violation only, but we'll keep counting across the rest of the pals so we can
1132 * give a final tally.
1133 */
1134 if (color_counts.size() > count_max && !hit_first_violation) {
1135 hit_first_violation = true;
1136 std::string error_message = services.diag.formatter().format(
1137 "in Porytiles palette '{}': found {}th globally unique color: {}",
1138 FormatParam{filename, Style::bold},
1140 FormatParam{pixel.to_jasc_str(), Style::bold});
1141 services.diag.error("global-color-count-violation", error_message);
1142 std::vector<std::size_t> violating_slot{};
1143 violating_slot.push_back(slot_index);
1144 services.diag.error_note(
1145 "global-color-count-violation",
1147 services.diag.formatter(),
1148 services.pal_printer,
1149 "violating slot",
1150 pal,
1151 filename,
1152 violating_slot));
1153 }
1154 } // END: loop over all slots in override pal
1155
1156 // Print note highlighting all unused slots in this pal
1157 if (!unused_slots.empty()) {
1158 services.diag.warning_note(
1159 "unused-manual-color",
1161 services.diag.formatter(),
1162 services.pal_printer,
1163 "slots with unused colors",
1164 pal,
1165 filename,
1166 unused_slots));
1167 }
1168 unused_slots.clear();
1169 }
1170 } // END: loop over all override pals
1171
1172 for (const auto &hint : hints) {
1173 std::vector<std::size_t> unused_slots{};
1174
1175 for (std::size_t slot_index = 0; slot_index < hint.pal().size(); ++slot_index) {
1176 const auto &pixel = hint.pal().at(slot_index);
1177
1178 /*
1179 * Warn user for nontransparent pal colors that satisfy either:
1180 *
1181 * 1. The pal color is not present in color_counts, i.e. it wasn't seen in any input assets.
1182 * 2. It's present in our unused_manual_colors set, which we update as we iterate over the pals.
1183 *
1184 * Condition 2) is vital because it allows us to detect and report all instances of an unused color,
1185 * even if it was duplicated multiple times in the manual palettes.
1186 */
1187 if ((!color_counts.contains(pixel) || unused_manual_colors.contains(pixel)) &&
1188 !pixel.is_transparent(extrinsic_transparency)) {
1189 services.diag.warning(
1190 "unused-manual-color",
1191 services.diag.formatter().format(
1192 "color '{}' in slot '{}' of palette hint '{}' is unused",
1193 FormatParam{pixel, Style::bold},
1194 FormatParam{slot_index, Style::bold},
1195 FormatParam{hint.name(), Style::bold}));
1196 unused_slots.push_back(slot_index);
1197 unused_manual_colors.insert(pixel);
1198 }
1199
1200 // Increment count for color.
1201 if (!pixel.is_transparent(extrinsic_transparency)) {
1202 color_counts[pixel]++;
1203 }
1204
1205 /*
1206 * Print the first violation only, but we'll keep counting across the rest of the hints so we can
1207 * give a final tally.
1208 */
1209 if (color_counts.size() > count_max && !hit_first_violation) {
1210 hit_first_violation = true;
1211 std::string error_message = services.diag.formatter().format(
1212 "in palette hint '{}': found {}th globally unique color: {}",
1213 FormatParam{hint.name(), Style::bold},
1215 FormatParam{pixel.to_jasc_str(), Style::bold});
1216 services.diag.error("global-color-count-violation", error_message);
1217 std::vector<std::size_t> violating_slot{};
1218 violating_slot.push_back(slot_index);
1219 services.diag.error_note(
1220 "global-color-count-violation",
1222 services.diag.formatter(),
1223 services.pal_printer,
1224 "violating slot",
1225 hint,
1226 hint.name(),
1227 violating_slot));
1228 }
1229 } // END: loop over all slots in pal hint
1230
1231 // Print note highlighting all unused slots in this pal
1232 if (!unused_slots.empty()) {
1233 services.diag.warning_note(
1234 "unused-manual-color",
1236 services.diag.formatter(),
1237 services.pal_printer,
1238 "slots with unused colors",
1239 hint,
1240 hint.name(),
1241 unused_slots));
1242 }
1243 unused_slots.clear();
1244 } // END: loop over all pal hints
1245
1246 if (color_counts.size() > count_max) {
1247 services.diag.error(
1248 "global-color-count-violation",
1249 services.diag.formatter().format(
1250 "global color count violation: found '{}' unique colors, limit is '{}'",
1251 FormatParam{color_counts.size(), Style::bold},
1252 FormatParam{count_max, Style::bold}));
1253 details::report_color_counts("global-color-count-violation", services, color_counts);
1254 services.diag.error_note(
1255 "global-color-count-violation",
1256 build_global_color_limit_lines(services.diag.formatter(), count_max, num_pals_cfg));
1257 return FormattableError{
1258 "Found '{}' unique colors globally, limit is '{}'.",
1259 FormatParam{color_counts.size(), Style::bold},
1260 FormatParam{count_max, Style::bold}};
1261 }
1262
1263 return {};
1264}
1265
1287 const TilesetCompileValidatorServices &services,
1288 const std::string &tileset_name,
1289 const std::vector<Metatile<Rgba32>> &metatiles,
1290 const std::map<std::string, Animation<Rgba32>> &anims,
1291 const std::array<std::optional<Palette<Rgba32, pal::max_size>>, pal::num_pals> &porytiles_pals,
1292 const std::vector<PaletteHint> &hints,
1293 const std::optional<std::array<Palette<Rgba32, pal::max_size>, pal::num_pals>> &porymap_pals)
1294{
1295 return {};
1296}
1297
1323 const TilesetCompileValidatorServices &services,
1324 const std::string &tileset_name,
1325 const std::map<std::string, Animation<Rgba32>> &anims)
1326{
1327 PT_UNWRAP_TILESET_CONFIG_REF(services.config, extrinsic_transparency, tileset_name, void);
1328 PT_UNWRAP_TILESET_CONFIG_REF(services.config, global_frame_linking, tileset_name, void);
1329 PT_UNWRAP_TILESET_CONFIG_REF(services.config, per_anim_overrides, tileset_name, void);
1330
1331 bool hit_error = false;
1332
1333 /*
1334 * Validation 0: Warn when animations with automatic frame linking are missing a key frame.
1335 *
1336 * In automatic mode, key.png is used to determine which tiles in tiles.png are animation tiles. Without it,
1337 * the first regular frame is used as a representative and animation tiles will not be linked to any metatiles.
1338 * This is allowed so users can incrementally build animation assets before committing to linking.
1339 */
1340 for (const auto &[anim_name, anim] : anims) {
1341 bool has_per_anim_override = per_anim_overrides.value().contains(anim_name);
1342 const ConfigValue<FrameLinking> effective_linking =
1343 (has_per_anim_override && per_anim_overrides.value().at(anim_name).linking.has_value())
1344 ? per_anim_overrides.derive(per_anim_overrides.value().at(anim_name).linking)
1345 : global_frame_linking;
1346
1347 if (effective_linking == FrameLinking::automatic && !anim.has_key_frame()) {
1348 std::vector<std::string> warning_lines;
1349 warning_lines.emplace_back(services.diag.formatter().format(
1350 "Animation '{}' has frame_linking '{}' but no key frame (key.png) was found.",
1351 FormatParam{anim_name, Style::bold},
1352 FormatParam{"automatic", Style::bold}));
1353 warning_lines.emplace_back("Animation tiles will not be linked to any metatiles.");
1354 services.diag.warning("missing-key-frame", warning_lines);
1355
1356 if (has_per_anim_override) {
1357 std::vector<std::string> note_lines;
1358 note_lines.push_back(services.diag.formatter().format(
1359 "Per-animation override for '{}' sets frame_linking to '{}'.",
1360 FormatParam{anim_name, Style::bold},
1361 FormatParam{"automatic", Style::bold}));
1362 note_lines.emplace_back("");
1363 note_lines.append_range(format_config_note(services.diag.formatter(), per_anim_overrides));
1364 services.diag.warning_note("missing-key-frame", note_lines);
1365 }
1366 else {
1367 services.diag.warning_note(
1368 "missing-key-frame", format_config_note(services.diag.formatter(), global_frame_linking));
1369 }
1370 }
1371 }
1372
1373 /*
1374 * Validation 1: Ensure no animation key frame tile is fully transparent.
1375 *
1376 * Key frames are used to index into animations from the layer sheet. If a key frame tile were transparent,
1377 * Porytiles couldn't distinguish it from the default transparent tile (tile 0). This restriction only applies to
1378 * actual key frames (key.png), not to representative frames used as fallbacks when no key frame is present.
1379 *
1380 * We collect all violations before failing so the user can see all problematic tiles at once.
1381 */
1382 for (const auto &[anim_name, anim] : anims) {
1383 if (!anim.has_key_frame()) {
1384 continue;
1385 }
1386
1387 const auto &key_frame = anim.key_frame();
1388 const auto &tiles = key_frame.tiles();
1389
1390 for (std::size_t tile_idx = 0; tile_idx < tiles.size(); ++tile_idx) {
1391 const auto &tile = tiles.at(tile_idx);
1392
1393 if (tile.is_transparent(extrinsic_transparency.value())) {
1394 hit_error = true;
1395 std::string error_message = "key frame tile is fully transparent";
1397 services,
1398 tile,
1399 anim_name,
1400 key_frame.frame_name(),
1401 tile_idx,
1402 "transparent-key-frame",
1403 error_message,
1404 extrinsic_transparency);
1405
1406 services.diag.error_note(
1407 "transparent-key-frame",
1408 std::vector<std::string>{
1409 "Key frame tiles cannot be fully transparent because they would be",
1410 "indistinguishable from the actual transparent tile on the layer PNGs."});
1411 }
1412 }
1413 }
1414
1415 if (hit_error) {
1416 return FormattableError{"Found animation(s) with transparent key frame tile(s)."};
1417 }
1418
1419 /*
1420 * Validation 2: Ensure there are no duplicate key frame tiles across all animations.
1421 *
1422 * Each key frame tile must be unique so that Porytiles can properly identify which animation a tile belongs to. We
1423 * collect all key frame tiles into a map and report any that appear in multiple locations.
1424 */
1425 std::map<PixelTile<Rgba32>, std::vector<std::pair<std::string, std::size_t>>> key_frame_occurrences;
1426
1427 for (const auto &[anim_name, anim] : anims) {
1428 if (!anim.has_key_frame()) {
1429 continue;
1430 }
1431
1432 const auto &key_frame = anim.key_frame();
1433 const auto &tiles = key_frame.tiles();
1434
1435 for (std::size_t tile_idx = 0; tile_idx < tiles.size(); ++tile_idx) {
1436 const auto &tile = tiles.at(tile_idx);
1437 key_frame_occurrences[tile].emplace_back(anim_name, tile_idx);
1438 }
1439 }
1440
1441 for (const auto &[tile, occurrences] : key_frame_occurrences) {
1442 if (occurrences.size() > 1) {
1443 hit_error = true;
1444
1445 // Report error for first occurrence
1446 const auto &[first_anim, first_idx] = occurrences.at(0);
1447 std::string error_message = "duplicate key frame tile";
1449 services,
1450 tile,
1451 first_anim,
1452 "key",
1453 first_idx,
1454 "duplicate-key-frame",
1455 error_message,
1456 extrinsic_transparency);
1457
1458 // Build note showing all locations where this tile appears
1459 std::vector<std::string> note_lines;
1460 note_lines.emplace_back("This tile appears as a key frame in multiple locations:");
1461 for (const auto &[anim_name, tile_idx] : occurrences) {
1462 note_lines.push_back(services.diag.formatter().format(
1463 " - anim '{}' subtile {}", FormatParam{anim_name, Style::bold}, FormatParam{tile_idx}));
1464 }
1465 note_lines.emplace_back("");
1466 note_lines.emplace_back("Each key frame tile must be unique so that Porytiles can");
1467 note_lines.emplace_back("identify which animation a tile belongs to at runtime.");
1468 services.diag.error_note("duplicate-key-frame", note_lines);
1469 }
1470 }
1471
1472 /*
1473 * Validation 3: Verify composite frame color counts.
1474 *
1475 * Each animation's composite frame aggregates all colors from all frames at each tile position. Since the palette
1476 * index is fixed for the entire animation lifecycle, each composite tile cannot exceed 15 unique colors.
1477 *
1478 * We iterate pixel by pixel across all frames for each tile position, tracking actual color counts. When a
1479 * violation is detected, we show the specific subtile and highlight the pixel that caused the overflow.
1480 */
1481 for (const auto &[anim_name, anim] : anims) {
1482 if (!anim.has_key_frame()) {
1483 continue;
1484 }
1485
1486 const auto &key_frame = anim.key_frame();
1487 const std::size_t tile_count = key_frame.tile_count();
1488
1489 for (std::size_t tile_idx = 0; tile_idx < tile_count; ++tile_idx) {
1490 // Track color counts across all frames for this tile position
1491 std::map<Rgba32, unsigned int> color_counts;
1492
1493 // Track violation details (frame name, tile, row, col) for later reporting
1494 bool found_violation = false;
1495 std::string violation_frame_name;
1496 const PixelTile<Rgba32> *violation_tile = nullptr;
1497 std::size_t violation_row = 0;
1498 std::size_t violation_col = 0;
1499 Rgba32 violation_pixel;
1500
1501 // Helper lambda to process a tile's pixels
1502 auto process_tile_pixels = [&](const PixelTile<Rgba32> &tile, const std::string &frame_name) {
1503 for (std::size_t row = 0; row < tile::side_length_pix; ++row) {
1504 for (std::size_t col = 0; col < tile::side_length_pix; ++col) {
1505 const auto &pixel = tile.at(row, col);
1506 if (!pixel.is_transparent(extrinsic_transparency)) {
1507 color_counts[pixel]++;
1508 }
1509
1510 // Record the first violation but continue counting for accurate totals
1511 if (color_counts.size() > pal::max_size - 1 && !found_violation) {
1512 found_violation = true;
1513 violation_frame_name = frame_name;
1514 violation_tile = &tile;
1515 violation_row = row;
1516 violation_col = col;
1517 violation_pixel = pixel;
1518 }
1519 }
1520 }
1521 };
1522
1523 // Process key frame pixels
1524 process_tile_pixels(key_frame.tile_at(tile_idx), key_frame.frame_name());
1525
1526 // Process regular frame pixels
1527 for (const auto &[frame_name, frame] : anim.frames()) {
1528 process_tile_pixels(frame.tile_at(tile_idx), frame_name);
1529 }
1530
1531 // Report violation after processing all frames (so we have complete color counts)
1532 if (found_violation) {
1533 hit_error = true;
1534
1535 std::string error_message = services.diag.formatter().format(
1536 "found {}th unique composite frame tile color: {}",
1538 FormatParam{violation_pixel.to_jasc_str(), Style::bold});
1540 services,
1541 *violation_tile,
1542 anim_name,
1543 violation_frame_name,
1544 tile_idx,
1545 violation_row,
1546 violation_col,
1547 "composite-color-count-violation",
1548 error_message,
1549 extrinsic_transparency);
1550
1551 std::vector<std::string> note_lines;
1552 note_lines.emplace_back("The composite frame aggregates all colors across all animation frames");
1553 note_lines.emplace_back("for each tile position. Since the palette index is fixed for the");
1554 note_lines.emplace_back("entire animation lifecycle, each composite tile cannot exceed 15 colors.");
1555 services.diag.error_note("composite-color-count-violation", note_lines);
1556
1557 // Print all frame tiles at this position to show what's contributing colors
1558 std::vector<std::string> frame_tiles_note;
1559 frame_tiles_note.emplace_back("Tiles at this position across all frames:");
1560
1561 // Print key frame tile
1562 frame_tiles_note.emplace_back("");
1563 frame_tiles_note.push_back(services.diag.formatter().format(
1564 "Frame '{}' subtile {}:", FormatParam{key_frame.frame_name(), Style::bold}, FormatParam{tile_idx}));
1565 auto key_tile_visual =
1566 services.tile_printer.print_tile(key_frame.tile_at(tile_idx), extrinsic_transparency);
1567 frame_tiles_note.append_range(key_tile_visual);
1568
1569 // Print regular frame tiles
1570 for (const auto &[frame_name, frame] : anim.frames()) {
1571 frame_tiles_note.emplace_back("");
1572 frame_tiles_note.push_back(services.diag.formatter().format(
1573 "Frame '{}' subtile {}:", FormatParam{frame_name, Style::bold}, FormatParam{tile_idx}));
1574 auto frame_tile_visual =
1575 services.tile_printer.print_tile(frame.tile_at(tile_idx), extrinsic_transparency);
1576 frame_tiles_note.append_range(frame_tile_visual);
1577 }
1578
1579 services.diag.error_note("composite-color-count-violation", frame_tiles_note);
1580
1581 details::report_color_counts("composite-color-count-violation", services, color_counts);
1582 }
1583 }
1584 }
1585
1586 if (hit_error) {
1587 return FormattableError{"Animation frame validation failed."};
1588 }
1589
1590 return {};
1591}
1592
1593} // namespace porytiles
Represents a single frame of an animation, containing tiles and a frame name.
A complete tileset animation with name, configuration, and frame data.
Definition animation.hpp:78
A result type that maintains a chainable sequence of errors for debugging and error reporting.
A container that wraps a configuration value with its name and source information.
ConfigValue< U > derive(const ConfigPODField< U > &override) const
Creates a child ConfigValue from a ConfigPODField, inheriting this value's source provenance.
const T & value() const &
Interface that defines a complete domain layer configuration.
A text parameter with associated styling for formatted output.
General-purpose error implementation with formatted message support.
Definition error.hpp:63
The core tileset entity - a 2x2 grid of PixelTile objects arranged into three layers.
Definition metatile.hpp:186
Represents a palette hint for the palette packing algorithm.
const std::string & name() const
const Palette< Rgba32 > & pal() const
A collection of printer functions for the Palette and related types.
std::vector< std::string > print_rgba_pal_counts(const std::map< Rgba32, unsigned int > &color_counts) const
A generic palette container for colors that support transparency checking.
Definition palette.hpp:47
ColorType slot_zero_color() const
Get the slot 0 palette color.
Definition palette.hpp:254
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 is_wildcard(std::size_t index) const
Checks if a slot is a wildcard.
Definition palette.hpp:198
An 8x8 tile backed by literal-array-based per-pixel storage of an arbitrary pixel type.
PixelType at(std::size_t i) const
Represents a 32-bit RGBA color.
Definition rgba32.hpp:23
static constexpr std::uint8_t alpha_transparent
Definition rgba32.hpp:25
bool is_extrinsically_transparent(const Rgba32 &extrinsic) const
Checks if this color matches the extrinsic transparency color.
Definition rgba32.cpp:14
std::string to_jasc_str() const
Definition rgba32.cpp:24
static constexpr std::uint8_t alpha_opaque
Definition rgba32.hpp:26
static const Style underline
Underline text formatting.
static const Style bold
Bold text formatting.
virtual std::string format(const std::string &format_str, const std::vector< FormatParam > &params) const
Formats a string with styled parameters using fmtlib syntax.
A collection of printer functions for various tile types.
virtual std::vector< std::string > print_metatile_pixel_highlight(const Metatile< Rgba32 > &metatile, metatile::Layer layer, metatile::Subtile subtile, std::size_t row, std::size_t col, const Rgba32 &extrinsic_transparency) const =0
virtual std::vector< std::string > print_tile_pixel_highlight(const PixelTile< Rgba32 > &tile, std::size_t row, std::size_t col, const Rgba32 &extrinsic_transparency) const =0
virtual std::vector< std::string > print_tile(const PixelTile< Rgba32 > &tile, const Rgba32 &extrinsic_transparency) const =0
virtual std::vector< std::string > print_metatile_tile_highlight(const Metatile< Rgba32 > &metatile, metatile::Layer layer, metatile::Subtile subtile, const Rgba32 &extrinsic_transparency) const =0
Abstract class for structured error reporting and diagnostic output.
virtual void warning_note(const std::string &tag, const std::vector< std::string > &lines) const =0
Display a tagged note message associated with a warning.
const TextFormatter & formatter() const
virtual void warning(const std::string &tag, const std::vector< std::string > &lines) const =0
Display a tagged warning message.
virtual void error(const std::string &tag, const std::vector< std::string > &lines) const =0
Display a tagged error message.
virtual void error_note(const std::string &tag, const std::vector< std::string > &lines) const =0
Display a tagged note message associated with an error.
std::string message_header(const TextFormatter &format, const std::string &anim_name, const std::string &frame_name, std::size_t internal_tile_index, std::size_t subtile_row, std::size_t subtile_col)
Definition animation.hpp:24
void report_validation_error_in_anim(const TilesetCompileValidatorServices &services, const PixelTile< Rgba32 > &tile, const std::string &anim_name, const std::string &frame_name, std::size_t internal_tile_index, std::size_t row, std::size_t col, const std::string &diagnostic_code, const std::string &error_message, const Rgba32 &extrinsic_transparency)
Reports a validation error at a specific pixel location within an animation frame tile.
void report_color_counts(const std::string &tag, const TilesetCompileValidatorServices &services, const std::map< Rgba32, unsigned int > &color_counts)
Emits a diagnostic note displaying the count of each unique color.
void report_validation_error_in_metatile(const TilesetCompileValidatorServices &services, const Metatile< Rgba32 > &metatile, std::size_t metatile_index, std::size_t internal_tile_index, std::size_t row, std::size_t col, const std::string &diagnostic_code, const std::string &error_message, const Rgba32 &extrinsic_transparency)
Reports a validation error at a specific pixel location within a metatile.
void report_validation_error_in_anim_tile(const TilesetCompileValidatorServices &services, const PixelTile< Rgba32 > &tile, const std::string &anim_name, const std::string &frame_name, std::size_t internal_tile_index, const std::string &diagnostic_code, const std::string &error_message, const Rgba32 &extrinsic_transparency)
Reports a validation error for an entire animation frame tile.
constexpr std::size_t entries_per_metatile_triple
Definition metatile.hpp:21
std::tuple< Layer, Subtile > from_internal_tile_index(std::size_t tile_index)
Decomposes an internal tile index into its layer and subtile position within a metatile.
Definition metatile.hpp:117
constexpr std::size_t tiles_per_metatile_layer
Definition metatile.hpp:17
std::string message_header(const TextFormatter &format, std::size_t index, Layer layer, Subtile subtile, std::size_t subtile_row, std::size_t subtile_col)
Definition metatile.hpp:130
Subtile subtile_from_index(std::size_t i)
Definition metatile.hpp:47
constexpr std::size_t num_pals
Definition palette.hpp:21
constexpr std::size_t max_size
Definition palette.hpp:19
constexpr std::size_t side_length_pix
std::vector< std::string > build_pal_hint_highlight_lines(const TextFormatter &format, const PalettePrinter &pal_printer, const std::string &message, const PaletteHint &hint, const std::string &pal_label, const std::vector< std::size_t > &violating_slots)
Builds note lines displaying a palette hint with highlighted violating slots.
std::vector< std::string > build_porytiles_pal_highlight_lines(const TextFormatter &format, const PalettePrinter &pal_printer, const std::string &message, const Palette< Rgba32, N > &pal, const std::string &pal_label, const std::vector< std::size_t > &violating_slots)
Builds note lines displaying a Porytiles palette with highlighted violating slots.
LayerMode
Specifies whether a metatile uses dual-layer or triple-layer mode.
Definition layer.hpp:21
ChainableResult< void > validate_porymap_pal(const TilesetCompileValidatorServices &services, const std::string &tileset_name, const Palette< Rgba32, pal::max_size > &pal, std::size_t pal_index)
Validates a Porymap palette for correctness according to GBA hardware constraints.
void panic(const StringViewSourceLoc &s)
Unconditionally terminates the program with a panic message.
Definition panic.cpp:43
ChainableResult< void > validate_porytiles_pal(const TilesetCompileValidatorServices &services, const std::string &tileset_name, const Palette< Rgba32, pal::max_size > &pal, std::size_t pal_index)
Validates a user-specified Porytiles override palette for correctness.
ChainableResult< void > validate_anim_frames(const TilesetCompileValidatorServices &services, const std::string &tileset_name, const std::map< std::string, Animation< Rgba32 > > &anims)
Validates animation frames for correctness according to tileset compilation constraints.
ChainableResult< void > validate_alpha_channels(const TilesetCompileValidatorServices &services, const std::string &tileset_name, const std::vector< Metatile< Rgba32 > > &metatiles, const std::map< std::string, Animation< Rgba32 > > &anims)
Validates that all pixel alpha channels in provided input have valid values.
ChainableResult< void > validate_pal_hint(const TilesetCompileValidatorServices &services, const std::string &tileset_name, const PaletteHint &hint)
Validates a user-specified palette hint for correctness.
LayerMode layer_mode_from_val(std::size_t s)
Converts a numeric value to LayerMode.
Definition layer.hpp:32
std::vector< std::string > build_subtraction_limit_lines(const TextFormatter &format, std::string_view label, std::size_t computed_limit, const ConfigValue< std::size_t > &total_cfg, const ConfigValue< std::size_t > &primary_cfg)
Builds note lines explaining a derived limit computed as total minus primary.
std::string pal_filename(std::size_t pal_index)
Constructs a palette filename from a palette index.
std::vector< std::string > format_config_note(const TextFormatter &format, const ConfigValue< T > &config)
Format a ConfigValue into diagnostic note lines.
ChainableResult< void > validate_metatile_count(const TilesetCompileValidatorServices &services, const std::string &tileset_name, bool is_secondary, const std::vector< Metatile< Rgba32 > > &metatiles)
Validates that the metatile count does not exceed the configured limit.
ChainableResult< void > validate_layer_mode(const TilesetCompileValidatorServices &services, const std::string &tileset_name, const std::vector< Metatile< Rgba32 > > &metatiles)
Validates that metatiles conform to the configured layer mode.
ChainableResult< void > validate_precision_loss(const TilesetCompileValidatorServices &services, const std::string &tileset_name, const std::vector< Metatile< Rgba32 > > &metatiles, const std::map< std::string, Animation< Rgba32 > > &anims, const std::array< std::optional< Palette< Rgba32, pal::max_size > >, pal::num_pals > &porytiles_pals, const std::vector< PaletteHint > &hints, const std::optional< std::array< Palette< Rgba32, pal::max_size >, pal::num_pals > > &porymap_pals)
Validates that no colors will suffer unacceptable precision loss during GBA color conversion.
std::vector< std::string > build_porymap_pal_highlight_lines(const TextFormatter &format, const PalettePrinter &pal_printer, const std::string &message, const Palette< Rgba32, N > &pal, const std::string &pal_label, const std::vector< std::size_t > &violating_slots)
Builds note lines displaying a Porymap palette with highlighted violating slots.
@ automatic
Use key.png for frame linking.
ChainableResult< void > validate_tile_color_count(const TilesetCompileValidatorServices &services, const std::string &tileset_name, const std::vector< Metatile< Rgba32 > > &metatiles, const std::map< std::string, Animation< Rgba32 > > &anims)
Validates that each individual tile does not exceed the per-tile color limit.
std::vector< std::string > build_global_color_limit_lines(const TextFormatter &format, std::size_t color_count_limit, const ConfigValue< std::size_t > &num_pals)
Builds note lines explaining the global color count limit for primary tileset compilation.
ChainableResult< void > validate_global_color_count(const TilesetCompileValidatorServices &services, const std::string &tileset_name, bool is_secondary, const std::vector< Metatile< Rgba32 > > &metatiles, const std::map< std::string, Animation< Rgba32 > > &anims, const std::array< std::optional< Palette< Rgba32, pal::max_size > >, pal::num_pals > &porytiles_pals, const std::vector< PaletteHint > &hints)
Validates that the total unique color count across all input does not exceed the global limit.
Utility functions for string manipulation and formatting.
Parameter store for common services used by tileset compile validators.
#define PT_UNWRAP_TILESET_CONFIG_REF(ref, config, tileset_name, return_type)
Unwraps a tileset-scoped config value via reference access, returning early if the value is not avail...