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
9#include "yaml-cpp/yaml.h"
10
14
15// The anonymous namespace ensures internal linkage per translation unit
16// This file is intentionally included only in yaml_file_provider.cpp
17namespace {
18
19using namespace porytiles2;
20
21// Static caches shared across all YamlFileProvider instances
22std::map<std::filesystem::path, YAML::Node> yaml_cache;
23std::map<std::filesystem::path, std::vector<std::string>> file_lines_cache;
24
36std::string get_line_content(const std::filesystem::path &path, std::size_t line_num)
37{
38 const auto it = file_lines_cache.find(path);
39 if (it != file_lines_cache.end() && line_num < it->second.size()) {
40 return it->second[line_num];
41 }
42 return "";
43}
44
58std::string make_source_string(const TextFormatter *format, const std::string &file_path, const YAML::Mark &mark)
59{
60 return format->format("{}:{}", FormatParam{file_path}, FormatParam{mark.line + 1});
61}
62
88std::vector<std::string> make_source_details(
89 const TextFormatter *format, const std::string &file_path, const YAML::Mark &mark, std::size_t window_size = 7)
90{
91 std::vector<std::string> details;
92
93 const std::filesystem::path path{file_path};
94 const auto it = file_lines_cache.find(path);
95 if (it == file_lines_cache.end()) {
96 return details;
97 }
98
99 const auto &lines = it->second;
100 const std::size_t line_num = mark.line; // 0-indexed
101
102 if (lines.empty() || line_num >= lines.size()) {
103 return details;
104 }
105
106 // Calculate window boundaries
107 const std::size_t half_window = (window_size - 1) / 2;
108 const std::size_t start = (line_num >= half_window) ? line_num - half_window : 0;
109 const std::size_t end = std::min(line_num + half_window + 1, lines.size());
110
111 // Build contextual view
112 for (std::size_t i = start; i < end; ++i) {
113 const auto formatted_arrow =
114 format->format("{}", FormatParam{"-> ", Style::bold | Style::italic | Style::yellow});
115 const std::string prefix = (i == line_num) ? formatted_arrow : " ";
116
117 if (i == line_num) {
118 // TODO: the spaces after the line number ":" are hardcoded
119 // If a user supplies a YAML file with 1000+ lines, it will mess up the formatting.
120 const auto highlight_line =
121 format->format("{}", FormatParam{lines[i], Style::bold | Style::italic | Style::yellow});
122 details.push_back(prefix + std::to_string(i + 1) + ": " + highlight_line);
123 }
124 else {
125 details.push_back(prefix + std::to_string(i + 1) + ": " + lines[i]);
126 }
127 }
128
129 return details;
130}
131
142parse_size_t(const TextFormatter *format, const YAML::Node &node, const std::string &key, const std::string &file_path)
143{
144 if (!node.IsDefined()) {
146 }
147
148 try {
149 const auto value = node.as<std::size_t>();
150 const auto mark = node.Mark();
151 const auto source = make_source_string(format, file_path, mark);
152 const auto details = make_source_details(format, file_path, mark);
153 return LayerValue<std::size_t>::valid(value, source, details);
154 }
155 catch (const YAML::Exception &e) {
156 const auto mark = node.Mark();
157 const auto error =
158 format->format("failed to parse '{}' as std::size_t: {}", FormatParam{key, Style::bold}, e.what());
159 const auto source = make_source_string(format, file_path, mark);
160 const auto details = make_source_details(format, file_path, mark);
161 return LayerValue<std::size_t>::invalid(error, source, details);
162 }
163}
164
175parse_bool(const TextFormatter *format, const YAML::Node &node, const std::string &key, const std::string &file_path)
176{
177 if (!node.IsDefined()) {
179 }
180
181 try {
182 const auto value = node.as<bool>();
183 const auto mark = node.Mark();
184 const auto source = make_source_string(format, file_path, mark);
185 const auto details = make_source_details(format, file_path, mark);
186 return LayerValue<bool>::valid(value, source, details);
187 }
188 catch (const YAML::Exception &e) {
189 const auto mark = node.Mark();
190 const auto error = format->format("failed to parse '{}' as bool: {}", FormatParam{key, Style::bold}, e.what());
191 const auto source = make_source_string(format, file_path, mark);
192 const auto details = make_source_details(format, file_path, mark);
193 return LayerValue<bool>::invalid(error, source, details);
194 }
195}
196
211parse_rgba32(const TextFormatter *format, const YAML::Node &node, const std::string &key, const std::string &file_path)
212{
213 if (!node.IsDefined()) {
215 }
216
217 try {
218 const auto mark = node.Mark();
219 const auto details = make_source_details(format, file_path, mark);
220
221 if (!node.IsSequence()) {
222 const auto error =
223 format->format("'{}' must be a sequence [r, g, b] or [r, g, b, a]", FormatParam{key, Style::bold});
224 const auto source = make_source_string(format, file_path, mark);
225 return LayerValue<Rgba32>::invalid(error, source, details);
226 }
227
228 if (node.size() < 3 || node.size() > 4) {
229 const auto error = format->format(
230 "'{}' must have 3 or 4 elements [r, g, b] or [r, g, b, a], got {}",
231 FormatParam{key, Style::bold},
232 FormatParam{node.size(), Style::bold});
233 const auto source = make_source_string(format, file_path, mark);
234 return LayerValue<Rgba32>::invalid(error, source, details);
235 }
236
237 const auto r = node[0].as<std::uint8_t>();
238 const auto g = node[1].as<std::uint8_t>();
239 const auto b = node[2].as<std::uint8_t>();
240 const auto a = (node.size() == 4) ? node[3].as<std::uint8_t>() : Rgba32::alpha_opaque;
241
242 const Rgba32 color{r, g, b, a};
243 const auto source = make_source_string(format, file_path, mark);
244 return LayerValue<Rgba32>::valid(color, source, details);
245 }
246 catch (const YAML::Exception &e) {
247 const auto mark = node.Mark();
248 const auto error =
249 format->format("failed to parse '{}' as Rgba32: {}", FormatParam{key, Style::bold}, e.what());
250 const auto source = make_source_string(format, file_path, mark);
251 const auto details = make_source_details(format, file_path, mark);
252 return LayerValue<Rgba32>::invalid(error, source, details);
253 }
254}
255
268LayerValue<TilesPalMode> parse_tiles_pal_mode(
269 const TextFormatter *format, const YAML::Node &node, const std::string &key, const std::string &file_path)
270{
271 if (!node.IsDefined()) {
273 }
274
275 try {
276 const auto mark = node.Mark();
277 const auto details = make_source_details(format, file_path, mark);
278 const auto str = node.as<std::string>();
279 const auto mode_opt = tiles_pal_mode_from_str(str);
280
281 if (!mode_opt.has_value()) {
282 const auto error = format->format(
283 "'{}' has invalid value '{}', expected 'true-color' or 'greyscale'",
284 FormatParam{key, Style::bold},
285 FormatParam{str, Style::bold});
286 const auto source = make_source_string(format, file_path, mark);
287 return LayerValue<TilesPalMode>::invalid(error, source, details);
288 }
289
290 const auto source = make_source_string(format, file_path, mark);
291 return LayerValue<TilesPalMode>::valid(mode_opt.value(), source, details);
292 }
293 catch (const YAML::Exception &e) {
294 const auto mark = node.Mark();
295 const auto error =
296 format->format("failed to parse '{}' as TilesPalMode: {}", FormatParam{key, Style::bold}, e.what());
297 const auto source = make_source_string(format, file_path, mark);
298 const auto details = make_source_details(format, file_path, mark);
299 return LayerValue<TilesPalMode>::invalid(error, source, details);
300 }
301}
302
313std::optional<YAML::Node> load_yaml_file(const std::filesystem::path &path)
314{
315 // Check cache first
316 const auto cache_it = yaml_cache.find(path);
317 if (cache_it != yaml_cache.end()) {
318 return cache_it->second;
319 }
320
321 // File doesn't exist, return nullopt
322 if (!std::filesystem::exists(path)) {
323 return std::nullopt;
324 }
325
326 // Try to load and cache the file
327 try {
328 auto node = YAML::LoadFile(path.string());
329 yaml_cache[path] = node;
330
331 // Also cache the file contents line-by-line for source info
332 std::ifstream file{path};
333 std::vector<std::string> lines;
334 std::string line;
335 while (std::getline(file, line)) {
336 lines.push_back(line);
337 }
338 file_lines_cache[path] = std::move(lines);
339
340 return node;
341 }
342 catch (const YAML::Exception &) {
343 // Failed to parse YAML, return nullopt
344 return std::nullopt;
345 }
346}
347
363std::vector<std::filesystem::path> get_tileset_config_path_chain(
364 const std::filesystem::path &project_root,
365 const TilesetArtifactKeyProvider *key_provider,
366 const std::string &tileset)
367{
368 /*
369 * TODO: once we add layout support, we'll need a separate method to resolve the config path chain for layouts,
370 * since layouts will have their own LayoutArtifactKeyProvider.
371 */
372
373 std::vector<std::filesystem::path> paths;
374
375 // Get tileset-specific config paths using the key provider
376 using enum TilesetArtifact::Type;
377 const auto tileset_local_config_key = key_provider->key_for(tileset, TilesetArtifact{local_config});
378 const auto tileset_config_key = key_provider->key_for(tileset, TilesetArtifact{config});
379
380 // Priority order (highest to lowest):
381 // 1. tileset_folder/porytiles.local.yaml
382 paths.push_back(std::filesystem::path{tileset_local_config_key.key()});
383
384 // 2. tileset_folder/porytiles.yaml
385 paths.push_back(std::filesystem::path{tileset_config_key.key()});
386
387 // 3. project_root/porytiles.local.yaml
388 paths.push_back(project_root / "porytiles.local.yaml");
389
390 // 4. project_root/porytiles.yaml
391 paths.push_back(project_root / "porytiles.yaml");
392
393 return paths;
394}
395
420template <typename T, typename LoadFunc, typename NodeExtractFunc, typename ParseFunc>
421LayerValue<T> search_config_files(
422 const TextFormatter *format,
423 const std::vector<std::filesystem::path> &paths,
424 LoadFunc load_func,
425 NodeExtractFunc extract_node_func,
426 ParseFunc parse_func,
427 const std::string &key)
428{
429 for (const auto &path : paths) {
430 const auto yaml_doc = load_func(path);
431 if (!yaml_doc.has_value()) {
432 // File doesn't exist or couldn't be loaded, try next file
433 continue;
434 }
435
436 try {
437 const auto node = extract_node_func(yaml_doc.value());
438 const auto result = parse_func(format, node, key, path.string());
439
440 // If we got a valid value or an error, return it immediately
441 if (result.state == ValidationState::valid || result.state == ValidationState::invalid) {
442 return result;
443 }
444
445 // If not_provided, continue to next file
446 }
447 catch (const YAML::Exception &) {
448 // Node extraction or parsing threw an exception, treat as not_provided for this file
449 // and continue to the next file
450 continue;
451 }
452 }
453
454 // Not found in any file
456}
457
458} // namespace
A text parameter with associated styling for formatted output.
Represents a 32-bit RGBA color.
Definition rgba32.hpp:21
static constexpr std::uint8_t alpha_opaque
Definition rgba32.hpp:24
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 interface for generating keys and discovering tileset artifacts in a backing store.
virtual ArtifactKey key_for(const std::string &tileset_name, const TilesetArtifact &artifact) const =0
Constructs a key for a given tileset artifact.
Represents a Pokémon Generation III decomp tileset artifact with type and optional metadata.
Type
Enumeration of all supported tileset artifact types.
std::optional< TilesPalMode > tiles_pal_mode_from_str(const std::string &str)
A small container that holds an optional-wrapped value, validation state, and metadata about the valu...
static LayerValue valid(T val, 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.