Porytiles
Loading...
Searching...
No Matches
palette_packer.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <format>
5#include <map>
6#include <optional>
7#include <set>
8#include <string>
9#include <vector>
10
22
23namespace {
24
25using namespace porytiles;
26
30FormatParam color_param(const Rgba32 &color)
31{
32 auto text =
33 std::to_string(color.red()) + ", " + std::to_string(color.green()) + ", " + std::to_string(color.blue());
34 return FormatParam{std::move(text), rgb_fg_style(color.red(), color.green(), color.blue()) | Style::bold};
35}
36
40struct ColorSetWithOccupancy {
41 ColorSet color_set;
42 std::size_t occupied_slots{};
43};
44
58[[nodiscard]] ColorSet build_color_set_from_tile(
59 const PixelTile<Rgba32> &tile, const ColorIndexMap<Rgba32> &color_map, const Rgba32 &extrinsic)
60{
61 ColorSet color_set{};
62 const auto unique_colors = tile.unique_nontransparent_colors(extrinsic);
63
64 for (const auto &color : unique_colors) {
65 const auto index_opt = color_map.index_at_color(color);
66 if (!index_opt.has_value()) {
67 /*
68 * This will throw if a hint contains the extrinsic transparency color, since the ColorIndexMap won't
69 * contain any transparency colors. Callers of the packer service should have used the PaletteValidator
70 * service to validate input palettes and generate good user diagnostics.
71 */
72 panic("tile color " + to_string(color) + " not found in color index map");
73 }
74 color_set.set(index_opt.value());
75 }
76
77 return color_set;
78}
79
92[[nodiscard]] ColorSetWithOccupancy
93build_color_set_from_pal(const Palette<Rgba32, pal::max_size> &pal, const ColorIndexMap<Rgba32> &color_map)
94{
95 ColorSet color_set{};
96 std::size_t occupied_slots = 0;
97
98 // Start from slot 1 (slot 0 is transparency)
99 for (std::size_t i = 1; i < pal.size(); ++i) {
100 if (pal.is_wildcard(i)) {
101 continue;
102 }
103 ++occupied_slots;
104 const auto color = pal.at(i);
105 const auto index_opt = color_map.index_at_color(color);
106 if (!index_opt.has_value()) {
107 /*
108 * This will throw if a pal contains the extrinsic transparency color, since the ColorIndexMap won't
109 * contain any transparency colors. Callers of the packer service should have used the PaletteValidator
110 * service to validate input palettes and generate good user diagnostics.
111 */
112 panic("pal color " + to_string(color) + " at slot " + std::to_string(i) + " not in color map");
113 }
114 color_set.set(index_opt.value());
115 }
116
117 return ColorSetWithOccupancy{color_set, occupied_slots};
118}
119
132[[nodiscard]] ColorSet build_color_set_from_hint_pal(const Palette<Rgba32> &pal, const ColorIndexMap<Rgba32> &color_map)
133{
134 ColorSet color_set{};
135
136 for (std::size_t i = 0; i < pal.size(); ++i) {
137 if (pal.is_wildcard(i)) {
138 panic("build_color_set_from_hint_palette palette contained unexpected wildcard");
139 }
140 const auto color = pal.at(i);
141 const auto index_opt = color_map.index_at_color(color);
142 if (!index_opt.has_value()) {
143 panic("hint color " + to_string(color) + " at slot " + std::to_string(i) + " not in color map");
144 }
145 color_set.set(index_opt.value());
146 }
147
148 return color_set;
149}
150
165[[nodiscard]] std::set<PixelTile<Rgba32>>
166build_anim_keyframe_set(const std::map<std::string, Animation<Rgba32>> &anims, const Rgba32 &extrinsic)
167{
168 std::set<PixelTile<Rgba32>> result;
169 for (const auto &[name, anim] : anims) {
170 if (anim.frames().empty()) {
171 continue;
172 }
173 const AnimFrame<Rgba32> &representative_frame =
174 anim.has_key_frame() ? anim.key_frame() : anim.frames().begin()->second;
175 for (const auto &tile : representative_frame.tiles()) {
176 if (tile.is_transparent(extrinsic)) {
177 continue;
178 }
180 result.insert(static_cast<const PixelTile<Rgba32> &>(canonical));
181 }
182 }
183 return result;
184}
185
196struct CombinedTiles {
197 std::vector<PixelTile<Rgba32>> tiles;
198 std::vector<PackableTile::Id> index_to_id;
199};
200
201[[nodiscard]] CombinedTiles
202build_combined_tiles(const PackingParams &params, const std::set<PixelTile<Rgba32>> &anim_keyframe_tiles)
203{
204 CombinedTiles combined;
205 combined.tiles.reserve(params.tiles_.size() + params.primary_tiles_.size());
206 combined.index_to_id.reserve(params.tiles_.size() + params.primary_tiles_.size());
207
208 for (std::size_t i = 0; i < params.tiles_.size(); ++i) {
210 if (anim_keyframe_tiles.contains(static_cast<const PixelTile<Rgba32> &>(canonical))) {
211 continue;
212 }
213 combined.tiles.push_back(params.tiles_.at(i));
214 combined.index_to_id.push_back(PackableTile::RegularId{i});
215 }
216
217 for (std::size_t i = 0; i < params.primary_tiles_.size(); ++i) {
218 const auto &primary_ref = params.primary_tiles_.at(i);
219 combined.tiles.push_back(primary_ref.tile);
220 combined.index_to_id.push_back(PackableTile::PrimaryTileId{i, primary_ref.pal_index});
221 }
222
223 return combined;
224}
225
229struct SharingGroupMember {
230 std::size_t tile_index;
231 std::size_t pal_index;
232 bool is_primary{false};
233};
234
241struct PartitionGroup {
242 std::vector<SharingGroupMember> members;
243
251 std::vector<std::size_t> color_version_tile_indices;
252
260 std::vector<std::pair<std::size_t, std::size_t>> primary_color_version_entries;
261};
262
270struct PartialAlignmentInfo {
271 std::size_t group_id;
272 std::size_t dropped_color_version_count;
273};
274
278struct PackingInputs {
279 std::vector<PackableTile> regular_tiles;
280 std::vector<PackableTile> hint_tiles;
281 std::set<PrefilledPalette> prefilled_palettes;
282};
283
291struct Phase2Result {
292 std::set<std::size_t> eligible_shape_indices;
293 std::vector<PartitionGroup> partition_groups;
294 std::map<std::size_t, std::size_t> shape_to_partition_index;
295};
296
300struct Phase3GroupResult {
301 std::size_t shape_group_index;
302 bool is_partial;
303 std::size_t dropped_color_version_count;
304 std::size_t matching_count;
305 std::map<std::size_t, std::vector<std::size_t>> members_by_pal;
306 bool has_primary_members{false};
307 std::map<std::size_t, std::vector<std::size_t>> primary_members_by_pal;
308};
309
317struct Phase3Result {
318 std::size_t aligned_count{};
319 std::size_t fully_aligned_count{};
320 std::size_t partially_aligned_count{};
321 std::vector<PartialAlignmentInfo> partial_alignment_infos;
322 std::vector<std::pair<std::size_t, PartitionGroup>> unaligned_partition_groups;
323 std::vector<Phase3GroupResult> aligned_groups;
324};
325
329struct VerifiedMember {
330 std::size_t regular_index;
331 std::size_t hw_pal;
332 CanonicalPixelTile<IndexPixel> canonical_indexed;
333 std::map<ShapeMask, Rgba32> colors;
334 bool is_primary{false};
335};
336
347[[nodiscard]] PackingInputs build_packing_inputs(const PackingParams &params)
348{
349 // Step 1: Convert regular tiles and anims to PackableTile vector
350 std::vector<PackableTile> regular_tiles;
351 regular_tiles.reserve(params.tiles_.size());
352 for (std::size_t i = 0; i < params.tiles_.size(); ++i) {
353 auto color_set =
354 build_color_set_from_tile(params.tiles_.at(i), params.color_map_, params.extrinsic_transparency_);
355 regular_tiles.emplace_back(PackableTile::RegularId{i}, color_set);
356 }
357 for (const auto &[anim_name, anim] : params.anims_) {
358 const auto &composite_frame = anim.composite_frame(params.extrinsic_transparency_);
359 for (std::size_t subtile_index = 0; subtile_index < composite_frame.tiles().size(); ++subtile_index) {
360 const auto &composite_tile = composite_frame.tiles().at(subtile_index);
361 auto color_set =
362 build_color_set_from_tile(composite_tile, params.color_map_, params.extrinsic_transparency_);
363 regular_tiles.emplace_back(PackableTile::AnimId{anim_name, subtile_index}, color_set);
364 }
365 }
366
367 // Step 2: Convert hints to PackableTile vector
368 std::vector<PackableTile> hint_tiles;
369 hint_tiles.reserve(params.hints_.size());
370 for (const PaletteHint &hint : params.hints_) {
371 auto color_set = build_color_set_from_hint_pal(hint.pal(), params.color_map_);
372 hint_tiles.emplace_back(PackableTile::HintId{hint.name()}, color_set);
373 }
374
375 // Step 3: Convert input prefilled palettes to PrefilledPalette set
376 std::set<PrefilledPalette> prefilled_palettes;
377 for (std::size_t i = 0; i < params.prefilled_pals_.size(); ++i) {
378 if (!params.prefilled_pals_[i].has_value()) {
379 continue;
380 }
381 auto [color_set, occupied_slots] =
382 build_color_set_from_pal(params.prefilled_pals_[i].value(), params.color_map_);
383 prefilled_palettes.insert(PrefilledPalette::partially_locked(i, color_set, occupied_slots));
384 }
385
386 return PackingInputs{std::move(regular_tiles), std::move(hint_tiles), std::move(prefilled_palettes)};
387}
388
399void populate_tile_to_pal(PalettePacking &packing, const PackingOutput &output)
400{
401 packing.tile_to_pal_.clear();
402 for (const auto &[tile_id, pal_index] : output.tile_to_pal_) {
403 std::visit(
404 [&packing, pal_index]<typename IdVariant>(IdVariant &&id) {
405 using Id = std::decay_t<IdVariant>;
406 if constexpr (std::is_same_v<Id, PackableTile::RegularId>) {
407 packing.tile_to_pal_[id.index] = pal_index;
408 }
409 else if constexpr (std::is_same_v<Id, PackableTile::AnimId>) {
410 // Anim tiles are packed for palette assignment but excluded from tile_to_pal_
411 }
412 else if constexpr (std::is_same_v<Id, PackableTile::HintId>) {
413 // We don't currently care to store where hints got assigned
414 }
415 else if constexpr (std::is_same_v<Id, PackableTile::PrefilledPaletteId>) {
416 // Nothing to do here, we only had these PackableTiles for internal bookkeeping
417 }
418 else if constexpr (std::is_same_v<Id, PackableTile::PrimaryTileId>) {
419 // Primary tiles are never in the packer's output; no-op for exhaustive visit
420 }
421 else {
422 static_assert(sizeof(Id) == 0, "Unhandled PackableTile::Id variant alternative");
423 }
424 },
425 tile_id);
426 }
427}
428
441[[nodiscard]] std::map<std::size_t, std::size_t> build_tile_pal_assignments(
442 const std::vector<ShapeGroup<Rgba32>> &shape_groups,
443 const CombinedTiles &combined,
444 const std::map<std::size_t, std::size_t> &tile_to_pal)
445{
446 std::map<std::size_t, std::size_t> tile_pal_assignments;
447 for (const auto &group : shape_groups) {
448 for (const auto &member : group.members) {
449 const auto &id = combined.index_to_id.at(member.tile_index);
450 if (std::holds_alternative<PackableTile::RegularId>(id)) {
451 auto regular_index = std::get<PackableTile::RegularId>(id).index;
452 if (tile_to_pal.contains(regular_index)) {
453 tile_pal_assignments[member.tile_index] = tile_to_pal.at(regular_index);
454 }
455 }
456 else if (std::holds_alternative<PackableTile::PrimaryTileId>(id)) {
457 auto primary_id = std::get<PackableTile::PrimaryTileId>(id);
458 tile_pal_assignments[member.tile_index] = primary_id.pal_index;
459 }
460 }
461 }
462 return tile_pal_assignments;
463}
464
477[[nodiscard]] Phase2Result compute_partition_groups(
478 const std::vector<ShapeGroup<Rgba32>> &shape_groups,
479 const CombinedTiles &combined,
480 const std::map<std::size_t, std::size_t> &tile_to_pal)
481{
482 Phase2Result result;
483 for (std::size_t sg_index = 0; sg_index < shape_groups.size(); ++sg_index) {
484 const auto &group = shape_groups.at(sg_index);
485 std::set<std::size_t> distinct_pals_set;
486 std::vector<SharingGroupMember> members;
487 std::set<std::map<ShapeMask, Rgba32>> seen_colors;
488 std::vector<std::size_t> color_version_tile_indices;
489 std::vector<std::pair<std::size_t, std::size_t>> primary_color_version_entries;
490
491 for (const auto &member : group.members) {
492 const auto &id = combined.index_to_id.at(member.tile_index);
493 std::size_t display_index{};
494 std::optional<std::size_t> pal_opt;
495
496 bool is_primary = false;
497 if (std::holds_alternative<PackableTile::RegularId>(id)) {
498 auto regular_index = std::get<PackableTile::RegularId>(id).index;
499 display_index = regular_index;
500 if (tile_to_pal.contains(regular_index)) {
501 pal_opt = tile_to_pal.at(regular_index);
502 }
503 }
504 else if (std::holds_alternative<PackableTile::PrimaryTileId>(id)) {
505 auto primary_id = std::get<PackableTile::PrimaryTileId>(id);
506 display_index = primary_id.tile_index;
507 pal_opt = primary_id.pal_index;
508 is_primary = true;
509 }
510
511 if (pal_opt.has_value()) {
512 members.push_back(SharingGroupMember{display_index, pal_opt.value(), is_primary});
513 distinct_pals_set.insert(pal_opt.value());
514 if (!seen_colors.contains(member.colors)) {
515 seen_colors.insert(member.colors);
516 if (!is_primary) {
517 color_version_tile_indices.push_back(display_index);
518 }
519 else {
520 primary_color_version_entries.emplace_back(display_index, pal_opt.value());
521 }
522 }
523 }
524 }
525
526 if (members.size() >= 2 && distinct_pals_set.size() >= 2) {
527 result.eligible_shape_indices.insert(sg_index);
528 PartitionGroup partition;
529 partition.members = std::move(members);
530 partition.color_version_tile_indices = std::move(color_version_tile_indices);
531 partition.primary_color_version_entries = std::move(primary_color_version_entries);
532 result.shape_to_partition_index[sg_index] = result.partition_groups.size();
533 result.partition_groups.push_back(std::move(partition));
534 }
535 }
536 return result;
537}
538
555[[nodiscard]] Phase3Result verify_sharing_alignment(
556 const std::vector<ShapeGroup<Rgba32>> &shape_groups,
557 const CombinedTiles &combined,
558 const std::map<std::size_t, std::size_t> &tile_pal_assignments,
559 const Phase2Result &phase2,
560 const std::array<std::optional<Palette<Rgba32, pal::max_size>>, pal::num_pals> &final_pals,
561 const Rgba32 &extrinsic)
562{
563 Phase3Result result;
564 std::set<std::size_t> aligned_shape_indices;
565
566 for (std::size_t sg_index = 0; sg_index < shape_groups.size(); ++sg_index) {
567 if (!phase2.eligible_shape_indices.contains(sg_index)) {
568 continue;
569 }
570 const auto &group = shape_groups.at(sg_index);
571 std::vector<VerifiedMember> verified;
572
573 for (const auto &member : group.members) {
574 if (!tile_pal_assignments.contains(member.tile_index)) {
575 continue;
576 }
577 const auto &id = combined.index_to_id.at(member.tile_index);
578 std::size_t display_index{};
579 if (std::holds_alternative<PackableTile::RegularId>(id)) {
580 display_index = std::get<PackableTile::RegularId>(id).index;
581 }
582 else if (std::holds_alternative<PackableTile::PrimaryTileId>(id)) {
583 display_index = std::get<PackableTile::PrimaryTileId>(id).tile_index;
584 }
585 else {
586 continue;
587 }
588 std::size_t hw = tile_pal_assignments.at(member.tile_index);
589
590 bool is_primary_member = std::holds_alternative<PackableTile::PrimaryTileId>(id);
591
592 const auto &tile = combined.tiles.at(member.tile_index);
593 auto indexed_tile = index_tile_from_color_tile(tile, final_pals.at(hw).value(), extrinsic);
595 verified.push_back(
596 VerifiedMember{display_index, hw, std::move(canonical), member.colors, is_primary_member});
597 }
598
599 if (verified.size() < 2) {
600 continue;
601 }
602
603 const auto &reference = verified.at(0).canonical_indexed;
604 std::vector<SharingGroupMember> result_members;
605 std::set<std::size_t> distinct_pals_set;
606
607 std::set<std::map<ShapeMask, Rgba32>> dropped_color_versions;
608 for (const auto &member : verified) {
609 const auto &member_as_tile = static_cast<const PixelTile<IndexPixel> &>(member.canonical_indexed);
610 const auto &ref_as_tile = static_cast<const PixelTile<IndexPixel> &>(reference);
611 if (member_as_tile != ref_as_tile) {
612 dropped_color_versions.insert(member.colors);
613 continue;
614 }
615 result_members.push_back(SharingGroupMember{member.regular_index, member.hw_pal, member.is_primary});
616 distinct_pals_set.insert(member.hw_pal);
617 }
618 const std::size_t dropped_color_version_count = dropped_color_versions.size();
619
620 if (result_members.size() >= 2 && distinct_pals_set.size() >= 2) {
621 aligned_shape_indices.insert(sg_index);
622
623 const bool is_partial = dropped_color_version_count > 0;
624 if (is_partial) {
625 ++result.partially_aligned_count;
626 result.partial_alignment_infos.push_back(PartialAlignmentInfo{sg_index, dropped_color_version_count});
627 }
628 else {
629 ++result.fully_aligned_count;
630 }
631
632 std::map<std::size_t, std::vector<std::size_t>> members_by_pal;
633 std::map<std::size_t, std::vector<std::size_t>> primary_members_by_pal;
634 bool has_primary = false;
635 for (const auto &member : result_members) {
636 if (member.is_primary) {
637 has_primary = true;
638 primary_members_by_pal[member.pal_index].push_back(member.tile_index);
639 continue;
640 }
641 members_by_pal[member.pal_index].push_back(member.tile_index);
642 }
643
644 result.aligned_groups.push_back(
645 Phase3GroupResult{
646 sg_index,
647 is_partial,
648 dropped_color_version_count,
649 result_members.size(),
650 std::move(members_by_pal),
651 has_primary,
652 std::move(primary_members_by_pal)});
653 }
654 }
655
656 result.aligned_count = aligned_shape_indices.size();
657
658 // Compute unaligned partition groups: eligible groups that did not produce a sharing result
659 for (const auto sg_index : phase2.eligible_shape_indices) {
660 if (!aligned_shape_indices.contains(sg_index)) {
661 result.unaligned_partition_groups.emplace_back(
662 sg_index, phase2.partition_groups.at(phase2.shape_to_partition_index.at(sg_index)));
663 }
664 }
665
666 return result;
667}
668
683void emit_phase1_diagnostics(
684 const TextFormatter &format,
685 const UserDiagnostics &diag,
686 const TilePrinter &tile_printer,
687 const std::vector<ShapeGroup<Rgba32>> &shape_groups,
688 const CombinedTiles &combined,
689 const PackingParams &params)
690{
691 for (std::size_t shape_group_index = 0; shape_group_index < shape_groups.size(); ++shape_group_index) {
692 const auto &shape_group = shape_groups.at(shape_group_index);
693 std::vector<std::size_t> color_version_tile_indices;
694 std::vector<std::size_t> all_tile_indices;
695 std::vector<std::pair<std::size_t, std::size_t>> primary_color_version_entries;
696 std::set<std::map<ShapeMask, Rgba32>> seen_colors;
697 for (const auto &member : shape_group.members) {
698 const auto &id = combined.index_to_id.at(member.tile_index);
699 if (std::holds_alternative<PackableTile::RegularId>(id)) {
700 auto regular_index = std::get<PackableTile::RegularId>(id).index;
701 all_tile_indices.push_back(regular_index);
702 if (!seen_colors.contains(member.colors)) {
703 seen_colors.insert(member.colors);
704 color_version_tile_indices.push_back(regular_index);
705 }
706 }
707 else if (std::holds_alternative<PackableTile::PrimaryTileId>(id)) {
708 auto primary_id = std::get<PackableTile::PrimaryTileId>(id);
709 if (!seen_colors.contains(member.colors)) {
710 seen_colors.insert(member.colors);
711 primary_color_version_entries.emplace_back(primary_id.tile_index, primary_id.pal_index);
712 }
713 }
714 }
715
716 const auto total_color_versions = color_version_tile_indices.size() + primary_color_version_entries.size();
717 auto phase1_tag = std::format("tile-sharing-shareable-tiles-{}", shape_group_index);
718
719 std::vector<std::string> remark_lines;
720 remark_lines.emplace_back(format.format(
721 "Detected sharing opportunity (group id '{}') with '{}' color version(s) across '{}' tilemap entries.",
722 FormatParam{shape_group_index, Style::bold},
723 FormatParam{total_color_versions, Style::bold},
724 FormatParam{all_tile_indices.size(), Style::bold}));
725 if (!primary_color_version_entries.empty()) {
726 remark_lines.emplace_back(format.format(
727 "'{}' color version(s) from paired primary.",
728 FormatParam{primary_color_version_entries.size(), Style::bold}));
729 }
730 if (!color_version_tile_indices.empty()) {
731 remark_lines.append_range(build_tile_sharing_color_version_tile_lines(
732 format, tile_printer, params.tiles_, params.extrinsic_transparency_, color_version_tile_indices));
733 }
734 if (!primary_color_version_entries.empty()) {
735 remark_lines.append_range(build_primary_tile_color_version_lines(
736 format,
737 tile_printer,
738 params.primary_tiles_,
740 primary_color_version_entries,
741 color_version_tile_indices.size()));
742 }
743 remark_lines.emplace_back(
744 format.format("All tilemap entries ('{}' total):", FormatParam{all_tile_indices.size(), Style::bold}));
745 remark_lines.append_range(build_truncated_tile_ref_lines(format, all_tile_indices));
746 diag.remark(phase1_tag, remark_lines);
747 }
748
749 diag.remark(
750 "tile-sharing-shareable-tiles-summary",
751 "Tile sharing detection: '{}' shareable shape group(s) found.",
752 FormatParam{shape_groups.size(), Style::bold});
753}
754
769void emit_phase2_diagnostics(
770 const TextFormatter &format,
771 const UserDiagnostics &diag,
772 const TilePrinter &tile_printer,
773 const std::vector<ShapeGroup<Rgba32>> &shape_groups,
774 const Phase2Result &phase2,
775 const PackingParams &params)
776{
777 for (std::size_t sg_index = 0; sg_index < shape_groups.size(); ++sg_index) {
778 if (!phase2.eligible_shape_indices.contains(sg_index)) {
779 continue;
780 }
781 const auto &partition = phase2.partition_groups.at(phase2.shape_to_partition_index.at(sg_index));
782
783 auto phase2_tag = std::format("tile-sharing-palette-partition-{}", sg_index);
784
785 // Separate primary and secondary members for diagnostic display
786 bool has_primary_members = false;
787 std::map<std::size_t, std::vector<std::size_t>> members_by_pal;
788 for (const auto &member : partition.members) {
789 if (member.is_primary) {
790 has_primary_members = true;
791 continue;
792 }
793 members_by_pal[member.pal_index].push_back(member.tile_index);
794 }
795
796 std::vector<std::string> remark_lines;
797 remark_lines.emplace_back(format.format(
798 "After palette packing (group id '{}'), '{}' color versions across '{}' tilemap entries eligible for "
799 "sharing.",
800 FormatParam{sg_index, Style::bold},
801 FormatParam{partition.color_version_tile_indices.size(), Style::bold},
802 FormatParam{partition.members.size(), Style::bold}));
803 if (has_primary_members) {
804 remark_lines.emplace_back("Includes cross-tileset member(s) from paired primary.");
805 }
806
807 if (!partition.color_version_tile_indices.empty()) {
808 remark_lines.append_range(build_tile_sharing_color_version_tile_lines(
809 format,
810 tile_printer,
811 params.tiles_,
813 partition.color_version_tile_indices));
814 }
815 if (!partition.primary_color_version_entries.empty()) {
816 remark_lines.append_range(build_primary_tile_color_version_lines(
817 format,
818 tile_printer,
819 params.primary_tiles_,
821 partition.primary_color_version_entries,
822 partition.color_version_tile_indices.size()));
823 }
824 remark_lines.append_range(build_per_palette_tile_ref_lines(format, members_by_pal));
825
826 if (params.tile_sharing_packing_ == TileSharingPacking::off) {
827 remark_lines.emplace_back("");
828 remark_lines.emplace_back("--------");
829 remark_lines.emplace_back("");
830 remark_lines.emplace_back("Caveat: the palette partition shown above may be coincidental.");
831 remark_lines.emplace_back(format.format(
832 "Packing '{}' does not actively separate shape-group siblings across palettes.",
833 FormatParam{to_string(params.tile_sharing_packing_.value()), Style::bold}));
834 remark_lines.emplace_back("");
835 remark_lines.append_range(format_config_note(format, params.tile_sharing_packing_));
836 }
837 diag.remark(phase2_tag, remark_lines);
838 }
839
840 diag.remark(
841 "tile-sharing-palette-partition-summary",
842 "Tile sharing partition: '{}' of '{}' shape group(s) eligible for sharing after palette packing.",
843 FormatParam{phase2.eligible_shape_indices.size(), Style::bold},
844 FormatParam{shape_groups.size(), Style::bold});
845}
846
860void emit_phase3_diagnostics(
861 const TextFormatter &format,
862 const UserDiagnostics &diag,
863 const TilePrinter &tile_printer,
864 const Phase3Result &phase3,
865 const PackingParams &params)
866{
867 for (const auto &group : phase3.aligned_groups) {
868 auto phase3_tag = std::format("tile-sharing-result-{}", group.shape_group_index);
869
870 std::vector<std::string> remark_lines;
871 if (group.is_partial) {
872 remark_lines.emplace_back(format.format(
873 "Tile sharing partially succeeded (group id '{}'): '{}' participating palette(s) (referenced "
874 "in '{}' tilemap entries).",
875 FormatParam{group.shape_group_index, Style::bold},
876 FormatParam{group.members_by_pal.size(), Style::bold},
877 FormatParam{group.matching_count, Style::bold}));
878 remark_lines.emplace_back(format.format(
879 "'{}' color version(s) dropped due to alignment divergence.",
880 FormatParam{group.dropped_color_version_count, Style::bold}));
881 }
882 else {
883 remark_lines.emplace_back(format.format(
884 "Tile sharing succeeded (group id '{}'): '{}' participating palette(s) (referenced in '{}' "
885 "tilemap entries).",
886 FormatParam{group.shape_group_index, Style::bold},
887 FormatParam{group.members_by_pal.size(), Style::bold},
888 FormatParam{group.matching_count, Style::bold}));
889 }
890 if (group.has_primary_members) {
891 remark_lines.emplace_back("Includes cross-tileset member(s) from paired primary.");
892 }
893 if (!group.members_by_pal.empty()) {
894 remark_lines.append_range(build_representative_tile_per_palette_lines(
895 format, tile_printer, params.tiles_, params.extrinsic_transparency_, group.members_by_pal));
896 remark_lines.append_range(build_per_palette_tile_ref_lines(format, group.members_by_pal));
897 }
898 if (!group.primary_members_by_pal.empty()) {
900 format,
901 tile_printer,
902 params.primary_tiles_,
904 group.primary_members_by_pal));
905 }
906
907 if (group.is_partial) {
908 remark_lines.emplace_back("");
909 remark_lines.emplace_back("--------");
910 remark_lines.emplace_back("");
911 remark_lines.emplace_back(format.format(
912 "'{}' color version(s) diverged from the reference and were dropped.",
913 FormatParam{group.dropped_color_version_count, Style::bold}));
914 }
915
916 if (params.tile_sharing_alignment_ == TileSharingAlignment::off) {
917 remark_lines.emplace_back("");
918 remark_lines.emplace_back("--------");
919 remark_lines.emplace_back("");
920 remark_lines.emplace_back("Caveat: the slot alignment shown above may be coincidental.");
921 remark_lines.emplace_back(format.format(
922 "Alignment '{}' does not actively align palette slots for tile sharing.",
923 FormatParam{to_string(params.tile_sharing_alignment_.value()), Style::bold}));
924 remark_lines.emplace_back("");
925 remark_lines.append_range(format_config_note(format, params.tile_sharing_alignment_));
926 }
927 diag.remark(phase3_tag, remark_lines);
928 }
929}
930
947void emit_sharing_summary(
948 const TextFormatter &format,
949 const UserDiagnostics &diag,
950 const TilePrinter &tile_printer,
951 const PackingParams &params,
952 const std::vector<ShapeGroup<Rgba32>> &shape_groups,
953 const Phase2Result &phase2,
954 const Phase3Result &phase3,
955 const AlignmentFailureCounts &failure_counts)
956{
957 const auto eligible = phase2.partition_groups.size();
958 const auto &fc = failure_counts;
959
960 constexpr auto tag = "tile-sharing-result-summary";
961
962 std::vector<std::string> remark_lines;
963 if (phase3.partially_aligned_count > 0) {
964 remark_lines.emplace_back("Tile sharing summary:");
965 remark_lines.emplace_back(format.format(
966 "'{}' detected → '{}' eligible after packing → '{}' aligned ('{}' fully, '{}' "
967 "partially).",
968 FormatParam{shape_groups.size(), Style::bold},
969 FormatParam{eligible, Style::bold},
970 FormatParam{phase3.aligned_count, Style::bold},
971 FormatParam{phase3.fully_aligned_count, Style::bold},
972 FormatParam{phase3.partially_aligned_count, Style::bold}));
973 }
974 else {
975 remark_lines.emplace_back("Tile sharing summary:");
976 remark_lines.emplace_back(format.format(
977 "'{}' detected → '{}' eligible after packing → '{}' aligned.",
978 FormatParam{shape_groups.size(), Style::bold},
979 FormatParam{eligible, Style::bold},
980 FormatParam{phase3.aligned_count, Style::bold}));
981 }
982
983 // Partially aligned groups listing
984 if (phase3.partially_aligned_count > 0) {
985 remark_lines.emplace_back("");
986 remark_lines.emplace_back("Partially aligned groups:");
987 for (const auto &info : phase3.partial_alignment_infos) {
988 remark_lines.emplace_back(format.format(
989 " Group '{}': '{}' color version(s) dropped.",
990 FormatParam{info.group_id, Style::bold},
991 FormatParam{info.dropped_color_version_count, Style::bold}));
992 }
993 remark_lines.emplace_back("");
994 }
995
996 const auto unaligned = eligible - phase3.aligned_count;
997 if (unaligned > 0) {
998 if (fc.total() == 0 && params.tile_sharing_alignment_ != TileSharingAlignment::off) {
999 panic("Hit 'Divergence with Zero Failures' case");
1000 }
1001
1002 remark_lines.emplace_back(
1003 format.format("'{}' eligible group(s) could not be aligned:", FormatParam{unaligned, Style::bold}));
1004
1005 for (const auto &[group_id, group] : phase3.unaligned_partition_groups) {
1006 remark_lines.emplace_back();
1007 bool has_primary_in_group = std::ranges::any_of(group.members, [](const auto &m) { return m.is_primary; });
1008 remark_lines.emplace_back(format.format(
1009 "Unaligned group (group id '{}'), '{}' color version(s):",
1010 FormatParam{group_id, Style::bold},
1011 FormatParam{group.color_version_tile_indices.size(), Style::bold}));
1012 if (has_primary_in_group) {
1013 remark_lines.emplace_back("Includes cross-tileset member(s) from paired primary.");
1014 }
1015 if (!group.color_version_tile_indices.empty()) {
1016 remark_lines.append_range(build_tile_sharing_color_version_tile_lines(
1017 format,
1018 tile_printer,
1019 params.tiles_,
1021 group.color_version_tile_indices));
1022 }
1023
1024 // Show palette assignments for each color version
1025 for (std::size_t v = 0; v < group.color_version_tile_indices.size(); ++v) {
1026 const auto cv_tile_index = group.color_version_tile_indices.at(v);
1027 std::set<std::size_t> pals_for_version;
1028 for (const auto &member : group.members) {
1029 if (member.tile_index == cv_tile_index) {
1030 pals_for_version.insert(member.pal_index);
1031 }
1032 }
1033 if (pals_for_version.empty()) {
1034 continue;
1035 }
1036 std::string pal_list;
1037 for (const auto pal : pals_for_version) {
1038 if (!pal_list.empty()) {
1039 pal_list += ", ";
1040 }
1041 pal_list += pal_filename(pal);
1042 }
1043 remark_lines.emplace_back(format.format(
1044 " Version '{}' assigned to palette(s): '{}'.",
1045 FormatParam{v + 1, Style::bold},
1046 FormatParam{pal_list, Style::bold}));
1047 }
1048
1049 // Show per-group failure details inline (only when alignment is active)
1050 if (params.tile_sharing_alignment_ != TileSharingAlignment::off) {
1051 const auto group_prefilled_dest =
1052 std::ranges::count_if(fc.prefilled_destination_conflict_details, [&](const auto &d) {
1053 return d.source_group_index == group_id;
1054 });
1055 const auto group_prefilled_src =
1056 std::ranges::count_if(fc.prefilled_source_conflict_details, [&](const auto &d) {
1057 return d.source_group_index == group_id;
1058 });
1059 const auto group_fww = std::ranges::count_if(
1060 fc.first_writer_wins_details, [&](const auto &d) { return d.source_group_index == group_id; });
1061 const auto group_mismatch =
1062 std::ranges::count_if(fc.post_resolution_mismatch_details, [&](const auto &d) {
1063 return d.source_group_index == group_id;
1064 });
1065 const auto group_total = group_prefilled_dest + group_prefilled_src + group_fww + group_mismatch;
1066
1067 if (group_total > 0) {
1068 remark_lines.emplace_back(
1069 format.format(" '{}' link failure(s) for this group:", FormatParam{group_total, Style::bold}));
1070 if (group_prefilled_dest > 0) {
1071 remark_lines.emplace_back(format.format(
1072 " Prefilled destination conflict: '{}'.",
1073 FormatParam{group_prefilled_dest, Style::bold}));
1074 for (const auto &detail : fc.prefilled_destination_conflict_details) {
1075 if (detail.source_group_index != group_id) {
1076 continue;
1077 }
1078 remark_lines.emplace_back(format.format(
1079 " '{}' slot '{}': color '{}' blocked by locked color '{}'.",
1080 FormatParam{pal_filename(detail.palette_index), Style::bold},
1081 FormatParam{detail.target_slot, Style::bold},
1082 color_param(detail.blocked_color),
1083 color_param(detail.locked_color)));
1084 }
1085 }
1086 if (group_fww > 0) {
1087 remark_lines.emplace_back(
1088 format.format(" Shared color conflict: '{}'.", FormatParam{group_fww, Style::bold}));
1089 for (const auto &detail : fc.first_writer_wins_details) {
1090 if (detail.source_group_index != group_id) {
1091 continue;
1092 }
1093 remark_lines.emplace_back(format.format(
1094 " '{}': color '{}' linked to '{}' by group '{}',",
1095 FormatParam{pal_filename(detail.source_pal_index), Style::bold},
1096 color_param(detail.source_color),
1097 FormatParam{pal_filename(detail.winning_ref_pal_index), Style::bold},
1098 FormatParam{detail.winning_group_index, Style::bold}));
1099 remark_lines.emplace_back(format.format(
1100 " this group wanted ref color '{}' in '{}'.",
1101 color_param(detail.losing_ref_color),
1102 FormatParam{pal_filename(detail.losing_ref_pal_index), Style::bold}));
1103 }
1104 }
1105 if (group_prefilled_src > 0) {
1106 remark_lines.emplace_back(format.format(
1107 " Prefilled source conflict: '{}'.", FormatParam{group_prefilled_src, Style::bold}));
1108 for (const auto &detail : fc.prefilled_source_conflict_details) {
1109 if (detail.source_group_index != group_id) {
1110 continue;
1111 }
1112 remark_lines.emplace_back(format.format(
1113 " '{}': prefilled color '{}' could not be linked to color '{}' in "
1114 "palette '{}'.",
1115 FormatParam{pal_filename(detail.source_pal_index), Style::bold},
1116 color_param(detail.source_color),
1117 color_param(detail.ref_color),
1118 FormatParam{pal_filename(detail.ref_pal_index), Style::bold}));
1119 }
1120 }
1121 if (group_mismatch > 0) {
1122 remark_lines.emplace_back(format.format(
1123 " Post-resolution slot mismatch: '{}'.", FormatParam{group_mismatch, Style::bold}));
1124 for (const auto &detail : fc.post_resolution_mismatch_details) {
1125 if (detail.source_group_index != group_id) {
1126 continue;
1127 }
1128 remark_lines.emplace_back(format.format(
1129 " '{}': color '{}' ended at slot '{}', but ref color '{}' in '{}' is at "
1130 "slot '{}'.",
1131 FormatParam{pal_filename(detail.source_pal_index), Style::bold},
1132 color_param(detail.source_color),
1133 FormatParam{detail.source_final_slot, Style::bold},
1134 color_param(detail.ref_color),
1135 FormatParam{pal_filename(detail.ref_pal_index), Style::bold},
1136 FormatParam{detail.ref_final_slot, Style::bold}));
1137 }
1138 }
1139 }
1140 }
1141 }
1142
1143 // Aggregate failure summary
1144 remark_lines.emplace_back();
1145 if (params.tile_sharing_alignment_ == TileSharingAlignment::off) {
1146 remark_lines.emplace_back("Alignment is 'off'. No slot alignment was attempted.");
1147 remark_lines.emplace_back("Switch to 'greedy' to enable palette slot alignment for eligible groups.");
1148 }
1149 else {
1150 remark_lines.emplace_back(format.format(
1151 "'{}' total link resolution failure(s) across all unaligned groups.",
1152 FormatParam{fc.total(), Style::bold}));
1153 }
1154
1155 // Actionable suggestions (skip for off mode, the suggestion is already above)
1156 remark_lines.emplace_back();
1157 if (phase3.partially_aligned_count > 0) {
1158 remark_lines.emplace_back(format.format(
1159 "Tile sharing is best-effort. '{}' of '{}' eligible group(s) aligned ('{}' fully, '{}' "
1160 "partially).",
1161 FormatParam{phase3.aligned_count, Style::bold},
1162 FormatParam{eligible, Style::bold},
1163 FormatParam{phase3.fully_aligned_count, Style::bold},
1164 FormatParam{phase3.partially_aligned_count, Style::bold}));
1165 }
1166 else {
1167 remark_lines.emplace_back(format.format(
1168 "Tile sharing is best-effort. '{}' of '{}' eligible group(s) aligned.",
1169 FormatParam{phase3.aligned_count, Style::bold},
1170 FormatParam{eligible, Style::bold}));
1171 }
1172 if (params.tile_sharing_alignment_ != TileSharingAlignment::off) {
1173 if (!fc.prefilled_destination_conflict_details.empty()) {
1174 remark_lines.emplace_back(
1175 "Adjusting the prefilled destination slots listed above may improve tile sharing.");
1176 }
1177 if (!fc.prefilled_source_conflict_details.empty()) {
1178 remark_lines.emplace_back(
1179 "Some link source colors are prefilled (locked) in their palettes and could not be "
1180 "reassigned.");
1181 remark_lines.emplace_back("Rearranging or wildcarding those prefilled slots may improve tile sharing.");
1182 }
1183 if (!fc.first_writer_wins_details.empty()) {
1184 remark_lines.emplace_back(format.format(
1185 "'{}' shared color conflict(s): multiple shape groups competed for the same color's link.",
1186 FormatParam{fc.first_writer_wins_details.size(), Style::bold}));
1187 remark_lines.emplace_back("This is expected with greedy alignment.");
1188 }
1189 if (!fc.post_resolution_mismatch_details.empty()) {
1190 remark_lines.emplace_back(format.format(
1191 "'{}' post-resolution slot mismatch(es): colors were displaced by eviction after successful "
1192 "resolution.",
1193 FormatParam{fc.post_resolution_mismatch_details.size(), Style::bold}));
1194 remark_lines.emplace_back("This occurs when multiple groups compete for the same palette slots.");
1195 }
1196 }
1197 remark_lines.emplace_back();
1198 remark_lines.emplace_back("For details on tile sharing, see:");
1199 remark_lines.emplace_back(" https://grunt-lucas.github.io/porytiles-user-docs/tile-sharing.html");
1200 }
1201
1202 diag.remark(tag, remark_lines);
1203}
1204
1213void emit_palette_diagnostics(
1214 const TextFormatter &format,
1215 const UserDiagnostics &diag,
1216 const PalettePrinter &pal_printer,
1217 const std::array<std::optional<Palette<Rgba32, pal::max_size>>, pal::num_pals> &pals)
1218{
1219 for (std::size_t i = 0; i < pal::num_pals; ++i) {
1220 const auto &maybe_packed_pal = pals.at(i);
1221 if (maybe_packed_pal.has_value()) {
1222 constexpr auto pal_tag = "palette-packing-result";
1223 std::vector<std::string> remark_lines;
1224 remark_lines.emplace_back(format.format("'{}' packing result:", FormatParam{pal_filename(i), Style::bold}));
1225 remark_lines.emplace_back();
1226 remark_lines.append_range(pal_printer.print_rgba_pal(maybe_packed_pal.value()));
1227 diag.remark(pal_tag, remark_lines);
1228 }
1229 }
1230}
1231
1232} // namespace
1233
1234namespace porytiles {
1235
1237{
1238 // Steps 1-3: Convert inputs to packer-internal forms
1239 auto [regular_tiles, hint_tiles, prefilled_palettes] = build_packing_inputs(params);
1240
1241 // Step 4: Create PackingInput and call low-level pack()
1242 PackingInput packing_input{
1243 std::move(regular_tiles),
1244 std::move(hint_tiles),
1245 std::move(prefilled_palettes),
1247
1248 // Build animation keyframe exclusion set (once, before any shape group analysis)
1249 const auto anim_keyframe_tiles = build_anim_keyframe_set(params.anims_, params.extrinsic_transparency_);
1250
1252 std::vector<std::string> err_lines;
1253 err_lines.emplace_back(format_->format(
1254 "Tile sharing packing '{}' is not yet implemented.",
1255 FormatParam{to_string(TileSharingPacking::optimal), Style::bold}));
1256 err_lines.emplace_back("");
1257 err_lines.append_range(format_config_note(*format_, params.tile_sharing_packing_));
1258 return ChainableResult<PalettePacking>{FormattableError{std::move(err_lines)}};
1259 }
1261 std::vector<std::string> err_lines;
1262 err_lines.emplace_back(format_->format(
1263 "Tile sharing alignment '{}' is not yet implemented.",
1264 FormatParam{to_string(TileSharingAlignment::optimal), Style::bold}));
1265 err_lines.emplace_back("");
1266 err_lines.append_range(format_config_note(*format_, params.tile_sharing_alignment_));
1267 return ChainableResult<PalettePacking>{FormattableError{std::move(err_lines)}};
1268 }
1269
1270 /*
1271 * For biased packing, build shape group metadata BEFORE the initial pack call so the strategy
1272 * receives sharing-aware input. For off packing, pack normally first (metadata is only needed
1273 * post-packing for diagnostics and alignment).
1274 */
1275 std::vector<ShapeGroup<Rgba32>> shape_groups_for_biased;
1276 CombinedTiles combined_for_biased;
1278 combined_for_biased = build_combined_tiles(params, anim_keyframe_tiles);
1279 shape_groups_for_biased = analyze_shape_groups(combined_for_biased.tiles, params.extrinsic_transparency_);
1280
1281 if (!shape_groups_for_biased.empty()) {
1282 auto metadata = build_shape_group_metadata(shape_groups_for_biased, combined_for_biased.index_to_id);
1283 packing_input.shape_group_metadata_ = std::move(metadata);
1284 }
1285 }
1286
1287 auto pack_result = strategy_->pack(packing_input);
1288
1290 packing_output, std::move(pack_result), PalettePacking, "Low-level palette packing failed.");
1291
1292 // Step 5a: Build tile_to_pal from PackingOutput
1293 PalettePacking packing{};
1294 populate_tile_to_pal(packing, packing_output);
1295
1296 /*
1297 * 5b: Always compute shape groups for three-phase diagnostics, then conditionally build Indirect links.
1298 *
1299 * Shape group analysis and Phase 1/2 data are always computed regardless of config, so diagnostics can be
1300 * emitted with appropriate caveats. Indirect link generation (alignment) is conditioned on
1301 * tile_sharing_alignment_.
1302 */
1303 std::vector<IndirectLink> indirect_links;
1304
1305 /*
1306 * Always compute shape groups for three-phase diagnostics. Reuse pre-pack results when packing is
1307 * biased (they were already computed for metadata injection).
1308 */
1309 CombinedTiles combined;
1310 std::vector<ShapeGroup<Rgba32>> shape_groups;
1311
1313 combined = std::move(combined_for_biased);
1314 shape_groups = std::move(shape_groups_for_biased);
1315 }
1316 else {
1317 combined = build_combined_tiles(params, anim_keyframe_tiles);
1318 shape_groups = analyze_shape_groups(combined.tiles, params.extrinsic_transparency_);
1319 }
1320
1321 // Phase 1: Detect sharing opportunities and emit diagnostics
1322 emit_phase1_diagnostics(*format_, *diag_, *tile_printer_, shape_groups, combined, params);
1323
1324 // Phase 2: Compute partition groups and emit diagnostics
1325 auto phase2 = compute_partition_groups(shape_groups, combined, packing.tile_to_pal_);
1326 emit_phase2_diagnostics(*format_, *diag_, *tile_printer_, shape_groups, phase2, params);
1327
1328 // Build tile-pal assignments from authoritative packing assignments
1329 std::map<std::size_t, std::size_t> tile_pal_assignments;
1330 if (!shape_groups.empty()) {
1331 tile_pal_assignments = build_tile_pal_assignments(shape_groups, combined, packing.tile_to_pal_);
1332 }
1333
1334 // Conditionally build Indirect links based on alignment
1335 if (params.tile_sharing_alignment_ == TileSharingAlignment::greedy && !shape_groups.empty()) {
1336 auto base_pals = build_all_output_palettes(
1337 packing_output.pals_,
1338 params.prefilled_pals_,
1339 params.color_map_,
1341 {} /* empty links */);
1342
1343 indirect_links = build_indirect_links(shape_groups, tile_pal_assignments, base_pals, params.prefilled_pals_);
1344 }
1345
1346 // Build final output palettes with Indirect links applied (or empty links for off mode)
1347 AlignmentFailureCounts failure_counts{};
1348 auto final_pals = build_all_output_palettes(
1349 packing_output.pals_,
1350 params.prefilled_pals_,
1351 params.color_map_,
1353 indirect_links,
1354 !indirect_links.empty() ? &failure_counts : nullptr);
1355
1356 // Phase 3: Verify sharing alignment against final palettes
1357 Phase3Result phase3;
1358 if (!shape_groups.empty()) {
1359 phase3 = verify_sharing_alignment(
1360 shape_groups, combined, tile_pal_assignments, phase2, final_pals, params.extrinsic_transparency_);
1361 }
1362 emit_phase3_diagnostics(*format_, *diag_, *tile_printer_, phase3, params);
1363
1364 // Tile sharing summary: emit when biased packing or greedy alignment is active
1367 emit_sharing_summary(*format_, *diag_, *tile_printer_, params, shape_groups, phase2, phase3, failure_counts);
1368 }
1369
1370 // Assemble final output palettes
1371 for (std::size_t i = 0; i < pal::num_pals; ++i) {
1372 if (final_pals.at(i).has_value()) {
1373 packing.pals_.at(i) = final_pals.at(i);
1374 }
1375 }
1376
1377 emit_palette_diagnostics(*format_, *diag_, *pal_printer_, packing.pals_);
1378
1379 return packing;
1380}
1381
1382} // namespace porytiles
#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.
Definition animation.hpp:78
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.
Definition color_set.hpp:26
A text parameter with associated styling for formatted output.
General-purpose error implementation with formatted message support.
Definition error.hpp:63
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 &params) 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_pal(const Palette< Rgba32, pal::max_size > &pal) const =0
A generic palette container for colors that support transparency checking.
Definition palette.hpp:47
std::size_t size() const
Returns the number of slots in the palette.
Definition palette.hpp:229
ColorType at(std::size_t index) const
Gets the color at a specific index.
Definition palette.hpp:276
bool is_wildcard(std::size_t index) const
Checks if a slot is a wildcard.
Definition palette.hpp:198
An 8x8 tile backed by literal-array-based per-pixel storage of an arbitrary pixel type.
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=pal::max_size - 1)
Creates a partially locked palette with available capacity.
Represents a 32-bit RGBA color.
Definition rgba32.hpp:23
std::uint8_t red() const
Definition rgba32.hpp:81
std::uint8_t blue() const
Definition rgba32.hpp:91
std::uint8_t green() const
Definition rgba32.hpp:86
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 > &params) 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_pals
Definition palette.hpp:21
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_pal)
Builds note lines showing one representative tile (ASCII art) per palette.
void panic(const StringViewSourceLoc &s)
Unconditionally terminates the program with a panic message.
Definition panic.cpp:43
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.
@ 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::vector< std::string > build_truncated_tile_ref_lines(const TextFormatter &format, const std::vector< std::size_t > &tile_indices)
Builds truncated tile reference lines, displaying up to 8 entries 2-per-line 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_pal_assignments, const std::array< std::optional< Palette< Rgba32, pal::max_size > >, pal::num_pals > &base_pals, const std::array< std::optional< Palette< Rgba32, pal::max_size > >, pal::num_pals > &prefilled_pals)
Builds Indirect links from shape groups and pre-computed palette assignments.
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_pal)
Builds per-palette tile reference lines with a header and truncated listing for each palette.
std::string pal_filename(std::size_t pal_index)
Constructs a palette filename from a palette index.
std::vector< std::string > format_config_note(const TextFormatter &format, const ConfigValue< T > &config)
Format a ConfigValue into diagnostic note lines.
@ 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::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_pal)
Builds note lines showing one representative primary tile (ASCII art) per palette.
std::array< std::optional< Palette< Rgba32, pal::max_size > >, pal::num_pals > build_all_output_palettes(const std::vector< PackedPalette > &packed_pals, const std::array< std::optional< Palette< Rgba32, pal::max_size > >, pal::num_pals > &prefilled_pals, 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_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.
Identifies a regular input tile.
Input data aggregate for the low-level palette packing algorithm.
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::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< pal::num_pals > available_pals_
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.
std::array< std::optional< Palette< Rgba32, pal::max_size > >, pal::num_pals > prefilled_pals_
Existing palettes with locked and wildcarded colors (from PorytilesTilesetComponent)
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::map< std::size_t, std::size_t > tile_to_pal_
Maps regular tile ids to their assigned hardware palette indices.
std::array< std::optional< Palette< Rgba32, pal::max_size > >, pal::num_pals > pals_
The final hardware palettes with colors assigned.
A group of tiles that share the same canonical shape but have different color assignments.