31 std::to_string(color.
red()) +
", " + std::to_string(color.
green()) +
", " + std::to_string(color.
blue());
36struct ColorSetWithOccupancy {
38 std::size_t occupied_slots{};
52[[nodiscard]]
ColorSet build_color_set_from_tile(
58 for (
const auto &color : unique_colors) {
60 if (!index_opt.has_value()) {
64 panic(
"tile color " +
to_string(color) +
" not found in color index map");
66 color_set.set(index_opt.value());
82[[nodiscard]] ColorSetWithOccupancy
86 std::size_t occupied_slots = 0;
89 for (std::size_t i = 1; i < palette.
size(); ++i) {
94 const auto color = palette.
at(i);
96 if (!index_opt.has_value()) {
100 panic(
"palette color " +
to_string(color) +
" at slot " + std::to_string(i) +
" not in color map");
102 color_set.set(index_opt.value());
105 return ColorSetWithOccupancy{color_set, occupied_slots};
123 for (std::size_t i = 0; i < palette.
size(); ++i) {
125 panic(
"build_color_set_from_hint_palette palette contained unexpected wildcard");
127 const auto color = palette.
at(i);
129 if (!index_opt.has_value()) {
130 panic(
"hint color " +
to_string(color) +
" at slot " + std::to_string(i) +
" not in color map");
132 color_set.set(index_opt.value());
150[[nodiscard]] std::set<PixelTile<Rgba32>>
153 std::set<PixelTile<Rgba32>> result;
154 for (
const auto &[
name, anim] : anims) {
155 if (anim.frames().empty()) {
159 anim.has_key_frame() ? anim.key_frame() : anim.frames().begin()->second;
160 for (
const auto &tile : representative_frame.tiles()) {
179struct CombinedTiles {
180 std::vector<PixelTile<Rgba32>> tiles;
181 std::vector<PackableTile::Id> index_to_id;
184[[nodiscard]] CombinedTiles
187 CombinedTiles combined;
191 for (std::size_t i = 0; i < params.
tiles_.size(); ++i) {
196 combined.tiles.push_back(params.
tiles_.at(i));
202 combined.tiles.push_back(primary_ref.tile);
210struct SharingGroupMember {
211 std::size_t tile_index;
212 std::size_t palette_index;
213 bool is_primary{
false};
220struct PartitionGroup {
221 std::vector<SharingGroupMember> members;
228 std::vector<std::size_t> color_version_tile_indices;
235 std::vector<std::pair<std::size_t, std::size_t>> primary_color_version_entries;
243struct PartialAlignmentInfo {
244 std::size_t group_id;
245 std::size_t dropped_color_version_count;
249struct PackingInputs {
250 std::vector<PackableTile> regular_tiles;
251 std::vector<PackableTile> hint_tiles;
252 std::set<PrefilledPalette> prefilled_palettes;
261 std::set<std::size_t> eligible_shape_indices;
262 std::vector<PartitionGroup> partition_groups;
263 std::map<std::size_t, std::size_t> shape_to_partition_index;
267struct Phase3GroupResult {
268 std::size_t shape_group_index;
270 std::size_t dropped_color_version_count;
271 std::size_t matching_count;
272 std::map<std::size_t, std::vector<std::size_t>> members_by_palette;
273 bool has_primary_members{
false};
274 std::map<std::size_t, std::vector<std::size_t>> primary_members_by_palette;
283 std::size_t aligned_count{};
284 std::size_t fully_aligned_count{};
285 std::size_t partially_aligned_count{};
286 std::vector<PartialAlignmentInfo> partial_alignment_infos;
287 std::vector<std::pair<std::size_t, PartitionGroup>> unaligned_partition_groups;
288 std::vector<Phase3GroupResult> aligned_groups;
292struct VerifiedMember {
293 std::size_t regular_index;
294 std::size_t hw_palette;
296 std::map<ShapeMask, Rgba32> colors;
297 bool is_primary{
false};
308[[nodiscard]] PackingInputs build_packing_inputs(
const PackingParams ¶ms)
311 std::vector<PackableTile> regular_tiles;
312 regular_tiles.reserve(params.
tiles_.size());
313 for (std::size_t i = 0; i < params.
tiles_.size(); ++i) {
318 for (
const auto &[anim_name, anim] : params.anims_) {
320 for (std::size_t subtile_index = 0; subtile_index < composite_frame.tiles().size(); ++subtile_index) {
321 const auto &composite_tile = composite_frame.tiles().at(subtile_index);
329 std::vector<PackableTile> hint_tiles;
330 hint_tiles.reserve(params.
hints_.size());
332 auto color_set = build_color_set_from_hint_palette(hint.palette(), params.
color_map_);
337 std::set<PrefilledPalette> prefilled_palettes;
342 auto [color_set, occupied_slots] =
347 return PackingInputs{std::move(regular_tiles), std::move(hint_tiles), std::move(prefilled_palettes)};
361 for (
const auto &[tile_id, palette_index] : output.tile_to_palette_) {
363 [&packing, palette_index]<
typename IdVariant>(IdVariant &&
id) {
364 using Id = std::decay_t<IdVariant>;
365 if constexpr (std::is_same_v<Id, PackableTile::RegularId>) {
368 else if constexpr (std::is_same_v<Id, PackableTile::AnimId>) {
371 else if constexpr (std::is_same_v<Id, PackableTile::HintId>) {
374 else if constexpr (std::is_same_v<Id, PackableTile::PrefilledPaletteId>) {
377 else if constexpr (std::is_same_v<Id, PackableTile::PrimaryTileId>) {
381 static_assert(
sizeof(Id) == 0,
"Unhandled PackableTile::Id variant alternative");
398[[nodiscard]] std::map<std::size_t, std::size_t> build_tile_palette_assignments(
400 const CombinedTiles &combined,
401 const std::map<std::size_t, std::size_t> &tile_to_palette)
403 std::map<std::size_t, std::size_t> tile_palette_assignments;
404 for (
const auto &group : shape_groups) {
405 for (
const auto &member : group.members) {
406 const auto &
id = combined.index_to_id.at(member.tile_index);
407 if (std::holds_alternative<PackableTile::RegularId>(
id)) {
408 auto regular_index = std::get<PackableTile::RegularId>(
id).index;
409 if (tile_to_palette.contains(regular_index)) {
410 tile_palette_assignments[member.tile_index] = tile_to_palette.at(regular_index);
413 else if (std::holds_alternative<PackableTile::PrimaryTileId>(
id)) {
414 auto primary_id = std::get<PackableTile::PrimaryTileId>(
id);
415 tile_palette_assignments[member.tile_index] = primary_id.palette_index;
419 return tile_palette_assignments;
432[[nodiscard]] Phase2Result compute_partition_groups(
434 const CombinedTiles &combined,
435 const std::map<std::size_t, std::size_t> &tile_to_palette)
438 for (std::size_t sg_index = 0; sg_index < shape_groups.size(); ++sg_index) {
439 const auto &group = shape_groups.at(sg_index);
440 std::set<std::size_t> distinct_palettes_set;
441 std::vector<SharingGroupMember> members;
442 std::set<std::map<ShapeMask, Rgba32>> seen_colors;
443 std::vector<std::size_t> color_version_tile_indices;
444 std::vector<std::pair<std::size_t, std::size_t>> primary_color_version_entries;
446 for (
const auto &member : group.members) {
447 const auto &
id = combined.index_to_id.at(member.tile_index);
448 std::size_t display_index{};
449 std::optional<std::size_t> palette_opt;
451 bool is_primary =
false;
452 if (std::holds_alternative<PackableTile::RegularId>(
id)) {
453 auto regular_index = std::get<PackableTile::RegularId>(
id).index;
454 display_index = regular_index;
455 if (tile_to_palette.contains(regular_index)) {
456 palette_opt = tile_to_palette.at(regular_index);
459 else if (std::holds_alternative<PackableTile::PrimaryTileId>(
id)) {
460 auto primary_id = std::get<PackableTile::PrimaryTileId>(
id);
461 display_index = primary_id.tile_index;
462 palette_opt = primary_id.palette_index;
466 if (palette_opt.has_value()) {
467 members.push_back(SharingGroupMember{display_index, palette_opt.value(), is_primary});
468 distinct_palettes_set.insert(palette_opt.value());
469 if (!seen_colors.contains(member.colors)) {
470 seen_colors.insert(member.colors);
472 color_version_tile_indices.push_back(display_index);
475 primary_color_version_entries.emplace_back(display_index, palette_opt.value());
481 if (members.size() >= 2 && distinct_palettes_set.size() >= 2) {
482 result.eligible_shape_indices.insert(sg_index);
483 PartitionGroup partition;
484 partition.members = std::move(members);
485 partition.color_version_tile_indices = std::move(color_version_tile_indices);
486 partition.primary_color_version_entries = std::move(primary_color_version_entries);
487 result.shape_to_partition_index[sg_index] = result.partition_groups.size();
488 result.partition_groups.push_back(std::move(partition));
508[[nodiscard]] Phase3Result verify_sharing_alignment(
510 const CombinedTiles &combined,
511 const std::map<std::size_t, std::size_t> &tile_palette_assignments,
512 const Phase2Result &phase2,
517 std::set<std::size_t> aligned_shape_indices;
519 for (std::size_t sg_index = 0; sg_index < shape_groups.size(); ++sg_index) {
520 if (!phase2.eligible_shape_indices.contains(sg_index)) {
523 const auto &group = shape_groups.at(sg_index);
524 std::vector<VerifiedMember> verified;
526 for (
const auto &member : group.members) {
527 if (!tile_palette_assignments.contains(member.tile_index)) {
530 const auto &
id = combined.index_to_id.at(member.tile_index);
531 std::size_t display_index{};
532 if (std::holds_alternative<PackableTile::RegularId>(
id)) {
533 display_index = std::get<PackableTile::RegularId>(
id).index;
535 else if (std::holds_alternative<PackableTile::PrimaryTileId>(
id)) {
536 display_index = std::get<PackableTile::PrimaryTileId>(
id).tile_index;
541 std::size_t hw = tile_palette_assignments.at(member.tile_index);
543 bool is_primary_member = std::holds_alternative<PackableTile::PrimaryTileId>(
id);
545 const auto &tile = combined.tiles.at(member.tile_index);
549 VerifiedMember{display_index, hw, std::move(
canonical), member.colors, is_primary_member});
552 if (verified.size() < 2) {
556 const auto &reference = verified.
at(0).canonical_indexed;
557 std::vector<SharingGroupMember> result_members;
558 std::set<std::size_t> distinct_palettes_set;
560 std::set<std::map<ShapeMask, Rgba32>> dropped_color_versions;
561 for (
const auto &member : verified) {
564 if (member_as_tile != ref_as_tile) {
565 dropped_color_versions.insert(member.colors);
568 result_members.push_back(SharingGroupMember{member.regular_index, member.hw_palette, member.is_primary});
569 distinct_palettes_set.insert(member.hw_palette);
571 const std::size_t dropped_color_version_count = dropped_color_versions.size();
573 if (result_members.size() >= 2 && distinct_palettes_set.size() >= 2) {
574 aligned_shape_indices.insert(sg_index);
576 const bool is_partial = dropped_color_version_count > 0;
578 ++result.partially_aligned_count;
579 result.partial_alignment_infos.push_back(PartialAlignmentInfo{sg_index, dropped_color_version_count});
582 ++result.fully_aligned_count;
585 std::map<std::size_t, std::vector<std::size_t>> members_by_palette;
586 std::map<std::size_t, std::vector<std::size_t>> primary_members_by_palette;
587 bool has_primary =
false;
588 for (
const auto &member : result_members) {
589 if (member.is_primary) {
591 primary_members_by_palette[member.palette_index].push_back(member.tile_index);
594 members_by_palette[member.palette_index].push_back(member.tile_index);
597 result.aligned_groups.push_back(
601 dropped_color_version_count,
602 result_members.size(),
603 std::move(members_by_palette),
605 std::move(primary_members_by_palette)});
609 result.aligned_count = aligned_shape_indices.size();
612 for (
const auto sg_index : phase2.eligible_shape_indices) {
613 if (!aligned_shape_indices.contains(sg_index)) {
614 result.unaligned_partition_groups.emplace_back(
615 sg_index, phase2.partition_groups.at(phase2.shape_to_partition_index.at(sg_index)));
634void emit_phase1_diagnostics(
639 const CombinedTiles &combined,
642 for (std::size_t shape_group_index = 0; shape_group_index < shape_groups.size(); ++shape_group_index) {
643 const auto &shape_group = shape_groups.at(shape_group_index);
644 std::vector<std::size_t> color_version_tile_indices;
645 std::vector<std::size_t> all_tile_indices;
646 std::vector<std::pair<std::size_t, std::size_t>> primary_color_version_entries;
647 std::set<std::map<ShapeMask, Rgba32>> seen_colors;
648 for (
const auto &member : shape_group.members) {
649 const auto &
id = combined.index_to_id.at(member.tile_index);
650 if (std::holds_alternative<PackableTile::RegularId>(
id)) {
651 auto regular_index = std::get<PackableTile::RegularId>(
id).index;
652 all_tile_indices.push_back(regular_index);
653 if (!seen_colors.contains(member.colors)) {
654 seen_colors.insert(member.colors);
655 color_version_tile_indices.push_back(regular_index);
658 else if (std::holds_alternative<PackableTile::PrimaryTileId>(
id)) {
659 auto primary_id = std::get<PackableTile::PrimaryTileId>(
id);
660 if (!seen_colors.contains(member.colors)) {
661 seen_colors.insert(member.colors);
662 primary_color_version_entries.emplace_back(primary_id.tile_index, primary_id.palette_index);
667 const auto total_color_versions = color_version_tile_indices.size() + primary_color_version_entries.size();
668 auto phase1_tag = std::format(
"tile-sharing-shareable-tiles-{}", shape_group_index);
670 std::vector<std::string> remark_lines;
671 remark_lines.emplace_back(format.
format(
672 "Detected sharing opportunity (group id '{}') with '{}' color version(s) across '{}' tilemap entries.",
675 FormatParam{all_tile_indices.size(), Style::bold}));
676 if (!primary_color_version_entries.empty()) {
677 remark_lines.emplace_back(format.
format(
678 "'{}' color version(s) from paired primary.",
679 FormatParam{primary_color_version_entries.size(), Style::bold}));
681 if (!color_version_tile_indices.empty()) {
685 if (!primary_color_version_entries.empty()) {
691 primary_color_version_entries,
692 color_version_tile_indices.size()));
694 remark_lines.emplace_back(
695 format.
format(
"All tilemap entries ('{}' total):",
FormatParam{all_tile_indices.size(), Style::bold}));
697 diag.
remark(phase1_tag, remark_lines);
701 "tile-sharing-shareable-tiles-summary",
702 "Tile sharing detection: '{}' shareable shape group(s) found.",
718void emit_phase2_diagnostics(
723 const Phase2Result &phase2,
726 for (std::size_t sg_index = 0; sg_index < shape_groups.size(); ++sg_index) {
727 if (!phase2.eligible_shape_indices.contains(sg_index)) {
730 const auto &partition = phase2.partition_groups.at(phase2.shape_to_partition_index.at(sg_index));
732 auto phase2_tag = std::format(
"tile-sharing-palette-partition-{}", sg_index);
735 bool has_primary_members =
false;
736 std::map<std::size_t, std::vector<std::size_t>> members_by_palette;
737 for (
const auto &member : partition.members) {
738 if (member.is_primary) {
739 has_primary_members =
true;
742 members_by_palette[member.palette_index].push_back(member.tile_index);
745 std::vector<std::string> remark_lines;
746 remark_lines.emplace_back(format.
format(
747 "After palette packing (group id '{}'), '{}' color versions across '{}' tilemap entries eligible for "
750 FormatParam{partition.color_version_tile_indices.size(), Style::bold},
751 FormatParam{partition.members.size(), Style::bold}));
752 if (has_primary_members) {
753 remark_lines.emplace_back(
"Includes cross-tileset member(s) from paired primary.");
756 if (!partition.color_version_tile_indices.empty()) {
762 partition.color_version_tile_indices));
764 if (!partition.primary_color_version_entries.empty()) {
770 partition.primary_color_version_entries,
771 partition.color_version_tile_indices.size()));
776 remark_lines.emplace_back(
"");
777 remark_lines.emplace_back(
"--------");
778 remark_lines.emplace_back(
"");
779 remark_lines.emplace_back(
"Caveat: the palette partition shown above may be coincidental.");
780 remark_lines.emplace_back(format.
format(
781 "Packing '{}' does not actively separate shape-group siblings across palettes.",
782 FormatParam{to_string(params.tile_sharing_packing_.value()), Style::bold}));
783 remark_lines.emplace_back(
"");
786 diag.
remark(phase2_tag, remark_lines);
790 "tile-sharing-palette-partition-summary",
791 "Tile sharing partition: '{}' of '{}' shape group(s) eligible for sharing after palette packing.",
807void emit_phase3_diagnostics(
811 const Phase3Result &phase3,
814 for (
const auto &group : phase3.aligned_groups) {
815 auto phase3_tag = std::format(
"tile-sharing-result-{}", group.shape_group_index);
817 std::vector<std::string> remark_lines;
818 if (group.is_partial) {
819 remark_lines.emplace_back(format.
format(
820 "Tile sharing partially succeeded (group id '{}'): '{}' participating palette(s) (referenced "
821 "in '{}' tilemap entries).",
823 FormatParam{group.members_by_palette.size(), Style::bold},
825 remark_lines.emplace_back(format.
format(
826 "'{}' color version(s) dropped due to alignment divergence.",
827 FormatParam{group.dropped_color_version_count, Style::bold}));
830 remark_lines.emplace_back(format.
format(
831 "Tile sharing succeeded (group id '{}'): '{}' participating palette(s) (referenced in '{}' "
834 FormatParam{group.members_by_palette.size(), Style::bold},
837 if (group.has_primary_members) {
838 remark_lines.emplace_back(
"Includes cross-tileset member(s) from paired primary.");
840 if (!group.members_by_palette.empty()) {
845 if (!group.primary_members_by_palette.empty()) {
851 group.primary_members_by_palette));
854 if (group.is_partial) {
855 remark_lines.emplace_back(
"");
856 remark_lines.emplace_back(
"--------");
857 remark_lines.emplace_back(
"");
858 remark_lines.emplace_back(format.
format(
859 "'{}' color version(s) diverged from the reference and were dropped.",
860 FormatParam{group.dropped_color_version_count, Style::bold}));
864 remark_lines.emplace_back(
"");
865 remark_lines.emplace_back(
"--------");
866 remark_lines.emplace_back(
"");
867 remark_lines.emplace_back(
"Caveat: the slot alignment shown above may be coincidental.");
868 remark_lines.emplace_back(format.
format(
869 "Alignment '{}' does not actively align palette slots for tile sharing.",
870 FormatParam{to_string(params.tile_sharing_alignment_.value()), Style::bold}));
871 remark_lines.emplace_back(
"");
874 diag.
remark(phase3_tag, remark_lines);
892void emit_sharing_summary(
898 const Phase2Result &phase2,
899 const Phase3Result &phase3,
902 const auto eligible = phase2.partition_groups.size();
903 const auto &fc = failure_counts;
905 constexpr auto tag =
"tile-sharing-result-summary";
907 std::vector<std::string> remark_lines;
908 if (phase3.partially_aligned_count > 0) {
909 remark_lines.emplace_back(
"Tile sharing summary:");
910 remark_lines.emplace_back(format.
format(
911 "'{}' detected → '{}' eligible after packing → '{}' aligned ('{}' fully, '{}' "
916 FormatParam{phase3.fully_aligned_count, Style::bold},
917 FormatParam{phase3.partially_aligned_count, Style::bold}));
920 remark_lines.emplace_back(
"Tile sharing summary:");
921 remark_lines.emplace_back(format.
format(
922 "'{}' detected → '{}' eligible after packing → '{}' aligned.",
929 if (phase3.partially_aligned_count > 0) {
930 remark_lines.emplace_back(
"");
931 remark_lines.emplace_back(
"Partially aligned groups:");
932 for (
const auto &info : phase3.partial_alignment_infos) {
933 remark_lines.emplace_back(format.
format(
934 " Group '{}': '{}' color version(s) dropped.",
936 FormatParam{info.dropped_color_version_count, Style::bold}));
938 remark_lines.emplace_back(
"");
941 const auto unaligned = eligible - phase3.aligned_count;
944 panic(
"Hit 'Divergence with Zero Failures' case");
947 remark_lines.emplace_back(
948 format.
format(
"'{}' eligible group(s) could not be aligned:",
FormatParam{unaligned, Style::bold}));
950 for (
const auto &[group_id, group] : phase3.unaligned_partition_groups) {
951 remark_lines.emplace_back();
952 bool has_primary_in_group = std::ranges::any_of(group.members, [](
const auto &m) { return m.is_primary; });
953 remark_lines.emplace_back(format.
format(
954 "Unaligned group (group id '{}'), '{}' color version(s):",
956 FormatParam{group.color_version_tile_indices.size(), Style::bold}));
957 if (has_primary_in_group) {
958 remark_lines.emplace_back(
"Includes cross-tileset member(s) from paired primary.");
960 if (!group.color_version_tile_indices.empty()) {
966 group.color_version_tile_indices));
970 for (std::size_t v = 0; v < group.color_version_tile_indices.size(); ++v) {
971 const auto cv_tile_index = group.color_version_tile_indices.at(v);
972 std::set<std::size_t> palettes_for_version;
973 for (
const auto &member : group.members) {
974 if (member.tile_index == cv_tile_index) {
975 palettes_for_version.insert(member.palette_index);
978 if (palettes_for_version.empty()) {
981 std::string palette_list;
982 for (
const auto palette : palettes_for_version) {
983 if (!palette_list.empty()) {
984 palette_list +=
", ";
988 remark_lines.emplace_back(format.
format(
989 " Version '{}' assigned to palette(s): '{}'.",
996 const auto group_prefilled_dest =
997 std::ranges::count_if(fc.prefilled_destination_conflict_details, [&](
const auto &d) {
998 return d.source_group_index == group_id;
1000 const auto group_prefilled_src =
1001 std::ranges::count_if(fc.prefilled_source_conflict_details, [&](
const auto &d) {
1002 return d.source_group_index == group_id;
1004 const auto group_fww = std::ranges::count_if(
1005 fc.first_writer_wins_details, [&](
const auto &d) { return d.source_group_index == group_id; });
1006 const auto group_mismatch =
1007 std::ranges::count_if(fc.post_resolution_mismatch_details, [&](
const auto &d) {
1008 return d.source_group_index == group_id;
1010 const auto group_total = group_prefilled_dest + group_prefilled_src + group_fww + group_mismatch;
1012 if (group_total > 0) {
1013 remark_lines.emplace_back(
1014 format.
format(
" '{}' link failure(s) for this group:",
FormatParam{group_total, Style::bold}));
1015 if (group_prefilled_dest > 0) {
1016 remark_lines.emplace_back(format.
format(
1017 " Prefilled destination conflict: '{}'.",
1019 for (
const auto &detail : fc.prefilled_destination_conflict_details) {
1020 if (detail.source_group_index != group_id) {
1023 remark_lines.emplace_back(format.
format(
1024 " '{}' slot '{}': color '{}' blocked by locked color '{}'.",
1025 FormatParam{palette_filename(detail.palette_index), Style::bold},
1027 color_param(detail.blocked_color),
1028 color_param(detail.locked_color)));
1031 if (group_fww > 0) {
1032 remark_lines.emplace_back(
1033 format.
format(
" Shared color conflict: '{}'.",
FormatParam{group_fww, Style::bold}));
1034 for (
const auto &detail : fc.first_writer_wins_details) {
1035 if (detail.source_group_index != group_id) {
1038 remark_lines.emplace_back(format.
format(
1039 " '{}': color '{}' linked to '{}' by group '{}',",
1040 FormatParam{palette_filename(detail.source_palette_index), Style::bold},
1041 color_param(detail.source_color),
1042 FormatParam{palette_filename(detail.winning_ref_palette_index), Style::bold},
1043 FormatParam{detail.winning_group_index, Style::bold}));
1044 remark_lines.emplace_back(format.
format(
1045 " this group wanted ref color '{}' in '{}'.",
1046 color_param(detail.losing_ref_color),
1047 FormatParam{palette_filename(detail.losing_ref_palette_index), Style::bold}));
1050 if (group_prefilled_src > 0) {
1051 remark_lines.emplace_back(format.
format(
1052 " Prefilled source conflict: '{}'.",
FormatParam{group_prefilled_src, Style::bold}));
1053 for (
const auto &detail : fc.prefilled_source_conflict_details) {
1054 if (detail.source_group_index != group_id) {
1057 remark_lines.emplace_back(format.
format(
1058 " '{}': prefilled color '{}' could not be linked to color '{}' in "
1060 FormatParam{palette_filename(detail.source_palette_index), Style::bold},
1061 color_param(detail.source_color),
1062 color_param(detail.ref_color),
1063 FormatParam{palette_filename(detail.ref_palette_index), Style::bold}));
1066 if (group_mismatch > 0) {
1067 remark_lines.emplace_back(format.
format(
1068 " Post-resolution slot mismatch: '{}'.",
FormatParam{group_mismatch, Style::bold}));
1069 for (
const auto &detail : fc.post_resolution_mismatch_details) {
1070 if (detail.source_group_index != group_id) {
1073 remark_lines.emplace_back(format.
format(
1074 " '{}': color '{}' ended at slot '{}', but ref color '{}' in '{}' is at "
1076 FormatParam{palette_filename(detail.source_palette_index), Style::bold},
1077 color_param(detail.source_color),
1078 FormatParam{detail.source_final_slot, Style::bold},
1079 color_param(detail.ref_color),
1080 FormatParam{palette_filename(detail.ref_palette_index), Style::bold},
1081 FormatParam{detail.ref_final_slot, Style::bold}));
1089 remark_lines.emplace_back();
1091 remark_lines.emplace_back(
"Alignment is 'off'. No slot alignment was attempted.");
1092 remark_lines.emplace_back(
"Switch to 'greedy' to enable palette slot alignment for eligible groups.");
1095 remark_lines.emplace_back(format.
format(
1096 "'{}' total link resolution failure(s) across all unaligned groups.",
1101 remark_lines.emplace_back();
1102 if (phase3.partially_aligned_count > 0) {
1103 remark_lines.emplace_back(format.
format(
1104 "Tile sharing is best-effort. '{}' of '{}' eligible group(s) aligned ('{}' fully, '{}' "
1108 FormatParam{phase3.fully_aligned_count, Style::bold},
1109 FormatParam{phase3.partially_aligned_count, Style::bold}));
1112 remark_lines.emplace_back(format.
format(
1113 "Tile sharing is best-effort. '{}' of '{}' eligible group(s) aligned.",
1118 if (!fc.prefilled_destination_conflict_details.empty()) {
1119 remark_lines.emplace_back(
1120 "Adjusting the prefilled destination slots listed above may improve tile sharing.");
1122 if (!fc.prefilled_source_conflict_details.empty()) {
1123 remark_lines.emplace_back(
1124 "Some link source colors are prefilled (locked) in their palettes and could not be "
1126 remark_lines.emplace_back(
"Rearranging or wildcarding those prefilled slots may improve tile sharing.");
1128 if (!fc.first_writer_wins_details.empty()) {
1129 remark_lines.emplace_back(format.
format(
1130 "'{}' shared color conflict(s): multiple shape groups competed for the same color's link.",
1131 FormatParam{fc.first_writer_wins_details.size(), Style::bold}));
1132 remark_lines.emplace_back(
"This is expected with greedy alignment.");
1134 if (!fc.post_resolution_mismatch_details.empty()) {
1135 remark_lines.emplace_back(format.
format(
1136 "'{}' post-resolution slot mismatch(es): colors were displaced by eviction after successful "
1138 FormatParam{fc.post_resolution_mismatch_details.size(), Style::bold}));
1139 remark_lines.emplace_back(
"This occurs when multiple groups compete for the same palette slots.");
1142 remark_lines.emplace_back();
1143 remark_lines.emplace_back(
"For details on tile sharing, see:");
1144 remark_lines.emplace_back(
" https://grunt-lucas.github.io/porytiles-user-docs/tile-sharing.html");
1147 diag.
remark(tag, remark_lines);
1156void emit_palette_diagnostics(
1163 const auto &maybe_packed_palette = palettes.at(i);
1164 if (maybe_packed_palette.has_value()) {
1165 constexpr auto palette_tag =
"palette-packing-result";
1166 std::vector<std::string> remark_lines;
1167 remark_lines.emplace_back(
1168 format.
format(
"'{}' packing result:",
FormatParam{palette_filename(i), Style::bold}));
1169 remark_lines.emplace_back();
1170 remark_lines.append_range(palette_printer.
print_rgba_palette(maybe_packed_palette.value()));
1171 diag.
remark(palette_tag, remark_lines);
1183 auto [regular_tiles, hint_tiles, prefilled_palettes] = build_packing_inputs(params);
1187 std::move(regular_tiles),
1188 std::move(hint_tiles),
1189 std::move(prefilled_palettes),
1196 std::vector<std::string> err_lines;
1197 err_lines.emplace_back(format_->
format(
1198 "Tile sharing packing '{}' is not yet implemented.",
1199 FormatParam{to_string(TileSharingPacking::optimal), Style::bold}));
1200 err_lines.emplace_back(
"");
1205 std::vector<std::string> err_lines;
1206 err_lines.emplace_back(format_->
format(
1207 "Tile sharing alignment '{}' is not yet implemented.",
1208 FormatParam{to_string(TileSharingAlignment::optimal), Style::bold}));
1209 err_lines.emplace_back(
"");
1217 std::vector<ShapeGroup<Rgba32>> shape_groups_for_biased;
1218 CombinedTiles combined_for_biased;
1220 combined_for_biased = build_combined_tiles(params, anim_keyframe_tiles);
1223 if (!shape_groups_for_biased.empty()) {
1225 packing_input.shape_group_metadata_ = std::move(metadata);
1229 auto pack_result = strategy_->
pack(packing_input);
1232 packing_output, std::move(pack_result),
PalettePacking,
"Low-level palette packing failed.");
1236 populate_tile_to_palette(packing, packing_output);
1243 std::vector<IndirectLink> indirect_links;
1247 CombinedTiles combined;
1248 std::vector<ShapeGroup<Rgba32>> shape_groups;
1251 combined = std::move(combined_for_biased);
1252 shape_groups = std::move(shape_groups_for_biased);
1255 combined = build_combined_tiles(params, anim_keyframe_tiles);
1260 emit_phase1_diagnostics(*format_, *diag_, *tile_printer_, shape_groups, combined, params);
1263 auto phase2 = compute_partition_groups(shape_groups, combined, packing.
tile_to_palette_);
1264 emit_phase2_diagnostics(*format_, *diag_, *tile_printer_, shape_groups, phase2, params);
1267 std::map<std::size_t, std::size_t> tile_palette_assignments;
1268 if (!shape_groups.empty()) {
1269 tile_palette_assignments = build_tile_palette_assignments(shape_groups, combined, packing.
tile_to_palette_);
1275 packing_output.palettes_,
1288 packing_output.palettes_,
1293 !indirect_links.empty() ? &failure_counts :
nullptr);
1296 Phase3Result phase3;
1297 if (!shape_groups.empty()) {
1298 phase3 = verify_sharing_alignment(
1301 emit_phase3_diagnostics(*format_, *diag_, *tile_printer_, phase3, params);
1306 emit_sharing_summary(*format_, *diag_, *tile_printer_, params, shape_groups, phase2, phase3, failure_counts);
1311 if (final_palettes.at(i).has_value()) {
1312 packing.
palettes_.at(i) = final_palettes.at(i);
1316 emit_palette_diagnostics(*format_, *diag_, *palette_printer_, packing.
palettes_);
#define PT_TRY_ASSIGN_CHAIN_ERR(var, expr, return_type,...)
Unwraps a ChainableResult, chaining a new error message on failure.
Represents a single frame of an animation, containing tiles and a frame name.
A complete tileset animation with name, configuration, and frame data.
A PixelTile representation that stores the canonical (lexicographically minimal) orientation among al...
A result type that maintains a chainable sequence of errors for debugging and error reporting.
A bidirectional mapping between pixel color values and sequential integer indices.
std::optional< ColorIndex > index_at_color(const PixelType &color) const
Retrieves the index associated with a given color.
A set of colors represented as a bitset.
virtual ChainableResult< PackingOutput > pack(const PackingInput &input) const =0
Packs tile colors into palettes according to the strategy's algorithm.
Represents a palette hint for the palette packing algorithm.
ChainableResult< PalettePacking > pack_tiles(const PackingParams ¶ms) const
Packs pixel tiles into hardware palettes using a high-level API.
Manages allocation of hardware palette indexes with stack-based checkout semantics.
A collection of printer functions for the Palette and related types.
virtual std::vector< std::string > print_rgba_palette(const Palette< Rgba32, palette::max_size > &palette) const =0
A generic palette container for colors that support transparency checking.
std::size_t size() const
Returns the number of slots in the palette.
ColorType at(std::size_t index) const
Gets the color at a specific index.
bool is_wildcard(std::size_t index) const
Checks if a slot is a wildcard.
An 8x8 tile backed by literal-array-based per-pixel storage of an arbitrary pixel type.
std::set< PixelType > unique_nontransparent_colors() const
Returns the set of unique non-transparent colors present in this PixelTile (intrinsic transparency on...
bool is_transparent() const
Checks if this entire PixelTile is transparent (intrinsic transparency only).
PixelType at(std::size_t i) const
static PrefilledPalette partially_locked(std::size_t hardware_index, ColorSet fixed_colors, std::size_t occupied_slots, std::size_t total_capacity=palette::max_size - 1)
Creates a partially locked palette with available capacity.
Represents a 32-bit RGBA color.
std::uint8_t blue() const
std::uint8_t green() const
static const Style bold
Bold text formatting.
Abstract base class for applying text styling with context-aware formatting.
virtual std::string format(const std::string &format_str, const std::vector< FormatParam > ¶ms) const
Formats a string with styled parameters using fmtlib syntax.
A collection of printer functions for various tile types.
Abstract class for structured error reporting and diagnostic output.
virtual void remark(const std::string &tag, const std::vector< std::string > &lines) const =0
Display a tagged remark message.
constexpr std::size_t num_palettes
void panic(const StringViewSourceLoc &s)
Unconditionally terminates the program with a panic message.
std::vector< ShapeGroup< PixelType > > analyze_shape_groups(const std::vector< PixelTile< PixelType > > &tiles, const PixelType &extrinsic)
Analyzes a collection of pixel tiles and groups them by canonical shape for tile sharing analysis.
std::vector< std::string > build_representative_tile_per_palette_lines(const TextFormatter &format, const TilePrinter &tile_printer, const std::vector< PixelTile< Rgba32 > > &pixel_tiles, const Rgba32 &extrinsic_transparency, const std::map< std::size_t, std::vector< std::size_t > > &members_by_palette)
Builds note lines showing one representative tile (ASCII art) per palette.
std::vector< std::string > build_primary_representative_tile_per_palette_lines(const TextFormatter &format, const TilePrinter &tile_printer, const std::vector< PackingParams::PrimaryTileRef > &primary_tiles, const Rgba32 &extrinsic_transparency, const std::map< std::size_t, std::vector< std::size_t > > &primary_members_by_palette)
Builds note lines showing one representative primary tile (ASCII art) per palette.
@ greedy
Best-effort alignment via indirect link resolution.
@ optimal
CSP-based globally optimal alignment (not yet implemented).
constexpr Style rgb_fg_style(std::uint8_t r, std::uint8_t g, std::uint8_t b)
Creates a Style value with a custom RGB foreground color.
@ canonical
Export tiles in canonical form without applying flip transformations.
PixelTile< IndexPixel > index_tile_from_color_tile(const PixelTile< ColorType > &tile, const Palette< ColorType, N > &palette)
Converts a PixelTile<ColorType> to indexed form using a palette (intrinsic transparency only).
std::array< std::optional< Palette< Rgba32, palette::max_size > >, palette::num_palettes > build_all_output_palettes(const std::vector< PackedPalette > &packed_palettes, const std::array< std::optional< Palette< Rgba32, palette::max_size > >, palette::num_palettes > &prefilled_palettes, const ColorIndexMap< Rgba32 > &color_map, const Rgba32 &default_slot_zero, const std::vector< IndirectLink > &indirect_links, AlignmentFailureCounts *failure_counts=nullptr)
Builds all output palettes from packed palettes and Indirect links in a single call.
std::vector< std::string > build_truncated_tile_ref_lines(const TextFormatter &format, const std::vector< std::size_t > &tile_indices)
Builds truncated tile reference lines, listing up to 8 entries comma-joined with ellipsis.
std::vector< IndirectLink > build_indirect_links(const std::vector< ShapeGroup< Rgba32 > > &shape_groups, const std::map< std::size_t, std::size_t > &tile_palette_assignments, const std::array< std::optional< Palette< Rgba32, palette::max_size > >, palette::num_palettes > &base_palettes, const std::array< std::optional< Palette< Rgba32, palette::max_size > >, palette::num_palettes > &prefilled_palettes)
Builds Indirect links from shape groups and pre-computed palette assignments.
std::vector< std::string > format_config_note(const TextFormatter &format, const ConfigValue< T > &config)
Format a ConfigValue into diagnostic note lines.
std::vector< std::string > build_per_palette_tile_ref_lines(const TextFormatter &format, const std::map< std::size_t, std::vector< std::size_t > > &members_by_palette)
Builds per-palette tile reference lines with a header and truncated listing for each palette.
@ optimal
Hard constraint rejecting sibling co-placement (not yet implemented).
@ biased
Soft penalty steers shape group siblings toward different palettes.
ShapeGroupMetadata build_shape_group_metadata(const std::vector< ShapeGroup< Rgba32 > > &shape_groups, const std::vector< PackableTile::Id > &combined_index_to_id)
Builds ShapeGroupMetadata from shape groups and a combined-index-to-ID mapping.
std::vector< std::string > build_primary_tile_color_version_lines(const TextFormatter &format, const TilePrinter &tile_printer, const std::vector< PackingParams::PrimaryTileRef > &primary_tiles, const Rgba32 &extrinsic_transparency, const std::vector< std::pair< std::size_t, std::size_t > > &primary_color_version_entries, std::size_t version_offset)
Builds note lines showing ASCII art for each primary tile color version in a sharing group.
std::string palette_filename(std::size_t palette_index)
Constructs a palette filename from a palette index.
std::vector< std::string > build_tile_sharing_color_version_tile_lines(const TextFormatter &format, const TilePrinter &tile_printer, const std::vector< PixelTile< Rgba32 > > &pixel_tiles, const Rgba32 &extrinsic_transparency, const std::vector< std::size_t > &color_version_tile_indices)
Builds note lines showing ASCII art for each color version tile in a sharing group.
std::string to_string(const PrimaryPairingMode m)
Converts a PrimaryPairingMode to its canonical string representation.
Detailed records of alignment failures during Indirect link application and chain resolution.
Identifies a tile created from an animation.
Identifies a tile created from a palette hint.
Identifies a tile reconstructed from a compiled primary tileset.
std::size_t palette_index
Identifies a regular input tile.
The final palette assignments after a successful packing operation.
The input parameters for a packing operation.
std::vector< PaletteHint > hints_
Priority tiles that guide packing (e.g., ensure certain colors group together)
std::array< std::optional< Palette< Rgba32, palette::max_size > >, palette::num_palettes > prefilled_palettes_
Existing palettes with locked and wildcarded colors (from PorytilesTilesetComponent)
std::vector< PrimaryTileRef > primary_tiles_
Reconstructed RGBA tiles from a compiled primary tileset for cross-tileset shape group analysis.
ConfigValue< TileSharingPacking > tile_sharing_packing_
Controls whether the packer considers shape group membership during packing.
std::bitset< palette::num_palettes > available_palettes_
Bitset specifying which hardware palettes are available for packing.
std::vector< PixelTile< Rgba32 > > tiles_
Raw pixel tiles to pack into palettes.
Rgba32 extrinsic_transparency_
The extrinsic transparency color (e.g., rgba_magenta)
ConfigValue< TileSharingAlignment > tile_sharing_alignment_
Controls palette slot alignment strategy for tile sharing deduplication.
ColorIndexMap< Rgba32 > color_map_
Bidirectional mapping between Rgba32 colors and ColorIndex.
std::map< std::string, Animation< Rgba32 > > anims_
RGBA animations to pack into palettes.
Result from the high-level tile packing operation.
std::array< std::optional< Palette< Rgba32, palette::max_size > >, palette::num_palettes > palettes_
The final hardware palettes with colors assigned.
std::map< std::size_t, std::size_t > tile_to_palette_
Maps regular tile ids to their assigned hardware palette indices.
A group of tiles that share the same canonical shape but have different color assignments.