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
29
30namespace porytiles {
31
44};
45
46namespace details {
47
65 const TilesetCompileValidatorServices &services,
66 const Metatile<Rgba32> &metatile,
67 std::size_t metatile_index,
68 std::size_t internal_tile_index,
69 std::size_t row,
70 std::size_t col,
71 const std::string &diagnostic_code,
72 const std::string &error_message,
73 const Rgba32 &extrinsic_transparency)
74{
75 auto [layer, subtile] = metatile::from_internal_tile_index(internal_tile_index);
76 std::vector errors = {services.diag.formatter().format(
77 "{}: {}",
79 metatile::message_header(services.diag.formatter(), metatile_index, layer, subtile, row, col), Style::bold},
80 FormatParam{error_message})};
81 std::vector highlight = services.tile_printer.print_metatile_pixel_highlight(
82 metatile, layer, subtile, row, col, extrinsic_transparency);
83 errors.append_range(highlight);
84 services.diag.error(diagnostic_code, errors);
85}
86
105 const TilesetCompileValidatorServices &services,
106 const PixelTile<Rgba32> &tile,
107 const std::string &anim_name,
108 const std::string &frame_name,
109 std::size_t internal_tile_index,
110 std::size_t row,
111 std::size_t col,
112 const std::string &diagnostic_code,
113 const std::string &error_message,
114 const Rgba32 &extrinsic_transparency)
115{
116 std::vector errors = {services.diag.formatter().format(
117 "{}: {}",
119 anim::message_header(services.diag.formatter(), anim_name, frame_name, internal_tile_index, row, col),
121 FormatParam{error_message})};
122 std::vector highlight = services.tile_printer.print_tile_pixel_highlight(tile, row, col, extrinsic_transparency);
123 errors.append_range(highlight);
124 services.diag.error(diagnostic_code, errors);
125}
126
143 const TilesetCompileValidatorServices &services,
144 const PixelTile<Rgba32> &tile,
145 const std::string &anim_name,
146 const std::string &frame_name,
147 std::size_t internal_tile_index,
148 const std::string &diagnostic_code,
149 const std::string &error_message,
150 const Rgba32 &extrinsic_transparency)
151{
152 std::vector errors = {services.diag.formatter().format(
153 "{}: {}",
155 anim::message_header(services.diag.formatter(), anim_name, frame_name, internal_tile_index), Style::bold},
156 FormatParam{error_message})};
157 std::vector tile_visual = services.tile_printer.print_tile(tile, extrinsic_transparency);
158 errors.append_range(tile_visual);
159 services.diag.error(diagnostic_code, errors);
160}
161
172 const std::string &tag,
173 const TilesetCompileValidatorServices &services,
174 const std::map<Rgba32, unsigned int> &color_counts)
175{
176 std::vector<std::string> color_lines;
177 color_lines.emplace_back("color counts:");
178 auto counts = services.palette_printer.print_rgba_palette_counts(color_counts);
179 color_lines.append_range(counts);
180 services.diag.error_note(tag, color_lines);
181}
182
183} // namespace details
184
201 const TilesetCompileValidatorServices &services,
202 const std::string &tileset_name,
203 bool is_secondary,
204 const std::vector<Metatile<Rgba32>> &metatiles)
205{
206 PT_UNWRAP_TILESET_CONFIG_REF(services.config, num_metatiles_in_primary, tileset_name, void);
207 PT_UNWRAP_TILESET_CONFIG_REF(services.config, num_metatiles_total, tileset_name, void);
208
209 std::size_t metatile_limit = is_secondary ? (num_metatiles_total.value() - num_metatiles_in_primary.value())
210 : num_metatiles_in_primary.value();
211
212 if (metatiles.size() > metatile_limit) {
213 services.diag.error(
214 "metatile-limit-exceeded",
215 "Too many metatiles ({}) in Porytiles component for tileset '{}'.",
216 FormatParam{metatiles.size(), Style::bold},
217 FormatParam{tileset_name, Style::bold});
218
219 std::vector<std::string> note_text;
220 if (is_secondary) {
221 note_text.append_range(build_subtraction_limit_lines(
222 services.diag.formatter(),
223 "Metatile limit",
224 metatile_limit,
225 num_metatiles_total,
226 num_metatiles_in_primary));
227 }
228 else {
229 note_text.push_back(
230 services.diag.formatter().format("Metatile limit is '{}'.", FormatParam{metatile_limit, Style::bold}));
231 note_text.emplace_back("");
232 note_text.append_range(format_config_note(services.diag.formatter(), num_metatiles_in_primary));
233 }
234 services.diag.error_note("metatile-limit-exceeded", note_text);
235
236 return FormattableError{
237 "Found '{}' metatiles, limit is '{}'.",
238 FormatParam{metatiles.size(), Style::bold},
239 FormatParam{metatile_limit, Style::bold}};
240 }
241
242 return {};
243}
244
264 const TilesetCompileValidatorServices &services,
265 const std::string &tileset_name,
267 std::size_t palette_index)
268{
269 PT_UNWRAP_TILESET_CONFIG_REF(services.config, extrinsic_transparency, tileset_name, void);
270
271 bool hit_error = false;
272 const std::string filename = palette_filename(palette_index);
273 std::vector<std::size_t> violating_slots{};
274
275 if (palette.is_wildcard(0)) {
276 panic("unexpected wildcard in Porymap palette slot 0");
277 }
278
279 // Check 1: Slot 0 should match extrinsic transparency (warning only)
280 const Rgba32 slot0_color = palette.slot_zero_color();
281 if (!slot0_color.is_extrinsically_transparent(extrinsic_transparency)) {
282 std::vector<std::string> warning_lines;
283 warning_lines.emplace_back(services.diag.formatter().format(
284 "Porymap palette '{}' slot 0 color '{}' does not match extrinsic transparency '{}'",
285 FormatParam{filename, Style::bold},
286 FormatParam{slot0_color.to_jasc_str(), Style::bold},
287 FormatParam{extrinsic_transparency.value().to_jasc_str(), Style::bold}));
288 warning_lines.emplace_back("Slot 0 is typically reserved for the transparency color.");
289 warning_lines.emplace_back("If you are using slot 0 for a .pla blend color, you can ignore this warning.");
290 services.diag.warning("porymap-palette-slot-0", warning_lines);
291
292 services.diag.warning_note(
293 "porymap-palette-slot-0",
295 services.diag.formatter(),
296 services.palette_printer,
297 "reserved transparency slot",
298 palette,
299 filename,
300 std::vector<std::size_t>{0}));
301 services.diag.warning_note(
302 "porymap-palette-slot-0", format_config_note(services.diag.formatter(), extrinsic_transparency));
303 }
304
305 // Check 2: Non-slot-0 positions cannot contain extrinsic transparency
306 for (std::size_t slot = 1; slot < palette.size(); ++slot) {
307 if (palette.is_wildcard(slot)) {
308 panic("unexpected wildcard in Porymap palette");
309 }
310 const Rgba32 color = palette.at(slot);
311 if (color.is_extrinsically_transparent(extrinsic_transparency)) {
312 hit_error = true;
313 violating_slots.push_back(slot);
314 std::vector<std::string> error_lines;
315 error_lines.emplace_back(services.diag.formatter().format(
316 "Porymap palette '{}' slot '{}' contains extrinsic transparency color '{}'",
317 FormatParam{filename, Style::bold},
318 FormatParam{slot, Style::bold},
319 FormatParam{color.to_jasc_str(), Style::bold}));
320 error_lines.emplace_back("Extrinsic transparency is not allowed in non-slot-0 positions.");
321 services.diag.error("porymap-palette-transparency", error_lines);
322 }
323 }
324
325 if (hit_error) {
326 // Print the palette with violating slots highlighted
327 services.diag.error_note(
328 "porymap-palette-transparency",
330 services.diag.formatter(),
331 services.palette_printer,
332 "slots with invalid extrinsic transparency",
333 palette,
334 filename,
335 violating_slots));
336 services.diag.error_note(
337 "porymap-palette-transparency", format_config_note(services.diag.formatter(), extrinsic_transparency));
338
339 return FormattableError{"Validation failed for Porymap palette '{}'.", FormatParam{filename, Style::bold}};
340 }
341
342 return {};
343}
344
365 const TilesetCompileValidatorServices &services,
366 const std::string &tileset_name,
368 std::size_t palette_index)
369{
370 PT_UNWRAP_TILESET_CONFIG_REF(services.config, extrinsic_transparency, tileset_name, void);
371
372 bool hit_error = false;
373 const std::string filename = palette_filename(palette_index);
374 std::vector<std::size_t> violating_slots;
375
376 // Check 1: Slot 0 should match extrinsic transparency (warning only)
377 if (!palette.is_wildcard(0)) {
378 const Rgba32 slot0_color = palette.slot_zero_color();
379 if (!slot0_color.is_extrinsically_transparent(extrinsic_transparency)) {
380 // Build the warning text
381 std::vector<std::string> warning_text;
382 warning_text.emplace_back(services.diag.formatter().format(
383 "Porytiles palette '{}' slot 0 color '{}' does not match extrinsic transparency '{}'",
384 FormatParam{filename, Style::bold},
385 FormatParam{slot0_color.to_jasc_str(), Style::bold},
386 FormatParam{extrinsic_transparency.value().to_jasc_str(), Style::bold}));
387 warning_text.emplace_back("Slot 0 is typically reserved for the transparency color.");
388 warning_text.emplace_back("If you are using slot 0 for a .pla blend color, you can ignore this warning.");
389 services.diag.warning("porytiles-palette-slot-0", warning_text);
390
391 // Print the palette and config notes.
392 services.diag.warning_note(
393 "porytiles-palette-slot-0",
395 services.diag.formatter(),
396 services.palette_printer,
397 "reserved transparency slot",
398 palette,
399 filename,
400 std::vector<std::size_t>{0}));
401 services.diag.warning_note(
402 "porytiles-palette-slot-0", format_config_note(services.diag.formatter(), extrinsic_transparency));
403 }
404 }
405
406 // Check 2: Non-slot-0 positions cannot contain extrinsic transparency
407 for (std::size_t slot = 1; slot < palette.size(); ++slot) {
408 if (palette.is_wildcard(slot)) {
409 continue;
410 }
411 const Rgba32 color = palette.at(slot);
412 if (color.is_extrinsically_transparent(extrinsic_transparency)) {
413 hit_error = true;
414 violating_slots.push_back(slot);
415 std::vector<std::string> error_lines;
416 error_lines.emplace_back(services.diag.formatter().format(
417 "Porytiles palette '{}' slot '{}' contains extrinsic transparency color '{}'",
418 FormatParam{filename, Style::bold},
419 FormatParam{slot, Style::bold},
420 FormatParam{color.to_jasc_str(), Style::bold}));
421 error_lines.emplace_back("Extrinsic transparency is not allowed in non-slot-0 positions.");
422 services.diag.error("porytiles-palette-transparency", error_lines);
423 }
424 }
425
426 if (hit_error) {
427 // Print the palette and config notes
428 services.diag.error_note(
429 "porytiles-palette-transparency",
431 services.diag.formatter(),
432 services.palette_printer,
433 "slots with invalid extrinsic transparency",
434 palette,
435 filename,
436 violating_slots));
437 services.diag.error_note(
438 "porytiles-palette-transparency", format_config_note(services.diag.formatter(), extrinsic_transparency));
439
440 return FormattableError{"Validation failed for Porytiles palette '{}'.", FormatParam{filename, Style::bold}};
441 }
442
443 return {};
444}
445
469 const TilesetCompileValidatorServices &services, const std::string &tileset_name, const PaletteHint &hint)
470{
471 PT_UNWRAP_TILESET_CONFIG_REF(services.config, extrinsic_transparency, tileset_name, void);
472
473 bool hit_any_error = false;
474 const std::string &hint_name = hint.name();
475
476 if (hint.palette().size() >= palette::max_size) {
477 services.diag.error(
478 "palette-hint-size-violation",
479 services.diag.formatter().format(
480 "palette hint '{}' has size '{}', max allowed size is '{}'",
481 FormatParam{hint_name, Style::bold},
482 FormatParam{hint.palette().size(), Style::bold},
483 FormatParam{palette::max_size - 1, Style::bold}));
484 services.diag.error_note(
485 "palette-hint-size-violation",
487 services.diag.formatter(),
488 services.palette_printer,
489 "invalid extra slots start here",
490 hint,
491 hint_name,
492 std::vector{palette::max_size}));
493 return FormattableError{"Validation failed for palette hint '{}'.", FormatParam{hint_name, Style::bold}};
494 }
495
496 std::set<Rgba32> seen_colors{};
497 std::vector<std::size_t> violating_slots{};
498
499 // Check 1: Extrinsic transparency not allowed in hints
500 for (std::size_t slot = 0; slot < hint.palette().size(); ++slot) {
501 if (hint.palette().is_wildcard(slot)) {
502 continue;
503 }
504 const Rgba32 color = hint.palette().at(slot);
505
506 if (color.is_extrinsically_transparent(extrinsic_transparency)) {
507 hit_any_error = true;
508 violating_slots.push_back(slot);
509 std::vector<std::string> error_lines;
510 error_lines.emplace_back(services.diag.formatter().format(
511 "palette hint '{}' slot '{}' contains extrinsic transparency color '{}'",
512 FormatParam{hint_name, Style::bold},
513 FormatParam{slot, Style::bold},
514 FormatParam{color.to_jasc_str(), Style::bold}));
515 error_lines.emplace_back("Extrinsic transparency is not allowed in palette hints.");
516 services.diag.error("palette-hint-transparency", error_lines);
517 }
518 }
519 if (!violating_slots.empty()) {
520 services.diag.error_note(
521 "palette-hint-transparency",
523 services.diag.formatter(),
524 services.palette_printer,
525 "slots with invalid extrinsic transparency",
526 hint,
527 hint_name,
528 violating_slots));
529 services.diag.error_note(
530 "palette-hint-transparency", format_config_note(services.diag.formatter(), extrinsic_transparency));
531 }
532 violating_slots.clear();
533
534 // Check 2: No duplicate colors
535 for (std::size_t slot = 0; slot < hint.palette().size(); ++slot) {
536 if (hint.palette().is_wildcard(slot)) {
537 continue;
538 }
539 const Rgba32 color = hint.palette().at(slot);
540
541 if (seen_colors.contains(color)) {
542 hit_any_error = true;
543 violating_slots.push_back(slot);
544 std::vector<std::string> error_lines;
545 error_lines.emplace_back(services.diag.formatter().format(
546 "palette hint '{}' contains duplicate color '{}' at slot '{}'",
547 FormatParam{hint_name, Style::bold},
548 FormatParam{color.to_jasc_str(), Style::bold},
549 FormatParam{slot, Style::bold}));
550 error_lines.emplace_back("Duplicate colors are not allowed in palette hints.");
551 services.diag.error("palette-hint-duplicate-color", error_lines);
552 }
553 seen_colors.insert(color);
554 }
555 if (!violating_slots.empty()) {
556 services.diag.error_note(
557 "palette-hint-duplicate-color",
559 services.diag.formatter(),
560 services.palette_printer,
561 "slots with invalid duplicate colors",
562 hint,
563 hint_name,
564 violating_slots));
565 }
566
567 if (hit_any_error) {
568 return FormattableError{"Validation failed for palette hint '{}'.", FormatParam{hint_name, Style::bold}};
569 }
570 return {};
571}
572
587 const TilesetCompileValidatorServices &services,
588 const std::string &tileset_name,
589 const std::vector<Metatile<Rgba32>> &metatiles,
590 const std::map<std::string, Animation<Rgba32>> &anims)
591{
592 PT_UNWRAP_TILESET_CONFIG_REF(services.config, extrinsic_transparency, tileset_name, void);
593
594 bool hit_error = false;
595
596 // Validate metatiles
597 std::size_t metatile_index = 0;
598 for (const auto &metatile : metatiles) {
599 const auto decomposed_metatile = metatile.decompose();
600
601 // Iterate over each internal tile
602 for (std::size_t internal_tile_index = 0; internal_tile_index < decomposed_metatile.size();
603 ++internal_tile_index) {
604 const auto &tile = decomposed_metatile[internal_tile_index];
605
606 // Iterate over each pixel in the current internal tile
607 for (std::size_t row = 0; row < tile::side_length_pix; ++row) {
608 for (std::size_t col = 0; col < tile::side_length_pix; ++col) {
609 const auto &pixel = tile.at(row, col);
610 if (pixel.alpha() != Rgba32::alpha_opaque && pixel.alpha() != Rgba32::alpha_transparent) {
611 hit_error = true;
612 std::string error_message = services.diag.formatter().format(
613 "invalid alpha channel: {}", FormatParam{std::to_string(pixel.alpha()), Style::bold});
615 services,
616 metatile,
617 metatile_index,
618 internal_tile_index,
619 row,
620 col,
621 "alpha-channel-violation",
622 error_message,
623 extrinsic_transparency);
624 }
625 }
626 }
627 }
628 metatile_index++;
629 }
630
631 // Lambda to avoid duplicating logic
632 auto validate_anim_frame = [&services, &hit_error, &extrinsic_transparency](
633 const std::string &anim_name, const AnimFrame<Rgba32> &frame) -> void {
634 const auto &frame_tiles = frame.tiles();
635
636 // Loop over each internal frame tile
637 for (std::size_t internal_tile_index = 0; internal_tile_index < frame_tiles.size(); ++internal_tile_index) {
638 const auto &tile = frame_tiles.at(internal_tile_index);
639
640 // loop over tile pixels, row-major
641 for (std::size_t row = 0; row < tile::side_length_pix; ++row) {
642 for (std::size_t col = 0; col < tile::side_length_pix; ++col) {
643 const auto &pixel = tile.at(row, col);
644
645 // validate
646 if (pixel.alpha() != Rgba32::alpha_opaque && pixel.alpha() != Rgba32::alpha_transparent) {
647 hit_error = true;
648 std::string error_message = services.diag.formatter().format(
649 "invalid alpha channel: {}", FormatParam{std::to_string(pixel.alpha()), Style::bold});
651 services,
652 tile,
653 anim_name,
654 frame.frame_name(),
655 internal_tile_index,
656 row,
657 col,
658 "alpha-channel-violation",
659 error_message,
660 extrinsic_transparency);
661 }
662 }
663 }
664 }
665 };
666
667 // Validate animations
668 for (const auto &[anim_name, anim] : anims) {
669 if (anim.has_key_frame()) {
670 const auto &key_frame = anim.key_frame();
671 validate_anim_frame(anim_name, key_frame);
672 }
673 for (const AnimFrame<Rgba32> &frame : anim.frames() | std::views::values) {
674 validate_anim_frame(anim_name, frame);
675 }
676 }
677
678 if (hit_error) {
679 return FormattableError{"Found input pixel(s) with invalid alpha channel value(s)."};
680 }
681
682 return {};
683}
684
704 const TilesetCompileValidatorServices &services,
705 const std::string &tileset_name,
706 const std::vector<Metatile<Rgba32>> &metatiles)
707{
708 PT_UNWRAP_TILESET_CONFIG_REF(services.config, extrinsic_transparency, tileset_name, void);
709 PT_UNWRAP_TILESET_CONFIG_REF(services.config, num_tiles_per_metatile, tileset_name, void);
710 PT_UNWRAP_TILESET_CONFIG_REF(services.config, ignore_triple_layer_content, tileset_name, void);
711 auto configured_layer_mode = layer_mode_from_val(num_tiles_per_metatile);
712
713 // If layer mode is triple, just return
714 if (configured_layer_mode == LayerMode::triple) {
715 return {};
716 }
717
718 // With ignore on, triple content in dual mode is a warning, not an error
719 const bool ignore_triple = ignore_triple_layer_content.value();
720
721 bool hit_error = false;
722 bool hit_violation = false;
723 std::size_t metatile_index = 0;
724
725 for (const auto &metatile : metatiles) {
726 // Check each of the 4 tile positions (northwest, northeast, southwest, southeast) independently: a subtile is
727 // a triple-layer region only when all three of its own layers carry content, regardless of its siblings.
728 for (std::size_t subtile_idx = 0; subtile_idx < metatile::tiles_per_metatile_layer; ++subtile_idx) {
729 const auto &bottom_tile = metatile.bottom(subtile_idx);
730 const auto &middle_tile = metatile.middle(subtile_idx);
731 const auto &top_tile = metatile.top(subtile_idx);
732 const auto subtile = metatile::subtile_from_index(subtile_idx);
733
734 // Check if each layer has at least one non-transparent pixel in this tile position
735 const bool bottom_has_opaque = !bottom_tile.is_transparent(extrinsic_transparency.value());
736 const bool middle_has_opaque = !middle_tile.is_transparent(extrinsic_transparency.value());
737 const bool top_has_opaque = !top_tile.is_transparent(extrinsic_transparency.value());
738
739 // If all three layers have opaque pixels in this tile position, this is a triple-layer region
740 const bool triple_layer_region = bottom_has_opaque && middle_has_opaque && top_has_opaque;
741
742 // Violation if the subtile is a triple-layer region in a dual-layer compilation. The
743 // ignore_triple_layer_content config decides whether it generates a hard error or a warning.
744 if (triple_layer_region && configured_layer_mode == LayerMode::dual) {
745 hit_violation = true;
746 std::vector lines = {services.diag.formatter().format(
747 "{}: {}",
749 metatile::message_header(services.diag.formatter(), metatile_index, subtile), Style::bold},
750 FormatParam{"non-transparent content on all three layers"})};
751 std::vector bottom_highlight = services.tile_printer.print_metatile_tile_highlight(
752 metatile, metatile::Layer::bottom, subtile, extrinsic_transparency);
753 std::vector middle_highlight = services.tile_printer.print_metatile_tile_highlight(
754 metatile, metatile::Layer::middle, subtile, extrinsic_transparency);
755 std::vector top_highlight = services.tile_printer.print_metatile_tile_highlight(
756 metatile, metatile::Layer::top, subtile, extrinsic_transparency);
757 lines.append_range(bottom_highlight);
758 lines.append_range(middle_highlight);
759 lines.append_range(top_highlight);
760 if (ignore_triple) {
761 services.diag.warning("layer-mode-violation", lines);
762 }
763 else {
764 hit_error = true;
765 services.diag.error("layer-mode-violation", lines);
766 }
767 }
768 }
769
770 metatile_index++;
771 }
772
773 if (hit_error) {
774 std::vector<std::string> note_text;
775 note_text.push_back(
776 services.diag.formatter().format("Implied layer mode is '{}'.", FormatParam{LayerMode::dual, Style::bold}));
777 note_text.emplace_back("");
778 note_text.append_range(format_config_note(services.diag.formatter(), num_tiles_per_metatile));
779 note_text.emplace_back("");
780 note_text.emplace_back("Consider enabling triple-layer metatiles.");
781 note_text.push_back(services.diag.formatter().format(
782 "To enable layer mode '{}' for your project:", FormatParam{LayerMode::triple, Style::bold}));
783 note_text.push_back(services.diag.formatter().format(
784 " - set '{}' = '{}'",
785 FormatParam{num_tiles_per_metatile.canonical_name(), Style::bold},
787 note_text.push_back(services.diag.formatter().format(
788 " - follow the steps here: {}",
789 FormatParam{"https://github.com/pret/pokeemerald/wiki/Triple-layer-metatiles", Style::underline}));
790 note_text.emplace_back("");
791 note_text.push_back(services.diag.formatter().format(
792 "Alternatively, set '{}' to force compilation in dual-layer mode. Porytiles will then drop a layer per "
793 "offending metatile, either by respecting a layer_type pin or by forcing each to layer type '{}'.",
794 FormatParam{ignore_triple_layer_content.canonical_name(), Style::bold},
796
797 // Emit note
798 services.diag.error_note("layer-mode-violation", note_text);
799
800 return FormattableError{"Found metatile(s) with mismatched implied layer mode."};
801 }
802
803 // Config ignore_triple_layer_content on and at least one metatile had triple content: warn once that the dual
804 // conversion will drop a layer group per offending metatile.
805 if (hit_violation) {
806 std::vector<std::string> note_text;
807 note_text.push_back(services.diag.formatter().format(
808 "Porytiles will drop a layer per offending metatile, either by respecting a layer_type pin or by forcing "
809 "each to layer type '{}'. This violation was explicitly downgraded from error to warning by:",
811 note_text.emplace_back("");
812 note_text.append_range(format_config_note(services.diag.formatter(), ignore_triple_layer_content));
813 services.diag.warning_note("layer-mode-violation", note_text);
814 }
815
816 return {};
817}
818
835 const TilesetCompileValidatorServices &services,
836 const std::string &tileset_name,
837 const std::vector<Metatile<Rgba32>> &metatiles,
838 const std::map<std::string, Animation<Rgba32>> &anims)
839{
840 PT_UNWRAP_TILESET_CONFIG_REF(services.config, extrinsic_transparency, tileset_name, void);
841
842 bool hit_error = false;
843 std::size_t metatile_index = 0;
844 for (const auto &metatile : metatiles) {
845 const auto decomposed_metatile = metatile.decompose();
846
847 // Iterate over each internal tile
848 for (std::size_t internal_tile_index = 0; internal_tile_index < decomposed_metatile.size();
849 ++internal_tile_index) {
850 const auto &tile = decomposed_metatile[internal_tile_index];
851 std::map<Rgba32, unsigned int> color_counts;
852
853 // Iterate over each pixel in the current internal tile
854 for (std::size_t row = 0; row < tile::side_length_pix; ++row) {
855 for (std::size_t col = 0; col < tile::side_length_pix; ++col) {
856 const auto &pixel = tile.at(row, col);
857 if (pixel.alpha() != Rgba32::alpha_transparent && pixel != extrinsic_transparency.value()) {
858 color_counts[pixel]++;
859 }
860
861 if (color_counts.size() > palette::max_size - 1) {
862 hit_error = true;
863 std::string error_message = services.diag.formatter().format(
864 "found {}th unique tile color: {}",
866 FormatParam{pixel.to_jasc_str(), Style::bold});
868 services,
869 metatile,
870 metatile_index,
871 internal_tile_index,
872 row,
873 col,
874 "tile-color-count-violation",
875 error_message,
876 extrinsic_transparency);
877 details::report_color_counts("tile-color-count-violation", services, color_counts);
878 goto next_tile;
879 }
880 }
881 }
882 next_tile:;
883 }
884 metatile_index++;
885 }
886
887 auto validate_anim_frame = [&services, &hit_error, &extrinsic_transparency](
888 const std::string &anim_name, const AnimFrame<Rgba32> &frame) -> void {
889 const auto &frame_tiles = frame.tiles();
890 // Iterate over each internal tile
891 for (std::size_t internal_tile_index = 0; internal_tile_index < frame_tiles.size(); ++internal_tile_index) {
892 const auto &tile = frame_tiles[internal_tile_index];
893 std::map<Rgba32, unsigned int> color_counts;
894
895 // Iterate over each pixel in the current internal tile
896 for (std::size_t row = 0; row < tile::side_length_pix; ++row) {
897 for (std::size_t col = 0; col < tile::side_length_pix; ++col) {
898 const auto &pixel = tile.at(row, col);
899 if (pixel.alpha() != Rgba32::alpha_transparent && pixel != extrinsic_transparency.value()) {
900 color_counts[pixel]++;
901 }
902
903 if (color_counts.size() > palette::max_size - 1) {
904 hit_error = true;
905 std::string error_message = services.diag.formatter().format(
906 "found {}th unique frame tile color: {}",
908 FormatParam{pixel.to_jasc_str(), Style::bold});
910 services,
911 tile,
912 anim_name,
913 frame.frame_name(),
914 internal_tile_index,
915 row,
916 col,
917 "tile-color-count-violation",
918 error_message,
919 extrinsic_transparency);
920 details::report_color_counts("tile-color-count-violation", services, color_counts);
921 goto next_tile;
922 }
923 }
924 }
925 next_tile:;
926 }
927 };
928
929 // Validate animations
930 for (const auto &[anim_name, anim] : anims) {
931 if (anim.has_key_frame()) {
932 const auto &key_frame = anim.key_frame();
933 validate_anim_frame(anim_name, key_frame);
934 }
935 for (const AnimFrame<Rgba32> &frame : anim.frames() | std::views::values) {
936 validate_anim_frame(anim_name, frame);
937 }
938 }
939
940 if (hit_error) {
941 return FormattableError{
942 "Found tile(s) with more than {} unique non-transparent pixels.", FormatParam{palette::max_size - 1}};
943 }
944
945 return {};
946}
947
970 const TilesetCompileValidatorServices &services,
971 const std::string &tileset_name,
972 bool is_secondary,
973 const std::vector<Metatile<Rgba32>> &metatiles,
974 const std::map<std::string, Animation<Rgba32>> &anims,
975 const std::array<std::optional<Palette<Rgba32, palette::max_size>>, palette::num_palettes> &porytiles_palettes,
976 const std::vector<PaletteHint> &hints)
977{
978 PT_UNWRAP_TILESET_CONFIG_REF(services.config, extrinsic_transparency, tileset_name, void);
979 PT_UNWRAP_TILESET_CONFIG_REF(services.config, num_palettes_in_primary, tileset_name, void);
980 PT_UNWRAP_TILESET_CONFIG_REF(services.config, num_palettes_total, tileset_name, void);
981
982 ConfigValue<std::size_t> num_palettes_cfg = is_secondary ? num_palettes_total : num_palettes_in_primary;
983 std::size_t count_max = num_palettes_cfg.value() * (palette::max_size - 1);
984
985 std::map<Rgba32, unsigned int> color_counts{};
986 bool hit_first_violation = false;
987
988 // Count colors in the input metatiles
989 std::size_t metatile_index = 0;
990 for (const auto &metatile : metatiles) {
991 const auto decomposed_metatile = metatile.decompose();
992 for (std::size_t internal_tile_index = 0; internal_tile_index < decomposed_metatile.size();
993 ++internal_tile_index) {
994 const auto &tile = decomposed_metatile[internal_tile_index];
995 for (std::size_t row = 0; row < tile::side_length_pix; ++row) {
996 for (std::size_t col = 0; col < tile::side_length_pix; ++col) {
997 const auto &pixel = tile.at(row, col);
998 if (!pixel.is_transparent(extrinsic_transparency)) {
999 color_counts[pixel]++;
1000 }
1001
1002 // Print the first violation only, but we'll keep counting across the rest of the metatiles so we
1003 // can give a final tally.
1004 if (color_counts.size() > count_max && !hit_first_violation) {
1005 hit_first_violation = true;
1006 std::string error_message = services.diag.formatter().format(
1007 "found {}th globally unique color: {}",
1009 FormatParam{pixel.to_jasc_str(), Style::bold});
1011 services,
1012 metatile,
1013 metatile_index,
1014 internal_tile_index,
1015 row,
1016 col,
1017 "global-color-count-violation",
1018 error_message,
1019 extrinsic_transparency);
1020 }
1021 }
1022 }
1023 }
1024 metatile_index++;
1025 }
1026
1027 // Count colors in the input anims
1028 auto validate_anim_frame = [&services, &hit_first_violation, &color_counts, &count_max, &extrinsic_transparency](
1029 const std::string &anim_name, const AnimFrame<Rgba32> &frame) -> void {
1030 const auto &frame_tiles = frame.tiles();
1031 // Loop over each internal frame tile
1032 for (std::size_t internal_tile_index = 0; internal_tile_index < frame_tiles.size(); ++internal_tile_index) {
1033 const auto &tile = frame_tiles.at(internal_tile_index);
1034
1035 // loop over tile pixels, row-major
1036 for (std::size_t row = 0; row < tile::side_length_pix; ++row) {
1037 for (std::size_t col = 0; col < tile::side_length_pix; ++col) {
1038 const auto &pixel = tile.at(row, col);
1039
1040 if (!pixel.is_transparent(extrinsic_transparency)) {
1041 color_counts[pixel]++;
1042 }
1043
1044 // Print the first violation only, but we'll keep counting across the rest of the anims so we can
1045 // give a final tally.
1046 if (color_counts.size() > count_max && !hit_first_violation) {
1047 hit_first_violation = true;
1048 std::string error_message = services.diag.formatter().format(
1049 "found {}th globally unique color: {}",
1051 FormatParam{pixel.to_jasc_str(), Style::bold});
1053 services,
1054 tile,
1055 anim_name,
1056 frame.frame_name(),
1057 internal_tile_index,
1058 row,
1059 col,
1060 "global-color-count-violation",
1061 error_message,
1062 extrinsic_transparency);
1063 }
1064 }
1065 }
1066 }
1067 };
1068
1069 for (const auto &[anim_name, anim] : anims) {
1070 if (anim.has_key_frame()) {
1071 const auto &key_frame = anim.key_frame();
1072 validate_anim_frame(anim_name, key_frame);
1073 }
1074 for (const AnimFrame<Rgba32> &frame : anim.frames() | std::views::values) {
1075 validate_anim_frame(anim_name, frame);
1076 }
1077 }
1078
1079 // Finally, check Porytiles override palettes and hints. We check these last, since we've now seen all colors in the
1080 // actual input assets. This allows us to warn the user if they've specified manual colors that never actually
1081 // appear in the input assets.
1082 std::set<Rgba32> unused_manual_colors{};
1083 for (std::size_t palette_index = 0; palette_index < num_palettes_cfg; ++palette_index) {
1084 const std::string filename = palette_filename(palette_index);
1085
1086 if (porytiles_palettes.at(palette_index).has_value()) {
1087 const auto &palette = porytiles_palettes.at(palette_index).value();
1088 std::vector<std::size_t> unused_slots{};
1089
1090 for (std::size_t slot_index = 0; slot_index < palette.size(); ++slot_index) {
1091 if (palette.is_wildcard(slot_index)) {
1092 continue;
1093 }
1094
1095 const auto &pixel = palette.at(slot_index);
1096 // Warn user for nontransparent palette colors that satisfy either:
1097 //
1098 // 1. The palette color is not present in color_counts, i.e. it wasn't seen in any input assets.
1099 // 2. It's present in our unused_manual_colors set, which we update as we iterate over the palettes.
1100 //
1101 // Condition 2) is vital because it allows us to detect and report all instances of an unused color,
1102 // even if it was duplicated multiple times in the manual palettes.
1103 if ((!color_counts.contains(pixel) || unused_manual_colors.contains(pixel)) &&
1104 !pixel.is_transparent(extrinsic_transparency)) {
1105 services.diag.warning(
1106 "unused-manual-color",
1107 services.diag.formatter().format(
1108 "color '{}' in slot '{}' of Porytiles palette '{}' is unused",
1109 FormatParam{pixel, Style::bold},
1110 FormatParam{slot_index, Style::bold},
1111 FormatParam{filename, Style::bold}));
1112 unused_slots.push_back(slot_index);
1113 unused_manual_colors.insert(pixel);
1114 }
1115
1116 // Increment count for color.
1117 if (!pixel.is_transparent(extrinsic_transparency)) {
1118 color_counts[pixel]++;
1119 }
1120
1121 // Print the first violation only, but we'll keep counting across the rest of the palettes so we can
1122 // give a final tally.
1123 if (color_counts.size() > count_max && !hit_first_violation) {
1124 hit_first_violation = true;
1125 std::string error_message = services.diag.formatter().format(
1126 "in Porytiles palette '{}': found {}th globally unique color: {}",
1127 FormatParam{filename, Style::bold},
1129 FormatParam{pixel.to_jasc_str(), Style::bold});
1130 services.diag.error("global-color-count-violation", error_message);
1131 std::vector<std::size_t> violating_slot{};
1132 violating_slot.push_back(slot_index);
1133 services.diag.error_note(
1134 "global-color-count-violation",
1136 services.diag.formatter(),
1137 services.palette_printer,
1138 "violating slot",
1139 palette,
1140 filename,
1141 violating_slot));
1142 }
1143 } // END: loop over all slots in override palette
1144
1145 // Print note highlighting all unused slots in this palette
1146 if (!unused_slots.empty()) {
1147 services.diag.warning_note(
1148 "unused-manual-color",
1150 services.diag.formatter(),
1151 services.palette_printer,
1152 "slots with unused colors",
1153 palette,
1154 filename,
1155 unused_slots));
1156 }
1157 unused_slots.clear();
1158 }
1159 } // END: loop over all override palettes
1160
1161 for (const auto &hint : hints) {
1162 std::vector<std::size_t> unused_slots{};
1163
1164 for (std::size_t slot_index = 0; slot_index < hint.palette().size(); ++slot_index) {
1165 const auto &pixel = hint.palette().at(slot_index);
1166
1167 // Warn user for nontransparent palette colors that satisfy either:
1168 //
1169 // 1. The palette color is not present in color_counts, i.e. it wasn't seen in any input assets.
1170 // 2. It's present in our unused_manual_colors set, which we update as we iterate over the palettes.
1171 //
1172 // Condition 2) is vital because it allows us to detect and report all instances of an unused color,
1173 // even if it was duplicated multiple times in the manual palettes.
1174 if ((!color_counts.contains(pixel) || unused_manual_colors.contains(pixel)) &&
1175 !pixel.is_transparent(extrinsic_transparency)) {
1176 services.diag.warning(
1177 "unused-manual-color",
1178 services.diag.formatter().format(
1179 "color '{}' in slot '{}' of palette hint '{}' is unused",
1180 FormatParam{pixel, Style::bold},
1181 FormatParam{slot_index, Style::bold},
1182 FormatParam{hint.name(), Style::bold}));
1183 unused_slots.push_back(slot_index);
1184 unused_manual_colors.insert(pixel);
1185 }
1186
1187 // Increment count for color.
1188 if (!pixel.is_transparent(extrinsic_transparency)) {
1189 color_counts[pixel]++;
1190 }
1191
1192 // Print the first violation only, but we'll keep counting across the rest of the hints so we can
1193 // give a final tally.
1194 if (color_counts.size() > count_max && !hit_first_violation) {
1195 hit_first_violation = true;
1196 std::string error_message = services.diag.formatter().format(
1197 "in palette hint '{}': found {}th globally unique color: {}",
1198 FormatParam{hint.name(), Style::bold},
1200 FormatParam{pixel.to_jasc_str(), Style::bold});
1201 services.diag.error("global-color-count-violation", error_message);
1202 std::vector<std::size_t> violating_slot{};
1203 violating_slot.push_back(slot_index);
1204 services.diag.error_note(
1205 "global-color-count-violation",
1207 services.diag.formatter(),
1208 services.palette_printer,
1209 "violating slot",
1210 hint,
1211 hint.name(),
1212 violating_slot));
1213 }
1214 } // END: loop over all slots in palette hint
1215
1216 // Print note highlighting all unused slots in this palette
1217 if (!unused_slots.empty()) {
1218 services.diag.warning_note(
1219 "unused-manual-color",
1221 services.diag.formatter(),
1222 services.palette_printer,
1223 "slots with unused colors",
1224 hint,
1225 hint.name(),
1226 unused_slots));
1227 }
1228 unused_slots.clear();
1229 } // END: loop over all palette hints
1230
1231 if (color_counts.size() > count_max) {
1232 services.diag.error(
1233 "global-color-count-violation",
1234 services.diag.formatter().format(
1235 "global color count violation: found '{}' unique colors, limit is '{}'",
1236 FormatParam{color_counts.size(), Style::bold},
1237 FormatParam{count_max, Style::bold}));
1238 details::report_color_counts("global-color-count-violation", services, color_counts);
1239 services.diag.error_note(
1240 "global-color-count-violation",
1241 build_global_color_limit_lines(services.diag.formatter(), count_max, num_palettes_cfg));
1242 return FormattableError{
1243 "Found '{}' unique colors globally, limit is '{}'.",
1244 FormatParam{color_counts.size(), Style::bold},
1245 FormatParam{count_max, Style::bold}};
1246 }
1247
1248 return {};
1249}
1250
1270 const TilesetCompileValidatorServices &services,
1271 const std::string &tileset_name,
1272 const std::vector<Metatile<Rgba32>> &metatiles,
1273 const std::map<std::string, Animation<Rgba32>> &anims,
1274 const std::array<std::optional<Palette<Rgba32, palette::max_size>>, palette::num_palettes> &porytiles_palettes,
1275 const std::vector<PaletteHint> &hints,
1276 const std::optional<std::array<Palette<Rgba32, palette::max_size>, palette::num_palettes>> &porymap_palettes)
1277{
1278 return {};
1279}
1280
1304 const TilesetCompileValidatorServices &services,
1305 const std::string &tileset_name,
1306 const std::map<std::string, Animation<Rgba32>> &anims)
1307{
1308 PT_UNWRAP_TILESET_CONFIG_REF(services.config, extrinsic_transparency, tileset_name, void);
1309 PT_UNWRAP_TILESET_CONFIG_REF(services.config, global_frame_linking, tileset_name, void);
1310 PT_UNWRAP_TILESET_CONFIG_REF(services.config, per_anim_overrides, tileset_name, void);
1311
1312 bool hit_error = false;
1313
1314 // Validation 0: Warn when animations with automatic frame linking are missing a key frame.
1315 //
1316 // In automatic mode, key.png is used to determine which tiles in tiles.png are animation tiles. Without it,
1317 // the first regular frame is used as a representative and animation tiles will not be linked to any metatiles.
1318 // This is allowed so users can incrementally build animation assets before committing to linking.
1319 for (const auto &[anim_name, anim] : anims) {
1320 bool has_per_anim_override = per_anim_overrides.value().contains(anim_name);
1321 const ConfigValue<FrameLinking> effective_linking =
1322 (has_per_anim_override && per_anim_overrides.value().at(anim_name).linking.has_value())
1323 ? per_anim_overrides.derive(per_anim_overrides.value().at(anim_name).linking)
1324 : global_frame_linking;
1325
1326 if (effective_linking == FrameLinking::automatic && !anim.has_key_frame()) {
1327 std::vector<std::string> warning_lines;
1328 warning_lines.emplace_back(services.diag.formatter().format(
1329 "Animation '{}' has frame_linking '{}' but no key frame (key.png) was found.",
1330 FormatParam{anim_name, Style::bold},
1331 FormatParam{"automatic", Style::bold}));
1332 warning_lines.emplace_back("Animation tiles will not be linked to any metatiles.");
1333 services.diag.warning("missing-key-frame", warning_lines);
1334
1335 if (has_per_anim_override) {
1336 std::vector<std::string> note_lines;
1337 note_lines.push_back(services.diag.formatter().format(
1338 "Per-animation override for '{}' sets frame_linking to '{}'.",
1339 FormatParam{anim_name, Style::bold},
1340 FormatParam{"automatic", Style::bold}));
1341 note_lines.emplace_back("");
1342 note_lines.append_range(format_config_note(services.diag.formatter(), per_anim_overrides));
1343 services.diag.warning_note("missing-key-frame", note_lines);
1344 }
1345 else {
1346 services.diag.warning_note(
1347 "missing-key-frame", format_config_note(services.diag.formatter(), global_frame_linking));
1348 }
1349 }
1350 }
1351
1352 // Validation 1: Ensure no animation key frame tile is fully transparent.
1353 //
1354 // Key frames are used to index into animations from the layer sheet. If a key frame tile were transparent,
1355 // Porytiles couldn't distinguish it from the default transparent tile (tile 0). This restriction only applies to
1356 // actual key frames (key.png), not to representative frames used as fallbacks when no key frame is present.
1357 //
1358 // We collect all violations before failing so the user can see all problematic tiles at once.
1359 for (const auto &[anim_name, anim] : anims) {
1360 if (!anim.has_key_frame()) {
1361 continue;
1362 }
1363
1364 const auto &key_frame = anim.key_frame();
1365 const auto &tiles = key_frame.tiles();
1366
1367 for (std::size_t tile_idx = 0; tile_idx < tiles.size(); ++tile_idx) {
1368 const auto &tile = tiles.at(tile_idx);
1369
1370 if (tile.is_transparent(extrinsic_transparency.value())) {
1371 hit_error = true;
1372 std::string error_message = "key frame tile is fully transparent";
1374 services,
1375 tile,
1376 anim_name,
1377 key_frame.frame_name(),
1378 tile_idx,
1379 "transparent-key-frame",
1380 error_message,
1381 extrinsic_transparency);
1382
1383 services.diag.error_note(
1384 "transparent-key-frame",
1385 std::vector<std::string>{
1386 "Key frame tiles cannot be fully transparent because they would be indistinguishable "
1387 "from the actual transparent tile on the layer PNGs."});
1388 }
1389 }
1390 }
1391
1392 if (hit_error) {
1393 return FormattableError{"Found animation(s) with transparent key frame tile(s)."};
1394 }
1395
1396 // Validation 2: Ensure there are no duplicate key frame tiles across all animations.
1397 //
1398 // Each key frame tile must be unique so that Porytiles can properly identify which animation a tile belongs to. We
1399 // collect all key frame tiles into a map and report any that appear in multiple locations.
1400 std::map<PixelTile<Rgba32>, std::vector<std::pair<std::string, std::size_t>>> key_frame_occurrences;
1401
1402 for (const auto &[anim_name, anim] : anims) {
1403 if (!anim.has_key_frame()) {
1404 continue;
1405 }
1406
1407 const auto &key_frame = anim.key_frame();
1408 const auto &tiles = key_frame.tiles();
1409
1410 for (std::size_t tile_idx = 0; tile_idx < tiles.size(); ++tile_idx) {
1411 const auto &tile = tiles.at(tile_idx);
1412 key_frame_occurrences[tile].emplace_back(anim_name, tile_idx);
1413 }
1414 }
1415
1416 for (const auto &[tile, occurrences] : key_frame_occurrences) {
1417 if (occurrences.size() > 1) {
1418 hit_error = true;
1419
1420 // Report error for first occurrence
1421 const auto &[first_anim, first_idx] = occurrences.at(0);
1422 std::string error_message = "duplicate key frame tile";
1424 services,
1425 tile,
1426 first_anim,
1427 "key",
1428 first_idx,
1429 "duplicate-key-frame",
1430 error_message,
1431 extrinsic_transparency);
1432
1433 // Build note showing all locations where this tile appears
1434 std::vector<std::string> note_lines;
1435 note_lines.emplace_back("This tile appears as a key frame in multiple locations:");
1436 for (const auto &[anim_name, tile_idx] : occurrences) {
1437 note_lines.push_back(services.diag.formatter().format(
1438 " - anim '{}' subtile {}", FormatParam{anim_name, Style::bold}, FormatParam{tile_idx}));
1439 }
1440 note_lines.emplace_back("");
1441 note_lines.emplace_back(
1442 "Each key frame tile must be unique so that Porytiles can identify which "
1443 "animation a tile belongs to at runtime.");
1444 services.diag.error_note("duplicate-key-frame", note_lines);
1445 }
1446 }
1447
1448 // Validation 3: Verify composite frame color counts.
1449 //
1450 // Each animation's composite frame aggregates all colors from all frames at each tile position. Since the palette
1451 // index is fixed for the entire animation lifecycle, each composite tile cannot exceed 15 unique colors.
1452 //
1453 // We iterate pixel by pixel across all frames for each tile position, tracking actual color counts. When a
1454 // violation is detected, we show the specific subtile and highlight the pixel that caused the overflow.
1455 for (const auto &[anim_name, anim] : anims) {
1456 if (!anim.has_key_frame()) {
1457 continue;
1458 }
1459
1460 const auto &key_frame = anim.key_frame();
1461 const std::size_t tile_count = key_frame.tile_count();
1462
1463 for (std::size_t tile_idx = 0; tile_idx < tile_count; ++tile_idx) {
1464 // Track color counts across all frames for this tile position
1465 std::map<Rgba32, unsigned int> color_counts;
1466
1467 // Track violation details (frame name, tile, row, col) for later reporting
1468 bool found_violation = false;
1469 std::string violation_frame_name;
1470 const PixelTile<Rgba32> *violation_tile = nullptr;
1471 std::size_t violation_row = 0;
1472 std::size_t violation_col = 0;
1473 Rgba32 violation_pixel;
1474
1475 // Helper lambda to process a tile's pixels
1476 auto process_tile_pixels = [&](const PixelTile<Rgba32> &tile, const std::string &frame_name) {
1477 for (std::size_t row = 0; row < tile::side_length_pix; ++row) {
1478 for (std::size_t col = 0; col < tile::side_length_pix; ++col) {
1479 const auto &pixel = tile.at(row, col);
1480 if (!pixel.is_transparent(extrinsic_transparency)) {
1481 color_counts[pixel]++;
1482 }
1483
1484 // Record the first violation but continue counting for accurate totals
1485 if (color_counts.size() > palette::max_size - 1 && !found_violation) {
1486 found_violation = true;
1487 violation_frame_name = frame_name;
1488 violation_tile = &tile;
1489 violation_row = row;
1490 violation_col = col;
1491 violation_pixel = pixel;
1492 }
1493 }
1494 }
1495 };
1496
1497 // Process key frame pixels
1498 process_tile_pixels(key_frame.tile_at(tile_idx), key_frame.frame_name());
1499
1500 // Process regular frame pixels
1501 for (const auto &[frame_name, frame] : anim.frames()) {
1502 process_tile_pixels(frame.tile_at(tile_idx), frame_name);
1503 }
1504
1505 // Report violation after processing all frames (so we have complete color counts)
1506 if (found_violation) {
1507 hit_error = true;
1508
1509 std::string error_message = services.diag.formatter().format(
1510 "found {}th unique composite frame tile color: {}",
1512 FormatParam{violation_pixel.to_jasc_str(), Style::bold});
1514 services,
1515 *violation_tile,
1516 anim_name,
1517 violation_frame_name,
1518 tile_idx,
1519 violation_row,
1520 violation_col,
1521 "composite-color-count-violation",
1522 error_message,
1523 extrinsic_transparency);
1524
1525 std::vector<std::string> note_lines;
1526 note_lines.emplace_back(
1527 "The composite frame aggregates all colors across all animation frames for each tile position. "
1528 "Since the palette index is fixed for the entire animation lifecycle, each composite tile cannot "
1529 "exceed 15 colors.");
1530 services.diag.error_note("composite-color-count-violation", note_lines);
1531
1532 // Print all frame tiles at this position to show what's contributing colors
1533 std::vector<std::string> frame_tiles_note;
1534 frame_tiles_note.emplace_back("Tiles at this position across all frames:");
1535
1536 // Print key frame tile
1537 frame_tiles_note.emplace_back("");
1538 frame_tiles_note.push_back(services.diag.formatter().format(
1539 "Frame '{}' subtile {}:", FormatParam{key_frame.frame_name(), Style::bold}, FormatParam{tile_idx}));
1540 auto key_tile_visual =
1541 services.tile_printer.print_tile(key_frame.tile_at(tile_idx), extrinsic_transparency);
1542 frame_tiles_note.append_range(key_tile_visual);
1543
1544 // Print regular frame tiles
1545 for (const auto &[frame_name, frame] : anim.frames()) {
1546 frame_tiles_note.emplace_back("");
1547 frame_tiles_note.push_back(services.diag.formatter().format(
1548 "Frame '{}' subtile {}:", FormatParam{frame_name, Style::bold}, FormatParam{tile_idx}));
1549 auto frame_tile_visual =
1550 services.tile_printer.print_tile(frame.tile_at(tile_idx), extrinsic_transparency);
1551 frame_tiles_note.append_range(frame_tile_visual);
1552 }
1553
1554 services.diag.error_note("composite-color-count-violation", frame_tiles_note);
1555
1556 details::report_color_counts("composite-color-count-violation", services, color_counts);
1557 }
1558 }
1559 }
1560
1561 if (hit_error) {
1562 return FormattableError{"Animation frame validation failed."};
1563 }
1564
1565 return {};
1566}
1567
1568} // 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:89
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:57
The core tileset entity - a 2x2 grid of PixelTile objects arranged into three layers.
Definition metatile.hpp:224
Represents a palette hint for the palette packing algorithm.
const std::string & name() const
const Palette< Rgba32 > & palette() const
A collection of printer functions for the Palette and related types.
std::vector< std::string > print_rgba_palette_counts(const std::map< Rgba32, unsigned int > &color_counts) const
A generic palette container for colors that support transparency checking.
Definition palette.hpp:45
ColorType slot_zero_color() const
Get the slot 0 palette color.
Definition palette.hpp:226
std::size_t size() const
Returns the number of slots in the palette.
Definition palette.hpp:203
ColorType at(std::size_t index) const
Gets the color at a specific index.
Definition palette.hpp:246
bool is_wildcard(std::size_t index) const
Checks if a slot is a wildcard.
Definition palette.hpp:176
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:21
static constexpr std::uint8_t alpha_transparent
Definition rgba32.hpp:23
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:24
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:37
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:26
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:157
constexpr std::size_t tiles_per_metatile_layer
Definition metatile.hpp:22
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:170
Subtile subtile_from_index(std::size_t i)
Definition metatile.hpp:91
constexpr std::size_t max_size
Definition palette.hpp:19
constexpr std::size_t num_palettes
Definition palette.hpp:21
constexpr std::size_t side_length_pix
ChainableResult< void > validate_palette_hint(const TilesetCompileValidatorServices &services, const std::string &tileset_name, const PaletteHint &hint)
Validates a user-specified palette hint for correctness.
void panic(const StringViewSourceLoc &s)
Unconditionally terminates the program with a panic message.
Definition panic.cpp:43
ChainableResult< void > validate_porymap_palette(const TilesetCompileValidatorServices &services, const std::string &tileset_name, const Palette< Rgba32, palette::max_size > &palette, std::size_t palette_index)
Validates a Porymap palette for correctness according to GBA hardware constraints.
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.
LayerMode layer_mode_from_val(std::size_t s)
Converts a numeric value to LayerMode.
Definition layer.hpp:29
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, palette::max_size > >, palette::num_palettes > &porytiles_palettes, const std::vector< PaletteHint > &hints)
Validates that the total unique color count across all input does not exceed the global limit.
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.
ChainableResult< void > validate_porytiles_palette(const TilesetCompileValidatorServices &services, const std::string &tileset_name, const Palette< Rgba32, palette::max_size > &palette, std::size_t palette_index)
Validates a user-specified Porytiles override palette for correctness.
std::vector< std::string > build_global_color_limit_lines(const TextFormatter &format, std::size_t color_count_limit, const ConfigValue< std::size_t > &num_palettes)
Builds note lines explaining the global color count limit for primary tileset compilation.
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, palette::max_size > >, palette::num_palettes > &porytiles_palettes, const std::vector< PaletteHint > &hints, const std::optional< std::array< Palette< Rgba32, palette::max_size >, palette::num_palettes > > &porymap_palettes)
Validates that no colors will suffer unacceptable precision loss during GBA color conversion.
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.
std::string palette_filename(std::size_t palette_index)
Constructs a palette filename from a palette index.
@ automatic
Use key.png for frame linking.
std::vector< std::string > build_porymap_palette_highlight_lines(const TextFormatter &format, const PalettePrinter &palette_printer, const std::string &message, const Palette< Rgba32, N > &palette, const std::string &palette_label, const std::vector< std::size_t > &violating_slots)
Builds note lines displaying a Porymap palette with highlighted violating slots.
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::string to_string(const PrimaryPairingMode m)
Converts a PrimaryPairingMode to its canonical string representation.
std::vector< std::string > build_palette_hint_highlight_lines(const TextFormatter &format, const PalettePrinter &palette_printer, const std::string &message, const PaletteHint &hint, const std::string &palette_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_palette_highlight_lines(const TextFormatter &format, const PalettePrinter &palette_printer, const std::string &message, const Palette< Rgba32, N > &palette, const std::string &palette_label, const std::vector< std::size_t > &violating_slots)
Builds note lines displaying a Porytiles palette with highlighted violating slots.
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...