Porytiles
Loading...
Searching...
No Matches
project_layout_metadata_provider.cpp
Go to the documentation of this file.
2
3#include <filesystem>
4#include <fstream>
5#include <map>
6#include <set>
7#include <string>
8#include <vector>
9
10#include "nlohmann/json.hpp"
11
15
16namespace {
17
18using namespace porytiles;
19
20const std::filesystem::path layouts_json_rel_path = std::filesystem::path{"data"} / "layouts" / "layouts.json";
21
36[[nodiscard]] ChainableResult<void> ensure_layouts_parsed(
37 const std::filesystem::path &project_root,
38 bool &layouts_parsed,
39 std::string &layouts_table_label,
40 std::vector<ProjectLayoutMetadata> &layout_entries,
41 std::map<std::string, std::size_t> &layout_index,
42 const TextFormatter *format)
43{
44 if (layouts_parsed) {
45 return {};
46 }
47
48 const auto layouts_path = project_root / layouts_json_rel_path;
49
50 std::ifstream file{layouts_path};
51 if (!file.is_open()) {
52 return FormattableError{
53 format->format("Failed to open layouts file '{}'.", FormatParam{layouts_path.string(), Style::bold})};
54 }
55
56 nlohmann::json json_data;
57 try {
58 file >> json_data;
59 }
60 catch (const nlohmann::json::parse_error &e) {
61 return FormattableError{format->format(
62 "Failed to parse layouts JSON from '{}': {}.",
63 FormatParam{layouts_path.string(), Style::bold},
64 FormatParam{std::string{e.what()}})};
65 }
66
67 if (!json_data.contains("layouts_table_label")) {
68 return FormattableError{format->format(
69 "Layouts file '{}' missing required field 'layouts_table_label'.",
70 FormatParam{layouts_path.string(), Style::bold})};
71 }
72
73 // Type-check the top-level fields before extracting them: an unchecked get<std::string>() on a non-string value
74 // throws an nlohmann type_error that would escape this function, so a malformed file must fail here as a
75 // ChainableResult like every other malformed-file case.
76 if (!json_data.at("layouts_table_label").is_string()) {
77 return FormattableError{format->format(
78 "Layouts file '{}' field 'layouts_table_label' must be a string.",
79 FormatParam{layouts_path.string(), Style::bold})};
80 }
81
82 if (!json_data.contains("layouts")) {
83 return FormattableError{format->format(
84 "Layouts file '{}' missing required field 'layouts'.", FormatParam{layouts_path.string(), Style::bold})};
85 }
86
87 if (!json_data.at("layouts").is_array()) {
88 return FormattableError{format->format(
89 "Layouts file '{}' field 'layouts' must be an array.", FormatParam{layouts_path.string(), Style::bold})};
90 }
91
92 // Build into local temporaries so that a failure mid-loop does not leave the mutable cache in a partially populated
93 // state. Only commit to the real cache on full success.
94 auto table_label = json_data.at("layouts_table_label").get<std::string>();
95
96 const auto &layouts_array = json_data.at("layouts");
97 std::vector<ProjectLayoutMetadata> entries;
98 std::map<std::string, std::size_t> index;
99 entries.reserve(layouts_array.size());
100
101 for (const auto &entry : layouts_array) {
102 try {
103 auto id = entry.at("id").get<std::string>();
104 auto name = entry.at("name").get<std::string>();
105 auto width = entry.at("width").get<std::size_t>();
106 auto height = entry.at("height").get<std::size_t>();
107 auto primary_tileset = entry.at("primary_tileset").get<std::string>();
108 auto secondary_tileset = entry.at("secondary_tileset").get<std::string>();
109 auto border_fp = std::filesystem::path{entry.at("border_filepath").get<std::string>()};
110 auto blockdata_fp = std::filesystem::path{entry.at("blockdata_filepath").get<std::string>()};
111
112 const auto idx = entries.size();
113 entries.push_back(
115 std::move(id),
116 std::move(name),
117 width,
118 height,
119 std::move(primary_tileset),
120 std::move(secondary_tileset),
121 std::move(border_fp),
122 std::move(blockdata_fp)});
123
124 index.emplace(entries.at(idx).id(), idx);
125 index.emplace(entries.at(idx).name(), idx);
126 }
127 catch (const nlohmann::json::exception &e) {
128 return FormattableError{format->format(
129 "Malformed layout entry in '{}': {}.",
130 FormatParam{layouts_path.string(), Style::bold},
131 FormatParam{std::string{e.what()}})};
132 }
133 }
134
135 layouts_table_label = std::move(table_label);
136 layout_entries = std::move(entries);
137 layout_index = std::move(index);
138 layouts_parsed = true;
139 return {};
140}
141
156[[nodiscard]] ChainableResult<const ProjectLayoutMetadata *> lookup_layout(
157 const std::string &layout_name_or_id,
158 const std::filesystem::path &project_root,
159 bool &layouts_parsed,
160 std::string &layouts_table_label,
161 std::vector<ProjectLayoutMetadata> &layout_entries,
162 std::map<std::string, std::size_t> &layout_index,
163 const TextFormatter *format)
164{
165 if (const auto ensure_result = ensure_layouts_parsed(
166 project_root, layouts_parsed, layouts_table_label, layout_entries, layout_index, format);
167 !ensure_result.has_value()) {
170 format->format("Failed to look up layout '{}'.", FormatParam{layout_name_or_id, Style::bold})},
171 ensure_result};
172 }
173
174 if (!layout_index.contains(layout_name_or_id)) {
175 return FormattableError{
176 format->format("Layout '{}' not found in layouts.json.", FormatParam{layout_name_or_id, Style::bold})};
177 }
178
179 return &layout_entries.at(layout_index.at(layout_name_or_id));
180}
181
182} // namespace
183
184namespace porytiles {
185
186bool ProjectLayoutMetadataProvider::exists(const std::string &layout_name_or_id) const
187{
188 if (const auto result = ensure_layouts_parsed(
189 project_root_, layouts_parsed_, layouts_table_label_, layout_entries_, layout_index_, format_);
190 !result.has_value()) {
191 return false;
192 }
193 return layout_index_.contains(layout_name_or_id);
194}
195
197{
199 layout,
200 ::lookup_layout(
201 layout_name_or_id,
202 project_root_,
203 layouts_parsed_,
204 layouts_table_label_,
205 layout_entries_,
206 layout_index_,
207 format_),
208 std::size_t);
209 return layout->width();
210}
211
213{
215 layout,
216 ::lookup_layout(
217 layout_name_or_id,
218 project_root_,
219 layouts_parsed_,
220 layouts_table_label_,
221 layout_entries_,
222 layout_index_,
223 format_),
224 std::size_t);
225 return layout->height();
226}
227
229{
231 layout,
232 ::lookup_layout(
233 layout_name_or_id,
234 project_root_,
235 layouts_parsed_,
236 layouts_table_label_,
237 layout_entries_,
238 layout_index_,
239 format_),
240 std::string);
241 return layout->primary_tileset();
242}
243
245ProjectLayoutMetadataProvider::secondary_tileset(const std::string &layout_name_or_id) const
246{
248 layout,
249 ::lookup_layout(
250 layout_name_or_id,
251 project_root_,
252 layouts_parsed_,
253 layouts_table_label_,
254 layout_entries_,
255 layout_index_,
256 format_),
257 std::string);
258 return layout->secondary_tileset();
259}
260
262{
264 ensure_layouts_parsed(
265 project_root_, layouts_parsed_, layouts_table_label_, layout_entries_, layout_index_, format_),
266 std::set<std::string>,
267 "Failed to enumerate layout names.");
268
269 std::set<std::string> names;
270 for (const auto &entry : layout_entries_) {
271 names.insert(entry.name());
272 }
273 return names;
274}
275
277{
279 ensure_layouts_parsed(
280 project_root_, layouts_parsed_, layouts_table_label_, layout_entries_, layout_index_, format_),
281 std::set<std::string>,
282 "Failed to enumerate layout IDs.");
283
284 std::set<std::string> ids;
285 for (const auto &entry : layout_entries_) {
286 ids.insert(entry.id());
287 }
288 return ids;
289}
290
292{
294 ensure_layouts_parsed(
295 project_root_, layouts_parsed_, layouts_table_label_, layout_entries_, layout_index_, format_),
296 std::string,
297 "Failed to get layouts table label.");
298 return layouts_table_label_;
299}
300
302ProjectLayoutMetadataProvider::border_filepath(const std::string &layout_name_or_id) const
303{
305 layout,
306 ::lookup_layout(
307 layout_name_or_id,
308 project_root_,
309 layouts_parsed_,
310 layouts_table_label_,
311 layout_entries_,
312 layout_index_,
313 format_),
314 std::filesystem::path);
315 return project_root_ / layout->border_filepath();
316}
317
319ProjectLayoutMetadataProvider::blockdata_filepath(const std::string &layout_name_or_id) const
320{
322 layout,
323 ::lookup_layout(
324 layout_name_or_id,
325 project_root_,
326 layouts_parsed_,
327 layouts_table_label_,
328 layout_entries_,
329 layout_index_,
330 format_),
331 std::filesystem::path);
332 return project_root_ / layout->blockdata_filepath();
333}
334
336{
337 layouts_parsed_ = false;
338 layouts_table_label_.clear();
339 layout_entries_.clear();
340 layout_index_.clear();
341}
342
343} // namespace porytiles
#define PT_TRY_ASSIGN_PASS_ERR(var, expr, return_type)
Unwraps a ChainableResult, passing through the error chain with an empty FormattableError when types ...
#define PT_TRY_CALL_CHAIN_ERR(expr, return_type,...)
Unwraps a void ChainableResult, chaining a new error message on failure.
A result type that maintains a chainable sequence of errors for debugging and error reporting.
A text parameter with associated styling for formatted output.
General-purpose error implementation with formatted message support.
Definition error.hpp:57
ChainableResult< std::string > primary_tileset(const std::string &layout_name_or_id) const override
Determines the given layout's primary tileset.
ChainableResult< std::set< std::string > > layout_ids() const override
Returns all layout IDs known to this provider.
ChainableResult< std::filesystem::path > blockdata_filepath(const std::string &layout_name_or_id) const
Resolves blockdata file path for a layout by parsing layouts.json.
ChainableResult< std::size_t > width(const std::string &layout_name_or_id) const override
Determines the given layout's width in metatiles.
void invalidate_metadata_cache() const
Invalidates the lazy-loaded layout cache, forcing a re-parse on next access.
ChainableResult< std::set< std::string > > layout_names() const override
Returns all layout names known to this provider.
ChainableResult< std::string > layouts_table_label() const
ChainableResult< std::filesystem::path > border_filepath(const std::string &layout_name_or_id) const
Resolves border file path for a layout by parsing layouts.json.
bool exists(const std::string &layout_name_or_id) const override
Checks whether a layout exists in the backing store for the given layout name or ID.
ChainableResult< std::string > secondary_tileset(const std::string &layout_name_or_id) const override
Determines the given layout's secondary tileset.
ChainableResult< std::size_t > height(const std::string &layout_name_or_id) const override
Determines the given layout's height in metatiles.
Represents parsed metadata for a single layout from layouts.json.
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.
std::string name
@ layout
Configuration scoped to a specific layout.