Porytiles
Loading...
Searching...
No Matches
yaml_file_provider_impl.ipp
Go to the documentation of this file.
1#pragma once
2
3#include <cstdint>
4#include <filesystem>
5#include <fstream>
6#include <map>
7#include <sstream>
8#include <string>
9
10#include "yaml-cpp/yaml.h"
11
35
36// The anonymous namespace ensures internal linkage per translation unit
37// This file is intentionally included only in yaml_file_provider.cpp
38namespace {
39
40using namespace porytiles;
41
42// Static caches shared across all YamlFileProvider instances
43std::map<std::filesystem::path, YAML::Node> yaml_cache;
44std::map<std::filesystem::path, std::vector<std::string>> file_lines_cache;
45
55std::string get_line_content(const std::filesystem::path &path, std::size_t line_num)
56{
57 const auto it = file_lines_cache.find(path);
58 if (it != file_lines_cache.end() && line_num < it->second.size()) {
59 return it->second[line_num];
60 }
61 return "";
62}
63
75std::string make_source_string(const TextFormatter *format, const std::string &file_path, const YAML::Mark &mark)
76{
77 return format->format("{}:{}", FormatParam{file_path}, FormatParam{mark.line + 1});
78}
79
102std::vector<std::string>
103make_source_details(const TextFormatter *format, const std::string &file_path, const YAML::Mark &mark)
104{
105 const std::filesystem::path path{file_path};
106 const auto it = file_lines_cache.find(path);
107 if (it == file_lines_cache.end()) {
108 return {};
109 }
110
111 const auto &lines = it->second;
112 const std::size_t line_num = mark.line; // 0-indexed
113
114 if (lines.empty() || line_num >= lines.size()) {
115 return {};
116 }
117
118 // Use FileHighlightPrinter (line_num is already 0-indexed)
119 const FileHighlightPrinter printer{format};
120 return printer.print(lines, std::vector{line_num});
121}
122
132void collect_yaml_paths(
133 const YAML::Node &node, const std::string &prefix, std::vector<std::pair<std::string, YAML::Mark>> &paths)
134{
135 if (!node.IsMap()) {
136 return;
137 }
138
139 for (const auto &kv : node) {
140 const auto key = kv.first.as<std::string>();
141 const auto full_path = prefix.empty() ? key : prefix + "." + key;
142 paths.emplace_back(full_path, kv.first.Mark());
143
144 // Recurse into nested maps
145 if (kv.second.IsMap()) {
146 collect_yaml_paths(kv.second, full_path, paths);
147 }
148 }
149}
150
163[[nodiscard]] bool validate_yaml_paths(
164 const TextFormatter *format,
165 const UserDiagnostics *diagnostics,
166 const std::filesystem::path &file_path,
167 const YAML::Node &node)
168{
169 if (diagnostics == nullptr) {
170 return false;
171 }
172
173 bool found_unknown = false;
174 std::vector<std::pair<std::string, YAML::Mark>> paths;
175 collect_yaml_paths(node, "", paths);
176
177 for (const auto &[path, mark] : paths) {
178 if (!valid_yaml_paths.contains(path)) {
179 // Skip children of map-type config values (dynamic keys like animation names)
180 bool is_map_child = false;
181 for (const auto &prefix : valid_yaml_map_prefixes) {
182 if (path.starts_with(prefix + ".")) {
183 is_map_child = true;
184 break;
185 }
186 }
187 if (is_map_child) {
188 continue;
189 }
190
191 const auto source = make_source_string(format, file_path.string(), mark);
192 auto details = make_source_details(format, file_path.string(), mark);
193
194 std::vector<std::string> error_lines;
195 error_lines.push_back(format->format("Unknown configuration key '{}'.", FormatParam{path, Style::bold}));
196 error_lines.emplace_back();
197 error_lines.push_back(format->format("Source: {}", FormatParam{source, Style::italic}));
198 error_lines.emplace_back();
199 for (auto &detail : details) {
200 error_lines.push_back(std::move(detail));
201 }
202
203 diagnostics->error("unknown-config-key", error_lines);
204 found_unknown = true;
205 }
206 }
207
208 return found_unknown;
209}
210
219parse_size_t(const TextFormatter *format, const YAML::Node &node, const std::string &key, const std::string &file_path)
220{
221 if (!node.IsDefined()) {
223 }
224
225 try {
226 const auto value = node.as<std::size_t>();
227 const auto mark = node.Mark();
228 const auto source = make_source_string(format, file_path, mark);
229 const auto details = make_source_details(format, file_path, mark);
230 return LayerValue<std::size_t>::valid(value, key, source, details);
231 }
232 catch (const YAML::Exception &e) {
233 const auto mark = node.Mark();
234 const auto error =
235 format->format("Failed to parse '{}' as integer: {}", FormatParam{key, Style::bold}, e.what());
236 const auto source = make_source_string(format, file_path, mark);
237 const auto details = make_source_details(format, file_path, mark);
238 return LayerValue<std::size_t>::invalid(error, source, details);
239 }
240}
241
253LayerValue<std::optional<std::size_t>> parse_optional_size_t(
254 const TextFormatter *format, const YAML::Node &node, const std::string &key, const std::string &file_path)
255{
256 if (!node.IsDefined()) {
258 }
259
260 try {
261 const auto value = node.as<std::size_t>();
262 const auto mark = node.Mark();
263 const auto source = make_source_string(format, file_path, mark);
264 const auto details = make_source_details(format, file_path, mark);
265 return LayerValue<std::optional<std::size_t>>::valid(std::optional<std::size_t>{value}, key, source, details);
266 }
267 catch (const YAML::Exception &e) {
268 const auto mark = node.Mark();
269 const auto error =
270 format->format("Failed to parse '{}' as integer: {}", FormatParam{key, Style::bold}, e.what());
271 const auto source = make_source_string(format, file_path, mark);
272 const auto details = make_source_details(format, file_path, mark);
274 }
275}
276
285parse_bool(const TextFormatter *format, const YAML::Node &node, const std::string &key, const std::string &file_path)
286{
287 if (!node.IsDefined()) {
289 }
290
291 try {
292 const auto value = node.as<bool>();
293 const auto mark = node.Mark();
294 const auto source = make_source_string(format, file_path, mark);
295 const auto details = make_source_details(format, file_path, mark);
296 return LayerValue<bool>::valid(value, key, source, details);
297 }
298 catch (const YAML::Exception &e) {
299 const auto mark = node.Mark();
300 const auto error =
301 format->format("Failed to parse '{}' as boolean: {}", FormatParam{key, Style::bold}, e.what());
302 const auto source = make_source_string(format, file_path, mark);
303 const auto details = make_source_details(format, file_path, mark);
304 return LayerValue<bool>::invalid(error, source, details);
305 }
306}
307
316parse_string(const TextFormatter *format, const YAML::Node &node, const std::string &key, const std::string &file_path)
317{
318 if (!node.IsDefined()) {
320 }
321
322 try {
323 const auto value = node.as<std::string>();
324 const auto mark = node.Mark();
325 const auto source = make_source_string(format, file_path, mark);
326 const auto details = make_source_details(format, file_path, mark);
327 return LayerValue<std::string>::valid(value, key, source, details);
328 }
329 catch (const YAML::Exception &e) {
330 const auto mark = node.Mark();
331 const auto error =
332 format->format("Failed to parse '{}' as string: {}", FormatParam{key, Style::bold}, e.what());
333 const auto source = make_source_string(format, file_path, mark);
334 const auto details = make_source_details(format, file_path, mark);
335 return LayerValue<std::string>::invalid(error, source, details);
336 }
337}
338
351parse_rgba32(const TextFormatter *format, const YAML::Node &node, const std::string &key, const std::string &file_path)
352{
353 if (!node.IsDefined()) {
355 }
356
357 try {
358 const auto mark = node.Mark();
359 const auto source = make_source_string(format, file_path, mark);
360 const auto details = make_source_details(format, file_path, mark);
361
362 if (!node.IsSequence()) {
363 const auto error =
364 format->format("'{}' must be a sequence [r, g, b] or [r, g, b, a]", FormatParam{key, Style::bold});
365 return LayerValue<Rgba32>::invalid(error, source, details);
366 }
367
368 if (node.size() < 3 || node.size() > 4) {
369 const auto error = format->format(
370 "'{}' must have 3 or 4 elements [r, g, b] or [r, g, b, a], got {}",
372 FormatParam{node.size(), Style::bold});
373 return LayerValue<Rgba32>::invalid(error, source, details);
374 }
375
376 const auto r = node[0].as<std::uint8_t>();
377 const auto g = node[1].as<std::uint8_t>();
378 const auto b = node[2].as<std::uint8_t>();
379 const auto a = (node.size() == 4) ? node[3].as<std::uint8_t>() : Rgba32::alpha_opaque;
380
381 const Rgba32 color{r, g, b, a};
382 return LayerValue<Rgba32>::valid(color, key, source, details);
383 }
384 catch (const YAML::Exception &e) {
385 const auto mark = node.Mark();
386 const auto error = format->format("Failed to parse '{}' as rgba: {}", FormatParam{key, Style::bold}, e.what());
387 const auto source = make_source_string(format, file_path, mark);
388 const auto details = make_source_details(format, file_path, mark);
389 return LayerValue<Rgba32>::invalid(error, source, details);
390 }
391}
392
393LayerValue<std::vector<PaletteHint>> parse_palette_hints(
394 const TextFormatter *format, const YAML::Node &node, const std::string &key, const std::string &file_path)
395{
396 if (!node.IsDefined()) {
398 }
399
400 try {
401 const auto mark = node.Mark();
402 const auto details = make_source_details(format, file_path, mark);
403
404 if (!node.IsSequence()) {
405 const auto error =
406 format->format("'{}' must be a sequence of palette hints", FormatParam{key, Style::bold});
407 const auto source = make_source_string(format, file_path, mark);
408 return LayerValue<std::vector<PaletteHint>>::invalid(error, source, details);
409 }
410
411 std::vector<PaletteHint> hints;
412 for (std::size_t i = 0; i < node.size(); ++i) {
413 const auto &hint_node = node[i];
414
415 if (!hint_node.IsMap()) {
416 const auto hint_mark = hint_node.Mark();
417 const auto error = format->format(
418 "'{}[{}]' must be a map with 'name' and 'colors' keys",
421 const auto source = make_source_string(format, file_path, hint_mark);
422 const auto hint_details = make_source_details(format, file_path, hint_mark);
423 return LayerValue<std::vector<PaletteHint>>::invalid(error, source, hint_details);
424 }
425
426 // Parse name field
427 const auto name_node = hint_node["name"];
428 if (!name_node.IsDefined()) {
429 const auto hint_mark = hint_node.Mark();
430 const auto error = format->format(
431 "'{}[{}]' is missing required 'name' field", FormatParam{key, Style::bold}, FormatParam{i});
432 const auto source = make_source_string(format, file_path, hint_mark);
433 const auto hint_details = make_source_details(format, file_path, hint_mark);
434 return LayerValue<std::vector<PaletteHint>>::invalid(error, source, hint_details);
435 }
436 const auto name = name_node.as<std::string>();
437
438 // Parse colors field
439 const auto colors_node = hint_node["colors"];
440 if (!colors_node.IsDefined()) {
441 const auto hint_mark = hint_node.Mark();
442 const auto error = format->format(
443 "'{}[{}]' is missing required 'colors' field", FormatParam{key, Style::bold}, FormatParam{i});
444 const auto source = make_source_string(format, file_path, hint_mark);
445 const auto hint_details = make_source_details(format, file_path, hint_mark);
446 return LayerValue<std::vector<PaletteHint>>::invalid(error, source, hint_details);
447 }
448
449 if (!colors_node.IsSequence()) {
450 const auto colors_mark = colors_node.Mark();
451 const auto error = format->format(
452 "'{}[{}].colors' must be a sequence of colors", FormatParam{key, Style::bold}, FormatParam{i});
453 const auto source = make_source_string(format, file_path, colors_mark);
454 const auto colors_details = make_source_details(format, file_path, colors_mark);
455 return LayerValue<std::vector<PaletteHint>>::invalid(error, source, colors_details);
456 }
457
458 // Parse each color
459 std::vector<Rgba32> colors;
460 for (std::size_t j = 0; j < colors_node.size(); ++j) {
461 const auto &color_node = colors_node[j];
462
463 if (!color_node.IsSequence() || color_node.size() != 3) {
464 const auto color_mark = color_node.Mark();
465 const auto error = format->format(
466 "'{}[{}].colors[{}]' must be [r, g, b]",
468 FormatParam{i},
469 FormatParam{j});
470 const auto source = make_source_string(format, file_path, color_mark);
471 const auto color_details = make_source_details(format, file_path, color_mark);
472 return LayerValue<std::vector<PaletteHint>>::invalid(error, source, color_details);
473 }
474
475 const auto r = color_node[0].as<std::uint8_t>();
476 const auto g = color_node[1].as<std::uint8_t>();
477 const auto b = color_node[2].as<std::uint8_t>();
478 const auto a = (color_node.size() == 4) ? color_node[3].as<std::uint8_t>() : Rgba32::alpha_opaque;
479
480 colors.emplace_back(r, g, b, a);
481 }
482
483 hints.emplace_back(name, Palette{std::move(colors)});
484 }
485
486 const auto source = make_source_string(format, file_path, mark);
487 return LayerValue<std::vector<PaletteHint>>::valid(std::move(hints), key, source, details);
488 }
489 catch (const YAML::Exception &e) {
490 const auto mark = node.Mark();
491 const auto error =
492 format->format("Failed to parse '{}' as palette hints: {}", FormatParam{key, Style::bold}, e.what());
493 const auto source = make_source_string(format, file_path, mark);
494 const auto details = make_source_details(format, file_path, mark);
495 return LayerValue<std::vector<PaletteHint>>::invalid(error, source, details);
496 }
497}
498
510LayerValue<std::vector<std::string>> parse_string_vector(
511 const TextFormatter *format, const YAML::Node &node, const std::string &key, const std::string &file_path)
512{
513 if (!node.IsDefined()) {
515 }
516
517 try {
518 const auto mark = node.Mark();
519 const auto source = make_source_string(format, file_path, mark);
520 const auto details = make_source_details(format, file_path, mark);
521
522 if (!node.IsSequence()) {
523 const auto error = format->format("'{}' must be a sequence of strings.", FormatParam{key, Style::bold});
524 return LayerValue<std::vector<std::string>>::invalid(error, source, details);
525 }
526
527 std::vector<std::string> result;
528 for (std::size_t i = 0; i < node.size(); ++i) {
529 result.push_back(node[i].as<std::string>());
530 }
531 return LayerValue<std::vector<std::string>>::valid(std::move(result), key, source, details);
532 }
533 catch (const YAML::Exception &e) {
534 const auto mark = node.Mark();
535 const auto error =
536 format->format("Failed to parse '{}' as string list: {}.", FormatParam{key, Style::bold}, e.what());
537 const auto source = make_source_string(format, file_path, mark);
538 const auto details = make_source_details(format, file_path, mark);
539 return LayerValue<std::vector<std::string>>::invalid(error, source, details);
540 }
541}
542
553LayerValue<TilesPaletteMode> parse_tiles_palette_mode(
554 const TextFormatter *format, const YAML::Node &node, const std::string &key, const std::string &file_path)
555{
556 if (!node.IsDefined()) {
558 }
559
560 try {
561 const auto mark = node.Mark();
562 const auto source = make_source_string(format, file_path, mark);
563 const auto details = make_source_details(format, file_path, mark);
564 const auto node_value = node.as<std::string>();
565 const auto mode_opt = tiles_palette_mode_from_str(node_value);
566
567 if (!mode_opt.has_value()) {
568 const auto error = format->format(
569 "'{}' has invalid value '{}'", FormatParam{key, Style::bold}, FormatParam{node_value, Style::bold});
570 return LayerValue<TilesPaletteMode>::invalid(error, source, details);
571 }
572
573 return LayerValue<TilesPaletteMode>::valid(mode_opt.value(), key, source, details);
574 }
575 catch (const YAML::Exception &e) {
576 const auto mark = node.Mark();
577 const auto error =
578 format->format("Failed to parse '{}' as TilesPaletteMode: {}", FormatParam{key, Style::bold}, e.what());
579 const auto source = make_source_string(format, file_path, mark);
580 const auto details = make_source_details(format, file_path, mark);
581 return LayerValue<TilesPaletteMode>::invalid(error, source, details);
582 }
583}
584
585LayerValue<ArtifactEditMode> parse_artifact_edit_mode(
586 const TextFormatter *format, const YAML::Node &node, const std::string &key, const std::string &file_path)
587{
588 if (!node.IsDefined()) {
590 }
591
592 try {
593 const auto mark = node.Mark();
594 const auto source = make_source_string(format, file_path, mark);
595 const auto details = make_source_details(format, file_path, mark);
596 const auto node_value = node.as<std::string>();
597 const auto mode_opt = artifact_edit_mode_from_str(node_value);
598
599 if (!mode_opt.has_value()) {
600 const auto error = format->format(
601 "'{}' has invalid value '{}'", FormatParam{key, Style::bold}, FormatParam{node_value, Style::bold});
602 return LayerValue<ArtifactEditMode>::invalid(error, source, details);
603 }
604
605 return LayerValue<ArtifactEditMode>::valid(mode_opt.value(), key, source, details);
606 }
607 catch (const YAML::Exception &e) {
608 const auto mark = node.Mark();
609 const auto error =
610 format->format("Failed to parse '{}' as ArtifactEditMode: {}", FormatParam{key, Style::bold}, e.what());
611 const auto source = make_source_string(format, file_path, mark);
612 const auto details = make_source_details(format, file_path, mark);
613 return LayerValue<ArtifactEditMode>::invalid(error, source, details);
614 }
615}
616
617LayerValue<AnimPaletteResolutionStrategy> parse_anim_palette_resolution_strategy(
618 const TextFormatter *format, const YAML::Node &node, const std::string &key, const std::string &file_path)
619{
620 if (!node.IsDefined()) {
622 }
623
624 try {
625 const auto mark = node.Mark();
626 const auto source = make_source_string(format, file_path, mark);
627 const auto details = make_source_details(format, file_path, mark);
628 const auto node_value = node.as<std::string>();
629 const auto mode_opt = anim_palette_resolution_strategy_from_str(node_value);
630
631 if (!mode_opt.has_value()) {
632 const auto error = format->format(
633 "'{}' has invalid value '{}'", FormatParam{key, Style::bold}, FormatParam{node_value, Style::bold});
635 }
636
637 return LayerValue<AnimPaletteResolutionStrategy>::valid(mode_opt.value(), key, source, details);
638 }
639 catch (const YAML::Exception &e) {
640 const auto mark = node.Mark();
641 const auto error = format->format(
642 "Failed to parse '{}' as AnimPaletteResolutionStrategy: {}", FormatParam{key, Style::bold}, e.what());
643 const auto source = make_source_string(format, file_path, mark);
644 const auto details = make_source_details(format, file_path, mark);
646 }
647}
648
649LayerValue<PerAnimOverrides> parse_per_anim_overrides(
650 const TextFormatter *format, const YAML::Node &node, const std::string &key, const std::string &file_path)
651{
652 if (!node.IsDefined()) {
654 }
655
656 try {
657 const auto mark = node.Mark();
658 const auto source = make_source_string(format, file_path, mark);
659 const auto details = make_source_details(format, file_path, mark);
660
661 if (!node.IsMap()) {
662 const auto error = format->format(
663 "'{}' must be a map of animation names to config objects.", FormatParam{key, Style::bold});
664 return LayerValue<PerAnimOverrides>::invalid(error, source, details);
665 }
666
667 PerAnimOverrides configs;
668 for (const auto &kv : node) {
669 const auto anim_name = kv.first.as<std::string>();
670 const auto &anim_node = kv.second;
671
672 PerAnimOverride anim_config;
673 anim_config.anim_name = anim_name;
674
675 if (!anim_node.IsMap()) {
676 const auto anim_mark = kv.first.Mark();
677 const auto anim_source = make_source_string(format, file_path, anim_mark);
678 const auto anim_details = make_source_details(format, file_path, anim_mark);
679 const auto error = format->format(
680 "'{}' animation '{}' must be a map.",
682 FormatParam{anim_name, Style::bold});
683 return LayerValue<PerAnimOverrides>::invalid(error, anim_source, anim_details);
684 }
685
686 // Parse frame_linking (optional)
687 if (anim_node["frame_linking"].IsDefined()) {
688 const auto linking_str = anim_node["frame_linking"].as<std::string>();
689 const auto linking_opt = frame_linking_from_str(linking_str);
690 if (!linking_opt.has_value()) {
691 const auto linking_mark = anim_node["frame_linking"].Mark();
692 const auto linking_source = make_source_string(format, file_path, linking_mark);
693 const auto linking_details = make_source_details(format, file_path, linking_mark);
694 const auto error = format->format(
695 "'{}' animation '{}' has invalid frame_linking value '{}'.",
697 FormatParam{anim_name, Style::bold},
698 FormatParam{linking_str, Style::bold});
699 return LayerValue<PerAnimOverrides>::invalid(error, linking_source, linking_details);
700 }
701 const auto fl_mark = anim_node["frame_linking"].Mark();
702 anim_config.linking = ConfigPODField{
703 linking_opt.value(),
704 key + "." + anim_name + ".frame_linking",
705 "Animation Config (" + anim_name + ") frame_linking",
706 make_source_string(format, file_path, fl_mark),
707 make_source_details(format, file_path, fl_mark)};
708 }
709
710 // Parse palette_resolution_strategy (optional scalar — per-anim middle tier)
711 if (anim_node["palette_resolution_strategy"].IsDefined()) {
712 const auto &strategy_node = anim_node["palette_resolution_strategy"];
713 const auto strategy_str = strategy_node.as<std::string>();
714 const auto strategy_opt = anim_palette_resolution_strategy_from_str(strategy_str);
715 if (!strategy_opt.has_value()) {
716 const auto strategy_mark = strategy_node.Mark();
717 const auto strategy_source = make_source_string(format, file_path, strategy_mark);
718 const auto strategy_details = make_source_details(format, file_path, strategy_mark);
719 const auto error = format->format(
720 "'{}' animation '{}' palette_resolution_strategy has invalid value '{}'.",
722 FormatParam{anim_name, Style::bold},
723 FormatParam{strategy_str, Style::bold});
724 return LayerValue<PerAnimOverrides>::invalid(error, strategy_source, strategy_details);
725 }
726 const auto palette_mark = strategy_node.Mark();
728 strategy_opt.value(),
729 key + "." + anim_name + ".palette_resolution_strategy",
730 "Animation Config (" + anim_name + ") per-anim strategy",
731 make_source_string(format, file_path, palette_mark),
732 make_source_details(format, file_path, palette_mark)};
733 }
734
735 // Parse key_frame_resolution_strategy (optional scalar — per-anim override)
736 if (anim_node["key_frame_resolution_strategy"].IsDefined()) {
737 const auto &strategy_node = anim_node["key_frame_resolution_strategy"];
738 const auto strategy_str = strategy_node.as<std::string>();
739 const auto strategy_opt = anim_key_frame_resolution_strategy_from_str(strategy_str);
740 if (!strategy_opt.has_value()) {
741 const auto strategy_mark = strategy_node.Mark();
742 const auto strategy_source = make_source_string(format, file_path, strategy_mark);
743 const auto strategy_details = make_source_details(format, file_path, strategy_mark);
744 const auto error = format->format(
745 "'{}' animation '{}' key_frame_resolution_strategy has invalid value '{}'.",
747 FormatParam{anim_name, Style::bold},
748 FormatParam{strategy_str, Style::bold});
749 return LayerValue<PerAnimOverrides>::invalid(error, strategy_source, strategy_details);
750 }
751 const auto kf_mark = strategy_node.Mark();
753 strategy_opt.value(),
754 key + "." + anim_name + ".key_frame_resolution_strategy",
755 "Animation Config (" + anim_name + ") key_frame_resolution_strategy",
756 make_source_string(format, file_path, kf_mark),
757 make_source_details(format, file_path, kf_mark)};
758 }
759
760 // Parse multi_palette_subtile_resolution_strategy (optional scalar — per-anim override)
761 if (anim_node["multi_palette_subtile_resolution_strategy"].IsDefined()) {
762 const auto &strategy_node = anim_node["multi_palette_subtile_resolution_strategy"];
763 const auto strategy_str = strategy_node.as<std::string>();
764 const auto strategy_opt = anim_multi_palette_subtile_resolution_strategy_from_str(strategy_str);
765 if (!strategy_opt.has_value()) {
766 const auto strategy_mark = strategy_node.Mark();
767 const auto strategy_source = make_source_string(format, file_path, strategy_mark);
768 const auto strategy_details = make_source_details(format, file_path, strategy_mark);
769 const auto error = format->format(
770 "'{}' animation '{}' multi_palette_subtile_resolution_strategy has invalid value '{}'.",
772 FormatParam{anim_name, Style::bold},
773 FormatParam{strategy_str, Style::bold});
774 return LayerValue<PerAnimOverrides>::invalid(error, strategy_source, strategy_details);
775 }
776 const auto mps_mark = strategy_node.Mark();
778 strategy_opt.value(),
779 key + "." + anim_name + ".multi_palette_subtile_resolution_strategy",
780 "Animation Config (" + anim_name + ") multi_palette_subtile_resolution_strategy",
781 make_source_string(format, file_path, mps_mark),
782 make_source_details(format, file_path, mps_mark)};
783 }
784
785 // Parse per_tile_palette_resolution_strategies (optional sequence — per-tile most specific tier)
786 if (anim_node["per_tile_palette_resolution_strategies"].IsDefined()) {
787 const auto &strategies_node = anim_node["per_tile_palette_resolution_strategies"];
788 if (!strategies_node.IsSequence()) {
789 const auto strategies_mark = strategies_node.Mark();
790 const auto strategies_source = make_source_string(format, file_path, strategies_mark);
791 const auto strategies_details = make_source_details(format, file_path, strategies_mark);
792 const auto error = format->format(
793 "'{}' animation '{}' per_tile_palette_resolution_strategies must be a sequence.",
795 FormatParam{anim_name, Style::bold});
796 return LayerValue<PerAnimOverrides>::invalid(error, strategies_source, strategies_details);
797 }
798
799 for (std::size_t i = 0; i < strategies_node.size(); ++i) {
800 const auto strategy_str = strategies_node[i].as<std::string>();
801 if (strategy_str == "_") {
802 anim_config.per_tile_palette_resolution_strategies.emplace_back();
803 }
804 else {
805 const auto strategy_opt = anim_palette_resolution_strategy_from_str(strategy_str);
806 if (!strategy_opt.has_value()) {
807 const auto strategy_mark = strategies_node[i].Mark();
808 const auto strategy_source = make_source_string(format, file_path, strategy_mark);
809 const auto strategy_details = make_source_details(format, file_path, strategy_mark);
810 const auto error = format->format(
811 "'{}' animation '{}' per_tile_palette_resolution_strategies[{}] has invalid value "
812 "'{}'.",
814 FormatParam{anim_name, Style::bold},
816 FormatParam{strategy_str, Style::bold});
817 return LayerValue<PerAnimOverrides>::invalid(error, strategy_source, strategy_details);
818 }
819 const auto tile_mark = strategies_node[i].Mark();
820 anim_config.per_tile_palette_resolution_strategies.push_back(
822 strategy_opt.value(),
823 key + "." + anim_name + ".per_tile_palette_resolution_strategies[" + std::to_string(i) +
824 "]",
825 "Animation Config (" + anim_name + ") subtile " + std::to_string(i),
826 make_source_string(format, file_path, tile_mark),
827 make_source_details(format, file_path, tile_mark)});
828 }
829 }
830 }
831
832 configs[anim_name] = std::move(anim_config);
833 }
834
835 return LayerValue<PerAnimOverrides>::valid(std::move(configs), key, source, details);
836 }
837 catch (const YAML::Exception &e) {
838 const auto mark = node.Mark();
839 const auto error =
840 format->format("Failed to parse '{}' as animation configs: {}.", FormatParam{key, Style::bold}, e.what());
841 const auto source = make_source_string(format, file_path, mark);
842 const auto details = make_source_details(format, file_path, mark);
843 return LayerValue<PerAnimOverrides>::invalid(error, source, details);
844 }
845}
846
847// Parses a mask scalar written as a string so hexadecimal (0x...), decimal, and octal literals all parse regardless of
848// yaml-cpp's numeric handling. Returns nullopt on any parse or 32-bit range failure.
849[[nodiscard]] std::optional<std::uint32_t> parse_mask_scalar(const std::string &text)
850{
851 try {
852 std::size_t consumed = 0;
853 const unsigned long parsed = std::stoul(text, &consumed, 0);
854 if (consumed != text.size() || parsed > 0xFFFFFFFFUL) {
855 return std::nullopt;
856 }
857 return static_cast<std::uint32_t>(parsed);
858 }
859 catch (const std::exception &) {
860 return std::nullopt;
861 }
862}
863
864// Accepts both the underscore and hyphen spellings of the header-format enum names.
865[[nodiscard]] std::optional<HeaderFormat> header_format_from_config_str(const std::string &text)
866{
867 if (text == "enums_only" || text == "enums-only") {
868 return HeaderFormat::enums_only;
869 }
870 if (text == "defines_only" || text == "defines-only") {
871 return HeaderFormat::defines_only;
872 }
873 if (text == "either") {
874 return HeaderFormat::either;
875 }
876 return std::nullopt;
877}
878
879// Role-name matching lives in one place (field_role_from_string next to the FieldRole enum); this thin wrapper keeps
880// the parse-layer call sites reading naturally. Unknown role names are rejected at the parse layer.
881[[nodiscard]] std::optional<FieldRole> field_role_from_config_str(const std::string &text)
882{
883 return field_role_from_string(text);
884}
885
886// Returns the first key of a YAML map that is not in the allowed set, or nullopt if all keys are known. Sequence
887// children of config values bypass the global unknown-key validator, so field/override entries police their own keys.
888[[nodiscard]] std::optional<std::string>
889first_unknown_key(const YAML::Node &map_node, const std::unordered_set<std::string> &allowed)
890{
891 for (const auto &kv : map_node) {
892 const auto member = kv.first.as<std::string>();
893 if (!allowed.contains(member)) {
894 return member;
895 }
896 }
897 return std::nullopt;
898}
899
900LayerValue<MetatileAttributeFieldDefinitions> parse_metatile_attribute_fields(
901 const TextFormatter *format, const YAML::Node &node, const std::string &key, const std::string &file_path)
902{
903 if (!node.IsDefined()) {
905 }
906
907 try {
908 const auto mark = node.Mark();
909 const auto source = make_source_string(format, file_path, mark);
910 const auto details = make_source_details(format, file_path, mark);
911
912 if (!node.IsSequence()) {
914 format->format("'{}' must be a sequence of field definitions.", FormatParam{key, Style::bold}),
915 source,
916 details);
917 }
918
919 const std::unordered_set<std::string> field_keys{"name", "mask", "default", "provider", "role"};
920 const std::unordered_set<std::string> provider_keys{"header", "prefix", "skipped", "format"};
921
923 for (std::size_t i = 0; i < node.size(); ++i) {
924 const auto &field_node = node[i];
925 const auto field_mark = field_node.Mark();
926 const auto field_source = make_source_string(format, file_path, field_mark);
927 const auto field_details = make_source_details(format, file_path, field_mark);
928
929 if (!field_node.IsMap()) {
931 format->format("'{}[{}]' must be a map.", FormatParam{key, Style::bold}, FormatParam{i}),
932 field_source,
933 field_details);
934 }
935 if (auto unknown = first_unknown_key(field_node, field_keys); unknown.has_value()) {
937 format->format(
938 "'{}[{}]' has unknown key '{}'.",
939 FormatParam{key, Style::bold},
940 FormatParam{i},
941 FormatParam{unknown.value(), Style::bold}),
942 field_source,
943 field_details);
944 }
945
947 const auto name_node = field_node["name"];
948 if (!name_node.IsDefined()) {
950 format->format(
951 "'{}[{}]' is missing required 'name' field.", FormatParam{key, Style::bold}, FormatParam{i}),
952 field_source,
953 field_details);
954 }
955 definition.name = name_node.as<std::string>();
956
957 for (const auto &[member, target] :
958 std::initializer_list<std::pair<const char *, std::optional<std::uint32_t> *>>{
959 {"mask", &definition.mask}, {"default", &definition.default_value}}) {
960 if (field_node[member].IsDefined()) {
961 const auto text = field_node[member].as<std::string>();
962 const auto parsed = parse_mask_scalar(text);
963 if (!parsed.has_value()) {
965 format->format(
966 "'{}[{}].{}' is not a valid 32-bit integer: '{}'.",
967 FormatParam{key, Style::bold},
968 FormatParam{i},
969 FormatParam{member, Style::bold},
970 FormatParam{text, Style::bold}),
971 field_source,
972 field_details);
973 }
974 *target = parsed;
975 }
976 }
977
978 if (field_node["provider"].IsDefined()) {
979 const auto &provider_node = field_node["provider"];
980 if (!provider_node.IsMap()) {
982 format->format(
983 "'{}[{}].provider' must be a map.", FormatParam{key, Style::bold}, FormatParam{i}),
984 field_source,
985 field_details);
986 }
987 if (auto unknown = first_unknown_key(provider_node, provider_keys); unknown.has_value()) {
989 format->format(
990 "'{}[{}].provider' has unknown key '{}'.",
991 FormatParam{key, Style::bold},
992 FormatParam{i},
993 FormatParam{unknown.value(), Style::bold}),
994 field_source,
995 field_details);
996 }
997
999 if (!provider_node["header"].IsDefined() || !provider_node["prefix"].IsDefined()) {
1001 format->format(
1002 "'{}[{}].provider' requires both 'header' and 'prefix'.",
1003 FormatParam{key, Style::bold},
1004 FormatParam{i}),
1005 field_source,
1006 field_details);
1007 }
1008 provider.header = provider_node["header"].as<std::string>();
1009 provider.prefix = provider_node["prefix"].as<std::string>();
1010 if (provider_node["skipped"].IsDefined()) {
1011 if (!provider_node["skipped"].IsSequence()) {
1013 format->format(
1014 "'{}[{}].provider.skipped' must be a sequence.",
1015 FormatParam{key, Style::bold},
1016 FormatParam{i}),
1017 field_source,
1018 field_details);
1019 }
1020 for (std::size_t j = 0; j < provider_node["skipped"].size(); ++j) {
1021 provider.skipped.insert(provider_node["skipped"][j].as<std::string>());
1022 }
1023 }
1024 if (provider_node["format"].IsDefined()) {
1025 const auto fmt_str = provider_node["format"].as<std::string>();
1026 const auto fmt = header_format_from_config_str(fmt_str);
1027 if (!fmt.has_value()) {
1029 format->format(
1030 "'{}[{}].provider.format' has invalid value '{}'.",
1031 FormatParam{key, Style::bold},
1032 FormatParam{i},
1033 FormatParam{fmt_str, Style::bold}),
1034 field_source,
1035 field_details);
1036 }
1037 provider.format = fmt.value();
1038 }
1039 definition.provider = std::move(provider);
1040 }
1041
1042 if (field_node["role"].IsDefined() && !field_node["role"].IsNull()) {
1043 // `role: null` on a definition is accepted for symmetry with overrides and means the same as omitting
1044 // it.
1045 const auto role_str = field_node["role"].as<std::string>();
1046 const auto role = field_role_from_config_str(role_str);
1047 if (!role.has_value()) {
1049 format->format(
1050 "'{}[{}].role' has invalid value '{}'; the only role is 'layer_type'.",
1051 FormatParam{key, Style::bold},
1052 FormatParam{i},
1053 FormatParam{role_str, Style::bold}),
1054 field_source,
1055 field_details);
1056 }
1057 definition.role = role;
1058 }
1059
1060 definitions.push_back(std::move(definition));
1061 }
1062
1063 return LayerValue<MetatileAttributeFieldDefinitions>::valid(std::move(definitions), key, source, details);
1064 }
1065 catch (const YAML::Exception &e) {
1066 const auto mark = node.Mark();
1067 const auto error = format->format(
1068 "Failed to parse '{}' as metatile attribute fields: {}.", FormatParam{key, Style::bold}, e.what());
1069 const auto source = make_source_string(format, file_path, mark);
1070 const auto details = make_source_details(format, file_path, mark);
1072 }
1073}
1074
1075LayerValue<MetatileAttributeFieldOverrides> parse_metatile_attribute_field_overrides(
1076 const TextFormatter *format, const YAML::Node &node, const std::string &key, const std::string &file_path)
1077{
1078 if (!node.IsDefined()) {
1080 }
1081
1082 try {
1083 const auto mark = node.Mark();
1084 const auto source = make_source_string(format, file_path, mark);
1085 const auto details = make_source_details(format, file_path, mark);
1086
1087 if (!node.IsMap()) {
1089 format->format("'{}' must be a map of field names to overrides.", FormatParam{key, Style::bold}),
1090 source,
1091 details);
1092 }
1093
1094 const std::unordered_set<std::string> override_keys{"mask", "default", "provider", "role"};
1095 const std::unordered_set<std::string> provider_keys{"header", "prefix", "skipped", "format"};
1096
1098 for (const auto &kv : node) {
1099 const auto field_name = kv.first.as<std::string>();
1100 const auto &override_node = kv.second;
1101 const auto field_mark = kv.first.Mark();
1102 const auto field_source = make_source_string(format, file_path, field_mark);
1103 const auto field_details = make_source_details(format, file_path, field_mark);
1104
1105 if (!override_node.IsMap()) {
1107 format->format(
1108 "'{}' override for '{}' must be a map.",
1109 FormatParam{key, Style::bold},
1110 FormatParam{field_name, Style::bold}),
1111 field_source,
1112 field_details);
1113 }
1114 if (auto unknown = first_unknown_key(override_node, override_keys); unknown.has_value()) {
1116 format->format(
1117 "'{}' override for '{}' has unknown key '{}'.",
1118 FormatParam{key, Style::bold},
1119 FormatParam{field_name, Style::bold},
1120 FormatParam{unknown.value(), Style::bold}),
1121 field_source,
1122 field_details);
1123 }
1124
1125 MetatileAttributeFieldOverride override_value;
1126 for (const auto &[member, target] :
1127 std::initializer_list<std::pair<const char *, std::optional<std::uint32_t> *>>{
1128 {"mask", &override_value.mask}, {"default", &override_value.default_value}}) {
1129 if (override_node[member].IsDefined()) {
1130 const auto text = override_node[member].as<std::string>();
1131 const auto parsed = parse_mask_scalar(text);
1132 if (!parsed.has_value()) {
1134 format->format(
1135 "'{}' override for '{}' has invalid '{}': '{}'.",
1136 FormatParam{key, Style::bold},
1137 FormatParam{field_name, Style::bold},
1138 FormatParam{member, Style::bold},
1139 FormatParam{text, Style::bold}),
1140 field_source,
1141 field_details);
1142 }
1143 *target = parsed;
1144 }
1145 }
1146
1147 if (override_node["provider"].IsDefined()) {
1148 const auto &provider_node = override_node["provider"];
1149 ProviderDefinitionOverride provider_override;
1150 if (provider_node.IsNull()) {
1151 // `provider: null` removes the provider entirely, turning the field into a raw field.
1152 provider_override.remove = true;
1153 }
1154 else if (provider_node.IsMap()) {
1155 if (auto unknown = first_unknown_key(provider_node, provider_keys); unknown.has_value()) {
1157 format->format(
1158 "'{}' override for '{}' has unknown provider key '{}'.",
1159 FormatParam{key, Style::bold},
1160 FormatParam{field_name, Style::bold},
1161 FormatParam{unknown.value(), Style::bold}),
1162 field_source,
1163 field_details);
1164 }
1165 if (provider_node["header"].IsDefined()) {
1166 provider_override.header = provider_node["header"].as<std::string>();
1167 }
1168 if (provider_node["prefix"].IsDefined()) {
1169 provider_override.prefix = provider_node["prefix"].as<std::string>();
1170 }
1171 if (provider_node["skipped"].IsDefined()) {
1172 if (!provider_node["skipped"].IsSequence()) {
1174 format->format(
1175 "'{}' override for '{}' provider.skipped must be a sequence.",
1176 FormatParam{key, Style::bold},
1177 FormatParam{field_name, Style::bold}),
1178 field_source,
1179 field_details);
1180 }
1181 std::unordered_set<std::string> skipped;
1182 for (std::size_t j = 0; j < provider_node["skipped"].size(); ++j) {
1183 skipped.insert(provider_node["skipped"][j].as<std::string>());
1184 }
1185 provider_override.skipped = std::move(skipped);
1186 }
1187 if (provider_node["format"].IsDefined()) {
1188 const auto fmt_str = provider_node["format"].as<std::string>();
1189 const auto fmt = header_format_from_config_str(fmt_str);
1190 if (!fmt.has_value()) {
1192 format->format(
1193 "'{}' override for '{}' provider.format has invalid value '{}'.",
1194 FormatParam{key, Style::bold},
1195 FormatParam{field_name, Style::bold},
1196 FormatParam{fmt_str, Style::bold}),
1197 field_source,
1198 field_details);
1199 }
1200 provider_override.format = fmt.value();
1201 }
1202 }
1203 else {
1205 format->format(
1206 "'{}' override for '{}' provider must be a map or null.",
1207 FormatParam{key, Style::bold},
1208 FormatParam{field_name, Style::bold}),
1209 field_source,
1210 field_details);
1211 }
1212 override_value.provider = std::move(provider_override);
1213 }
1214
1215 if (override_node["role"].IsDefined()) {
1216 const auto &role_node = override_node["role"];
1217 if (role_node.IsNull()) {
1218 // `role: null` clears the baseline field's role.
1219 override_value.role = std::optional<FieldRole>{std::nullopt};
1220 }
1221 else {
1222 const auto role_str = role_node.as<std::string>();
1223 const auto role = field_role_from_config_str(role_str);
1224 if (!role.has_value()) {
1226 format->format(
1227 "'{}' override for '{}' has invalid 'role' value '{}'; the only role is "
1228 "'layer_type'.",
1229 FormatParam{key, Style::bold},
1230 FormatParam{field_name, Style::bold},
1231 FormatParam{role_str, Style::bold}),
1232 field_source,
1233 field_details);
1234 }
1235 override_value.role = role;
1236 }
1237 }
1238
1239 overrides[field_name] = std::move(override_value);
1240 }
1241
1242 return LayerValue<MetatileAttributeFieldOverrides>::valid(std::move(overrides), key, source, details);
1243 }
1244 catch (const YAML::Exception &e) {
1245 const auto mark = node.Mark();
1246 const auto error = format->format(
1247 "Failed to parse '{}' as metatile attribute field overrides: {}.", FormatParam{key, Style::bold}, e.what());
1248 const auto source = make_source_string(format, file_path, mark);
1249 const auto details = make_source_details(format, file_path, mark);
1251 }
1252}
1253
1254LayerValue<RolePinDefinitions> parse_role_pins(
1255 const TextFormatter *format, const YAML::Node &node, const std::string &key, const std::string &file_path)
1256{
1257 if (!node.IsDefined()) {
1259 }
1260
1261 try {
1262 const auto mark = node.Mark();
1263 const auto source = make_source_string(format, file_path, mark);
1264 const auto details = make_source_details(format, file_path, mark);
1265
1266 if (!node.IsSequence()) {
1268 format->format("'{}' must be a sequence of role pin definitions.", FormatParam{key, Style::bold}),
1269 source,
1270 details);
1271 }
1272
1273 const std::unordered_set<std::string> pin_keys{"role"};
1274
1275 RolePinDefinitions definitions;
1276 // A role may be pinned at most once. That is the only cross-entry rule left: a pin column's header is fixed at
1277 // pin_column_name(role), so one role means one column and there is nothing else two entries could collide on.
1278 std::unordered_set<std::string> seen_roles;
1279 for (std::size_t i = 0; i < node.size(); ++i) {
1280 const auto &pin_node = node[i];
1281 const auto pin_mark = pin_node.Mark();
1282 const auto pin_source = make_source_string(format, file_path, pin_mark);
1283 const auto pin_details = make_source_details(format, file_path, pin_mark);
1284
1285 if (!pin_node.IsMap()) {
1287 format->format("'{}[{}]' must be a map.", FormatParam{key, Style::bold}, FormatParam{i}),
1288 pin_source,
1289 pin_details);
1290 }
1291 if (auto unknown = first_unknown_key(pin_node, pin_keys); unknown.has_value()) {
1293 format->format(
1294 "'{}[{}]' has unknown key '{}'.",
1295 FormatParam{key, Style::bold},
1296 FormatParam{i},
1297 FormatParam{unknown.value(), Style::bold}),
1298 pin_source,
1299 pin_details);
1300 }
1301
1302 const auto role_node = pin_node["role"];
1303 if (!role_node.IsDefined()) {
1305 format->format(
1306 "'{}[{}]' is missing required 'role' field.", FormatParam{key, Style::bold}, FormatParam{i}),
1307 pin_source,
1308 pin_details);
1309 }
1310 const auto role_str = role_node.as<std::string>();
1311 const auto role = field_role_from_config_str(role_str);
1312 if (!role.has_value()) {
1314 format->format(
1315 "'{}[{}].role' has invalid value '{}'; the only role is 'layer_type'.",
1316 FormatParam{key, Style::bold},
1317 FormatParam{i},
1318 FormatParam{role_str, Style::bold}),
1319 pin_source,
1320 pin_details);
1321 }
1322 if (!seen_roles.insert(role_str).second) {
1324 format->format(
1325 "'{}[{}]' repeats role '{}'; each role may be pinned at most once.",
1326 FormatParam{key, Style::bold},
1327 FormatParam{i},
1328 FormatParam{role_str, Style::bold}),
1329 pin_source,
1330 pin_details);
1331 }
1332
1333 RolePinDefinition definition;
1334 definition.role = role.value();
1335 definitions.push_back(definition);
1336 }
1337
1338 return LayerValue<RolePinDefinitions>::valid(std::move(definitions), key, source, details);
1339 }
1340 catch (const YAML::Exception &e) {
1341 const auto mark = node.Mark();
1342 const auto error =
1343 format->format("Failed to parse '{}' as role pins: {}.", FormatParam{key, Style::bold}, e.what());
1344 const auto source = make_source_string(format, file_path, mark);
1345 const auto details = make_source_details(format, file_path, mark);
1346 return LayerValue<RolePinDefinitions>::invalid(error, source, details);
1347 }
1348}
1349
1350LayerValue<AnimKeyFrameResolutionStrategy> parse_anim_key_frame_resolution_strategy(
1351 const TextFormatter *format, const YAML::Node &node, const std::string &key, const std::string &file_path)
1352{
1353 if (!node.IsDefined()) {
1355 }
1356
1357 try {
1358 const auto mark = node.Mark();
1359 const auto source = make_source_string(format, file_path, mark);
1360 const auto details = make_source_details(format, file_path, mark);
1361 const auto node_value = node.as<std::string>();
1362 const auto mode_opt = anim_key_frame_resolution_strategy_from_str(node_value);
1363
1364 if (!mode_opt.has_value()) {
1365 const auto error = format->format(
1366 "'{}' has invalid value '{}'.", FormatParam{key, Style::bold}, FormatParam{node_value, Style::bold});
1368 }
1369
1370 return LayerValue<AnimKeyFrameResolutionStrategy>::valid(mode_opt.value(), key, source, details);
1371 }
1372 catch (const YAML::Exception &e) {
1373 const auto mark = node.Mark();
1374 const auto error = format->format(
1375 "Failed to parse '{}' as AnimKeyFrameResolutionStrategy: {}.", FormatParam{key, Style::bold}, e.what());
1376 const auto source = make_source_string(format, file_path, mark);
1377 const auto details = make_source_details(format, file_path, mark);
1379 }
1380}
1381
1382LayerValue<AnimMultiPaletteSubtileResolutionStrategy> parse_anim_multi_palette_subtile_resolution_strategy(
1383 const TextFormatter *format, const YAML::Node &node, const std::string &key, const std::string &file_path)
1384{
1385 if (!node.IsDefined()) {
1387 }
1388
1389 try {
1390 const auto mark = node.Mark();
1391 const auto source = make_source_string(format, file_path, mark);
1392 const auto details = make_source_details(format, file_path, mark);
1393 const auto node_value = node.as<std::string>();
1394 const auto mode_opt = anim_multi_palette_subtile_resolution_strategy_from_str(node_value);
1395
1396 if (!mode_opt.has_value()) {
1397 const auto error = format->format(
1398 "'{}' has invalid value '{}'.", FormatParam{key, Style::bold}, FormatParam{node_value, Style::bold});
1400 }
1401
1402 return LayerValue<AnimMultiPaletteSubtileResolutionStrategy>::valid(mode_opt.value(), key, source, details);
1403 }
1404 catch (const YAML::Exception &e) {
1405 const auto mark = node.Mark();
1406 const auto error = format->format(
1407 "Failed to parse '{}' as AnimMultiPaletteSubtileResolutionStrategy: {}.",
1409 e.what());
1410 const auto source = make_source_string(format, file_path, mark);
1411 const auto details = make_source_details(format, file_path, mark);
1413 }
1414}
1415
1416LayerValue<FrameLinking> parse_frame_linking(
1417 const TextFormatter *format, const YAML::Node &node, const std::string &key, const std::string &file_path)
1418{
1419 if (!node.IsDefined()) {
1421 }
1422
1423 try {
1424 const auto mark = node.Mark();
1425 const auto source = make_source_string(format, file_path, mark);
1426 const auto details = make_source_details(format, file_path, mark);
1427 const auto node_value = node.as<std::string>();
1428 const auto mode_opt = frame_linking_from_str(node_value);
1429
1430 if (!mode_opt.has_value()) {
1431 const auto error = format->format(
1432 "'{}' has invalid value '{}'.", FormatParam{key, Style::bold}, FormatParam{node_value, Style::bold});
1433 return LayerValue<FrameLinking>::invalid(error, source, details);
1434 }
1435
1436 return LayerValue<FrameLinking>::valid(mode_opt.value(), key, source, details);
1437 }
1438 catch (const YAML::Exception &e) {
1439 const auto mark = node.Mark();
1440 const auto error =
1441 format->format("Failed to parse '{}' as FrameLinking: {}.", FormatParam{key, Style::bold}, e.what());
1442 const auto source = make_source_string(format, file_path, mark);
1443 const auto details = make_source_details(format, file_path, mark);
1444 return LayerValue<FrameLinking>::invalid(error, source, details);
1445 }
1446}
1447
1448LayerValue<TileSharingPacking> parse_tile_sharing_packing(
1449 const TextFormatter *format, const YAML::Node &node, const std::string &key, const std::string &file_path)
1450{
1451 if (!node.IsDefined()) {
1453 }
1454
1455 try {
1456 const auto mark = node.Mark();
1457 const auto source = make_source_string(format, file_path, mark);
1458 const auto details = make_source_details(format, file_path, mark);
1459 const auto node_value = node.as<std::string>();
1460 const auto mode_opt = tile_sharing_packing_from_str(node_value);
1461
1462 if (!mode_opt.has_value()) {
1463 const auto error = format->format(
1464 "'{}' has invalid value '{}'.", FormatParam{key, Style::bold}, FormatParam{node_value, Style::bold});
1465 return LayerValue<TileSharingPacking>::invalid(error, source, details);
1466 }
1467
1468 return LayerValue<TileSharingPacking>::valid(mode_opt.value(), key, source, details);
1469 }
1470 catch (const YAML::Exception &e) {
1471 const auto mark = node.Mark();
1472 const auto error =
1473 format->format("Failed to parse '{}' as TileSharingPacking: {}.", FormatParam{key, Style::bold}, e.what());
1474 const auto source = make_source_string(format, file_path, mark);
1475 const auto details = make_source_details(format, file_path, mark);
1476 return LayerValue<TileSharingPacking>::invalid(error, source, details);
1477 }
1478}
1479
1480LayerValue<TileSharingAlignment> parse_tile_sharing_alignment(
1481 const TextFormatter *format, const YAML::Node &node, const std::string &key, const std::string &file_path)
1482{
1483 if (!node.IsDefined()) {
1485 }
1486
1487 try {
1488 const auto mark = node.Mark();
1489 const auto source = make_source_string(format, file_path, mark);
1490 const auto details = make_source_details(format, file_path, mark);
1491 const auto node_value = node.as<std::string>();
1492 const auto mode_opt = tile_sharing_alignment_from_str(node_value);
1493
1494 if (!mode_opt.has_value()) {
1495 const auto error = format->format(
1496 "'{}' has invalid value '{}'.", FormatParam{key, Style::bold}, FormatParam{node_value, Style::bold});
1497 return LayerValue<TileSharingAlignment>::invalid(error, source, details);
1498 }
1499
1500 return LayerValue<TileSharingAlignment>::valid(mode_opt.value(), key, source, details);
1501 }
1502 catch (const YAML::Exception &e) {
1503 const auto mark = node.Mark();
1504 const auto error = format->format(
1505 "Failed to parse '{}' as TileSharingAlignment: {}.", FormatParam{key, Style::bold}, e.what());
1506 const auto source = make_source_string(format, file_path, mark);
1507 const auto details = make_source_details(format, file_path, mark);
1508 return LayerValue<TileSharingAlignment>::invalid(error, source, details);
1509 }
1510}
1511
1512LayerValue<PackingStrategyType> parse_packing_strategy_type(
1513 const TextFormatter *format, const YAML::Node &node, const std::string &key, const std::string &file_path)
1514{
1515 if (!node.IsDefined()) {
1517 }
1518
1519 try {
1520 const auto mark = node.Mark();
1521 const auto source = make_source_string(format, file_path, mark);
1522 const auto details = make_source_details(format, file_path, mark);
1523 const auto node_value = node.as<std::string>();
1524 const auto mode_opt = packing_strategy_type_from_str(node_value);
1525
1526 if (!mode_opt.has_value()) {
1527 const auto error = format->format(
1528 "'{}' has invalid value '{}'.", FormatParam{key, Style::bold}, FormatParam{node_value, Style::bold});
1529 return LayerValue<PackingStrategyType>::invalid(error, source, details);
1530 }
1531
1532 return LayerValue<PackingStrategyType>::valid(mode_opt.value(), key, source, details);
1533 }
1534 catch (const YAML::Exception &e) {
1535 const auto mark = node.Mark();
1536 const auto error =
1537 format->format("Failed to parse '{}' as PackingStrategyType: {}.", FormatParam{key, Style::bold}, e.what());
1538 const auto source = make_source_string(format, file_path, mark);
1539 const auto details = make_source_details(format, file_path, mark);
1540 return LayerValue<PackingStrategyType>::invalid(error, source, details);
1541 }
1542}
1543
1544LayerValue<PrimaryPairingMode> parse_primary_pairing_mode(
1545 const TextFormatter *format, const YAML::Node &node, const std::string &key, const std::string &file_path)
1546{
1547 if (!node.IsDefined()) {
1549 }
1550
1551 try {
1552 const auto mark = node.Mark();
1553 const auto source = make_source_string(format, file_path, mark);
1554 const auto details = make_source_details(format, file_path, mark);
1555 const auto node_value = node.as<std::string>();
1556 const auto mode_opt = primary_pairing_mode_from_str(node_value);
1557
1558 if (!mode_opt.has_value()) {
1559 const auto error = format->format(
1560 "'{}' has invalid value '{}'.", FormatParam{key, Style::bold}, FormatParam{node_value, Style::bold});
1561 return LayerValue<PrimaryPairingMode>::invalid(error, source, details);
1562 }
1563
1564 return LayerValue<PrimaryPairingMode>::valid(mode_opt.value(), key, source, details);
1565 }
1566 catch (const YAML::Exception &e) {
1567 const auto mark = node.Mark();
1568 const auto error =
1569 format->format("Failed to parse '{}' as PrimaryPairingMode: {}.", FormatParam{key, Style::bold}, e.what());
1570 const auto source = make_source_string(format, file_path, mark);
1571 const auto details = make_source_details(format, file_path, mark);
1572 return LayerValue<PrimaryPairingMode>::invalid(error, source, details);
1573 }
1574}
1575
1576LayerValue<PackingStrategyParams> parse_packing_strategy_params(
1577 const TextFormatter *format, const YAML::Node &node, const std::string &key, const std::string &file_path)
1578{
1579 if (!node.IsDefined()) {
1581 }
1582
1583 try {
1584 const auto mark = node.Mark();
1585 const auto source = make_source_string(format, file_path, mark);
1586 const auto details = make_source_details(format, file_path, mark);
1587
1588 if (!node.IsMap()) {
1589 const auto error = format->format(
1590 "'{}' must be a map of strategy names to parameter objects.", FormatParam{key, Style::bold});
1591 return LayerValue<PackingStrategyParams>::invalid(error, source, details);
1592 }
1593
1594 PackingStrategyParams params;
1595
1596 // Parse backtracking sub-map
1597 if (node["backtracking"].IsDefined()) {
1598 const auto &bt_node = node["backtracking"];
1599 if (!bt_node.IsMap()) {
1600 const auto bt_mark = bt_node.Mark();
1601 const auto bt_source = make_source_string(format, file_path, bt_mark);
1602 const auto bt_details = make_source_details(format, file_path, bt_mark);
1603 const auto error = format->format("'{}' backtracking must be a map.", FormatParam{key, Style::bold});
1604 return LayerValue<PackingStrategyParams>::invalid(error, bt_source, bt_details);
1605 }
1606
1607 if (bt_node["search_algorithm"].IsDefined()) {
1608 const auto &sa_node = bt_node["search_algorithm"];
1609 const auto sa_str = sa_node.as<std::string>();
1610 const auto sa_opt = search_algorithm_from_str(sa_str);
1611 if (!sa_opt.has_value()) {
1612 const auto sa_mark = sa_node.Mark();
1613 const auto sa_source = make_source_string(format, file_path, sa_mark);
1614 const auto sa_details = make_source_details(format, file_path, sa_mark);
1615 const auto error = format->format(
1616 "'{}' backtracking search_algorithm has invalid value '{}'.",
1618 FormatParam{sa_str, Style::bold});
1619 return LayerValue<PackingStrategyParams>::invalid(error, sa_source, sa_details);
1620 }
1621 const auto sa_mark = sa_node.Mark();
1623 sa_opt.value(),
1624 key + ".backtracking.search_algorithm",
1625 "Packing Strategy Params (backtracking) search_algorithm",
1626 make_source_string(format, file_path, sa_mark),
1627 make_source_details(format, file_path, sa_mark)};
1628 }
1629
1630 if (bt_node["node_cutoff"].IsDefined()) {
1631 const auto &nc_node = bt_node["node_cutoff"];
1632 const auto nc_val = nc_node.as<std::size_t>();
1633 const auto nc_mark = nc_node.Mark();
1635 nc_val,
1636 key + ".backtracking.node_cutoff",
1637 "Packing Strategy Params (backtracking) node_cutoff",
1638 make_source_string(format, file_path, nc_mark),
1639 make_source_details(format, file_path, nc_mark)};
1640 }
1641
1642 if (bt_node["best_branches"].IsDefined()) {
1643 const auto &bb_node = bt_node["best_branches"];
1644 const auto bb_val = bb_node.as<std::size_t>();
1645 const auto bb_mark = bb_node.Mark();
1647 bb_val,
1648 key + ".backtracking.best_branches",
1649 "Packing Strategy Params (backtracking) best_branches",
1650 make_source_string(format, file_path, bb_mark),
1651 make_source_details(format, file_path, bb_mark)};
1652 }
1653
1654 if (bt_node["smart_prune"].IsDefined()) {
1655 const auto &sp_node = bt_node["smart_prune"];
1656 const auto sp_val = sp_node.as<bool>();
1657 const auto sp_mark = sp_node.Mark();
1659 sp_val,
1660 key + ".backtracking.smart_prune",
1661 "Packing Strategy Params (backtracking) smart_prune",
1662 make_source_string(format, file_path, sp_mark),
1663 make_source_details(format, file_path, sp_mark)};
1664 }
1665 }
1666
1667 // Parse overload_and_remove sub-map
1668 if (node["overload_and_remove"].IsDefined()) {
1669 const auto &oar_node = node["overload_and_remove"];
1670 if (!oar_node.IsMap()) {
1671 const auto oar_mark = oar_node.Mark();
1672 const auto oar_source = make_source_string(format, file_path, oar_mark);
1673 const auto oar_details = make_source_details(format, file_path, oar_mark);
1674 const auto error =
1675 format->format("'{}' overload_and_remove must be a map.", FormatParam{key, Style::bold});
1676 return LayerValue<PackingStrategyParams>::invalid(error, oar_source, oar_details);
1677 }
1678
1679 if (oar_node["max_attempts"].IsDefined()) {
1680 const auto &ma_node = oar_node["max_attempts"];
1681 const auto ma_val = ma_node.as<std::size_t>();
1682 const auto ma_mark = ma_node.Mark();
1684 ma_val,
1685 key + ".overload_and_remove.max_attempts",
1686 "Packing Strategy Params (overload_and_remove) max_attempts",
1687 make_source_string(format, file_path, ma_mark),
1688 make_source_details(format, file_path, ma_mark)};
1689 }
1690
1691 if (oar_node["seed"].IsDefined()) {
1692 const auto &seed_node = oar_node["seed"];
1693 const auto seed_val = seed_node.as<std::uint64_t>();
1694 const auto seed_mark = seed_node.Mark();
1696 seed_val,
1697 key + ".overload_and_remove.seed",
1698 "Packing Strategy Params (overload_and_remove) seed",
1699 make_source_string(format, file_path, seed_mark),
1700 make_source_details(format, file_path, seed_mark)};
1701 }
1702
1703 if (oar_node["shuffle_strategy"].IsDefined()) {
1704 const auto &ss_node = oar_node["shuffle_strategy"];
1705 const auto ss_str = ss_node.as<std::string>();
1706 const auto ss_opt = shuffle_strategy_from_str(ss_str);
1707 if (!ss_opt.has_value()) {
1708 const auto ss_mark = ss_node.Mark();
1709 const auto ss_source = make_source_string(format, file_path, ss_mark);
1710 const auto ss_details = make_source_details(format, file_path, ss_mark);
1711 const auto error = format->format(
1712 "'{}' overload_and_remove shuffle_strategy has invalid value '{}'.",
1714 FormatParam{ss_str, Style::bold});
1715 return LayerValue<PackingStrategyParams>::invalid(error, ss_source, ss_details);
1716 }
1717 const auto ss_mark = ss_node.Mark();
1719 ss_opt.value(),
1720 key + ".overload_and_remove.shuffle_strategy",
1721 "Packing Strategy Params (overload_and_remove) shuffle_strategy",
1722 make_source_string(format, file_path, ss_mark),
1723 make_source_details(format, file_path, ss_mark)};
1724 }
1725 }
1726
1727 return LayerValue<PackingStrategyParams>::valid(std::move(params), key, source, details);
1728 }
1729 catch (const YAML::Exception &e) {
1730 const auto mark = node.Mark();
1731 const auto error = format->format(
1732 "Failed to parse '{}' as packing strategy params: {}.", FormatParam{key, Style::bold}, e.what());
1733 const auto source = make_source_string(format, file_path, mark);
1734 const auto details = make_source_details(format, file_path, mark);
1735 return LayerValue<PackingStrategyParams>::invalid(error, source, details);
1736 }
1737}
1738
1751std::optional<YAML::Node> load_yaml_file(
1752 const std::filesystem::path &path,
1753 const TextFormatter *format = nullptr,
1754 const UserDiagnostics *diagnostics = nullptr,
1755 bool *out_had_unknown_keys = nullptr)
1756{
1757 // Check cache first
1758 const auto cache_it = yaml_cache.find(path);
1759 if (cache_it != yaml_cache.end()) {
1760 return cache_it->second;
1761 }
1762
1763 // File doesn't exist, return nullopt
1764 if (!std::filesystem::exists(path)) {
1765 return std::nullopt;
1766 }
1767
1768 // Try to load and cache the file
1769 try {
1770 auto node = YAML::LoadFile(path.string());
1771 yaml_cache[path] = node;
1772
1773 // Also cache the file contents line-by-line for source info
1774 std::ifstream file{path};
1775 std::vector<std::string> lines;
1776 std::string line;
1777 while (std::getline(file, line)) {
1778 lines.push_back(line);
1779 }
1780 file_lines_cache[path] = std::move(lines);
1781
1782 // Validate paths if diagnostics is provided
1783 if (format != nullptr && diagnostics != nullptr) {
1784 if (validate_yaml_paths(format, diagnostics, path, node) && out_had_unknown_keys != nullptr) {
1785 *out_had_unknown_keys = true;
1786 }
1787 }
1788
1789 return node;
1790 }
1791 catch (const YAML::Exception &) {
1792 // Failed to parse YAML, return nullopt
1793 return std::nullopt;
1794 }
1795}
1796
1812std::vector<std::filesystem::path>
1813get_tileset_config_path_chain(const std::filesystem::path &project_root, const std::string &tileset)
1814{
1815 std::vector<std::filesystem::path> paths;
1816
1817 // Porytiles utility directory root
1818 const auto porytiles_dir = project_root / "porytiles";
1819
1820 // Priority order (highest to lowest):
1821 // 1. porytiles/tilesets/{tileset_name}/config.local.yaml
1822 paths.push_back(porytiles_dir / "tilesets" / tileset / "config.local.yaml");
1823
1824 // 2. porytiles/tilesets/{tileset_name}/config.yaml
1825 paths.push_back(porytiles_dir / "tilesets" / tileset / "config.yaml");
1826
1827 // 3. porytiles/config.local.yaml
1828 paths.push_back(porytiles_dir / "config.local.yaml");
1829
1830 // 4. porytiles/config.yaml
1831 paths.push_back(porytiles_dir / "config.yaml");
1832
1833 return paths;
1834}
1835
1847get_config_path_chain(const std::filesystem::path &project_root, ConfigScopeType type, const std::string &scope)
1848{
1849 switch (type) {
1850 case ConfigScopeType::tileset:
1851 return get_tileset_config_path_chain(project_root, scope);
1852 case ConfigScopeType::layout:
1853 panic("Layout config path chain resolution is not yet implemented.");
1854 }
1855 // Should never reach here
1856 panic("Invalid ConfigScopeType");
1857}
1858
1872[[nodiscard]] bool preload_and_validate_yaml_files(
1873 const TextFormatter *format,
1874 const UserDiagnostics *diagnostics,
1875 const std::filesystem::path &project_root,
1876 ConfigScopeType type,
1877 const std::string &scope)
1878{
1879 auto paths_result = get_config_path_chain(project_root, type, scope);
1880 if (!paths_result.has_value()) {
1881 return false;
1882 }
1883
1884 bool had_unknown_keys = false;
1885 for (const auto &path : paths_result.value()) {
1886 load_yaml_file(path, format, diagnostics, &had_unknown_keys);
1887 }
1888
1889 return had_unknown_keys;
1890}
1891
1915template <typename T, typename LoadFunc, typename NodeExtractFunc, typename ParseFunc>
1916LayerValue<T> search_config_files(
1917 const TextFormatter *format,
1918 const std::vector<std::filesystem::path> &paths,
1919 LoadFunc load_func,
1920 NodeExtractFunc extract_node_func,
1921 ParseFunc parse_func,
1922 const std::string &key,
1923 const std::string &provider_name)
1924{
1925 for (const auto &path : paths) {
1926 const auto yaml_doc = load_func(path);
1927 if (!yaml_doc.has_value()) {
1928 // File doesn't exist or couldn't be loaded, try next file
1929 continue;
1930 }
1931
1932 try {
1933 const auto node = extract_node_func(yaml_doc.value());
1934 auto result = parse_func(format, node, key, path.string());
1935
1936 // If we got a valid value or an error, return it immediately
1937 if (result.state == ValidationState::valid || result.state == ValidationState::invalid) {
1938 result.source_key = provider_name;
1939 return result;
1940 }
1941
1942 // If not_provided, continue to next file
1943 }
1944 catch (const YAML::Exception &) {
1945 // Node extraction or parsing threw an exception, treat as not_provided for this file
1946 // and continue to the next file
1947 continue;
1948 }
1949 }
1950
1951 // Not found in any file
1953}
1954
1955} // namespace
A result type that maintains a chainable sequence of errors for debugging and error reporting.
A service for printing file lines with highlighted lines and line numbers.
A text parameter with associated styling for formatted output.
A generic palette container for colors that support transparency checking.
Definition palette.hpp:45
Represents a 32-bit RGBA color.
Definition rgba32.hpp:21
static constexpr std::uint8_t alpha_opaque
Definition rgba32.hpp:24
static const Style bold
Bold text formatting.
Abstract base class for applying text styling with context-aware formatting.
virtual std::string format(const std::string &format_str, const std::vector< FormatParam > &params) const
Formats a string with styled parameters using fmtlib syntax.
Abstract class for structured error reporting and diagnostic output.
virtual void error(const std::string &tag, const std::vector< std::string > &lines) const =0
Display a tagged error message.
std::string name
std::optional< ProviderDefinition > provider
const std::unordered_set< std::string > valid_yaml_paths
Set of valid YAML configuration paths.
std::optional< ArtifactEditMode > artifact_edit_mode_from_str(const std::string &str)
Parses a string into a ArtifactEditMode with fuzzy matching.
std::map< std::string, MetatileAttributeFieldOverride > MetatileAttributeFieldOverrides
A map from field name to its override; applied at schema load time.
std::vector< RolePinDefinition > RolePinDefinitions
An ordered list of role pin definitions; order is display/declaration order (also CSV column order).
void panic(const StringViewSourceLoc &s)
Unconditionally terminates the program with a panic message.
Definition panic.cpp:43
std::optional< PackingStrategyType > packing_strategy_type_from_str(const std::string &str)
Parses a string into a PackingStrategyType with fuzzy matching.
std::optional< FrameLinking > frame_linking_from_str(const std::string &str)
Parses a string into a FrameLinking with fuzzy matching.
std::optional< PrimaryPairingMode > primary_pairing_mode_from_str(const std::string &str)
Parses a string into a PrimaryPairingMode with fuzzy matching.
std::optional< AnimMultiPaletteSubtileResolutionStrategy > anim_multi_palette_subtile_resolution_strategy_from_str(const std::string &str)
Parses a string into a AnimMultiPaletteSubtileResolutionStrategy with fuzzy matching.
const std::unordered_set< std::string > valid_yaml_map_prefixes
Set of YAML path prefixes that represent map-type configuration values.
std::optional< ShuffleStrategy > shuffle_strategy_from_str(const std::string &str)
Parses a string into a ShuffleStrategy with fuzzy matching.
std::vector< MetatileAttributeFieldDefinition > MetatileAttributeFieldDefinitions
An ordered list of metatile attribute field definitions; order is display/declaration order.
std::unordered_map< std::string, PerAnimOverride > PerAnimOverrides
Per-animation configuration map.
@ not_provided
nothing attribute-related was found; other providers should be consulted
@ valid
one or more usable candidate sets were inferred
@ invalid
no layout could be determined from what the project declares (fatal at resolution time)
@ error
Emit a formatted error and fail decompilation.
std::optional< FieldRole > field_role_from_string(const std::string &text)
Parses a FieldRole from its string form, or nullopt when the name is not a known role.
std::optional< TileSharingPacking > tile_sharing_packing_from_str(const std::string &str)
Parses a string into a TileSharingPacking with fuzzy matching.
std::optional< TileSharingAlignment > tile_sharing_alignment_from_str(const std::string &str)
Parses a string into a TileSharingAlignment with fuzzy matching.
ConfigScopeType
Specifies the scope type for configuration value lookups.
@ tileset
Configuration scoped to a specific tileset.
std::optional< AnimPaletteResolutionStrategy > anim_palette_resolution_strategy_from_str(const std::string &str)
Parses a string into a AnimPaletteResolutionStrategy with fuzzy matching.
std::optional< TilesPaletteMode > tiles_palette_mode_from_str(const std::string &str)
Parses a string into a TilesPaletteMode with fuzzy matching.
std::optional< SearchAlgorithm > search_algorithm_from_str(const std::string &str)
Parses a string into a SearchAlgorithm with fuzzy matching.
std::optional< AnimKeyFrameResolutionStrategy > anim_key_frame_resolution_strategy_from_str(const std::string &str)
Parses a string into a AnimKeyFrameResolutionStrategy with fuzzy matching.
Utility functions for string manipulation and formatting.
ConfigPODField< std::size_t > best_branches
ConfigPODField< std::size_t > node_cutoff
ConfigPODField< SearchAlgorithm > search_algorithm
A lightweight wrapper for per-field configuration values with source metadata.
A small container that holds an optional-wrapped value, validation state, and metadata about the valu...
static LayerValue valid(T val, std::string source_key, std::string source_info)
Creates a LayerValue representing a valid configuration value.
static LayerValue not_provided()
Creates a LayerValue representing that the provider does not supply this configuration.
static LayerValue invalid(std::string error, std::string source_info)
Creates a LayerValue representing an invalid configuration value.
A user-authored (or inferred) description of one metatile attribute field.
A partial override of a single field, merged additively onto a baseline definition.
std::optional< ProviderDefinitionOverride > provider
ConfigPODField< ShuffleStrategy > shuffle_strategy
ConfigPODField< std::uint64_t > seed
ConfigPODField< std::size_t > max_attempts
Container for per-strategy packing parameters.
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
A partial override of a field's provider definition.
std::optional< std::unordered_set< std::string > > skipped
Describes where and how a field's named values are declared.
A user request to emit a trailing pin column for one schema role in attributes.csv.