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
34
35namespace {
36
37using namespace porytiles;
38
39[[nodiscard]] ChainableResult<std::size_t> internal_png_palette_strategy(
40 const Animation<IndexPixel> &anim,
41 const std::array<Palette<Rgba32, palette::max_size>, palette::num_palettes> &tileset_palettes,
42 const ConfigValue<Rgba32> &extrinsic_transparency,
43 const UserDiagnostics &diag,
44 const PalettePrinter &palette_printer)
45{
46 if (!anim.has_frames()) {
47 panic("anim '" + anim.name() + "' has no frames");
48 }
49
50 const auto &representative_frame = anim.frames().begin()->second;
51 const auto &representative_palette = representative_frame.palette();
52
53 // Representative palette must have exactly 16 colors to match GBA palette format
54 if (representative_palette.size() != palette::max_size) {
55 std::vector<std::string> err_msg{};
56 err_msg.emplace_back(diag.formatter().format(
57 "Representative frame '{}' internal palette size '{}': must be '{}'.",
58 FormatParam{representative_frame.frame_name(), Style::bold},
59 FormatParam{representative_palette.size(), Style::bold},
60 FormatParam{palette::max_size, Style::bold}));
61 err_msg.emplace_back("");
62 err_msg.append_range(palette_printer.print_rgba_palette(representative_palette));
63 return FormattableError{err_msg};
64 }
65
66 // Check for extrinsic transparency in non-slot-0 positions in representative palette
67 std::vector<std::size_t> extrinsic_transparency_slots;
68 for (std::size_t slot = 1; slot < palette::max_size; ++slot) {
69 const Rgba32 &color = representative_palette.at(slot);
70 if (color.is_extrinsically_transparent(extrinsic_transparency)) {
71 extrinsic_transparency_slots.push_back(slot);
72 }
73 }
74
75 if (!extrinsic_transparency_slots.empty()) {
76 std::string slot_list;
77 for (const auto &slot : extrinsic_transparency_slots) {
78 if (!slot_list.empty()) {
79 slot_list += ", ";
80 }
81 slot_list += std::to_string(slot);
82 }
83 std::vector<std::string> err_msg{};
84 err_msg.emplace_back(diag.formatter().format(
85 "Representative frame '{}' palette contains extrinsic transparency color '{}' in non-zero slot(s): {}",
86 FormatParam{representative_frame.frame_name(), Style::bold},
87 FormatParam{extrinsic_transparency.value().to_jasc_str(), Style::bold},
88 FormatParam{slot_list, Style::bold}));
89 err_msg.emplace_back("");
90 err_msg.emplace_back("The extrinsic transparency color should only appear in slot 0.");
91 err_msg.emplace_back("Either correct the PNG palette or change the extrinsic transparency setting.");
92 err_msg.emplace_back("");
93 err_msg.append_range(
94 palette_printer.print_rgba_palette_with_highlights(representative_palette, extrinsic_transparency_slots));
95 err_msg.append_range(format_config_note_with_separator(diag.formatter(), extrinsic_transparency));
96 return FormattableError{err_msg};
97 }
98
99 // Check all frames share the same internal palette as the representative
100 for (const auto &[frame_name, frame] : anim.frames()) {
101 if (&frame == &representative_frame) {
102 continue;
103 }
104 const auto &frame_palette = frame.palette();
105 bool palettes_match = (frame_palette.size() == representative_palette.size());
106 if (palettes_match) {
107 for (std::size_t slot = 0; slot < representative_palette.size(); ++slot) {
108 if (frame_palette.at(slot) != representative_palette.at(slot)) {
109 palettes_match = false;
110 break;
111 }
112 }
113 }
114 if (!palettes_match) {
115 std::vector<std::string> err_msg{};
116 err_msg.emplace_back(diag.formatter().format(
117 "Animation '{}' frame '{}' has an internal palette that does not match the representative frame '{}' "
118 "palette.",
120 FormatParam{frame_name, Style::bold},
121 FormatParam{representative_frame.frame_name(), Style::bold}));
122 err_msg.emplace_back("");
123 err_msg.emplace_back(diag.formatter().format(
124 "Representative frame '{}' palette:", FormatParam{representative_frame.frame_name(), Style::bold}));
125 err_msg.append_range(palette_printer.print_rgba_palette(representative_palette));
126 err_msg.emplace_back("");
127 err_msg.emplace_back(diag.formatter().format("Frame '{}' palette:", FormatParam{frame_name, Style::bold}));
128 err_msg.append_range(palette_printer.print_rgba_palette(frame_palette));
129 return FormattableError{err_msg};
130 }
131 }
132
133 // Now that we fully validated the representative palette, and we confirmed that all frame palettes match, we can
134 // try to match the representative palette to one of the tileset palettes.
135 for (std::size_t palette_idx = 0; palette_idx < tileset_palettes.size(); ++palette_idx) {
136 bool matches = true;
137 for (std::size_t slot = 1; slot < palette::max_size; ++slot) {
138 const Rgba32 &png_palette_color = representative_palette.at(slot);
139
140 // This should never happen, we returned early above if we hit this
141 if (png_palette_color.is_transparent(extrinsic_transparency)) {
142 panic("png_palette slot " + std::to_string(slot) + " is extrinsically transparent");
143 }
144
145 const Rgba32 &tileset_color = tileset_palettes[palette_idx].at(slot);
146 if (png_palette_color != tileset_color) {
147 matches = false;
148 break;
149 }
150 }
151
152 if (matches) {
153 // Emit remark showing matched palette
154 std::vector<std::string> remark_lines;
155 remark_lines.emplace_back(diag.formatter().format(
156 "Animation '{}' representative frame '{}' internal palette matched Porymap palette '{}':",
157 FormatParam{anim.name(), Style::bold},
158 FormatParam{representative_frame.frame_name(), Style::bold},
159 FormatParam{palette_filename(palette_idx), Style::bold}));
160 remark_lines.emplace_back("");
161 remark_lines.append_range(palette_printer.print_rgba_palette(tileset_palettes[palette_idx]));
162 diag.remark("animation-palette-resolution-strategy", remark_lines);
163 return palette_idx;
164 }
165 }
166
167 std::vector<std::string> err_msg{};
168 err_msg.emplace_back(diag.formatter().format(
169 "Failed to find matching palette for internal palette of representative frame '{}'.",
170 FormatParam{representative_frame.frame_name(), Style::bold}));
171 err_msg.emplace_back("");
172 err_msg.append_range(palette_printer.print_rgba_palette(representative_palette));
173 return FormattableError{err_msg};
174}
175
176[[nodiscard]] std::optional<std::size_t> extract_palette_index(AnimPaletteResolutionStrategy strategy)
177{
178 switch (strategy) {
179 case AnimPaletteResolutionStrategy::palette_00:
180 return 0;
181 case AnimPaletteResolutionStrategy::palette_01:
182 return 1;
183 case AnimPaletteResolutionStrategy::palette_02:
184 return 2;
185 case AnimPaletteResolutionStrategy::palette_03:
186 return 3;
187 case AnimPaletteResolutionStrategy::palette_04:
188 return 4;
189 case AnimPaletteResolutionStrategy::palette_05:
190 return 5;
191 case AnimPaletteResolutionStrategy::palette_06:
192 return 6;
193 case AnimPaletteResolutionStrategy::palette_07:
194 return 7;
195 case AnimPaletteResolutionStrategy::palette_08:
196 return 8;
197 case AnimPaletteResolutionStrategy::palette_09:
198 return 9;
199 case AnimPaletteResolutionStrategy::palette_10:
200 return 10;
201 case AnimPaletteResolutionStrategy::palette_11:
202 return 11;
203 case AnimPaletteResolutionStrategy::palette_12:
204 return 12;
205 case AnimPaletteResolutionStrategy::palette_13:
206 return 13;
207 case AnimPaletteResolutionStrategy::palette_14:
208 return 14;
209 case AnimPaletteResolutionStrategy::palette_15:
210 return 15;
211 default:
212 return std::nullopt;
213 }
214}
215
236[[nodiscard]] ChainableResult<std::size_t> resolve_subtile_palette(
237 const std::string &anim_name,
238 std::size_t subtile_index,
239 std::size_t tile_index,
240 std::span<const TilemapEntry> metatiles_bin,
242 const ConfigValue<AnimMultiPaletteSubtileResolutionStrategy> &multi_palette_strategy,
243 const Animation<IndexPixel> &anim,
245 const Image<IndexPixel> &tiles_png,
246 const ConfigValue<Rgba32> &extrinsic_transparency,
247 const UserDiagnostics &diag,
248 const PalettePrinter &palette_printer,
249 const TilePrinter &tile_printer,
250 std::optional<std::size_t> &internal_png_palette_cache)
251{
252 // Check the palette_00..palette_15 strategies first: direct palette index assignment
253 const auto explicit_palette = extract_palette_index(strategy.value());
254 if (explicit_palette.has_value()) {
255 diag.remark(
256 "animation-palette-resolution-strategy",
257 {diag.formatter().format(
258 "Animation '{}' subtile {} using explicit palette '{}'.",
259 FormatParam{anim_name, Style::bold},
260 FormatParam{subtile_index, Style::bold},
261 FormatParam{palette_filename(*explicit_palette), Style::bold})});
262 diag.remark_note("animation-palette-resolution-strategy", format_config_note(diag.formatter(), strategy));
263 return *explicit_palette;
264 }
265
266 switch (strategy.value()) {
267 case AnimPaletteResolutionStrategy::scan_local_metatiles: {
268 std::set<std::size_t> found_for_subtile{};
269
270 for (const auto &entry : metatiles_bin) {
271 if (entry.tile_index() == tile_index) {
272 found_for_subtile.insert(entry.palette_index());
273 }
274 }
275
276 if (found_for_subtile.empty()) {
277 std::vector<std::string> err_msg{};
278 err_msg.emplace_back(diag.formatter().format(
279 "Animation '{}' subtile {} at tile index '{}' is not referenced in local metatiles.",
280 FormatParam{anim_name, Style::bold},
281 FormatParam{subtile_index, Style::bold},
282 FormatParam{tile_index, Style::bold}));
283 err_msg.emplace_back(
284 "Consider using a different palette resolution strategy (e.g. 'palette-00', "
285 "'internal-png-palette', etc.).");
286 err_msg.append_range(format_config_note_with_separator(diag.formatter(), strategy));
287 return FormattableError{err_msg};
288 }
289
290 if (found_for_subtile.size() > 1) {
291 // A single tile index can be referenced by multiple metatile entries with different palette indices.
292 // This is valid GBA behavior. The hardware selects palette per metatile entry, not per tile.
293 //
294 // The multi_palette_strategy config determines how to handle this case.
295 std::string palette_list;
296 for (const auto &palette_idx : found_for_subtile) {
297 if (!palette_list.empty()) {
298 palette_list += ", ";
299 }
300 palette_list += palette_filename(palette_idx);
301 }
302
303 switch (multi_palette_strategy.value()) {
304 case AnimMultiPaletteSubtileResolutionStrategy::error: {
305 std::vector<std::string> err_msg;
306 err_msg.push_back(diag.formatter().format(
307 "Animation '{}' subtile {} at tile index '{}' is referenced with multiple palettes: {}.",
308 FormatParam{anim_name, Style::bold},
309 FormatParam{subtile_index, Style::bold},
310 FormatParam{tile_index, Style::bold},
311 FormatParam{palette_list, Style::bold}));
312 err_msg.emplace_back(
313 "Picking one palette arbitrarily would produce incorrect RGBA output in the layer PNGs.");
314
315 const PixelTile<IndexPixel> index_tile = extract_single_tile(tiles_png, tile_index);
316 err_msg.emplace_back("");
317 for (const auto &palette_idx : found_for_subtile) {
319 index_tile, palettes.at(palette_idx), extrinsic_transparency.value());
320 err_msg.push_back(diag.formatter().format(
321 "Tile under palette '{}':", FormatParam{palette_filename(palette_idx), Style::bold}));
322 err_msg.append_range(tile_printer.print_tile(rgba_tile, extrinsic_transparency.value()));
323 }
324
325 err_msg.emplace_back("");
326 err_msg.emplace_back(
327 "Consider using an explicit palette resolution strategy (e.g. 'palette-00') to resolve the "
328 "ambiguity.");
329 err_msg.append_range(format_config_note_with_separator(diag.formatter(), strategy));
330 return FormattableError{err_msg};
331 }
332 case AnimMultiPaletteSubtileResolutionStrategy::warning: {
333 const std::size_t chosen_palette = *found_for_subtile.begin();
334
335 std::vector<std::string> warn_msg;
336 warn_msg.push_back(diag.formatter().format(
337 "Animation '{}' subtile {} at tile index '{}' is referenced with multiple palettes: {}.",
338 FormatParam{anim_name, Style::bold},
339 FormatParam{subtile_index, Style::bold},
340 FormatParam{tile_index, Style::bold},
341 FormatParam{palette_list, Style::bold}));
342 warn_msg.push_back(diag.formatter().format(
343 "Using palette '{}'. Set 'frame_linking: manual' to handle palette assignment via overrides.",
344 FormatParam{palette_filename(chosen_palette), Style::bold}));
345
346 const PixelTile<IndexPixel> warn_index_tile = extract_single_tile(tiles_png, tile_index);
347 warn_msg.emplace_back("");
348 for (const auto &palette_idx : found_for_subtile) {
350 warn_index_tile, palettes.at(palette_idx), extrinsic_transparency.value());
351 warn_msg.push_back(diag.formatter().format(
352 "Tile under palette '{}':", FormatParam{palette_filename(palette_idx), Style::bold}));
353 warn_msg.append_range(tile_printer.print_tile(rgba_tile, extrinsic_transparency.value()));
354 }
355
356 warn_msg.append_range(format_config_note_with_separator(diag.formatter(), multi_palette_strategy));
357 diag.warning("animation-multi-pal-subtile", warn_msg);
358
359 return chosen_palette;
360 }
361 case AnimMultiPaletteSubtileResolutionStrategy::split:
362 return FormattableError{
363 "The 'split' mode for multi-palette subtile resolution is not yet implemented."};
364 }
365 }
366
367 return *found_for_subtile.begin();
368 }
369
370 case AnimPaletteResolutionStrategy::internal_png_palette: {
371 if (internal_png_palette_cache.has_value()) {
372 return *internal_png_palette_cache;
373 }
374 std::vector<std::string> err_msg{};
375 err_msg.emplace_back(diag.formatter().format(
376 "Palette resolution strategy '{}' failed.",
377 FormatParam{to_string(AnimPaletteResolutionStrategy::internal_png_palette), Style::bold}));
378 err_msg.append_range(format_config_note_with_separator(diag.formatter(), strategy));
380 match,
381 internal_png_palette_strategy(anim, palettes, extrinsic_transparency, diag, palette_printer),
382 std::size_t,
383 err_msg);
384 internal_png_palette_cache = match;
385 return match;
386 }
387
388 case AnimPaletteResolutionStrategy::scan_all_tilesets:
389 panic("scan_all_tilesets not yet implemented");
390
391 default:
392 panic("unhandled AnimPaletteResolutionStrategy value");
393 }
394}
395
396[[nodiscard]] ChainableResult<std::vector<std::size_t>> find_palettes_for_anim_tiles(
397 const std::string &anim_name,
398 std::size_t tile_offset,
399 std::size_t tile_count,
400 std::span<const TilemapEntry> metatiles_bin,
401 const std::vector<ConfigValue<AnimPaletteResolutionStrategy>> &per_subtile_strategies,
402 const ConfigValue<AnimMultiPaletteSubtileResolutionStrategy> &multi_palette_strategy,
403 const Animation<IndexPixel> &anim,
405 const Image<IndexPixel> &tiles_png,
406 const ConfigValue<Rgba32> &extrinsic_transparency,
407 const UserDiagnostics &diag,
408 const PalettePrinter &palette_printer,
409 const TilePrinter &tile_printer)
410{
411 if (per_subtile_strategies.size() != tile_count) {
412 panic(
413 "per_subtile_strategies size " + std::to_string(per_subtile_strategies.size()) + " != tile_count " +
414 std::to_string(tile_count));
415 }
416
417 std::vector<std::size_t> per_tile_palettes(tile_count);
418 std::optional<std::size_t> internal_png_palette_cache;
419
420 for (std::size_t i = 0; i < tile_count; ++i) {
421 const std::size_t tile_index = tile_offset + i;
423 palette_idx,
424 resolve_subtile_palette(
425 anim_name,
426 i,
427 tile_index,
428 metatiles_bin,
429 per_subtile_strategies[i],
430 multi_palette_strategy,
431 anim,
432 palettes,
433 tiles_png,
434 extrinsic_transparency,
435 diag,
436 palette_printer,
437 tile_printer,
438 internal_png_palette_cache),
439 std::vector<std::size_t>,
440 diag.formatter().format(
441 "Failed to resolve palette for animation '{}' subtile {}.",
442 FormatParam{anim_name, Style::bold},
443 FormatParam{i, Style::bold}));
444 per_tile_palettes[i] = palette_idx;
445 }
446
447 // Emit a remark if multiple distinct palettes are used across subtiles
448 if (tile_count > 1) {
449 const std::size_t first_palette = per_tile_palettes.at(0);
450 const bool uses_multiple_palettes =
451 !std::ranges::all_of(per_tile_palettes, [&](std::size_t idx) { return idx == first_palette; });
452 if (uses_multiple_palettes) {
453 std::set<std::size_t> unique_palettes{per_tile_palettes.begin(), per_tile_palettes.end()};
454 std::string palette_list;
455 for (const auto &palette_idx : unique_palettes) {
456 if (!palette_list.empty()) {
457 palette_list += ", ";
458 }
459 palette_list += palette_filename(palette_idx);
460 }
461 diag.remark(
462 "animation-palette-resolution-strategy",
463 {diag.formatter().format(
464 "Animation '{}' uses multiple palettes across subtiles: {}.",
465 FormatParam{anim_name, Style::bold},
466 FormatParam{palette_list, Style::bold})});
467 }
468 }
469
470 return per_tile_palettes;
471}
472
473struct DuplicateInfo {
474 std::vector<std::size_t> inter_anim_indices;
475 std::vector<std::size_t> cross_range_indices;
476 std::vector<std::pair<std::size_t, std::size_t>> intra_anim_pairs;
477
478 [[nodiscard]] bool any() const
479 {
480 return !inter_anim_indices.empty() || !cross_range_indices.empty() || !intra_anim_pairs.empty();
481 }
482};
483
497[[nodiscard]] DuplicateInfo categorize_duplicate_key_frame_tiles(
498 const std::vector<PixelTile<Rgba32>> &key_frame_canonical_rgba_tiles,
499 const std::set<PixelTile<Rgba32>> &inter_anim_canonical_tiles,
500 const std::vector<const std::set<PixelTile<Rgba32>> *> &external_canonical_rgba_tiles)
501{
502 DuplicateInfo info;
503
504 // Map from canonical decoded tile to first index seen
505 std::map<PixelTile<Rgba32>, std::size_t> seen;
506
507 for (std::size_t i = 0; i < key_frame_canonical_rgba_tiles.size(); ++i) {
508 const PixelTile<Rgba32> &base = key_frame_canonical_rgba_tiles[i];
509
510 // Check inter-animation before cross-range (more specific category wins)
511 if (inter_anim_canonical_tiles.contains(base)) {
512 info.inter_anim_indices.push_back(i);
513 }
514 else if (external_canonical_rgba_tiles[i]->contains(base)) {
515 info.cross_range_indices.push_back(i);
516 }
517
518 auto [it, inserted] = seen.emplace(base, i);
519 if (!inserted) {
520 info.intra_anim_pairs.emplace_back(it->second, i);
521 }
522 }
523
524 return info;
525}
526
532void backport_mangles_to_tiles_png(
533 PorymapTilesetComponent &component, std::size_t base_tile_offset, const std::set<TileMangleRecord> &records)
534{
535 Image<IndexPixel> tiles_img = component.tiles_png();
536 constexpr std::size_t tiles_per_row = metatile::metatiles_per_row * metatile::tiles_per_side;
537
538 // Mangle records are non-overlapping: each targets a distinct tile_index (guaranteed by mangle_duplicates).
539 // Sequential application is therefore safe and order-independent, producing results consistent with the in-memory
540 // key frame tiles that were mangled during decompilation.
541 for (const auto &record : records) {
542 const std::size_t global_tile_idx = base_tile_offset + record.tile_index;
543 const std::size_t tile_row = global_tile_idx / tiles_per_row;
544 const std::size_t tile_col = global_tile_idx % tiles_per_row;
545
546 for (const auto &change : record.pixel_changes) {
547 const auto [pixel_row, pixel_col] = tile::index_to_row_col(change.pixel_index);
548 const std::size_t img_row = tile_row * tile::side_length_pix + pixel_row;
549 const std::size_t img_col = tile_col * tile::side_length_pix + pixel_col;
550
551 tiles_img.set(img_row, img_col, change.mangled_pixel);
552 }
553 }
554
555 component.tiles_png(tiles_img);
556}
557
558} // namespace
559
560namespace porytiles {
561
563 const std::string &tileset_name,
564 const Animation<IndexPixel> &anim,
565 const std::set<PixelTile<Rgba32>> &inter_anim_canonical_tiles,
566 PorymapTilesetComponent &porymap_component) const
567{
568 // Unwrap config values
569 PT_UNWRAP_TILESET_CONFIG_PTR(config_, extrinsic_transparency, tileset_name, Animation<Rgba32>);
570 PT_UNWRAP_TILESET_CONFIG_PTR(config_, global_anim_palette_resolution_strategy, tileset_name, Animation<Rgba32>);
571 PT_UNWRAP_TILESET_CONFIG_PTR(config_, global_anim_key_frame_resolution_strategy, tileset_name, Animation<Rgba32>);
573 config_, global_anim_multi_palette_subtile_resolution_strategy, tileset_name, Animation<Rgba32>);
574 PT_UNWRAP_TILESET_CONFIG_PTR(config_, global_frame_linking, tileset_name, Animation<Rgba32>);
575 PT_UNWRAP_TILESET_CONFIG_PTR(config_, per_anim_overrides, tileset_name, Animation<Rgba32>);
576
577 // Read data from porymap_component
578 const auto &palettes = porymap_component.palettes();
579 const auto &metatiles_bin = porymap_component.metatiles_bin();
580 const auto &tiles_png = porymap_component.tiles_png();
581
582 Animation<Rgba32> result{anim.name()};
583 result.params(anim.params());
584
585 // Get the tile offset from animation params to determine which tile index to look for in metatiles
586 const std::size_t tile_offset = anim.params().tile_offset();
587 const std::size_t tile_count = anim.params().tile_count();
588
589 // Validate tile range before any arithmetic that assumes tile_count > 0
590 if (tile_count == 0) {
591 return FormattableError{
592 std::vector<std::string>{
593 "Animation '{}' has a tile count of '{}'.",
594 "The animation's generated C code may not have been parsed correctly, or the animation defines no "
595 "parameters."},
596 std::vector<std::vector<FormatParam>>{
597 {FormatParam{anim.name(), Style::bold}, FormatParam{tile_count, Style::bold}}, {}}};
598 }
599 if (tile_offset == 0) {
600 return FormattableError{
601 std::vector<std::string>{
602 "Animation '{}' has a tile offset of '{}'.",
603 "Tile 0 in tiles.png is reserved and cannot be an animation tile."},
604 std::vector<std::vector<FormatParam>>{
605 {FormatParam{anim.name(), Style::bold}, FormatParam{tile_offset, Style::bold}}, {}}};
606 }
607
608 // Build per-subtile palette resolution strategies using a three-tier cascade:
609 // 1. Per-tile (per_tile_palette_resolution_strategies[i]): most specific
610 // 2. Per-anim (palette_resolution_strategy): middle tier
611 // 3. Global (global_anim_palette_resolution_strategy): least specific fallback
612 std::vector<ConfigValue<AnimPaletteResolutionStrategy>> per_subtile_strategies;
613 per_subtile_strategies.reserve(tile_count);
614
615 const auto &configs_map = per_anim_overrides.value();
616 const auto anim_cfg_it = configs_map.find(anim.name());
617 const PerAnimOverride *anim_cfg_ptr = (anim_cfg_it != configs_map.end()) ? &anim_cfg_it->second : nullptr;
618
619 if (anim_cfg_ptr != nullptr) {
620 const PerAnimOverride &anim_cfg = *anim_cfg_ptr;
621
622 // Determine the "effective default" for this animation: per-anim if set, otherwise global
623 const ConfigValue<AnimPaletteResolutionStrategy> effective_default =
625 ? per_anim_overrides.derive(anim_cfg.palette_resolution_strategy)
626 : global_anim_palette_resolution_strategy;
627
628 if (!anim_cfg.per_tile_palette_resolution_strategies.empty()) {
629 if (anim_cfg.per_tile_palette_resolution_strategies.size() != tile_count) {
630 return FormattableError{
631 std::vector<std::string>{
632 "Animation '{}' config 'per_tile_palette_resolution_strategies' has '{}' entries, but "
633 "animation has '{}' subtiles.",
634 "The per_tile_palette_resolution_strategies list must have exactly one entry per subtile."},
635 std::vector<std::vector<FormatParam>>{
636 {FormatParam{anim.name(), Style::bold},
638 FormatParam{tile_count, Style::bold}},
639 {}}};
640 }
641 for (std::size_t i = 0; i < tile_count; ++i) {
642 if (anim_cfg.per_tile_palette_resolution_strategies[i].has_value()) {
643 per_subtile_strategies.push_back(
644 per_anim_overrides.derive(anim_cfg.per_tile_palette_resolution_strategies[i]));
645 }
646 else {
647 per_subtile_strategies.push_back(effective_default);
648 }
649 }
650 }
651 else {
652 // AnimConfig exists but has no per-tile strategies. Use effective default for all subtiles
653 for (std::size_t i = 0; i < tile_count; ++i) {
654 per_subtile_strategies.push_back(effective_default);
655 }
656 }
657 }
658 else {
659 // No AnimConfig for this animation. Use global for all subtiles
660 for (std::size_t i = 0; i < tile_count; ++i) {
661 per_subtile_strategies.push_back(global_anim_palette_resolution_strategy);
662 }
663 }
664
665 // Compute the effective multi-palette subtile resolution strategy: per-anim override wins, otherwise global
666 // fallback.
667 const ConfigValue<AnimMultiPaletteSubtileResolutionStrategy> effective_multi_palette_strategy =
668 (anim_cfg_ptr != nullptr && anim_cfg_ptr->multi_palette_subtile_resolution_strategy.has_value())
669 ? per_anim_overrides.derive(anim_cfg_ptr->multi_palette_subtile_resolution_strategy)
670 : global_anim_multi_palette_subtile_resolution_strategy;
671
672 // Resolve effective FrameLinking for this animation
673 const ConfigValue<FrameLinking> effective_linking = (anim_cfg_ptr != nullptr && anim_cfg_ptr->linking.has_value())
674 ? per_anim_overrides.derive(anim_cfg_ptr->linking)
675 : global_frame_linking;
676
677 if (effective_linking == FrameLinking::hybrid) {
678 std::vector<std::string> err_msg{};
679 err_msg.emplace_back(diag_->formatter().format(
680 "Hybrid frame linking is not yet implemented (animation '{}').", FormatParam{anim.name(), Style::bold}));
681 err_msg.emplace_back("Use 'automatic' or 'manual' frame linking until hybrid support.");
682 err_msg.append_range(format_config_note_with_separator(diag_->formatter(), effective_linking));
683 return FormattableError{err_msg};
684 }
685
686 // Manual mode: extract override entries from metatiles_bin and skip key frame generation.
687 // Regular frames are still decompiled using the per-subtile palette resolution cascade.
688 if (effective_linking == FrameLinking::manual) {
689 std::vector<AnimOverrideEntry> overrides;
690 const std::size_t num_metatiles = metatiles_bin.size() / metatile::entries_per_metatile_triple;
691 for (std::size_t mt_idx = 0; mt_idx < num_metatiles; ++mt_idx) {
692 for (std::size_t local_idx = 0; local_idx < metatile::entries_per_metatile_triple; ++local_idx) {
693 const auto &entry = metatiles_bin[mt_idx * metatile::entries_per_metatile_triple + local_idx];
694 if (entry.tile_index() >= tile_offset && entry.tile_index() < tile_offset + tile_count) {
695 auto [layer, subtile] = metatile::from_internal_tile_index(local_idx);
696 overrides.push_back(
698 mt_idx,
699 layer,
700 subtile,
701 entry.tile_index() - tile_offset,
702 entry.palette_index(),
703 entry.h_flip(),
704 entry.v_flip()});
705 }
706 }
707 }
708
709 AnimParams result_params = anim.params();
710 result_params.overrides(std::move(overrides));
711 result.params(std::move(result_params));
712
713 // Decompile regular frames using per-subtile palette resolution
715 manual_palette_indices,
716 find_palettes_for_anim_tiles(
717 anim.name(),
718 tile_offset,
719 tile_count,
720 metatiles_bin,
721 per_subtile_strategies,
722 effective_multi_palette_strategy,
723 anim,
724 palettes,
725 tiles_png,
726 extrinsic_transparency,
727 *diag_,
728 *palette_printer_,
729 *tile_printer_),
731 diag_->formatter().format(
732 "Failed to find palette for animation '{}'.", FormatParam{anim.name(), Style::bold}));
733
734 for (const auto &frame : anim.frames_values()) {
735 std::vector<PixelTile<Rgba32>> rgba_tiles;
736 rgba_tiles.reserve(frame.tiles().size());
737 for (std::size_t i = 0; i < frame.tiles().size(); ++i) {
738 rgba_tiles.push_back(color_tile_from_index_tile(
739 frame.tiles()[i], palettes.at(manual_palette_indices[i]), extrinsic_transparency.value()));
740 }
741 AnimFrame rgba_frame{frame.frame_name(), std::move(rgba_tiles)};
742 result.put_frame(frame.frame_name(), std::move(rgba_frame));
743 }
744
745 return result;
746 }
747
748 // Recover per-subtile palette indices
750 palette_indices,
751 find_palettes_for_anim_tiles(
752 anim.name(),
753 tile_offset,
754 tile_count,
755 metatiles_bin,
756 per_subtile_strategies,
757 effective_multi_palette_strategy,
758 anim,
759 palettes,
760 tiles_png,
761 extrinsic_transparency,
762 *diag_,
763 *palette_printer_,
764 *tile_printer_),
766 diag_->formatter().format("Failed to find palette for animation '{}'.", FormatParam{anim.name(), Style::bold}));
767
768 // Build per-tile palette pointer vector for the mangler and conversion
769 std::vector<const Palette<Rgba32, palette::max_size> *> palette_ptrs;
770 palette_ptrs.reserve(palette_indices.size());
771 for (std::size_t idx : palette_indices) {
772 palette_ptrs.push_back(&palettes.at(idx));
773 }
774
775 // Extract key frame tiles from tiles.png
776 std::vector<PixelTile<IndexPixel>> key_frame_index_tiles =
777 extract_tiles_from_image(tiles_png, tile_offset, tile_count);
778
779 // Decode each key frame subtile to its canonical RGBA form. Duplicate detection and mangling both operate on
780 // decoded colors because the compile-side key frame validation compares key.png tiles by color: two index tiles
781 // that reference different palette slots holding the same color are duplicates there, even though they differ in
782 // index space.
783 std::vector<PixelTile<Rgba32>> key_frame_canonical_rgba_tiles;
784 key_frame_canonical_rgba_tiles.reserve(tile_count);
785 for (std::size_t i = 0; i < key_frame_index_tiles.size(); ++i) {
786 key_frame_canonical_rgba_tiles.push_back(canonical_color_tile_from_index_tile(
787 key_frame_index_tiles[i], *palette_ptrs[i], extrinsic_transparency.value()));
788 }
789
790 // Build, per distinct subtile palette, the canonical RGBA forms of all tiles.png tiles OUTSIDE the current
791 // animation's key frame range. Tiles carry no palette of their own in tiles.png, so each one is resolved under the
792 // palettes this animation's subtiles actually use: subtile i is compared against external tiles resolved under
793 // subtile i's palette.
794 const std::size_t total_tiles =
795 (tiles_png.height() / tile::side_length_pix) * (tiles_png.width() / tile::side_length_pix);
796 const std::set<std::size_t> distinct_palette_indices{palette_indices.begin(), palette_indices.end()};
797 std::map<std::size_t, std::set<PixelTile<Rgba32>>> external_rgba_by_palette;
798 for (const std::size_t palette_idx : distinct_palette_indices) {
799 auto &tile_set = external_rgba_by_palette[palette_idx];
800 for (std::size_t i = 0; i < total_tiles; ++i) {
801 if (i >= tile_offset && i < tile_offset + tile_count) {
802 continue;
803 }
805 extract_single_tile(tiles_png, i), palettes.at(palette_idx), extrinsic_transparency.value()));
806 }
807 }
808
809 // Combined per-palette sets (external plus inter-animation) back the mangler's uniqueness checks. The
810 // external-only sets stay separate so duplicate categorization can distinguish cross-range duplicates from
811 // inter-animation duplicates.
812 std::map<std::size_t, std::set<PixelTile<Rgba32>>> combined_rgba_by_palette = external_rgba_by_palette;
813 for (auto &tile_set : combined_rgba_by_palette | std::views::values) {
814 tile_set.insert(inter_anim_canonical_tiles.begin(), inter_anim_canonical_tiles.end());
815 }
816
817 std::vector<const std::set<PixelTile<Rgba32>> *> external_per_subtile;
818 std::vector<const std::set<PixelTile<Rgba32>> *> combined_per_subtile;
819 external_per_subtile.reserve(tile_count);
820 combined_per_subtile.reserve(tile_count);
821 for (const std::size_t palette_idx : palette_indices) {
822 external_per_subtile.push_back(&external_rgba_by_palette.at(palette_idx));
823 combined_per_subtile.push_back(&combined_rgba_by_palette.at(palette_idx));
824 }
825
826 // Compute the effective key frame resolution strategy: per-anim override wins, otherwise global fallback.
827 const ConfigValue<AnimKeyFrameResolutionStrategy> effective_key_frame_strategy =
828 (anim_cfg_ptr != nullptr && anim_cfg_ptr->key_frame_resolution_strategy.has_value())
829 ? per_anim_overrides.derive(anim_cfg_ptr->key_frame_resolution_strategy)
830 : global_anim_key_frame_resolution_strategy;
831
832 // Detect duplicate key frame tiles on canonical decoded RGBA forms. Detects:
833 // - Inter-animation duplicates: animation tile matches another animation's key frame tile
834 // - Cross-range duplicates: animation tile matches a non-animation tile in tiles.png
835 // - Intra-animation duplicates: two animation tiles match each other
836 const auto dup_info = categorize_duplicate_key_frame_tiles(
837 key_frame_canonical_rgba_tiles, inter_anim_canonical_tiles, external_per_subtile);
838 if (dup_info.any()) {
839 switch (effective_key_frame_strategy.value()) {
841 std::vector<std::string> err_msg{};
842 err_msg.emplace_back(diag_->formatter().format(
843 "Animation '{}' has duplicate key frame tiles:", FormatParam{anim.name(), Style::bold}));
844 for (const auto &idx : dup_info.inter_anim_indices) {
845 err_msg.emplace_back(diag_->formatter().format(
846 " - Tile {} matches another animation's key frame tile.", FormatParam{idx, Style::bold}));
847 }
848 for (const auto &idx : dup_info.cross_range_indices) {
849 err_msg.emplace_back(diag_->formatter().format(
850 " - Tile {} matches a non-animation tile in tiles.png.", FormatParam{idx, Style::bold}));
851 }
852 for (const auto &[i, j] : dup_info.intra_anim_pairs) {
853 err_msg.emplace_back(diag_->formatter().format(
854 " - Tile {} and tile {} match.", FormatParam{i, Style::bold}, FormatParam{j, Style::bold}));
855 }
856 err_msg.emplace_back("");
857 err_msg.emplace_back(
858 "Tiles are compared by resolved color: flip-equivalent tiles, and tiles that reference different "
859 "palette slots holding the same color, count as duplicates.");
860 err_msg.emplace_back("");
861 err_msg.emplace_back("Consider using 'mangle' strategy to auto-resolve.");
862 err_msg.append_range(format_config_note_with_separator(diag_->formatter(), effective_key_frame_strategy));
863 return FormattableError{err_msg};
864 }
865
867 panic("warning not yet implemented");
868 }
869
871 AnimKeyFrameMangler mangler{diag_, tile_printer_};
873 mangle_result,
874 mangler.mangle_duplicates(
875 anim.name(),
876 std::move(key_frame_index_tiles),
877 palette_ptrs,
878 extrinsic_transparency.value(),
879 combined_per_subtile),
881 diag_->formatter().format(
882 "Failed to mangle duplicate key frame tiles for animation '{}'.",
883 FormatParam{anim.name(), Style::bold}));
884 key_frame_index_tiles = std::move(mangle_result.tiles);
885
886 // Backport changes to tiles.png
887 if (!mangle_result.mangle_records.empty()) {
888 backport_mangles_to_tiles_png(porymap_component, tile_offset, mangle_result.mangle_records);
889 }
890 break;
891 }
892
893 default:
894 panic("unhandled AnimKeyFrameResolutionStrategy value");
895 }
896 }
897
898 // Decompile key frame tiles to Rgba32 using per-subtile palettes
899 std::vector<PixelTile<Rgba32>> key_frame_rgba_tiles;
900 key_frame_rgba_tiles.reserve(key_frame_index_tiles.size());
901 for (std::size_t i = 0; i < key_frame_index_tiles.size(); ++i) {
902 key_frame_rgba_tiles.push_back(color_tile_from_index_tile(
903 key_frame_index_tiles[i], palettes.at(palette_indices[i]), extrinsic_transparency.value()));
904 }
905
906 // Set the key frame on the result
907 AnimFrame key_frame{"key", std::move(key_frame_rgba_tiles)};
908 result.key_frame(std::move(key_frame));
909
910 for (const auto &frame : anim.frames_values()) {
911 if (frame.tiles().size() != tile_count) {
912 panic(
913 "frame '" + frame.frame_name() + "' tile count " + std::to_string(frame.tiles().size()) +
914 " != animation tile_count " + std::to_string(tile_count));
915 }
916
917 std::vector<PixelTile<Rgba32>> rgba_tiles;
918 rgba_tiles.reserve(frame.tiles().size());
919
920 for (std::size_t i = 0; i < frame.tiles().size(); ++i) {
921 rgba_tiles.push_back(color_tile_from_index_tile(
922 frame.tiles()[i], palettes.at(palette_indices[i]), extrinsic_transparency.value()));
923 }
924
925 AnimFrame rgba_frame{frame.frame_name(), std::move(rgba_tiles)};
926 result.put_frame(frame.frame_name(), std::move(rgba_frame));
927 }
928
929 return result;
930}
931
932} // 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< Rgba32 > > &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:89
const AnimParams & params() const
bool has_frames() const
Checks if this animation has any frames.
const std::string & name() const
Definition animation.hpp:97
std::vector< AnimFrame< PixelType > > frames_values() const
const std::map< std::string, AnimFrame< PixelType > > & frames() const
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:57
A template for two-dimensional images with arbitrarily typed pixel values.
Definition image.hpp:21
void set(std::size_t i, PixelType pixel)
Sets the pixel value at a given one-dimensional pixel index.
Definition image.hpp:78
std::size_t width() const
Definition image.hpp:102
std::size_t height() const
Definition image.hpp:107
A collection of printer functions for the Palette and related types.
virtual std::vector< std::string > print_rgba_palette_with_highlights(const Palette< Rgba32 > &palette, const std::vector< std::size_t > &slots) const =0
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
An 8x8 tile backed by literal-array-based per-pixel storage of an arbitrary pixel type.
const std::vector< TilemapEntry > & metatiles_bin() const
const Image< IndexPixel > & tiles_png() const
const std::array< Palette< Rgba32, palette::max_size >, palette::num_palettes > & palettes() const
Represents a 32-bit RGBA color.
Definition rgba32.hpp:21
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:26
constexpr std::size_t tiles_per_side
Definition metatile.hpp:21
constexpr std::size_t metatiles_per_row
Definition metatile.hpp:27
std::tuple< Layer, Subtile > from_internal_tile_index(std::size_t tile_index)
Decomposes an internal tile index into its layer and subtile position within a metatile.
Definition metatile.hpp:157
constexpr std::size_t max_size
Definition palette.hpp:19
constexpr std::size_t num_palettes
Definition palette.hpp:21
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.
AnimPaletteResolutionStrategy
Strategy for determining which palette to use when decompiling animation tiles.
PixelTile< ColorType > canonical_color_tile_from_index_tile(const PixelTile< IndexPixel > &index_tile, const Palette< ColorType, N > &palette, const ColorType &extrinsic)
Converts a PixelTile<IndexPixel> to its canonical color form using a palette (extrinsic transparency)...
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.
std::string palette_filename(std::size_t palette_index)
Constructs a palette filename from a palette index.
@ 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 palette_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< AnimPaletteResolutionStrategy > > per_tile_palette_resolution_strategies
ConfigPODField< AnimKeyFrameResolutionStrategy > key_frame_resolution_strategy
ConfigPODField< AnimMultiPaletteSubtileResolutionStrategy > multi_palette_subtile_resolution_strategy
ConfigPODField< FrameLinking > linking
ConfigPODField< AnimPaletteResolutionStrategy > palette_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...