Porytiles
Loading...
Searching...
No Matches
anim_decompiler.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <iterator>
5#include <map>
6#include <optional>
7#include <ranges>
8#include <set>
9#include <string>
10#include <utility>
11#include <vector>
12
35
36namespace {
37
38using namespace porytiles;
39
40[[nodiscard]] ChainableResult<std::size_t> internal_png_pal_strategy(
41 const Animation<IndexPixel> &anim,
42 const std::array<Palette<Rgba32, pal::max_size>, pal::num_pals> &tileset_pals,
43 const ConfigValue<Rgba32> &extrinsic_transparency,
44 const UserDiagnostics &diag,
45 const PalettePrinter &pal_printer)
46{
47 if (!anim.has_frames()) {
48 panic("anim '" + anim.name() + "' has no frames");
49 }
50
51 const auto &representative_frame = anim.frames().begin()->second;
52 const auto &representative_pal = representative_frame.palette();
53
54 // Representative pal must have exactly 16 colors to match GBA palette format
55 if (representative_pal.size() != pal::max_size) {
56 std::vector<std::string> err_msg{};
57 err_msg.emplace_back(diag.formatter().format(
58 "Representative frame '{}' internal palette size '{}': must be '{}'.",
59 FormatParam{representative_frame.frame_name(), Style::bold},
60 FormatParam{representative_pal.size(), Style::bold},
61 FormatParam{pal::max_size, Style::bold}));
62 err_msg.emplace_back("");
63 err_msg.append_range(pal_printer.print_rgba_pal(representative_pal));
64 return FormattableError{err_msg};
65 }
66
67 // Check for extrinsic transparency in non-slot-0 positions in representative pal
68 std::vector<std::size_t> extrinsic_transparency_slots;
69 for (std::size_t slot = 1; slot < pal::max_size; ++slot) {
70 const Rgba32 &color = representative_pal.at(slot);
71 if (color.is_extrinsically_transparent(extrinsic_transparency)) {
72 extrinsic_transparency_slots.push_back(slot);
73 }
74 }
75
76 if (!extrinsic_transparency_slots.empty()) {
77 std::string slot_list;
78 for (const auto &slot : extrinsic_transparency_slots) {
79 if (!slot_list.empty()) {
80 slot_list += ", ";
81 }
82 slot_list += std::to_string(slot);
83 }
84 std::vector<std::string> err_msg{};
85 err_msg.emplace_back(diag.formatter().format(
86 "Representative frame '{}' palette contains extrinsic transparency color '{}' in non-zero slot(s): {}",
87 FormatParam{representative_frame.frame_name(), Style::bold},
88 FormatParam{extrinsic_transparency.value().to_jasc_str(), Style::bold},
89 FormatParam{slot_list, Style::bold}));
90 err_msg.emplace_back("");
91 err_msg.emplace_back("The extrinsic transparency color should only appear in slot 0.");
92 err_msg.emplace_back("Either correct the PNG palette or change the extrinsic transparency setting.");
93 err_msg.emplace_back("");
94 err_msg.append_range(
95 pal_printer.print_rgba_pal_with_highlights(representative_pal, extrinsic_transparency_slots));
96 err_msg.append_range(format_config_note_with_separator(diag.formatter(), extrinsic_transparency));
97 return FormattableError{err_msg};
98 }
99
100 // Check all frames share the same internal palette as the representative
101 for (const auto &[frame_name, frame] : anim.frames()) {
102 if (&frame == &representative_frame) {
103 continue;
104 }
105 const auto &frame_pal = frame.palette();
106 bool palettes_match = (frame_pal.size() == representative_pal.size());
107 if (palettes_match) {
108 for (std::size_t slot = 0; slot < representative_pal.size(); ++slot) {
109 if (frame_pal.at(slot) != representative_pal.at(slot)) {
110 palettes_match = false;
111 break;
112 }
113 }
114 }
115 if (!palettes_match) {
116 std::vector<std::string> err_msg{};
117 err_msg.emplace_back(diag.formatter().format(
118 "Animation '{}' frame '{}' has an internal palette that does not match the representative frame '{}' "
119 "palette.",
121 FormatParam{frame_name, Style::bold},
122 FormatParam{representative_frame.frame_name(), Style::bold}));
123 err_msg.emplace_back("");
124 err_msg.emplace_back(diag.formatter().format(
125 "Representative frame '{}' palette:", FormatParam{representative_frame.frame_name(), Style::bold}));
126 err_msg.append_range(pal_printer.print_rgba_pal(representative_pal));
127 err_msg.emplace_back("");
128 err_msg.emplace_back(diag.formatter().format("Frame '{}' palette:", FormatParam{frame_name, Style::bold}));
129 err_msg.append_range(pal_printer.print_rgba_pal(frame_pal));
130 return FormattableError{err_msg};
131 }
132 }
133
134 /*
135 * Now that we fully validated the representative pal, and we confirmed that all frame pals match, we can try to
136 * match the representative pal to one of the tileset pals.
137 */
138 for (std::size_t pal_idx = 0; pal_idx < tileset_pals.size(); ++pal_idx) {
139 bool matches = true;
140 for (std::size_t slot = 1; slot < pal::max_size; ++slot) {
141 const Rgba32 &png_pal_color = representative_pal.at(slot);
142
143 // This should never happen, we returned early above if we hit this
144 if (png_pal_color.is_transparent(extrinsic_transparency)) {
145 panic("png_pal slot " + std::to_string(slot) + " is extrinsically transparent");
146 }
147
148 const Rgba32 &tileset_color = tileset_pals[pal_idx].at(slot);
149 if (png_pal_color != tileset_color) {
150 matches = false;
151 break;
152 }
153 }
154
155 if (matches) {
156 // Emit remark showing matched palette
157 std::vector<std::string> remark_lines;
158 remark_lines.emplace_back(diag.formatter().format(
159 "Animation '{}' representative frame '{}' internal palette matched Porymap palette '{}':",
160 FormatParam{anim.name(), Style::bold},
161 FormatParam{representative_frame.frame_name(), Style::bold},
162 FormatParam{pal_filename(pal_idx), Style::bold}));
163 remark_lines.emplace_back("");
164 remark_lines.append_range(pal_printer.print_rgba_pal(tileset_pals[pal_idx]));
165 diag.remark("animation-palette-resolution-strategy", remark_lines);
166 return pal_idx;
167 }
168 }
169
170 std::vector<std::string> err_msg{};
171 err_msg.emplace_back(diag.formatter().format(
172 "Failed to find matching palette for internal palette of representative frame '{}'.",
173 FormatParam{representative_frame.frame_name(), Style::bold}));
174 err_msg.emplace_back("");
175 err_msg.append_range(pal_printer.print_rgba_pal(representative_pal));
176 return FormattableError{err_msg};
177}
178
179[[nodiscard]] std::optional<std::size_t> extract_pal_index(AnimPalResolutionStrategy strategy)
180{
181 switch (strategy) {
182 case AnimPalResolutionStrategy::palette_00:
183 return 0;
184 case AnimPalResolutionStrategy::palette_01:
185 return 1;
186 case AnimPalResolutionStrategy::palette_02:
187 return 2;
188 case AnimPalResolutionStrategy::palette_03:
189 return 3;
190 case AnimPalResolutionStrategy::palette_04:
191 return 4;
192 case AnimPalResolutionStrategy::palette_05:
193 return 5;
194 case AnimPalResolutionStrategy::palette_06:
195 return 6;
196 case AnimPalResolutionStrategy::palette_07:
197 return 7;
198 case AnimPalResolutionStrategy::palette_08:
199 return 8;
200 case AnimPalResolutionStrategy::palette_09:
201 return 9;
202 case AnimPalResolutionStrategy::palette_10:
203 return 10;
204 case AnimPalResolutionStrategy::palette_11:
205 return 11;
206 case AnimPalResolutionStrategy::palette_12:
207 return 12;
208 case AnimPalResolutionStrategy::palette_13:
209 return 13;
210 case AnimPalResolutionStrategy::palette_14:
211 return 14;
212 case AnimPalResolutionStrategy::palette_15:
213 return 15;
214 default:
215 return std::nullopt;
216 }
217}
218
241[[nodiscard]] ChainableResult<std::size_t> resolve_subtile_palette(
242 const std::string &anim_name,
243 std::size_t subtile_index,
244 std::size_t tile_index,
245 std::span<const TilemapEntry> metatiles_bin,
248 const Animation<IndexPixel> &anim,
249 const std::array<Palette<Rgba32, pal::max_size>, pal::num_pals> &pals,
250 const Image<IndexPixel> &tiles_png,
251 const ConfigValue<Rgba32> &extrinsic_transparency,
252 const UserDiagnostics &diag,
253 const PalettePrinter &pal_printer,
254 const TilePrinter &tile_printer,
255 std::optional<std::size_t> &internal_png_pal_cache)
256{
257 // Check pal_N strategies first: direct palette index assignment
258 const auto explicit_pal = extract_pal_index(strategy.value());
259 if (explicit_pal.has_value()) {
260 diag.remark(
261 "animation-palette-resolution-strategy",
262 {diag.formatter().format(
263 "Animation '{}' subtile {} using explicit palette '{}'.",
264 FormatParam{anim_name, Style::bold},
265 FormatParam{subtile_index, Style::bold},
266 FormatParam{pal_filename(*explicit_pal), Style::bold})});
267 diag.remark_note("animation-palette-resolution-strategy", format_config_note(diag.formatter(), strategy));
268 return *explicit_pal;
269 }
270
271 switch (strategy.value()) {
272 case AnimPalResolutionStrategy::scan_local_metatiles: {
273 std::set<std::size_t> found_for_subtile{};
274
275 for (const auto &entry : metatiles_bin) {
276 if (entry.tile_index() == tile_index) {
277 found_for_subtile.insert(entry.pal_index());
278 }
279 }
280
281 if (found_for_subtile.empty()) {
282 std::vector<std::string> err_msg{};
283 err_msg.emplace_back(diag.formatter().format(
284 "Animation '{}' subtile {} at tile index '{}' is not referenced in local metatiles.",
285 FormatParam{anim_name, Style::bold},
286 FormatParam{subtile_index, Style::bold},
287 FormatParam{tile_index, Style::bold}));
288 err_msg.emplace_back(
289 "Consider using a different palette resolution strategy (e.g. 'palette-00', "
290 "'internal-png-palette', etc.).");
291 err_msg.append_range(format_config_note_with_separator(diag.formatter(), strategy));
292 return FormattableError{err_msg};
293 }
294
295 if (found_for_subtile.size() > 1) {
296 /*
297 * A single tile index can be referenced by multiple metatile entries with different palette indices.
298 * This is valid GBA behavior. The hardware selects palette per metatile entry, not per tile.
299 *
300 * The multi_pal_strategy config determines how to handle this case.
301 */
302 std::string pal_list;
303 for (const auto &pal_idx : found_for_subtile) {
304 if (!pal_list.empty()) {
305 pal_list += ", ";
306 }
307 pal_list += pal_filename(pal_idx);
308 }
309
310 switch (multi_pal_strategy.value()) {
311 case AnimMultiPalSubtileResolutionStrategy::error: {
312 std::vector<std::string> err_msg;
313 err_msg.push_back(diag.formatter().format(
314 "Animation '{}' subtile {} at tile index '{}' is referenced with multiple palettes: {}.",
315 FormatParam{anim_name, Style::bold},
316 FormatParam{subtile_index, Style::bold},
317 FormatParam{tile_index, Style::bold},
318 FormatParam{pal_list, Style::bold}));
319 err_msg.emplace_back(
320 "Picking one palette arbitrarily would produce incorrect RGBA output in the layer PNGs.");
321
322 const PixelTile<IndexPixel> index_tile = extract_single_tile(tiles_png, tile_index);
323 err_msg.emplace_back("");
324 for (const auto &pal_idx : found_for_subtile) {
325 const PixelTile<Rgba32> rgba_tile =
326 color_tile_from_index_tile(index_tile, pals.at(pal_idx), extrinsic_transparency.value());
327 err_msg.push_back(diag.formatter().format(
328 "Tile under palette '{}':", FormatParam{pal_filename(pal_idx), Style::bold}));
329 err_msg.append_range(tile_printer.print_tile(rgba_tile, extrinsic_transparency.value()));
330 }
331
332 err_msg.emplace_back("");
333 err_msg.emplace_back(
334 "Consider using an explicit palette resolution strategy (e.g. 'palette-00') to resolve the "
335 "ambiguity.");
336 err_msg.append_range(format_config_note_with_separator(diag.formatter(), strategy));
337 return FormattableError{err_msg};
338 }
339 case AnimMultiPalSubtileResolutionStrategy::warning: {
340 const std::size_t chosen_pal = *found_for_subtile.begin();
341
342 std::vector<std::string> warn_msg;
343 warn_msg.push_back(diag.formatter().format(
344 "Animation '{}' subtile {} at tile index '{}' is referenced with multiple palettes: {}.",
345 FormatParam{anim_name, Style::bold},
346 FormatParam{subtile_index, Style::bold},
347 FormatParam{tile_index, Style::bold},
348 FormatParam{pal_list, Style::bold}));
349 warn_msg.push_back(diag.formatter().format(
350 "Using palette '{}'. Set 'frame_linking: manual' to handle palette assignment via overrides.",
351 FormatParam{pal_filename(chosen_pal), Style::bold}));
352
353 const PixelTile<IndexPixel> warn_index_tile = extract_single_tile(tiles_png, tile_index);
354 warn_msg.emplace_back("");
355 for (const auto &pal_idx : found_for_subtile) {
356 const PixelTile<Rgba32> rgba_tile =
357 color_tile_from_index_tile(warn_index_tile, pals.at(pal_idx), extrinsic_transparency.value());
358 warn_msg.push_back(diag.formatter().format(
359 "Tile under palette '{}':", FormatParam{pal_filename(pal_idx), Style::bold}));
360 warn_msg.append_range(tile_printer.print_tile(rgba_tile, extrinsic_transparency.value()));
361 }
362
363 warn_msg.append_range(format_config_note_with_separator(diag.formatter(), multi_pal_strategy));
364 diag.warning("animation-multi-pal-subtile", warn_msg);
365
366 return chosen_pal;
367 }
368 case AnimMultiPalSubtileResolutionStrategy::split:
369 return FormattableError{"The 'split' mode for multi-pal subtile resolution is not yet implemented."};
370 }
371 }
372
373 return *found_for_subtile.begin();
374 }
375
376 case AnimPalResolutionStrategy::internal_png_pal: {
377 if (internal_png_pal_cache.has_value()) {
378 return *internal_png_pal_cache;
379 }
380 std::vector<std::string> err_msg{};
381 err_msg.emplace_back(diag.formatter().format(
382 "Palette resolution strategy '{}' failed.",
383 FormatParam{to_string(AnimPalResolutionStrategy::internal_png_pal), Style::bold}));
384 err_msg.append_range(format_config_note_with_separator(diag.formatter(), strategy));
386 match,
387 internal_png_pal_strategy(anim, pals, extrinsic_transparency, diag, pal_printer),
388 std::size_t,
389 err_msg);
390 internal_png_pal_cache = match;
391 return match;
392 }
393
394 case AnimPalResolutionStrategy::scan_all_tilesets:
395 panic("scan_all_tilesets not yet implemented");
396
397 default:
398 panic("unhandled AnimPalResolutionStrategy value");
399 }
400}
401
402[[nodiscard]] ChainableResult<std::vector<std::size_t>> find_pals_for_anim_tiles(
403 const std::string &anim_name,
404 std::size_t tile_offset,
405 std::size_t tile_count,
406 std::span<const TilemapEntry> metatiles_bin,
407 const std::vector<ConfigValue<AnimPalResolutionStrategy>> &per_subtile_strategies,
409 const Animation<IndexPixel> &anim,
410 const std::array<Palette<Rgba32, pal::max_size>, pal::num_pals> &pals,
411 const Image<IndexPixel> &tiles_png,
412 const ConfigValue<Rgba32> &extrinsic_transparency,
413 const UserDiagnostics &diag,
414 const PalettePrinter &pal_printer,
415 const TilePrinter &tile_printer)
416{
417 if (per_subtile_strategies.size() != tile_count) {
418 panic(
419 "per_subtile_strategies size " + std::to_string(per_subtile_strategies.size()) + " != tile_count " +
420 std::to_string(tile_count));
421 }
422
423 std::vector<std::size_t> per_tile_pals(tile_count);
424 std::optional<std::size_t> internal_png_pal_cache;
425
426 for (std::size_t i = 0; i < tile_count; ++i) {
427 const std::size_t tile_index = tile_offset + i;
429 pal_idx,
430 resolve_subtile_palette(
431 anim_name,
432 i,
433 tile_index,
434 metatiles_bin,
435 per_subtile_strategies[i],
436 multi_pal_strategy,
437 anim,
438 pals,
439 tiles_png,
440 extrinsic_transparency,
441 diag,
442 pal_printer,
443 tile_printer,
444 internal_png_pal_cache),
445 std::vector<std::size_t>,
446 diag.formatter().format(
447 "Failed to resolve palette for animation '{}' subtile {}.",
448 FormatParam{anim_name, Style::bold},
449 FormatParam{i, Style::bold}));
450 per_tile_pals[i] = pal_idx;
451 }
452
453 // Emit a remark if multiple distinct palettes are used across subtiles
454 if (tile_count > 1) {
455 const std::size_t first_pal = per_tile_pals.at(0);
456 const bool uses_multiple_palettes =
457 !std::ranges::all_of(per_tile_pals, [&](std::size_t idx) { return idx == first_pal; });
458 if (uses_multiple_palettes) {
459 std::set<std::size_t> unique_pals{per_tile_pals.begin(), per_tile_pals.end()};
460 std::string pal_list;
461 for (const auto &pal_idx : unique_pals) {
462 if (!pal_list.empty()) {
463 pal_list += ", ";
464 }
465 pal_list += pal_filename(pal_idx);
466 }
467 diag.remark(
468 "animation-palette-resolution-strategy",
469 {diag.formatter().format(
470 "Animation '{}' uses multiple palettes across subtiles: {}.",
471 FormatParam{anim_name, Style::bold},
472 FormatParam{pal_list, Style::bold})});
473 }
474 }
475
476 return per_tile_pals;
477}
478
494[[nodiscard]] bool has_duplicate_key_frame_tiles(
495 const std::vector<PixelTile<IndexPixel>> &key_frame_tiles,
496 const std::set<PixelTile<IndexPixel>> &inter_anim_canonical_tiles,
497 const std::set<PixelTile<IndexPixel>> &external_canonical_tiles)
498{
499 std::set<PixelTile<IndexPixel>> seen;
500 for (const auto &tile : key_frame_tiles) {
501 const CanonicalPixelTile canonical{tile};
502 const PixelTile<IndexPixel> &base = canonical;
503 if (inter_anim_canonical_tiles.contains(base)) {
504 return true;
505 }
506 if (external_canonical_tiles.contains(base)) {
507 return true;
508 }
509 if (!seen.insert(base).second) {
510 return true;
511 }
512 }
513 return false;
514}
515
516struct DuplicateInfo {
517 std::vector<std::size_t> inter_anim_indices;
518 std::vector<std::size_t> cross_range_indices;
519 std::vector<std::pair<std::size_t, std::size_t>> intra_anim_pairs;
520};
521
538[[nodiscard]] DuplicateInfo categorize_duplicate_key_frame_tiles(
539 const std::vector<PixelTile<IndexPixel>> &key_frame_tiles,
540 const std::set<PixelTile<IndexPixel>> &inter_anim_canonical_tiles,
541 const std::set<PixelTile<IndexPixel>> &external_canonical_tiles)
542{
543 DuplicateInfo info;
544
545 // Map from canonical base tile to first index seen
546 std::map<PixelTile<IndexPixel>, std::size_t> seen;
547
548 for (std::size_t i = 0; i < key_frame_tiles.size(); ++i) {
549 const CanonicalPixelTile canonical{key_frame_tiles[i]};
550 const PixelTile<IndexPixel> &base = canonical;
551
552 // Check inter-animation before cross-range (more specific category wins)
553 if (inter_anim_canonical_tiles.contains(base)) {
554 info.inter_anim_indices.push_back(i);
555 }
556 else if (external_canonical_tiles.contains(base)) {
557 info.cross_range_indices.push_back(i);
558 }
559
560 auto [it, inserted] = seen.emplace(base, i);
561 if (!inserted) {
562 info.intra_anim_pairs.emplace_back(it->second, i);
563 }
564 }
565
566 return info;
567}
568
576void backport_mangles_to_tiles_png(
577 PorymapTilesetComponent &component, std::size_t base_tile_offset, const std::set<TileMangleRecord> &records)
578{
579 Image<IndexPixel> tiles_img = component.tiles_png();
580 constexpr std::size_t tiles_per_row = metatile::metatiles_per_row * metatile::tiles_per_side;
581
582 /*
583 * Mangle records are non-overlapping: each targets a distinct tile_index (guaranteed by mangle_duplicates).
584 * Sequential application is therefore safe and order-independent, producing results consistent with the in-memory
585 * key frame tiles that were mangled during decompilation.
586 */
587 for (const auto &record : records) {
588 const std::size_t global_tile_idx = base_tile_offset + record.tile_index;
589 const std::size_t tile_row = global_tile_idx / tiles_per_row;
590 const std::size_t tile_col = global_tile_idx % tiles_per_row;
591
592 for (const auto &change : record.pixel_changes) {
593 const auto [pixel_row, pixel_col] = tile::index_to_row_col(change.pixel_index);
594 const std::size_t img_row = tile_row * tile::side_length_pix + pixel_row;
595 const std::size_t img_col = tile_col * tile::side_length_pix + pixel_col;
596
597 tiles_img.set(img_row, img_col, change.mangled_pixel);
598 }
599 }
600
601 component.tiles_png(tiles_img);
602}
603
604} // namespace
605
606namespace porytiles {
607
609 const std::string &tileset_name,
610 const Animation<IndexPixel> &anim,
611 const std::set<PixelTile<IndexPixel>> &inter_anim_canonical_tiles,
612 PorymapTilesetComponent &porymap_component) const
613{
614 // Unwrap config values
615 PT_UNWRAP_TILESET_CONFIG_PTR(config_, extrinsic_transparency, tileset_name, Animation<Rgba32>);
616 PT_UNWRAP_TILESET_CONFIG_PTR(config_, global_anim_pal_resolution_strategy, tileset_name, Animation<Rgba32>);
617 PT_UNWRAP_TILESET_CONFIG_PTR(config_, global_anim_key_frame_resolution_strategy, tileset_name, Animation<Rgba32>);
619 config_, global_anim_multi_pal_subtile_resolution_strategy, tileset_name, Animation<Rgba32>);
620 PT_UNWRAP_TILESET_CONFIG_PTR(config_, global_frame_linking, tileset_name, Animation<Rgba32>);
621 PT_UNWRAP_TILESET_CONFIG_PTR(config_, per_anim_overrides, tileset_name, Animation<Rgba32>);
622
623 // Read data from porymap_component
624 const auto &pals = porymap_component.pals();
625 const auto &metatiles_bin = porymap_component.metatiles_bin();
626 const auto &tiles_png = porymap_component.tiles_png();
627
628 Animation<Rgba32> result{anim.name()};
629 result.params(anim.params());
630
631 // Get the tile offset from animation params to determine which tile index to look for in metatiles
632 const std::size_t tile_offset = anim.params().tile_offset();
633 const std::size_t tile_count = anim.params().tile_count();
634
635 // Validate tile range before any arithmetic that assumes tile_count > 0
636 if (tile_count == 0) {
637 return FormattableError{
638 std::vector<std::string>{
639 "Animation '{}' has a tile count of '{}'.",
640 "The animation's generated C code may not have been parsed correctly, or the animation defines no "
641 "parameters."},
642 std::vector<std::vector<FormatParam>>{
643 {FormatParam{anim.name(), Style::bold}, FormatParam{tile_count, Style::bold}}, {}}};
644 }
645 if (tile_offset == 0) {
646 return FormattableError{
647 std::vector<std::string>{
648 "Animation '{}' has a tile offset of '{}'.",
649 "Tile 0 in tiles.png is reserved and cannot be an animation tile."},
650 std::vector<std::vector<FormatParam>>{
651 {FormatParam{anim.name(), Style::bold}, FormatParam{tile_offset, Style::bold}}, {}}};
652 }
653
654 /*
655 * Build per-subtile palette resolution strategies using a three-tier cascade:
656 * 1. Per-tile (per_tile_pal_resolution_strategies[i]): most specific
657 * 2. Per-anim (pal_resolution_strategy): middle tier
658 * 3. Global (global_anim_pal_resolution_strategy): least specific fallback
659 */
660 std::vector<ConfigValue<AnimPalResolutionStrategy>> per_subtile_strategies;
661 per_subtile_strategies.reserve(tile_count);
662
663 const auto &configs_map = per_anim_overrides.value();
664 const auto anim_cfg_it = configs_map.find(anim.name());
665 const PerAnimOverride *anim_cfg_ptr = (anim_cfg_it != configs_map.end()) ? &anim_cfg_it->second : nullptr;
666
667 if (anim_cfg_ptr != nullptr) {
668 const PerAnimOverride &anim_cfg = *anim_cfg_ptr;
669
670 // Determine the "effective default" for this animation: per-anim if set, otherwise global
671 const ConfigValue<AnimPalResolutionStrategy> effective_default =
672 anim_cfg.pal_resolution_strategy.has_value() ? per_anim_overrides.derive(anim_cfg.pal_resolution_strategy)
673 : global_anim_pal_resolution_strategy;
674
675 if (!anim_cfg.per_tile_pal_resolution_strategies.empty()) {
676 if (anim_cfg.per_tile_pal_resolution_strategies.size() != tile_count) {
677 return FormattableError{
678 std::vector<std::string>{
679 "Animation '{}' config 'per_tile_palette_resolution_strategies' has '{}' entries, but "
680 "animation has '{}' subtiles.",
681 "The per_tile_palette_resolution_strategies list must have exactly one entry per subtile."},
682 std::vector<std::vector<FormatParam>>{
683 {FormatParam{anim.name(), Style::bold},
685 FormatParam{tile_count, Style::bold}},
686 {}}};
687 }
688 for (std::size_t i = 0; i < tile_count; ++i) {
689 if (anim_cfg.per_tile_pal_resolution_strategies[i].has_value()) {
690 per_subtile_strategies.push_back(
691 per_anim_overrides.derive(anim_cfg.per_tile_pal_resolution_strategies[i]));
692 }
693 else {
694 per_subtile_strategies.push_back(effective_default);
695 }
696 }
697 }
698 else {
699 // AnimConfig exists but has no per-tile strategies. Use effective default for all subtiles
700 for (std::size_t i = 0; i < tile_count; ++i) {
701 per_subtile_strategies.push_back(effective_default);
702 }
703 }
704 }
705 else {
706 // No AnimConfig for this animation. Use global for all subtiles
707 for (std::size_t i = 0; i < tile_count; ++i) {
708 per_subtile_strategies.push_back(global_anim_pal_resolution_strategy);
709 }
710 }
711
712 /*
713 * Compute the effective multi-pal subtile resolution strategy: per-anim override wins, otherwise global fallback.
714 */
715 const ConfigValue<AnimMultiPalSubtileResolutionStrategy> effective_multi_pal_strategy =
716 (anim_cfg_ptr != nullptr && anim_cfg_ptr->multi_pal_subtile_resolution_strategy.has_value())
717 ? per_anim_overrides.derive(anim_cfg_ptr->multi_pal_subtile_resolution_strategy)
718 : global_anim_multi_pal_subtile_resolution_strategy;
719
720 // Resolve effective FrameLinking for this animation
721 const ConfigValue<FrameLinking> effective_linking = (anim_cfg_ptr != nullptr && anim_cfg_ptr->linking.has_value())
722 ? per_anim_overrides.derive(anim_cfg_ptr->linking)
723 : global_frame_linking;
724
725 if (effective_linking == FrameLinking::hybrid) {
726 std::vector<std::string> err_msg{};
727 err_msg.emplace_back(diag_->formatter().format(
728 "Hybrid frame linking is not yet implemented (animation '{}').", FormatParam{anim.name(), Style::bold}));
729 err_msg.emplace_back("Use 'automatic' or 'manual' frame linking until hybrid support.");
730 err_msg.append_range(format_config_note_with_separator(diag_->formatter(), effective_linking));
731 return FormattableError{err_msg};
732 }
733
734 /*
735 * Manual mode: extract override entries from metatiles_bin and skip key frame generation.
736 * Regular frames are still decompiled using the per-subtile palette resolution cascade.
737 */
738 if (effective_linking == FrameLinking::manual) {
739 std::vector<AnimOverrideEntry> overrides;
740 const std::size_t num_metatiles = metatiles_bin.size() / metatile::entries_per_metatile_triple;
741 for (std::size_t mt_idx = 0; mt_idx < num_metatiles; ++mt_idx) {
742 for (std::size_t local_idx = 0; local_idx < metatile::entries_per_metatile_triple; ++local_idx) {
743 const auto &entry = metatiles_bin[mt_idx * metatile::entries_per_metatile_triple + local_idx];
744 if (entry.tile_index() >= tile_offset && entry.tile_index() < tile_offset + tile_count) {
745 auto [layer, subtile] = metatile::from_internal_tile_index(local_idx);
746 overrides.push_back(
748 mt_idx,
749 layer,
750 subtile,
751 entry.tile_index() - tile_offset,
752 entry.pal_index(),
753 entry.h_flip(),
754 entry.v_flip()});
755 }
756 }
757 }
758
759 AnimParams result_params = anim.params();
760 result_params.overrides(std::move(overrides));
761 result.params(std::move(result_params));
762
763 // Decompile regular frames using per-subtile palette resolution
765 manual_pal_indices,
766 find_pals_for_anim_tiles(
767 anim.name(),
768 tile_offset,
769 tile_count,
770 metatiles_bin,
771 per_subtile_strategies,
772 effective_multi_pal_strategy,
773 anim,
774 pals,
775 tiles_png,
776 extrinsic_transparency,
777 *diag_,
778 *pal_printer_,
779 *tile_printer_),
781 diag_->formatter().format(
782 "Failed to find palette for animation '{}'.", FormatParam{anim.name(), Style::bold}));
783
784 for (const auto &frame : anim.frames_values()) {
785 std::vector<PixelTile<Rgba32>> rgba_tiles;
786 rgba_tiles.reserve(frame.tiles().size());
787 for (std::size_t i = 0; i < frame.tiles().size(); ++i) {
788 rgba_tiles.push_back(color_tile_from_index_tile(
789 frame.tiles()[i], pals.at(manual_pal_indices[i]), extrinsic_transparency.value()));
790 }
791 AnimFrame rgba_frame{frame.frame_name(), std::move(rgba_tiles)};
792 result.put_frame(frame.frame_name(), std::move(rgba_frame));
793 }
794
795 return result;
796 }
797
798 // Recover per-subtile palette indices
800 pal_indices,
801 find_pals_for_anim_tiles(
802 anim.name(),
803 tile_offset,
804 tile_count,
805 metatiles_bin,
806 per_subtile_strategies,
807 effective_multi_pal_strategy,
808 anim,
809 pals,
810 tiles_png,
811 extrinsic_transparency,
812 *diag_,
813 *pal_printer_,
814 *tile_printer_),
816 diag_->formatter().format("Failed to find palette for animation '{}'.", FormatParam{anim.name(), Style::bold}));
817
818 // Build per-tile palette pointer vector for the mangler and conversion
819 std::vector<const Palette<Rgba32, pal::max_size> *> pal_ptrs;
820 pal_ptrs.reserve(pal_indices.size());
821 for (std::size_t idx : pal_indices) {
822 pal_ptrs.push_back(&pals.at(idx));
823 }
824
825 // Extract key frame tiles from tiles.png
826 std::vector<PixelTile<IndexPixel>> key_frame_index_tiles =
827 extract_tiles_from_image(tiles_png, tile_offset, tile_count);
828
829 /*
830 * Build canonical tile set for all tiles in tiles.png OUTSIDE the current animation's key frame range, then merge
831 * in inter-animation canonical tiles. The combined set is used by the mangler to avoid producing canonical
832 * collisions with any already-used tile. For duplicate *categorization* (inter-anim vs cross-range), the separate
833 * inter_anim_canonical_tiles parameter is checked first so that tiles appearing in both sets are correctly reported
834 * as inter-animation duplicates rather than cross-range duplicates.
835 */
836 const std::size_t total_tiles =
837 (tiles_png.height() / tile::side_length_pix) * (tiles_png.width() / tile::side_length_pix);
838 std::set<PixelTile<IndexPixel>> existing_canonical_tiles;
839 for (std::size_t i = 0; i < total_tiles; ++i) {
840 if (i >= tile_offset && i < tile_offset + tile_count) {
841 continue;
842 }
844 const PixelTile<IndexPixel> &base = canonical;
845 existing_canonical_tiles.insert(base);
846 }
847
848 existing_canonical_tiles.insert(inter_anim_canonical_tiles.begin(), inter_anim_canonical_tiles.end());
849
850 /*
851 * Compute the effective key frame resolution strategy: per-anim override wins, otherwise global fallback.
852 */
853 const ConfigValue<AnimKeyFrameResolutionStrategy> effective_key_frame_strategy =
854 (anim_cfg_ptr != nullptr && anim_cfg_ptr->key_frame_resolution_strategy.has_value())
855 ? per_anim_overrides.derive(anim_cfg_ptr->key_frame_resolution_strategy)
856 : global_anim_key_frame_resolution_strategy;
857
858 /*
859 * Check for duplicate key frame tiles using canonical (flip-equivalent) comparison. Detects:
860 * - Inter-animation duplicates: animation tile matches another animation's key frame tile
861 * - Cross-range duplicates: animation tile matches a non-animation tile in tiles.png
862 * - Intra-animation duplicates: two animation tiles are flip-equivalent to each other
863 */
864 if (has_duplicate_key_frame_tiles(key_frame_index_tiles, inter_anim_canonical_tiles, existing_canonical_tiles)) {
865 switch (effective_key_frame_strategy.value()) {
867 const auto dup_info = categorize_duplicate_key_frame_tiles(
868 key_frame_index_tiles, inter_anim_canonical_tiles, existing_canonical_tiles);
869 std::vector<std::string> err_msg{};
870 err_msg.emplace_back(diag_->formatter().format(
871 "Animation '{}' has duplicate key frame tiles (flip-equivalent tiles are considered duplicates):",
872 FormatParam{anim.name(), Style::bold}));
873 for (const auto &idx : dup_info.inter_anim_indices) {
874 err_msg.emplace_back(diag_->formatter().format(
875 " - Tile {} is flip-equivalent to another animation's key frame tile.",
876 FormatParam{idx, Style::bold}));
877 }
878 for (const auto &idx : dup_info.cross_range_indices) {
879 err_msg.emplace_back(diag_->formatter().format(
880 " - Tile {} is flip-equivalent to a non-animation tile in tiles.png.",
881 FormatParam{idx, Style::bold}));
882 }
883 for (const auto &[i, j] : dup_info.intra_anim_pairs) {
884 err_msg.emplace_back(diag_->formatter().format(
885 " - Tile {} and tile {} are flip-equivalent.",
888 }
889 err_msg.emplace_back("");
890 err_msg.emplace_back("Consider using 'mangle' strategy to auto-resolve.");
891 err_msg.append_range(format_config_note_with_separator(diag_->formatter(), effective_key_frame_strategy));
892 return FormattableError{err_msg};
893 }
894
896 panic("warning not yet implemented");
897 }
898
900 AnimKeyFrameMangler mangler{diag_, tile_printer_};
902 mangle_result,
903 mangler.mangle_duplicates(
904 anim.name(),
905 std::move(key_frame_index_tiles),
906 pal_ptrs,
907 extrinsic_transparency.value(),
908 existing_canonical_tiles),
910 diag_->formatter().format(
911 "Failed to mangle duplicate key frame tiles for animation '{}'.",
912 FormatParam{anim.name(), Style::bold}));
913 key_frame_index_tiles = std::move(mangle_result.tiles);
914
915 // Backport changes to tiles.png
916 if (!mangle_result.mangle_records.empty()) {
917 backport_mangles_to_tiles_png(porymap_component, tile_offset, mangle_result.mangle_records);
918 }
919 break;
920 }
921
922 default:
923 panic("unhandled AnimKeyFrameResolutionStrategy value");
924 }
925 }
926
927 // Decompile key frame tiles to Rgba32 using per-subtile palettes
928 std::vector<PixelTile<Rgba32>> key_frame_rgba_tiles;
929 key_frame_rgba_tiles.reserve(key_frame_index_tiles.size());
930 for (std::size_t i = 0; i < key_frame_index_tiles.size(); ++i) {
931 key_frame_rgba_tiles.push_back(color_tile_from_index_tile(
932 key_frame_index_tiles[i], pals.at(pal_indices[i]), extrinsic_transparency.value()));
933 }
934
935 // Set the key frame on the result
936 AnimFrame key_frame{"key", std::move(key_frame_rgba_tiles)};
937 result.key_frame(std::move(key_frame));
938
939 for (const auto &frame : anim.frames_values()) {
940 if (frame.tiles().size() != tile_count) {
941 panic(
942 "frame '" + frame.frame_name() + "' tile count " + std::to_string(frame.tiles().size()) +
943 " != animation tile_count " + std::to_string(tile_count));
944 }
945
946 std::vector<PixelTile<Rgba32>> rgba_tiles;
947 rgba_tiles.reserve(frame.tiles().size());
948
949 for (std::size_t i = 0; i < frame.tiles().size(); ++i) {
950 rgba_tiles.push_back(
951 color_tile_from_index_tile(frame.tiles()[i], pals.at(pal_indices[i]), extrinsic_transparency.value()));
952 }
953
954 AnimFrame rgba_frame{frame.frame_name(), std::move(rgba_tiles)};
955 result.put_frame(frame.frame_name(), std::move(rgba_frame));
956 }
957
958 return result;
959}
960
961} // namespace porytiles
#define PT_TRY_ASSIGN_CHAIN_ERR(var, expr, return_type,...)
Unwraps a ChainableResult, chaining a new error message on failure.
ChainableResult< Animation< Rgba32 > > decompile_animation(const std::string &tileset_name, const Animation< IndexPixel > &anim, const std::set< PixelTile< IndexPixel > > &inter_anim_canonical_tiles, PorymapTilesetComponent &porymap_component) const
Decompiles an IndexPixel animation to Rgba32 format.
Represents a single frame of an animation, containing tiles and a frame name.
const std::string & frame_name() const
Service that mangles duplicate key frame tiles to make them unique.
Configuration parameters for a single tileset animation.
const std::vector< AnimOverrideEntry > & overrides() const
Returns the manual override entries for this animation.
std::size_t tile_offset() const
Returns the VRAM tile offset for this animation.
std::size_t tile_count() const
Returns the number of tiles per animation frame.
A complete tileset animation with name, configuration, and frame data.
Definition animation.hpp:78
const AnimParams & params() const
Definition animation.hpp:91
bool has_frames() const
Checks if this animation has any frames.
const std::string & name() const
Definition animation.hpp:86
std::vector< AnimFrame< PixelType > > frames_values() const
const std::map< std::string, AnimFrame< PixelType > > & frames() const
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 container that wraps a configuration value with its name and source information.
ConfigValue< U > derive(const ConfigPODField< U > &override) const
Creates a child ConfigValue from a ConfigPODField, inheriting this value's source provenance.
const T & value() const &
A text parameter with associated styling for formatted output.
General-purpose error implementation with formatted message support.
Definition error.hpp:63
A template for two-dimensional images with arbitrarily typed pixel values.
Definition image.hpp:23
void set(std::size_t i, PixelType pixel)
Sets the pixel value at a given one-dimensional pixel index.
Definition image.hpp:88
std::size_t width() const
Definition image.hpp:114
std::size_t height() const
Definition image.hpp:119
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
virtual std::vector< std::string > print_rgba_pal_with_highlights(const Palette< Rgba32 > &pal, const std::vector< std::size_t > &slots) const =0
A generic palette container for colors that support transparency checking.
Definition palette.hpp:47
An 8x8 tile backed by literal-array-based per-pixel storage of an arbitrary pixel type.
const std::array< Palette< Rgba32, pal::max_size >, pal::num_pals > & pals() const
const std::vector< TilemapEntry > & metatiles_bin() const
const Image< IndexPixel > & tiles_png() const
Represents a 32-bit RGBA color.
Definition rgba32.hpp:23
bool is_transparent(const Rgba32 &extrinsic) const
Checks if this color should be treated as transparent.
Definition rgba32.cpp:19
bool is_extrinsically_transparent(const Rgba32 &extrinsic) const
Checks if this color matches the extrinsic transparency color.
Definition rgba32.cpp:14
static const Style bold
Bold text formatting.
virtual std::string format(const std::string &format_str, const std::vector< FormatParam > &params) const
Formats a string with styled parameters using fmtlib syntax.
A collection of printer functions for various tile types.
virtual std::vector< std::string > print_tile(const PixelTile< Rgba32 > &tile, const Rgba32 &extrinsic_transparency) const =0
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.
const TextFormatter & formatter() const
virtual void warning(const std::string &tag, const std::vector< std::string > &lines) const =0
Display a tagged warning message.
virtual void remark_note(const std::string &tag, const std::vector< std::string > &lines) const =0
Display a tagged note message associated with a remark.
constexpr std::size_t entries_per_metatile_triple
Definition metatile.hpp:21
constexpr std::size_t tiles_per_side
Definition metatile.hpp:16
constexpr std::size_t metatiles_per_row
Definition metatile.hpp:22
std::tuple< Layer, Subtile > from_internal_tile_index(std::size_t tile_index)
Decomposes an internal tile index into its layer and subtile position within a metatile.
Definition metatile.hpp:117
constexpr std::size_t num_pals
Definition palette.hpp:21
constexpr std::size_t max_size
Definition palette.hpp:19
constexpr std::size_t side_length_pix
constexpr std::pair< std::size_t, std::size_t > index_to_row_col(std::size_t index)
Converts a linear index to row and column coordinates.
PixelTile< PixelType > extract_single_tile(const Image< PixelType > &img, std::size_t tile_idx, std::size_t tiles_per_row=metatile::metatiles_per_row *metatile::tiles_per_side)
Extracts a single tile from an image at a given tile index.
void panic(const StringViewSourceLoc &s)
Unconditionally terminates the program with a panic message.
Definition panic.cpp:43
std::vector< PixelTile< PixelType > > extract_tiles_from_image(const Image< PixelType > &img, std::size_t tile_offset, std::size_t tile_count, std::size_t tiles_per_row=16)
Extracts a subset of 8x8 tiles from a tileset image at a specific offset.
AnimPalResolutionStrategy
Strategy for determining which palette to use when decompiling animation tiles.
@ canonical
Export tiles in canonical form without applying flip transformations.
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.
std::vector< std::string > format_config_note_with_separator(const TextFormatter &format, const ConfigValue< T > &config)
Format a ConfigValue into diagnostic note lines with a separator.
@ warning
Emit a warning and continue decompilation.
@ mangle
Mangle duplicate tiles to make them unique, then backport changes to tiles.png.
@ error
Emit a formatted error and fail decompilation.
@ manual
Use manual overrides in anim.json.
@ hybrid
Automatic key.png linking plus manual override pass (not yet implemented).
PixelTile< ColorType > color_tile_from_index_tile(const PixelTile< IndexPixel > &index_tile, const Palette< ColorType, N > &palette)
Converts a PixelTile<IndexPixel> to a PixelTile<ColorType> using a palette (intrinsic transparency).
A manual override that maps a specific metatile entry to an animation subtile.
std::size_t pal_index
The palette index to use for this tile.
bool has_value() const
Checks whether this field has a value set.
Per-animation configuration override for animation decompilation.
std::vector< ConfigPODField< AnimPalResolutionStrategy > > per_tile_pal_resolution_strategies
ConfigPODField< AnimMultiPalSubtileResolutionStrategy > multi_pal_subtile_resolution_strategy
ConfigPODField< AnimKeyFrameResolutionStrategy > key_frame_resolution_strategy
ConfigPODField< FrameLinking > linking
ConfigPODField< AnimPalResolutionStrategy > pal_resolution_strategy
#define PT_UNWRAP_TILESET_CONFIG_PTR(ptr, config, tileset_name, return_type)
Unwraps a tileset-scoped config value via pointer access, returning early if the value is not availab...