Porytiles
Loading...
Searching...
No Matches
diagnostics.cpp
Go to the documentation of this file.
2
3#include <any>
4#include <ranges>
5#include <sstream>
6#include <type_traits>
7#include <unistd.h>
8#include <unordered_set>
9
11
12namespace {
13
14using namespace porytiles;
15
16constexpr std::size_t DIAG_MARGIN_SIZE = 7;
17
18void AssertArgSize(std::size_t expected, std::size_t actual, const char *func_name) {
19 if (actual != expected) {
20 Panic(fmt::format("{}: found {} args but expected {}", func_name, actual, expected));
21 }
22}
23
24template <typename T>
25T AnyCastOrPanic(const std::any &a, const std::source_location &loc) {
26 try {
27 return std::any_cast<T>(a);
28 } catch (std::bad_any_cast &) {
29 Panic(fmt::format("bad any cast: {}:{}", loc.file_name(), loc.line()));
30 }
31}
32
33template <typename T>
34const T &AnyCastOrPanic(const std::any *a, const std::source_location &loc) {
35 auto any_unwrapped = any_cast<T>(a);
36 if (any_unwrapped == nullptr) {
37 Panic(fmt::format("bad any cast: {}:{}", loc.file_name(), loc.line()));
38 }
39 return *any_unwrapped;
40}
41
42void PushToStream(std::stringstream &ss, const std::string_view s, const std::size_t n) {
43 for (std::size_t i = 0; i < n; i++) {
44 ss << s;
45 }
46}
47
48void ResetStream(std::stringstream &ss) {
49 ss.clear();
50 ss.str(std::string{});
51}
52
53// TODO : this is using code from the legacy library, refactor
54// std::vector<std::string> BuildTileHighlight(const DiagEngine &eng, const DiagLevel in_flight_level,
55// const RGBATile &tile, const std::size_t row, const std::size_t col) {
56// std::vector<std::string> highlight{};
57// std::stringstream ss{};
58// const fmt::terminal_color level_color = ColorForLevel(in_flight_level);
59
60// // TODO : std::variant here, see note below
61// // Eventually we can remove this outer check by introducing better metadata
62// // handling in RGBTile. Specifically, metadata can be a std::variant that
63// // changes based on the TileType. Then, we can use the visitor pattern to
64// // create different visit implementations for the different TileTypes.
65// if (tile.type == TileType::LAYERED) {
66// for (std::size_t i = 0; i < 16; i++) {
67// for (std::size_t j = 0; j < 16; j++) {
68// // First cell of each row is margin followed by a bar: " |"
69// if (j == 0) {
70// PushToStream(ss, " ", DIAG_MARGIN_SIZE);
71// ss << "|";
72// }
73
74// // General case. Decide if we are drawing the highlighted tile
75// // and pixel. If not, draw a "-".
76
77// auto styled_x = eng.Style(" X ", fg(level_color) | fmt::emphasis::bold);
78// auto styled_star = eng.Style(" * ", fmt::emphasis::bold);
79// if (tile.subtile == Subtile::NORTHWEST && i < 8 && j < 8) {
80// if (row == i && col == j) {
81// ss << format(fmt::runtime("{}"), styled_x);
82// } else {
83// ss << format(fmt::runtime("{}"), styled_star);
84// }
85// } else if (tile.subtile == Subtile::NORTHEAST && i < 8 && j >= 8) {
86// if (row == i && col == j - 8) {
87// ss << format(fmt::runtime("{}"), styled_x);
88// } else {
89// ss << format(fmt::runtime("{}"), styled_star);
90// }
91// } else if (tile.subtile == Subtile::SOUTHWEST && i >= 8 && j < 8) {
92// if (row == i - 8 && col == j) {
93// ss << format(fmt::runtime("{}"), styled_x);
94// } else {
95// ss << format(fmt::runtime("{}"), styled_star);
96// }
97// } else if (tile.subtile == Subtile::SOUTHEAST && i >= 8 && j >= 8) {
98// if (row == i - 8 && col == j - 8) {
99// ss << format(fmt::runtime("{}"), styled_x);
100// } else {
101// ss << format(fmt::runtime("{}"), styled_star);
102// }
103// } else {
104// ss << " - ";
105// }
106
107// // If we're at the midpoint cell, add an extra space.
108// if (j == 7) {
109// ss << " ";
110// }
111
112// // Reset once this row is exhausted
113// if (j == 15) {
114// highlight.push_back(ss.str());
115// ResetStream(ss);
116// }
117// }
118
119// // Insert a spacer line between top and bottom tiles
120// if (i == 7) {
121// PushToStream(ss, " ", DIAG_MARGIN_SIZE);
122// ss << "|";
123// highlight.push_back(ss.str());
124// ResetStream(ss);
125// }
126// }
127// }
128// return highlight;
129// }
130
131} // namespace
132
133namespace porytiles {
134std::string LevelToStr(DiagLevel level) {
135 switch (level) {
137 return "ignored";
138 case DiagLevel::kNote:
139 return "note";
141 return "remark";
143 return "warning";
145 return "error";
147 return "fatal error";
148 default:
149 Panic("level_to_str: unknown diag_level");
150 }
151}
152
153fmt::terminal_color ColorForLevel(DiagLevel level) {
154 switch (level) {
156 return fmt::terminal_color::white;
157 case DiagLevel::kNote:
158 return fmt::terminal_color::cyan;
160 return fmt::terminal_color::green;
162 return fmt::terminal_color::magenta;
165 return fmt::terminal_color::red;
166 default:
167 Panic("color_for_level: unknown diag_level");
168 }
169}
170
172 switch (level) {
174 return 0;
175 case DiagLevel::kNote:
176 return 1;
178 return 2;
180 return 3;
182 return 4;
184 return 5;
185 }
186 return -1;
187}
188
190 consumed_count_++;
191}
192
194 return false;
195}
196
198 Panic("ignore_consumer::consumed_at: not implemented");
199}
200
201std::uint64_t IgnoreConsumer::ConsumedCount() const {
202 return consumed_count_;
203}
204
206 consumed_count_++;
207 const auto msg = diag.msg();
208 std::fputs(msg.c_str(), stderr);
209}
210
212 return isatty(fileno(stderr));
213}
214
216 Panic("stderr_consumer::consumed_at: not implemented");
217}
218
219std::uint64_t StderrConsumer::ConsumedCount() const {
220 return consumed_count_;
221}
222
224 diags_.emplace_back(diag);
225}
226
228 return false;
229}
230
232 try {
233 return diags_.at(i);
234 } catch (const std::out_of_range &) {
235 Panic(fmt::format("vector_consumer::at: index {} out of range for size {}", i, diags_.size()));
236 }
237}
238
239std::uint64_t VectorConsumer::ConsumedCount() const {
240 return diags_.size();
241}
242
243// clang-format off
244static const DiagTempl N_GENERIC_TEMPL{NoteGeneric, DiagLevel::kNote, "{}", {}};
245
246static const DiagTempl W_COLOR_PRECISION_LOSS_NOTE_TEMPL{
247 "color-precision-loss-previously-seen-note",
249 [](const DiagEngine &eng, const DiagLevel in_flight_level, const std::vector<std::any> &args) -> std::vector<std::string> {
250 AssertArgSize(4, args.size(), std::source_location::current().function_name());
251 std::vector<std::string> msg{};
252
253 // const auto tile = AnyCastOrPanic<RGBATile>(args[0], std::source_location::current());
254 // const auto color = AnyCastOrPanic<std::string>(args[1], std::source_location::current());
255 // const auto row = AnyCastOrPanic<std::size_t>(args[2], std::source_location::current());
256 // const auto col = AnyCastOrPanic<std::size_t>(args[3], std::source_location::current());
257
258 // // FIXME : this template is incomplete, we want to show the mode since it's
259 // // possible to have precision loss across a primary-secondary boundary
260 // constexpr auto msg_templ = "{}: previously saw: '{}' at col '{}', row '{}'";
261 // msg.push_back(fmt::format(msg_templ, eng.Bold(tile.prettify()), eng.Bold(color), eng.Bold(col),
262 // eng.Bold(row)));
263 // // auto highlight = BuildTileHighlight(eng, in_flight_level, tile, row, col);
264 // // msg.insert(std::end(msg), std::begin(highlight), std::end(highlight));
265
266 return msg;
267 }
268};
269static const DiagTempl W_COLOR_PRECISION_LOSS_TEMPL{
272 [](const DiagEngine &eng, const DiagLevel in_flight_level, const std::vector<std::any> &args) -> std::vector<std::string> {
273 AssertArgSize(5, args.size(), std::source_location::current().function_name());
274 std::vector<std::string> msg{};
275
276 // const auto tile = AnyCastOrPanic<RGBATile>(args[0], std::source_location::current());
277 // const auto color = AnyCastOrPanic<std::string>(args[1], std::source_location::current());
278 // const auto mode = AnyCastOrPanic<std::string>(args[2], std::source_location::current());
279 // const auto row = AnyCastOrPanic<std::size_t>(args[3], std::source_location::current());
280 // const auto col = AnyCastOrPanic<std::size_t>(args[4], std::source_location::current());
281
282 // constexpr auto msg_templ = "{} {}: collapsed to duplicate BGR: '{}' at col '{}', row '{}'";
283 // msg.push_back(fmt::format(msg_templ, eng.Bold(mode), eng.Bold(tile.prettify()), eng.Bold(color), eng.Bold(col),
284 // eng.Bold(row)));
285 // auto highlight = BuildTileHighlight(eng, in_flight_level, tile, row, col);
286 // msg.insert(std::end(msg), std::begin(highlight), std::end(highlight));
287
288 return msg;
289 },
290 {
291 W_COLOR_PRECISION_LOSS_NOTE_TEMPL
292 }
293};
294
295// TODO : show mode information (primary vs secondary)
296static const DiagTempl W_KEY_FRAME_NO_MATCHING_TILE_TEMPL{
299 "animation '{}' key frame tile '{}' was not present in any metatile entries",
300 {}
301};
302
303// TODO : show mode information (primary vs secondary)
304static const DiagTempl W_KEY_FRAME_MISSING_COLORS_NOTE_TEMPL{
305 "key-frame-missing-colors-list-note",
307 [](const DiagEngine &eng, const DiagLevel in_flight_level, const std::vector<std::any> &args) -> std::vector<std::string> {
308 AssertArgSize(1, args.size(), std::source_location::current().function_name());
309 std::vector<std::string> msg{};
310
311 // const auto missing_colors =
312 // AnyCastOrPanic<std::vector<RGBA32>>(&args[0], std::source_location::current());
313 // msg.emplace_back("the following colors were missing from the key frame tile:");
314 // std::stringstream ss{};
315 // PushToStream(ss, " ", DIAG_MARGIN_SIZE);
316 // ss << "|--- {}";
317 // for (const auto &color : missing_colors) {
318 // msg.push_back(fmt::format(fmt::runtime(ss.str()), eng.Bold(color.jasc())));
319 // }
320 // ResetStream(ss);
321 // PushToStream(ss, " ", DIAG_MARGIN_SIZE);
322 // ss << "| If left uncorrected, this may lead to the issue described here:";
323 // msg.push_back(ss.str());
324 // ResetStream(ss);
325 // PushToStream(ss, " ", DIAG_MARGIN_SIZE);
326 // ss << "| https://github.com/grunt-lucas/porytiles/issues/60";
327 // msg.push_back(ss.str());
328 return msg;
329 }
330};
331static const DiagTempl W_KEY_FRAME_MISSING_COLORS_TEMPL{
334 [](const DiagEngine &eng, const DiagLevel in_flight_level, const std::vector<std::any> &args) -> std::vector<std::string> {
335 AssertArgSize(2, args.size(), std::source_location::current().function_name());
336 std::vector<std::string> msg{};
337
338 const auto anim_name = AnyCastOrPanic<std::string>(args[0], std::source_location::current());
339 const auto tile_index = AnyCastOrPanic<std::size_t>(args[1], std::source_location::current());
340 constexpr auto msg_templ = "anim '{}' key frame tile '{}' missing essential colors";
341
342 msg.push_back(fmt::format(msg_templ, eng.Bold(anim_name), eng.Bold(tile_index)));
343 return msg;
344 },
345 {
346 W_KEY_FRAME_MISSING_COLORS_NOTE_TEMPL
347 }
348};
349
350// TODO : make message shorter, possibly shorten file name?
351static const DiagTempl W_ATTRIBUTE_FORMAT_MISMATCH_TEMPL{
354 "{}: too {} attribute columns for base game '{}'",
355 {
356 DiagTempl{
357 "attribute-format-mismatch-note",
359 "unspecified columns will receive default values"
360 }
361 }
362};
363
364static const DiagTempl W_MISSING_ATTRIBUTES_CSV_TEMPL{
367 "{}: attributes.csv did not exist",
368 {
369 DiagTempl{
370 "missing-attr-csv-note",
372 "all attributes will receive default or inferred values"
373 }
374 }
375};
376
377static const DiagTempl W_UNUSED_ATTRIBUTE_TEMPL{
380 "found attribute for nonexistent metatile ID '{}'",
381 {
382 DiagTempl{
383 "unused-attribute-note",
385 "{} metatiles found at source path '{}'"
386 }
387 }
388};
389
390static const DiagTempl W_TRANSPARENCY_COLLAPSE_TEMPL{
393 "color '{}' at {} '{}' subtile pixel col '{}', row '{}' collapsed to transparent under BGR conversion",
394 {
395 DiagTempl{
396 "transparency-collapse-note",
398 "if you did not intend this pixel to be transparent, edit the color on the respective layer sheet"
399 }
400 }
401};
402
403static const DiagTempl W_UNUSED_MANUAL_PAL_COLOR_TEMPL{
404 WarnUnusedManualPalColor, DiagLevel::kWarning, "{}: '{}' was not used in layers or anims", {}
405};
406
407static const DiagTempl W_TILE_INDEX_OUT_OF_RANGE_TEMPL{
410 "{} '{}': tile index '{}' out of range (sheet size = {})",
411 {
412 DiagTempl{
413 "tile-index-out-of-range-note",
415 "substituting primary tile 0 (transparent tile) so decompilation can continue"
416 }
417 }
418};
419
420static const DiagTempl W_PALETTE_INDEX_OUT_OF_RANGE_TEMPL{
423 "{} '{}': palette index '{}' out of range (numPalettesTotal = {})",
424 {
425 DiagTempl{
426 "palette-index-out-of-range-note",
428 "substituting palette 0 so decompilation can continue"
429 }
430 }
431};
432
433static const DiagTempl E_GENERIC_TEMPL{ErrGeneric, DiagLevel::kError, "{}", {}};
434
435static const DiagTempl E_FATAL_GENERIC_TEMPL{FatalGeneric, DiagLevel::kFatal, "{}", {}};
436
437static const std::unordered_map<const char *, DiagTempl> DIAG_TEMPLS{
438 // Standalone notes
439 {NoteGeneric, N_GENERIC_TEMPL},
440
441 // Tileset compilation warnings
442 {WarnColorPrecisionLoss, W_COLOR_PRECISION_LOSS_TEMPL},
443 {WarnKeyFrameNoMatchingTile, W_KEY_FRAME_NO_MATCHING_TILE_TEMPL},
444 {WarnKeyFrameMissingColors, W_KEY_FRAME_MISSING_COLORS_TEMPL},
445 {WarnAttributeFormatMismatch, W_ATTRIBUTE_FORMAT_MISMATCH_TEMPL},
446 {WarnMissingAttributesCsv, W_MISSING_ATTRIBUTES_CSV_TEMPL},
447 {WarnUnusedAttribute, W_UNUSED_ATTRIBUTE_TEMPL},
448 {WarnTransparencyCollapse, W_TRANSPARENCY_COLLAPSE_TEMPL},
449 {WarnUnusedManualPalColor, W_UNUSED_MANUAL_PAL_COLOR_TEMPL},
450
451 // Tileset decompilation warnings
452 {WarnTileIndexOutOfRange, W_TILE_INDEX_OUT_OF_RANGE_TEMPL},
453 {WarnPaletteIndexOutOfRange, W_PALETTE_INDEX_OUT_OF_RANGE_TEMPL},
454
455 // Generic errors
456 {ErrGeneric, E_GENERIC_TEMPL},
457 {FatalGeneric, E_FATAL_GENERIC_TEMPL}
458};
459// clang-format on
460
461DiagTempl DiagFor(const std::string_view name) {
462 AssertOrPanic(DIAG_TEMPLS.contains(name.data()), fmt::format("diag_template_for: unknown diagnostic: {}", name));
463 return DIAG_TEMPLS.at(name.data());
464}
465
466std::vector<const char *> AllDiagNames() {
467 std::vector<const char *> keys{};
468 keys.reserve(DIAG_TEMPLS.size());
469 for (const auto &key : DIAG_TEMPLS | std::views::keys) {
470 keys.push_back(key);
471 }
472 return keys;
473}
474
475std::vector<const char *> AllDiagNames(const DiagLevel level) {
476 std::vector<const char *> keys{};
477 keys.reserve(DIAG_TEMPLS.size());
478 for (const auto &[name, templ] : DIAG_TEMPLS) {
479 if (templ.level() == level) {
480 keys.push_back(name);
481 }
482 }
483 return keys;
484}
485
486} // namespace porytiles
Coordinates the generation and consumption of diagnostic messages.
Defines a reusable template for standardized diagnostic reporting.
bool IsATty() const override
InFlightDiag ConsumedAt(std::size_t i) const override
std::uint64_t ConsumedCount() const override
void Consume(const InFlightDiag &diag) override
Represents an in-flight diagnostic.
std::string msg() const noexcept
bool IsATty() const override
InFlightDiag ConsumedAt(std::size_t i) const override
void Consume(const InFlightDiag &diag) override
std::uint64_t ConsumedCount() const override
std::uint64_t ConsumedCount() const override
bool IsATty() const override
void Consume(const InFlightDiag &diag) override
InFlightDiag ConsumedAt(std::size_t i) const override
constexpr auto WarnTransparencyCollapse
void Panic(const StringViewSourceLoc &s) noexcept
Definition panic.hpp:31
void AssertOrPanic(const bool condition, const StringViewSourceLoc &s)
Definition panic.hpp:36
DiagTempl DiagFor(std::string_view name)
Retrieves the DiagTempl corresponding to a given diagnostic name.
constexpr auto WarnColorPrecisionLoss
constexpr auto NoteGeneric
constexpr auto FatalGeneric
std::vector< const char * > AllDiagNames()
Gets an iterable view of all DiagTempl names in the internal table.
constexpr auto WarnUnusedAttribute
constexpr auto WarnAttributeFormatMismatch
constexpr auto WarnMissingAttributesCsv
constexpr auto WarnTileIndexOutOfRange
constexpr auto WarnPaletteIndexOutOfRange
std::string LevelToStr(DiagLevel level)
int LevelPriority(DiagLevel level)
fmt::terminal_color ColorForLevel(DiagLevel level)
constexpr auto ErrGeneric
constexpr auto WarnKeyFrameMissingColors
constexpr auto WarnKeyFrameNoMatchingTile
constexpr auto WarnUnusedManualPalColor