Porytiles
Loading...
Searching...
No Matches
anim_key_frame_mangler.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <cstdint>
6#include <map>
7#include <optional>
8#include <set>
9#include <string>
10#include <vector>
11
21
22namespace {
23
24using namespace porytiles;
25
26/*
27 * Pixel priority order for mangling (least visually impactful first)
28 * Corners: (0,0), (0,7), (7,0), (7,7) -> indices 0, 7, 56, 63
29 * Corners: (0,0), (0,7), (7,0), (7,7) -> indices 0, 7, 56, 63
30 * Top edge: 1-6
31 * Left edge: 8, 16, 24, 32, 40, 48
32 * Right edge: 15, 23, 31, 39, 47, 55
33 * Bottom edge: 57-62
34 * Interior: remaining pixels
35 */
36constexpr std::array<std::size_t, tile::size_pix> pixel_priority_order = {
37 // Corners first (least visible)
38 0,
39 7,
40 56,
41 63,
42 // Top edge
43 1,
44 2,
45 3,
46 4,
47 5,
48 6,
49 // Left edge
50 8,
51 16,
52 24,
53 32,
54 40,
55 48,
56 // Right edge
57 15,
58 23,
59 31,
60 39,
61 47,
62 55,
63 // Bottom edge
64 57,
65 58,
66 59,
67 60,
68 61,
69 62,
70 // Interior (most visible, last resort)
71 9,
72 10,
73 11,
74 12,
75 13,
76 14,
77 17,
78 18,
79 19,
80 20,
81 21,
82 22,
83 25,
84 26,
85 27,
86 28,
87 29,
88 30,
89 33,
90 34,
91 35,
92 36,
93 37,
94 38,
95 41,
96 42,
97 43,
98 44,
99 45,
100 46,
101 49,
102 50,
103 51,
104 52,
105 53,
106 54};
107
119int color_distance_squared(const Rgba32 &a, const Rgba32 &b)
120{
121 const int dr = static_cast<int>(a.red()) - static_cast<int>(b.red());
122 const int dg = static_cast<int>(a.green()) - static_cast<int>(b.green());
123 const int db = static_cast<int>(a.blue()) - static_cast<int>(b.blue());
124 return dr * dr + dg * dg + db * db;
125}
126
140[[nodiscard]] std::vector<std::size_t>
141find_alternative_colors_sorted(std::size_t current_color_index, const Palette<Rgba32, pal::max_size> &palette)
142{
143 struct ColorCandidate {
144 std::size_t index;
145 int distance;
146 };
147
148 const Rgba32 current_color = palette.at(current_color_index);
149 std::vector<ColorCandidate> candidates;
150
151 // Search ALL palette colors (1-15), not just those in the tile
152 for (std::size_t candidate_index = 1; candidate_index < pal::max_size; ++candidate_index) {
153 if (candidate_index == current_color_index) {
154 continue;
155 }
156
157 const Rgba32 candidate_color = palette.at(candidate_index);
158 const int distance = color_distance_squared(current_color, candidate_color);
159 candidates.push_back({candidate_index, distance});
160 }
161
162 std::ranges::sort(candidates, [](const ColorCandidate &a, const ColorCandidate &b) {
163 if (a.distance != b.distance) {
164 return a.distance < b.distance;
165 }
166 return a.index < b.index;
167 });
168
169 std::vector<std::size_t> result;
170 result.reserve(candidates.size());
171 for (const auto &c : candidates) {
172 result.push_back(c.index);
173 }
174 return result;
175}
176
184[[nodiscard]] IndexPixel make_mangled_pixel(std::size_t original_palette_index, std::size_t alt_color)
185{
186 return IndexPixel{(original_palette_index << 4) | alt_color};
187}
188
211std::optional<std::pair<PixelTile<IndexPixel>, TileMangleRecord>> try_mangle_tile(
212 const PixelTile<IndexPixel> &tile,
213 std::size_t tile_index,
214 const Palette<Rgba32, pal::max_size> &palette,
215 const std::set<PixelTile<IndexPixel>> &all_existing_canonical_tiles)
216{
217 // Phase 1: single-pixel swaps (preferred, minimal visual impact)
218 for (std::size_t pixel_index : pixel_priority_order) {
219 const IndexPixel original_pixel = tile.at(pixel_index);
220
221 const std::vector<std::size_t> alternatives =
222 find_alternative_colors_sorted(original_pixel.color_index(), palette);
223
224 for (const std::size_t alt_color : alternatives) {
225 const IndexPixel mangled_pixel = make_mangled_pixel(original_pixel.palette_index(), alt_color);
226
227 PixelTile<IndexPixel> candidate_tile = tile;
228 candidate_tile.set(pixel_index, mangled_pixel);
229
230 CanonicalPixelTile<IndexPixel> canonical_candidate{candidate_tile};
231 const PixelTile<IndexPixel> &candidate_base = canonical_candidate;
232 if (!all_existing_canonical_tiles.contains(candidate_base)) {
233 TileMangleRecord record{
234 .tile_index = tile_index,
235 .pixel_changes = {PixelMangleChange{
236 .pixel_index = pixel_index, .original_pixel = original_pixel, .mangled_pixel = mangled_pixel}}};
237 return std::make_pair(candidate_tile, record);
238 }
239 }
240 }
241
242 /*
243 * Phase 2: two-pixel swaps (fallback for heavily saturated canonical tile sets).
244 * Loop ordering: position pair (p1, p2) outermost, then color alternatives innermost. This ensures we prefer the
245 * least visible pixel positions before trying more visible ones, consistent with the Phase 1 priority ordering.
246 */
247 for (std::size_t p1_idx = 0; p1_idx < pixel_priority_order.size(); ++p1_idx) {
248 const std::size_t p1 = pixel_priority_order[p1_idx];
249 const IndexPixel p1_original = tile.at(p1);
250
251 const std::vector<std::size_t> p1_alternatives =
252 find_alternative_colors_sorted(p1_original.color_index(), palette);
253
254 for (std::size_t p2_idx = p1_idx + 1; p2_idx < pixel_priority_order.size(); ++p2_idx) {
255 const std::size_t p2 = pixel_priority_order[p2_idx];
256 const IndexPixel p2_original = tile.at(p2);
257
258 const std::vector<std::size_t> p2_alternatives =
259 find_alternative_colors_sorted(p2_original.color_index(), palette);
260
261 for (const std::size_t p1_alt : p1_alternatives) {
262 const IndexPixel p1_mangled = make_mangled_pixel(p1_original.palette_index(), p1_alt);
263
264 for (const std::size_t p2_alt : p2_alternatives) {
265 const IndexPixel p2_mangled = make_mangled_pixel(p2_original.palette_index(), p2_alt);
266
267 PixelTile<IndexPixel> candidate_tile = tile;
268 candidate_tile.set(p1, p1_mangled);
269 candidate_tile.set(p2, p2_mangled);
270
271 CanonicalPixelTile<IndexPixel> canonical_candidate{candidate_tile};
272 const PixelTile<IndexPixel> &candidate_base = canonical_candidate;
273 if (!all_existing_canonical_tiles.contains(candidate_base)) {
274 TileMangleRecord record{
275 .tile_index = tile_index,
276 .pixel_changes = {
278 .pixel_index = p1, .original_pixel = p1_original, .mangled_pixel = p1_mangled},
280 .pixel_index = p2, .original_pixel = p2_original, .mangled_pixel = p2_mangled}}};
281 return std::make_pair(candidate_tile, record);
282 }
283 }
284 }
285 }
286 }
287
288 // No valid mangle found
289 return std::nullopt;
290}
291
292} // namespace
293
294namespace porytiles {
295
297 gsl::not_null<const UserDiagnostics *> diag, gsl::not_null<const TilePrinter *> tile_printer)
298 : diag_{diag}, tile_printer_{tile_printer}
299{
300}
301
303 const std::string &anim_name,
304 std::vector<PixelTile<IndexPixel>> tiles,
305 const std::vector<const Palette<Rgba32, pal::max_size> *> &palettes,
306 const Rgba32 &extrinsic_transparency,
307 const std::set<PixelTile<IndexPixel>> &existing_canonical_tiles) const
308{
309 if (palettes.size() != tiles.size()) {
310 panic("palettes size " + std::to_string(palettes.size()) + " != tiles size " + std::to_string(tiles.size()));
311 }
312
313 MangleResult result;
314 result.tiles = std::move(tiles);
315
316 /*
317 * Build a set of all canonical tiles we need to be unique against. This includes the input existing_canonical_tiles
318 * plus tiles we've already processed. Using canonical forms ensures tiles that are flip-equivalent are treated as
319 * duplicates.
320 */
321 std::set<PixelTile<IndexPixel>> all_canonical_tiles = existing_canonical_tiles;
322
323 // Map to track which canonical tiles we've seen at which indices (for duplicate detection)
324 std::map<PixelTile<IndexPixel>, std::size_t> canonical_first_occurrence;
325
326 /*
327 * Each tile index is visited exactly once. A tile is mangled at most once, producing at most one TileMangleRecord.
328 * This guarantees that mangle_records contains non-overlapping entries (no two records share the same tile_index),
329 * so they can be applied independently in any order.
330 */
331 for (std::size_t i = 0; i < result.tiles.size(); ++i) {
332 PixelTile<IndexPixel> &current_tile = result.tiles[i];
333
334 // Canonicalize the current tile for duplicate checking
335 CanonicalPixelTile canonical_current{current_tile};
336 const PixelTile<IndexPixel> &current_base = canonical_current;
337
338 // Check if this tile is a duplicate (either of existing_canonical_tiles or a previous tile in this batch)
339 auto it = canonical_first_occurrence.find(current_base);
340 const bool is_duplicate_of_previous = it != canonical_first_occurrence.end();
341 const bool is_duplicate_of_existing =
342 existing_canonical_tiles.contains(current_base) && !is_duplicate_of_previous;
343
344 if (is_duplicate_of_previous || is_duplicate_of_existing) {
345 // Need to mangle this tile
346 const PixelTile<IndexPixel> original_tile = current_tile;
347 const std::optional<std::pair<PixelTile<IndexPixel>, TileMangleRecord>> mangle_result =
348 try_mangle_tile(current_tile, i, *palettes[i], all_canonical_tiles);
349
350 if (!mangle_result.has_value()) {
351 // Could not mangle the tile - this is an error
352 std::vector<std::string> err_msg;
353 err_msg.push_back(diag_->formatter().format(
354 "Failed to mangle duplicate key frame tile {} in animation '{}'.",
355 FormatParam{i, Style::bold},
356 FormatParam{anim_name, Style::bold}));
357 err_msg.emplace_back();
358 err_msg.emplace_back("The tile could not be modified to be unique. This may occur if:");
359 err_msg.emplace_back(" - All possible pixel swaps still produce duplicate tiles");
360 return FormattableError{err_msg};
361 }
362
363 // Apply the mangle
364 current_tile = mangle_result->first;
365 result.mangle_records.insert(mangle_result->second);
366
367 // Emit a remark about the mangle
368 std::vector<std::string> remark_lines;
369 for (const auto &change : mangle_result->second.pixel_changes) {
370 const auto [pixel_row, pixel_col] = tile::index_to_row_col(change.pixel_index);
371 remark_lines.push_back(diag_->formatter().format(
372 "Mangled tile {} in animation '{}': pixel ({},{}) changed from index {} to {}.",
373 FormatParam{i, Style::bold},
374 FormatParam{anim_name, Style::bold},
375 FormatParam{pixel_row},
376 FormatParam{pixel_col},
377 FormatParam{change.original_pixel.index()},
378 FormatParam{change.mangled_pixel.index()}));
379 }
380
381 const PixelTile<Rgba32> original_rgba =
382 color_tile_from_index_tile(original_tile, *palettes[i], extrinsic_transparency);
383 const PixelTile<Rgba32> mangled_rgba =
384 color_tile_from_index_tile(current_tile, *palettes[i], extrinsic_transparency);
385
386 remark_lines.emplace_back("");
387 remark_lines.emplace_back("Original tile:");
388 remark_lines.append_range(tile_printer_->print_tile(original_rgba, extrinsic_transparency));
389
390 remark_lines.emplace_back("Mangled tile:");
391 remark_lines.append_range(tile_printer_->print_tile(mangled_rgba, extrinsic_transparency));
392
393 diag_->remark("anim-key-frame-mangle", remark_lines);
394 }
395
396 // Re-canonicalize after potential mangle and add to tracking sets
397 CanonicalPixelTile final_canonical{current_tile};
398 const PixelTile<IndexPixel> &final_base = final_canonical;
399 canonical_first_occurrence.emplace(final_base, i);
400 all_canonical_tiles.insert(final_base);
401 }
402
403 return result;
404}
405
406} // namespace porytiles
AnimKeyFrameMangler(gsl::not_null< const UserDiagnostics * > diag, gsl::not_null< const TilePrinter * > tile_printer)
ChainableResult< MangleResult > mangle_duplicates(const std::string &anim_name, std::vector< PixelTile< IndexPixel > > tiles, const std::vector< const Palette< Rgba32, pal::max_size > * > &palettes, const Rgba32 &extrinsic_transparency, const std::set< PixelTile< IndexPixel > > &existing_tiles) const
Mangles duplicate tiles to make them unique.
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 text parameter with associated styling for formatted output.
General-purpose error implementation with formatted message support.
Definition error.hpp:63
Represents an indexed color pixel.
std::size_t palette_index() const
Returns the palette index (upper 4 bits).
std::size_t color_index() const
Returns the color index within a palette (lower 4 bits).
A generic palette container for colors that support transparency checking.
Definition palette.hpp:47
ColorType at(std::size_t index) const
Gets the color at a specific index.
Definition palette.hpp:276
An 8x8 tile backed by literal-array-based per-pixel storage of an arbitrary pixel type.
PixelType at(std::size_t i) const
void set(std::size_t i, const PixelType &p)
Represents a 32-bit RGBA color.
Definition rgba32.hpp:23
std::uint8_t red() const
Definition rgba32.hpp:81
std::uint8_t blue() const
Definition rgba32.hpp:91
std::uint8_t green() const
Definition rgba32.hpp:86
virtual std::string format(const std::string &format_str, const std::vector< FormatParam > &params) const
Formats a string with styled parameters using fmtlib syntax.
virtual std::vector< std::string > print_tile(const PixelTile< Rgba32 > &tile, const Rgba32 &extrinsic_transparency) const =0
virtual void remark(const std::string &tag, const std::vector< std::string > &lines) const =0
Display a tagged remark message.
const TextFormatter & formatter() const
constexpr std::size_t max_size
Definition palette.hpp:19
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.
void panic(const StringViewSourceLoc &s)
Unconditionally terminates the program with a panic message.
Definition panic.cpp:43
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).
Result of the mangling operation.
std::set< TileMangleRecord > mangle_records
Record of all modifications made (ordered by tile_index)
std::vector< PixelTile< IndexPixel > > tiles
The tiles after mangling (unique)
A single pixel modification within a tile.
std::size_t pixel_index
Which pixel in the tile (0-63, linear index)
Record of all pixel modifications made to a single tile during mangling.
std::size_t tile_index
Which tile in the key frame (0-based index, unique across records)