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
28FormatParam color_param(const Rgba32 &color)
29{
30 auto text =
31 std::to_string(color.red()) + ", " + std::to_string(color.green()) + ", " + std::to_string(color.blue());
32 return FormatParam{std::move(text), rgb_fg_style(color.red(), color.green(), color.blue()) | Style::bold};
33}
34
36struct ColorSetWithOccupancy {
37 ColorSet color_set;
38 std::size_t occupied_slots{};
39};
40
52[[nodiscard]] ColorSet build_color_set_from_tile(
53 const PixelTile<Rgba32> &tile, const ColorIndexMap<Rgba32> &color_map, const Rgba32 &extrinsic)
54{
55 ColorSet color_set{};
56 const auto unique_colors = tile.unique_nontransparent_colors(extrinsic);
57
58 for (const auto &color : unique_colors) {
59 const auto index_opt = color_map.index_at_color(color);
60 if (!index_opt.has_value()) {
61 // This will throw if a hint contains the extrinsic transparency color, since the ColorIndexMap won't
62 // contain any transparency colors. Callers of the packer service should have used the PaletteValidator
63 // service to validate input palettes and generate good user diagnostics.
64 panic("tile color " + to_string(color) + " not found in color index map");
65 }
66 color_set.set(index_opt.value());
67 }
68
69 return color_set;
70}
71
82[[nodiscard]] ColorSetWithOccupancy
83build_color_set_from_palette(const Palette<Rgba32, palette::max_size> &palette, const ColorIndexMap<Rgba32> &color_map)
84{
85 ColorSet color_set{};
86 std::size_t occupied_slots = 0;
87
88 // Start from slot 1 (slot 0 is transparency)
89 for (std::size_t i = 1; i < palette.size(); ++i) {
90 if (palette.is_wildcard(i)) {
91 continue;
92 }
93 ++occupied_slots;
94 const auto color = palette.at(i);
95 const auto index_opt = color_map.index_at_color(color);
96 if (!index_opt.has_value()) {
97 // This will throw if a palette contains the extrinsic transparency color, since the ColorIndexMap won't
98 // contain any transparency colors. Callers of the packer service should have used the PaletteValidator
99 // service to validate input palettes and generate good user diagnostics.
100 panic("palette color " + to_string(color) + " at slot " + std::to_string(i) + " not in color map");
101 }
102 color_set.set(index_opt.value());
103 }
104
105 return ColorSetWithOccupancy{color_set, occupied_slots};
106}
107
118[[nodiscard]] ColorSet
119build_color_set_from_hint_palette(const Palette<Rgba32> &palette, const ColorIndexMap<Rgba32> &color_map)
120{
121 ColorSet color_set{};
122
123 for (std::size_t i = 0; i < palette.size(); ++i) {
124 if (palette.is_wildcard(i)) {
125 panic("build_color_set_from_hint_palette palette contained unexpected wildcard");
126 }
127 const auto color = palette.at(i);
128 const auto index_opt = color_map.index_at_color(color);
129 if (!index_opt.has_value()) {
130 panic("hint color " + to_string(color) + " at slot " + std::to_string(i) + " not in color map");
131 }
132 color_set.set(index_opt.value());
133 }
134
135 return color_set;
136}
137
150[[nodiscard]] std::set<PixelTile<Rgba32>>
151build_anim_keyframe_set(const std::map<std::string, Animation<Rgba32>> &anims, const Rgba32 &extrinsic)
152{
153 std::set<PixelTile<Rgba32>> result;
154 for (const auto &[name, anim] : anims) {
155 if (anim.frames().empty()) {
156 continue;
157 }
158 const AnimFrame<Rgba32> &representative_frame =
159 anim.has_key_frame() ? anim.key_frame() : anim.frames().begin()->second;
160 for (const auto &tile : representative_frame.tiles()) {
161 if (tile.is_transparent(extrinsic)) {
162 continue;
163 }
165 result.insert(static_cast<const PixelTile<Rgba32> &>(canonical));
166 }
167 }
168 return result;
169}
170
179struct CombinedTiles {
180 std::vector<PixelTile<Rgba32>> tiles;
181 std::vector<PackableTile::Id> index_to_id;
182};
183
184[[nodiscard]] CombinedTiles
185build_combined_tiles(const PackingParams &params, const std::set<PixelTile<Rgba32>> &anim_keyframe_tiles)
186{
187 CombinedTiles combined;
188 combined.tiles.reserve(params.tiles_.size() + params.primary_tiles_.size());
189 combined.index_to_id.reserve(params.tiles_.size() + params.primary_tiles_.size());
190
191 for (std::size_t i = 0; i < params.tiles_.size(); ++i) {
193 if (anim_keyframe_tiles.contains(static_cast<const PixelTile<Rgba32> &>(canonical))) {
194 continue;
195 }
196 combined.tiles.push_back(params.tiles_.at(i));
197 combined.index_to_id.push_back(PackableTile::RegularId{i});
198 }
199
200 for (std::size_t i = 0; i < params.primary_tiles_.size(); ++i) {
201 const auto &primary_ref = params.primary_tiles_.at(i);
202 combined.tiles.push_back(primary_ref.tile);
203 combined.index_to_id.push_back(PackableTile::PrimaryTileId{i, primary_ref.palette_index});
204 }
205
206 return combined;
207}
208
210struct SharingGroupMember {
211 std::size_t tile_index;
212 std::size_t palette_index;
213 bool is_primary{false};
214};
215
220struct PartitionGroup {
221 std::vector<SharingGroupMember> members;
222
228 std::vector<std::size_t> color_version_tile_indices;
229
235 std::vector<std::pair<std::size_t, std::size_t>> primary_color_version_entries;
236};
237
243struct PartialAlignmentInfo {
244 std::size_t group_id;
245 std::size_t dropped_color_version_count;
246};
247
249struct PackingInputs {
250 std::vector<PackableTile> regular_tiles;
251 std::vector<PackableTile> hint_tiles;
252 std::set<PrefilledPalette> prefilled_palettes;
253};
254
260struct Phase2Result {
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;
264};
265
267struct Phase3GroupResult {
268 std::size_t shape_group_index;
269 bool is_partial;
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;
275};
276
282struct Phase3Result {
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;
289};
290
292struct VerifiedMember {
293 std::size_t regular_index;
294 std::size_t hw_palette;
295 CanonicalPixelTile<IndexPixel> canonical_indexed;
296 std::map<ShapeMask, Rgba32> colors;
297 bool is_primary{false};
298};
299
308[[nodiscard]] PackingInputs build_packing_inputs(const PackingParams &params)
309{
310 // Step 1: Convert regular tiles and anims to PackableTile vector
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) {
314 auto color_set =
315 build_color_set_from_tile(params.tiles_.at(i), params.color_map_, params.extrinsic_transparency_);
316 regular_tiles.emplace_back(PackableTile::RegularId{i}, color_set);
317 }
318 for (const auto &[anim_name, anim] : params.anims_) {
319 const auto &composite_frame = anim.composite_frame(params.extrinsic_transparency_);
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);
322 auto color_set =
323 build_color_set_from_tile(composite_tile, params.color_map_, params.extrinsic_transparency_);
324 regular_tiles.emplace_back(PackableTile::AnimId{anim_name, subtile_index}, color_set);
325 }
326 }
327
328 // Step 2: Convert hints to PackableTile vector
329 std::vector<PackableTile> hint_tiles;
330 hint_tiles.reserve(params.hints_.size());
331 for (const PaletteHint &hint : params.hints_) {
332 auto color_set = build_color_set_from_hint_palette(hint.palette(), params.color_map_);
333 hint_tiles.emplace_back(PackableTile::HintId{hint.name()}, color_set);
334 }
335
336 // Step 3: Convert input prefilled palettes to PrefilledPalette set
337 std::set<PrefilledPalette> prefilled_palettes;
338 for (std::size_t i = 0; i < params.prefilled_palettes_.size(); ++i) {
339 if (!params.prefilled_palettes_[i].has_value()) {
340 continue;
341 }
342 auto [color_set, occupied_slots] =
343 build_color_set_from_palette(params.prefilled_palettes_[i].value(), params.color_map_);
344 prefilled_palettes.insert(PrefilledPalette::partially_locked(i, color_set, occupied_slots));
345 }
346
347 return PackingInputs{std::move(regular_tiles), std::move(hint_tiles), std::move(prefilled_palettes)};
348}
349
358void populate_tile_to_palette(PalettePacking &packing, const PackingOutput &output)
359{
360 packing.tile_to_palette_.clear();
361 for (const auto &[tile_id, palette_index] : output.tile_to_palette_) {
362 std::visit(
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>) {
366 packing.tile_to_palette_[id.index] = palette_index;
367 }
368 else if constexpr (std::is_same_v<Id, PackableTile::AnimId>) {
369 // Anim tiles are packed for palette assignment but excluded from tile_to_palette_
370 }
371 else if constexpr (std::is_same_v<Id, PackableTile::HintId>) {
372 // We don't currently care to store where hints got assigned
373 }
374 else if constexpr (std::is_same_v<Id, PackableTile::PrefilledPaletteId>) {
375 // Nothing to do here, we only had these PackableTiles for internal bookkeeping
376 }
377 else if constexpr (std::is_same_v<Id, PackableTile::PrimaryTileId>) {
378 // Primary tiles are never in the packer's output; no-op for exhaustive visit
379 }
380 else {
381 static_assert(sizeof(Id) == 0, "Unhandled PackableTile::Id variant alternative");
382 }
383 },
384 tile_id);
385 }
386}
387
398[[nodiscard]] std::map<std::size_t, std::size_t> build_tile_palette_assignments(
399 const std::vector<ShapeGroup<Rgba32>> &shape_groups,
400 const CombinedTiles &combined,
401 const std::map<std::size_t, std::size_t> &tile_to_palette)
402{
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);
411 }
412 }
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;
416 }
417 }
418 }
419 return tile_palette_assignments;
420}
421
432[[nodiscard]] Phase2Result compute_partition_groups(
433 const std::vector<ShapeGroup<Rgba32>> &shape_groups,
434 const CombinedTiles &combined,
435 const std::map<std::size_t, std::size_t> &tile_to_palette)
436{
437 Phase2Result result;
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;
445
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;
450
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);
457 }
458 }
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;
463 is_primary = true;
464 }
465
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);
471 if (!is_primary) {
472 color_version_tile_indices.push_back(display_index);
473 }
474 else {
475 primary_color_version_entries.emplace_back(display_index, palette_opt.value());
476 }
477 }
478 }
479 }
480
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));
489 }
490 }
491 return result;
492}
493
508[[nodiscard]] Phase3Result verify_sharing_alignment(
509 const std::vector<ShapeGroup<Rgba32>> &shape_groups,
510 const CombinedTiles &combined,
511 const std::map<std::size_t, std::size_t> &tile_palette_assignments,
512 const Phase2Result &phase2,
513 const std::array<std::optional<Palette<Rgba32, palette::max_size>>, palette::num_palettes> &final_palettes,
514 const Rgba32 &extrinsic)
515{
516 Phase3Result result;
517 std::set<std::size_t> aligned_shape_indices;
518
519 for (std::size_t sg_index = 0; sg_index < shape_groups.size(); ++sg_index) {
520 if (!phase2.eligible_shape_indices.contains(sg_index)) {
521 continue;
522 }
523 const auto &group = shape_groups.at(sg_index);
524 std::vector<VerifiedMember> verified;
525
526 for (const auto &member : group.members) {
527 if (!tile_palette_assignments.contains(member.tile_index)) {
528 continue;
529 }
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;
534 }
535 else if (std::holds_alternative<PackableTile::PrimaryTileId>(id)) {
536 display_index = std::get<PackableTile::PrimaryTileId>(id).tile_index;
537 }
538 else {
539 continue;
540 }
541 std::size_t hw = tile_palette_assignments.at(member.tile_index);
542
543 bool is_primary_member = std::holds_alternative<PackableTile::PrimaryTileId>(id);
544
545 const auto &tile = combined.tiles.at(member.tile_index);
546 auto indexed_tile = index_tile_from_color_tile(tile, final_palettes.at(hw).value(), extrinsic);
548 verified.push_back(
549 VerifiedMember{display_index, hw, std::move(canonical), member.colors, is_primary_member});
550 }
551
552 if (verified.size() < 2) {
553 continue;
554 }
555
556 const auto &reference = verified.at(0).canonical_indexed;
557 std::vector<SharingGroupMember> result_members;
558 std::set<std::size_t> distinct_palettes_set;
559
560 std::set<std::map<ShapeMask, Rgba32>> dropped_color_versions;
561 for (const auto &member : verified) {
562 const auto &member_as_tile = static_cast<const PixelTile<IndexPixel> &>(member.canonical_indexed);
563 const auto &ref_as_tile = static_cast<const PixelTile<IndexPixel> &>(reference);
564 if (member_as_tile != ref_as_tile) {
565 dropped_color_versions.insert(member.colors);
566 continue;
567 }
568 result_members.push_back(SharingGroupMember{member.regular_index, member.hw_palette, member.is_primary});
569 distinct_palettes_set.insert(member.hw_palette);
570 }
571 const std::size_t dropped_color_version_count = dropped_color_versions.size();
572
573 if (result_members.size() >= 2 && distinct_palettes_set.size() >= 2) {
574 aligned_shape_indices.insert(sg_index);
575
576 const bool is_partial = dropped_color_version_count > 0;
577 if (is_partial) {
578 ++result.partially_aligned_count;
579 result.partial_alignment_infos.push_back(PartialAlignmentInfo{sg_index, dropped_color_version_count});
580 }
581 else {
582 ++result.fully_aligned_count;
583 }
584
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) {
590 has_primary = true;
591 primary_members_by_palette[member.palette_index].push_back(member.tile_index);
592 continue;
593 }
594 members_by_palette[member.palette_index].push_back(member.tile_index);
595 }
596
597 result.aligned_groups.push_back(
598 Phase3GroupResult{
599 sg_index,
600 is_partial,
601 dropped_color_version_count,
602 result_members.size(),
603 std::move(members_by_palette),
604 has_primary,
605 std::move(primary_members_by_palette)});
606 }
607 }
608
609 result.aligned_count = aligned_shape_indices.size();
610
611 // Compute unaligned partition groups: eligible groups that did not produce a sharing result
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)));
616 }
617 }
618
619 return result;
620}
621
634void emit_phase1_diagnostics(
635 const TextFormatter &format,
636 const UserDiagnostics &diag,
637 const TilePrinter &tile_printer,
638 const std::vector<ShapeGroup<Rgba32>> &shape_groups,
639 const CombinedTiles &combined,
640 const PackingParams &params)
641{
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);
656 }
657 }
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);
663 }
664 }
665 }
666
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);
669
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.",
673 FormatParam{shape_group_index, Style::bold},
674 FormatParam{total_color_versions, Style::bold},
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}));
680 }
681 if (!color_version_tile_indices.empty()) {
682 remark_lines.append_range(build_tile_sharing_color_version_tile_lines(
683 format, tile_printer, params.tiles_, params.extrinsic_transparency_, color_version_tile_indices));
684 }
685 if (!primary_color_version_entries.empty()) {
686 remark_lines.append_range(build_primary_tile_color_version_lines(
687 format,
688 tile_printer,
689 params.primary_tiles_,
691 primary_color_version_entries,
692 color_version_tile_indices.size()));
693 }
694 remark_lines.emplace_back(
695 format.format("All tilemap entries ('{}' total):", FormatParam{all_tile_indices.size(), Style::bold}));
696 remark_lines.append_range(build_truncated_tile_ref_lines(format, all_tile_indices));
697 diag.remark(phase1_tag, remark_lines);
698 }
699
700 diag.remark(
701 "tile-sharing-shareable-tiles-summary",
702 "Tile sharing detection: '{}' shareable shape group(s) found.",
703 FormatParam{shape_groups.size(), Style::bold});
704}
705
718void emit_phase2_diagnostics(
719 const TextFormatter &format,
720 const UserDiagnostics &diag,
721 const TilePrinter &tile_printer,
722 const std::vector<ShapeGroup<Rgba32>> &shape_groups,
723 const Phase2Result &phase2,
724 const PackingParams &params)
725{
726 for (std::size_t sg_index = 0; sg_index < shape_groups.size(); ++sg_index) {
727 if (!phase2.eligible_shape_indices.contains(sg_index)) {
728 continue;
729 }
730 const auto &partition = phase2.partition_groups.at(phase2.shape_to_partition_index.at(sg_index));
731
732 auto phase2_tag = std::format("tile-sharing-palette-partition-{}", sg_index);
733
734 // Separate primary and secondary members for diagnostic display
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;
740 continue;
741 }
742 members_by_palette[member.palette_index].push_back(member.tile_index);
743 }
744
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 "
748 "sharing.",
749 FormatParam{sg_index, Style::bold},
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.");
754 }
755
756 if (!partition.color_version_tile_indices.empty()) {
757 remark_lines.append_range(build_tile_sharing_color_version_tile_lines(
758 format,
759 tile_printer,
760 params.tiles_,
762 partition.color_version_tile_indices));
763 }
764 if (!partition.primary_color_version_entries.empty()) {
765 remark_lines.append_range(build_primary_tile_color_version_lines(
766 format,
767 tile_printer,
768 params.primary_tiles_,
770 partition.primary_color_version_entries,
771 partition.color_version_tile_indices.size()));
772 }
773 remark_lines.append_range(build_per_palette_tile_ref_lines(format, members_by_palette));
774
775 if (params.tile_sharing_packing_ == TileSharingPacking::off) {
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("");
784 remark_lines.append_range(format_config_note(format, params.tile_sharing_packing_));
785 }
786 diag.remark(phase2_tag, remark_lines);
787 }
788
789 diag.remark(
790 "tile-sharing-palette-partition-summary",
791 "Tile sharing partition: '{}' of '{}' shape group(s) eligible for sharing after palette packing.",
792 FormatParam{phase2.eligible_shape_indices.size(), Style::bold},
793 FormatParam{shape_groups.size(), Style::bold});
794}
795
807void emit_phase3_diagnostics(
808 const TextFormatter &format,
809 const UserDiagnostics &diag,
810 const TilePrinter &tile_printer,
811 const Phase3Result &phase3,
812 const PackingParams &params)
813{
814 for (const auto &group : phase3.aligned_groups) {
815 auto phase3_tag = std::format("tile-sharing-result-{}", group.shape_group_index);
816
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).",
822 FormatParam{group.shape_group_index, Style::bold},
823 FormatParam{group.members_by_palette.size(), Style::bold},
824 FormatParam{group.matching_count, 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}));
828 }
829 else {
830 remark_lines.emplace_back(format.format(
831 "Tile sharing succeeded (group id '{}'): '{}' participating palette(s) (referenced in '{}' "
832 "tilemap entries).",
833 FormatParam{group.shape_group_index, Style::bold},
834 FormatParam{group.members_by_palette.size(), Style::bold},
835 FormatParam{group.matching_count, Style::bold}));
836 }
837 if (group.has_primary_members) {
838 remark_lines.emplace_back("Includes cross-tileset member(s) from paired primary.");
839 }
840 if (!group.members_by_palette.empty()) {
841 remark_lines.append_range(build_representative_tile_per_palette_lines(
842 format, tile_printer, params.tiles_, params.extrinsic_transparency_, group.members_by_palette));
843 remark_lines.append_range(build_per_palette_tile_ref_lines(format, group.members_by_palette));
844 }
845 if (!group.primary_members_by_palette.empty()) {
847 format,
848 tile_printer,
849 params.primary_tiles_,
851 group.primary_members_by_palette));
852 }
853
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}));
861 }
862
863 if (params.tile_sharing_alignment_ == TileSharingAlignment::off) {
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("");
872 remark_lines.append_range(format_config_note(format, params.tile_sharing_alignment_));
873 }
874 diag.remark(phase3_tag, remark_lines);
875 }
876}
877
892void emit_sharing_summary(
893 const TextFormatter &format,
894 const UserDiagnostics &diag,
895 const TilePrinter &tile_printer,
896 const PackingParams &params,
897 const std::vector<ShapeGroup<Rgba32>> &shape_groups,
898 const Phase2Result &phase2,
899 const Phase3Result &phase3,
900 const AlignmentFailureCounts &failure_counts)
901{
902 const auto eligible = phase2.partition_groups.size();
903 const auto &fc = failure_counts;
904
905 constexpr auto tag = "tile-sharing-result-summary";
906
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, '{}' "
912 "partially).",
913 FormatParam{shape_groups.size(), Style::bold},
914 FormatParam{eligible, Style::bold},
915 FormatParam{phase3.aligned_count, Style::bold},
916 FormatParam{phase3.fully_aligned_count, Style::bold},
917 FormatParam{phase3.partially_aligned_count, Style::bold}));
918 }
919 else {
920 remark_lines.emplace_back("Tile sharing summary:");
921 remark_lines.emplace_back(format.format(
922 "'{}' detected → '{}' eligible after packing → '{}' aligned.",
923 FormatParam{shape_groups.size(), Style::bold},
924 FormatParam{eligible, Style::bold},
925 FormatParam{phase3.aligned_count, Style::bold}));
926 }
927
928 // Partially aligned groups listing
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.",
935 FormatParam{info.group_id, Style::bold},
936 FormatParam{info.dropped_color_version_count, Style::bold}));
937 }
938 remark_lines.emplace_back("");
939 }
940
941 const auto unaligned = eligible - phase3.aligned_count;
942 if (unaligned > 0) {
943 if (fc.total() == 0 && params.tile_sharing_alignment_ != TileSharingAlignment::off) {
944 panic("Hit 'Divergence with Zero Failures' case");
945 }
946
947 remark_lines.emplace_back(
948 format.format("'{}' eligible group(s) could not be aligned:", FormatParam{unaligned, Style::bold}));
949
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):",
955 FormatParam{group_id, Style::bold},
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.");
959 }
960 if (!group.color_version_tile_indices.empty()) {
961 remark_lines.append_range(build_tile_sharing_color_version_tile_lines(
962 format,
963 tile_printer,
964 params.tiles_,
966 group.color_version_tile_indices));
967 }
968
969 // Show palette assignments for each color version
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);
976 }
977 }
978 if (palettes_for_version.empty()) {
979 continue;
980 }
981 std::string palette_list;
982 for (const auto palette : palettes_for_version) {
983 if (!palette_list.empty()) {
984 palette_list += ", ";
985 }
986 palette_list += palette_filename(palette);
987 }
988 remark_lines.emplace_back(format.format(
989 " Version '{}' assigned to palette(s): '{}'.",
990 FormatParam{v + 1, Style::bold},
991 FormatParam{palette_list, Style::bold}));
992 }
993
994 // Show per-group failure details inline (only when alignment is active)
995 if (params.tile_sharing_alignment_ != TileSharingAlignment::off) {
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;
999 });
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;
1003 });
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;
1009 });
1010 const auto group_total = group_prefilled_dest + group_prefilled_src + group_fww + group_mismatch;
1011
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: '{}'.",
1018 FormatParam{group_prefilled_dest, Style::bold}));
1019 for (const auto &detail : fc.prefilled_destination_conflict_details) {
1020 if (detail.source_group_index != group_id) {
1021 continue;
1022 }
1023 remark_lines.emplace_back(format.format(
1024 " '{}' slot '{}': color '{}' blocked by locked color '{}'.",
1025 FormatParam{palette_filename(detail.palette_index), Style::bold},
1026 FormatParam{detail.target_slot, Style::bold},
1027 color_param(detail.blocked_color),
1028 color_param(detail.locked_color)));
1029 }
1030 }
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) {
1036 continue;
1037 }
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}));
1048 }
1049 }
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) {
1055 continue;
1056 }
1057 remark_lines.emplace_back(format.format(
1058 " '{}': prefilled color '{}' could not be linked to color '{}' in "
1059 "palette '{}'.",
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}));
1064 }
1065 }
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) {
1071 continue;
1072 }
1073 remark_lines.emplace_back(format.format(
1074 " '{}': color '{}' ended at slot '{}', but ref color '{}' in '{}' is at "
1075 "slot '{}'.",
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}));
1082 }
1083 }
1084 }
1085 }
1086 }
1087
1088 // Aggregate failure summary
1089 remark_lines.emplace_back();
1090 if (params.tile_sharing_alignment_ == TileSharingAlignment::off) {
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.");
1093 }
1094 else {
1095 remark_lines.emplace_back(format.format(
1096 "'{}' total link resolution failure(s) across all unaligned groups.",
1097 FormatParam{fc.total(), Style::bold}));
1098 }
1099
1100 // Actionable suggestions (skip for off mode, the suggestion is already above)
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, '{}' "
1105 "partially).",
1106 FormatParam{phase3.aligned_count, Style::bold},
1107 FormatParam{eligible, Style::bold},
1108 FormatParam{phase3.fully_aligned_count, Style::bold},
1109 FormatParam{phase3.partially_aligned_count, Style::bold}));
1110 }
1111 else {
1112 remark_lines.emplace_back(format.format(
1113 "Tile sharing is best-effort. '{}' of '{}' eligible group(s) aligned.",
1114 FormatParam{phase3.aligned_count, Style::bold},
1115 FormatParam{eligible, Style::bold}));
1116 }
1117 if (params.tile_sharing_alignment_ != TileSharingAlignment::off) {
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.");
1121 }
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 "
1125 "reassigned.");
1126 remark_lines.emplace_back("Rearranging or wildcarding those prefilled slots may improve tile sharing.");
1127 }
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.");
1133 }
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 "
1137 "resolution.",
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.");
1140 }
1141 }
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");
1145 }
1146
1147 diag.remark(tag, remark_lines);
1148}
1149
1156void emit_palette_diagnostics(
1157 const TextFormatter &format,
1158 const UserDiagnostics &diag,
1159 const PalettePrinter &palette_printer,
1160 const std::array<std::optional<Palette<Rgba32, palette::max_size>>, palette::num_palettes> &palettes)
1161{
1162 for (std::size_t i = 0; i < palette::num_palettes; ++i) {
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);
1172 }
1173 }
1174}
1175
1176} // namespace
1177
1178namespace porytiles {
1179
1181{
1182 // Steps 1-3: Convert inputs to packer-internal forms
1183 auto [regular_tiles, hint_tiles, prefilled_palettes] = build_packing_inputs(params);
1184
1185 // Step 4: Create PackingInput and call low-level pack()
1186 PackingInput packing_input{
1187 std::move(regular_tiles),
1188 std::move(hint_tiles),
1189 std::move(prefilled_palettes),
1191
1192 // Build animation keyframe exclusion set (once, before any shape group analysis)
1193 const auto anim_keyframe_tiles = build_anim_keyframe_set(params.anims_, params.extrinsic_transparency_);
1194
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("");
1201 err_lines.append_range(format_config_note(*format_, params.tile_sharing_packing_));
1202 return ChainableResult<PalettePacking>{FormattableError{std::move(err_lines)}};
1203 }
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("");
1210 err_lines.append_range(format_config_note(*format_, params.tile_sharing_alignment_));
1211 return ChainableResult<PalettePacking>{FormattableError{std::move(err_lines)}};
1212 }
1213
1214 // For biased packing, build shape group metadata BEFORE the initial pack call so the strategy
1215 // receives sharing-aware input. For off packing, pack normally first (metadata is only needed
1216 // post-packing for diagnostics and alignment).
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);
1221 shape_groups_for_biased = analyze_shape_groups(combined_for_biased.tiles, params.extrinsic_transparency_);
1222
1223 if (!shape_groups_for_biased.empty()) {
1224 auto metadata = build_shape_group_metadata(shape_groups_for_biased, combined_for_biased.index_to_id);
1225 packing_input.shape_group_metadata_ = std::move(metadata);
1226 }
1227 }
1228
1229 auto pack_result = strategy_->pack(packing_input);
1230
1232 packing_output, std::move(pack_result), PalettePacking, "Low-level palette packing failed.");
1233
1234 // Step 5a: Build tile_to_palette from PackingOutput
1235 PalettePacking packing{};
1236 populate_tile_to_palette(packing, packing_output);
1237
1238 // 5b: Always compute shape groups for three-phase diagnostics, then conditionally build Indirect links.
1239 //
1240 // Shape group analysis and Phase 1/2 data are always computed regardless of config, so diagnostics can be
1241 // emitted with appropriate caveats. Indirect link generation (alignment) is conditioned on
1242 // tile_sharing_alignment_.
1243 std::vector<IndirectLink> indirect_links;
1244
1245 // Always compute shape groups for three-phase diagnostics. Reuse pre-pack results when packing is
1246 // biased (they were already computed for metadata injection).
1247 CombinedTiles combined;
1248 std::vector<ShapeGroup<Rgba32>> shape_groups;
1249
1251 combined = std::move(combined_for_biased);
1252 shape_groups = std::move(shape_groups_for_biased);
1253 }
1254 else {
1255 combined = build_combined_tiles(params, anim_keyframe_tiles);
1256 shape_groups = analyze_shape_groups(combined.tiles, params.extrinsic_transparency_);
1257 }
1258
1259 // Phase 1: Detect sharing opportunities and emit diagnostics
1260 emit_phase1_diagnostics(*format_, *diag_, *tile_printer_, shape_groups, combined, params);
1261
1262 // Phase 2: Compute partition groups and emit diagnostics
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);
1265
1266 // Build tile-palette assignments from authoritative packing assignments
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_);
1270 }
1271
1272 // Conditionally build Indirect links based on alignment
1273 if (params.tile_sharing_alignment_ == TileSharingAlignment::greedy && !shape_groups.empty()) {
1274 auto base_palettes = build_all_output_palettes(
1275 packing_output.palettes_,
1276 params.prefilled_palettes_,
1277 params.color_map_,
1279 {} /* empty links */);
1280
1281 indirect_links =
1282 build_indirect_links(shape_groups, tile_palette_assignments, base_palettes, params.prefilled_palettes_);
1283 }
1284
1285 // Build final output palettes with Indirect links applied (or empty links for off mode)
1286 AlignmentFailureCounts failure_counts{};
1287 auto final_palettes = build_all_output_palettes(
1288 packing_output.palettes_,
1289 params.prefilled_palettes_,
1290 params.color_map_,
1292 indirect_links,
1293 !indirect_links.empty() ? &failure_counts : nullptr);
1294
1295 // Phase 3: Verify sharing alignment against final palettes
1296 Phase3Result phase3;
1297 if (!shape_groups.empty()) {
1298 phase3 = verify_sharing_alignment(
1299 shape_groups, combined, tile_palette_assignments, phase2, final_palettes, params.extrinsic_transparency_);
1300 }
1301 emit_phase3_diagnostics(*format_, *diag_, *tile_printer_, phase3, params);
1302
1303 // Tile sharing summary: emit when biased packing or greedy alignment is active
1306 emit_sharing_summary(*format_, *diag_, *tile_printer_, params, shape_groups, phase2, phase3, failure_counts);
1307 }
1308
1309 // Assemble final output palettes
1310 for (std::size_t i = 0; i < palette::num_palettes; ++i) {
1311 if (final_palettes.at(i).has_value()) {
1312 packing.palettes_.at(i) = final_palettes.at(i);
1313 }
1314 }
1315
1316 emit_palette_diagnostics(*format_, *diag_, *palette_printer_, packing.palettes_);
1317
1318 return packing;
1319}
1320
1321} // 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:89
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:22
A text parameter with associated styling for formatted output.
General-purpose error implementation with formatted message support.
Definition error.hpp:57
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_palette(const Palette< Rgba32, palette::max_size > &palette) const =0
A generic palette container for colors that support transparency checking.
Definition palette.hpp:45
std::size_t size() const
Returns the number of slots in the palette.
Definition palette.hpp:203
ColorType at(std::size_t index) const
Gets the color at a specific index.
Definition palette.hpp:246
bool is_wildcard(std::size_t index) const
Checks if a slot is a wildcard.
Definition palette.hpp:176
An 8x8 tile backed by literal-array-based per-pixel storage of an arbitrary pixel type.
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.
Definition rgba32.hpp:21
std::uint8_t red() const
Definition rgba32.hpp:73
std::uint8_t blue() const
Definition rgba32.hpp:83
std::uint8_t green() const
Definition rgba32.hpp:78
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.
std::string name
constexpr std::size_t num_palettes
Definition palette.hpp:21
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.
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.
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::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.