Porytiles
Loading...
Searching...
No Matches
attributes_csv_loader.cpp
Go to the documentation of this file.
2
3#include <filesystem>
4#include <fstream>
5#include <unordered_map>
6
10
11namespace {
12
13using namespace porytiles;
14
15enum class CsvFormat { emerald, firered };
16
17struct CsvRow {
18 std::size_t metatile_id;
19 std::string behavior;
20 std::string terrain_type;
21 std::string encounter_type;
22};
23
24ChainableResult<CsvRow> parse_emerald_csv_row(
25 const std::string &line,
26 std::size_t line_index,
27 const std::filesystem::path &path,
28 const std::vector<std::string> &all_lines,
29 const TextFormatter &format,
30 const FileHighlightPrinter &file_printer)
31{
32 auto columns = split(line, ",");
33
34 if (columns.size() < 2) {
35 std::vector<std::string> err_lines{};
36 err_lines.push_back(format.format(
37 "{}:{}: expected at least 2 columns (id,behavior), found {}",
38 FormatParam{path.string(), Style::bold},
39 FormatParam{line_index + 1, Style::bold},
40 FormatParam{columns.size()}));
41 err_lines.emplace_back();
42 err_lines.append_range(file_printer.print(all_lines, std::vector{line_index}));
43 return FormattableError{std::move(err_lines)};
44 }
45
46 trim(columns[0]);
47 trim(columns[1]);
48
49 auto id_result = parse_int<int>(columns[0], 0);
50 if (!id_result.has_value()) {
51 std::vector<std::string> err_lines{};
52 err_lines.push_back(format.format(
53 "{}:{}: invalid metatile id '{}': {}",
54 FormatParam{path.string(), Style::bold},
55 FormatParam{line_index + 1, Style::bold},
56 FormatParam{columns[0], Style::bold},
57 FormatParam{id_result.error()}));
58 err_lines.emplace_back();
59 err_lines.append_range(file_printer.print(all_lines, std::vector{line_index}));
60 return FormattableError{std::move(err_lines)};
61 }
62
63 if (id_result.value() < 0) {
64 std::vector<std::string> err_lines{};
65 err_lines.push_back(format.format(
66 "{}:{}: metatile id '{}' cannot be negative",
67 FormatParam{path.string(), Style::bold},
68 FormatParam{line_index + 1, Style::bold},
69 FormatParam{columns[0], Style::bold}));
70 err_lines.emplace_back();
71 err_lines.append_range(file_printer.print(all_lines, std::vector{line_index}));
72 return FormattableError{std::move(err_lines)};
73 }
74
75 return CsvRow{static_cast<std::size_t>(id_result.value()), columns[1], "", ""};
76}
77
78ChainableResult<CsvRow> parse_firered_csv_row(
79 const std::string &line,
80 std::size_t line_index,
81 const std::filesystem::path &path,
82 const std::vector<std::string> &all_lines,
83 const TextFormatter &format,
84 const FileHighlightPrinter &file_printer)
85{
86 auto columns = split(line, ",");
87
88 if (columns.size() < 4) {
89 std::vector<std::string> err_lines{};
90 err_lines.push_back(format.format(
91 "{}:{}: expected 4 columns (id,behavior,terrainType,encounterType), found {}",
92 FormatParam{path.string(), Style::bold},
93 FormatParam{line_index + 1, Style::bold},
94 FormatParam{columns.size()}));
95 err_lines.emplace_back();
96 err_lines.append_range(file_printer.print(all_lines, std::vector{line_index}));
97 return FormattableError{std::move(err_lines)};
98 }
99
100 trim(columns[0]);
101 trim(columns[1]);
102 trim(columns[2]);
103 trim(columns[3]);
104
105 auto id_result = parse_int<int>(columns[0], 0);
106 if (!id_result.has_value()) {
107 std::vector<std::string> err_lines{};
108 err_lines.push_back(format.format(
109 "{}:{}: invalid metatile id '{}': {}",
110 FormatParam{path.string(), Style::bold},
111 FormatParam{line_index + 1, Style::bold},
112 FormatParam{columns[0], Style::bold},
113 FormatParam{id_result.error()}));
114 err_lines.emplace_back();
115 err_lines.append_range(file_printer.print(all_lines, std::vector{line_index}));
116 return FormattableError{std::move(err_lines)};
117 }
118
119 if (id_result.value() < 0) {
120 std::vector<std::string> err_lines{};
121 err_lines.push_back(format.format(
122 "{}:{}: metatile id '{}' cannot be negative",
123 FormatParam{path.string(), Style::bold},
124 FormatParam{line_index + 1, Style::bold},
125 FormatParam{columns[0], Style::bold}));
126 err_lines.emplace_back();
127 err_lines.append_range(file_printer.print(all_lines, std::vector{line_index}));
128 return FormattableError{std::move(err_lines)};
129 }
130
131 return CsvRow{static_cast<std::size_t>(id_result.value()), columns[1], columns[2], columns[3]};
132}
133
135 const std::filesystem::path &path,
136 const BehaviorMapProvider &behavior_map,
137 std::optional<BaseGame> base_game,
138 const TerrainTypeMapProvider *terrain_map,
139 const EncounterTypeMapProvider *encounter_map,
140 const TextFormatter &format,
141 const FileHighlightPrinter &file_printer)
142{
143 if (!exists(path)) {
144 return FormattableError{"{}: file does not exist.", FormatParam{path.string(), Style::bold}};
145 }
146
147 // Slurp entire file into vector for FileHighlightPrinter support
148 std::vector<std::string> lines{};
149 {
150 std::ifstream stream{path};
151 std::string line_buf{};
152 while (std::getline(stream, line_buf)) {
153 std::ignore = trim_line_ending(line_buf);
154 lines.push_back(line_buf);
155 }
156 }
157
158 if (lines.empty()) {
159 return FormattableError{
160 "{}: file is empty, expected header 'id,behavior' or 'id,behavior,terrainType,encounterType'",
161 FormatParam{path.string(), Style::bold}};
162 }
163
164 // Validate header line (index 0) and auto-detect format
165 auto header_columns = split(lines[0], ",");
166 for (auto &col : header_columns) {
167 trim(col);
168 }
169
170 CsvFormat csv_format{};
171
172 if (header_columns.size() >= 4 && header_columns[0] == "id" && header_columns[1] == "behavior" &&
173 header_columns[2] == "terrainType" && header_columns[3] == "encounterType") {
174 csv_format = CsvFormat::firered;
175 }
176 else if (header_columns.size() >= 2 && header_columns[0] == "id" && header_columns[1] == "behavior") {
177 csv_format = CsvFormat::emerald;
178 }
179 else {
180 std::vector<std::string> err_lines{};
181 err_lines.push_back(format.format(
182 "{}:{}: invalid header, expected 'id,behavior' or 'id,behavior,terrainType,encounterType' but found '{}'",
183 FormatParam{path.string(), Style::bold},
184 FormatParam{"1", Style::bold},
185 FormatParam{lines[0], Style::bold}));
186 err_lines.emplace_back();
187 err_lines.append_range(file_printer.print(lines, std::vector<std::size_t>{0}));
188 return FormattableError{std::move(err_lines)};
189 }
190
191 // Validate detected format against base game if provided
192 if (base_game.has_value()) {
193 if (csv_format == CsvFormat::firered && base_game.value() != BaseGame::pokefirered) {
194 std::vector<std::string> err_lines{};
195 err_lines.push_back(format.format(
196 "{}:{}: CSV has FireRed format (id,behavior,terrainType,encounterType) but project base game is '{}'",
197 FormatParam{path.string(), Style::bold},
198 FormatParam{"1", Style::bold},
199 FormatParam{to_string(base_game.value()), Style::bold}));
200 err_lines.emplace_back();
201 err_lines.append_range(file_printer.print(lines, std::vector<std::size_t>{0}));
202 return FormattableError{std::move(err_lines)};
203 }
204 if (csv_format == CsvFormat::emerald && base_game.value() == BaseGame::pokefirered) {
205 std::vector<std::string> err_lines{};
206 err_lines.push_back(format.format(
207 "{}:{}: CSV has Emerald format (id,behavior) but project base game is '{}'",
208 FormatParam{path.string(), Style::bold},
209 FormatParam{"1", Style::bold},
210 FormatParam{to_string(base_game.value()), Style::bold}));
211 err_lines.emplace_back();
212 err_lines.append_range(file_printer.print(lines, std::vector<std::size_t>{0}));
213 return FormattableError{std::move(err_lines)};
214 }
215 }
216
217 // Validate provider availability for FireRed format
218 if (csv_format == CsvFormat::firered) {
219 if (terrain_map == nullptr) {
220 panic("FireRed CSV format requires a terrain type provider but none was provided.");
221 }
222 if (encounter_map == nullptr) {
223 panic("FireRed CSV format requires an encounter type provider but none was provided.");
224 }
225 }
226
227 // Parse data rows (starting at index 1)
228 std::map<std::size_t, MetatileAttribute> result{};
229 std::unordered_map<std::size_t, std::size_t> id_to_line_index{};
230
231 for (std::size_t line_index = 1; line_index < lines.size(); ++line_index) {
232 const auto &line = lines[line_index];
233
234 if (line.empty()) {
235 continue;
236 }
237
238 ChainableResult<CsvRow> row_result =
239 csv_format == CsvFormat::firered
240 ? parse_firered_csv_row(line, line_index, path, lines, format, file_printer)
241 : parse_emerald_csv_row(line, line_index, path, lines, format, file_printer);
242
243 if (!row_result.has_value()) {
245 FormattableError{"Failed to parse CSV row."}, row_result};
246 }
247
248 const auto &row = row_result.value();
249
250 if (id_to_line_index.contains(row.metatile_id)) {
251 const std::size_t original_line_index = id_to_line_index.at(row.metatile_id);
252 std::vector<std::string> err_lines{};
253
254 // Header for duplicate location
255 err_lines.push_back(format.format(
256 "{}:{}: duplicate metatile id '{}'",
257 FormatParam{path.string(), Style::bold},
258 FormatParam{line_index + 1, Style::bold},
259 FormatParam{row.metatile_id, Style::bold}));
260 err_lines.emplace_back();
261
262 // File context for duplicate
263 err_lines.append_range(file_printer.print(lines, std::vector{line_index}));
264 err_lines.emplace_back();
265
266 // Note about original location
267 err_lines.push_back(format.format(
268 "{} originally defined at line {}:",
269 FormatParam{"note:", Style::cyan | Style::bold},
270 FormatParam{original_line_index + 1}));
271
272 // File context for original
273 err_lines.append_range(file_printer.print(lines, std::vector{original_line_index}));
274
275 return FormattableError{std::move(err_lines)};
276 }
277 id_to_line_index.emplace(row.metatile_id, line_index);
278
279 auto behavior_value = behavior_map.lookup(row.behavior);
280 if (!behavior_value.has_value()) {
281 std::vector<std::string> err_lines{};
282 err_lines.push_back(format.format(
283 "{}:{}: unknown metatile behavior '{}'",
284 FormatParam{path.string(), Style::bold},
285 FormatParam{line_index + 1, Style::bold},
286 FormatParam{row.behavior, Style::bold}));
287 err_lines.emplace_back();
288 err_lines.append_range(file_printer.print(lines, std::vector{line_index}));
290 FormattableError{std::move(err_lines)}, behavior_value};
291 }
292
293 if (csv_format == CsvFormat::firered) {
294 // Resolve terrain type
295 auto terrain_value = terrain_map->lookup(row.terrain_type);
296 if (!terrain_value.has_value()) {
297 std::vector<std::string> err_lines{};
298 err_lines.push_back(format.format(
299 "{}:{}: unknown terrain type '{}'",
300 FormatParam{path.string(), Style::bold},
301 FormatParam{line_index + 1, Style::bold},
302 FormatParam{row.terrain_type, Style::bold}));
303 err_lines.emplace_back();
304 err_lines.append_range(file_printer.print(lines, std::vector{line_index}));
306 FormattableError{std::move(err_lines)}, terrain_value};
307 }
308
309 // Resolve encounter type
310 auto encounter_value = encounter_map->lookup(row.encounter_type);
311 if (!encounter_value.has_value()) {
312 std::vector<std::string> err_lines{};
313 err_lines.push_back(format.format(
314 "{}:{}: unknown encounter type '{}'",
315 FormatParam{path.string(), Style::bold},
316 FormatParam{line_index + 1, Style::bold},
317 FormatParam{row.encounter_type, Style::bold}));
318 err_lines.emplace_back();
319 err_lines.append_range(file_printer.print(lines, std::vector{line_index}));
321 FormattableError{std::move(err_lines)}, encounter_value};
322 }
323
324 result.emplace(
325 row.metatile_id,
327 LayerType::normal,
328 behavior_value.value(),
329 terrain_value.value(),
330 encounter_value.value(),
331 0,
332 0,
333 0,
334 false});
335 }
336 else {
337 result.emplace(row.metatile_id, MetatileAttribute{LayerType::normal, behavior_value.value()});
338 }
339 }
340
341 return result;
342}
343
344} // namespace
345
346namespace porytiles {
347
349AttributesCsvLoader::load(const std::filesystem::path &path) const
350{
351 return parse_attributes_csv(
352 path, *behavior_map_, base_game_, terrain_map_, encounter_map_, *format_, *file_printer_);
353}
354
355} // namespace porytiles
ChainableResult< std::map< std::size_t, MetatileAttribute > > load(const std::filesystem::path &path) const
Loads metatile attributes from a CSV file.
Abstract interface for providing two-way metatile behavior mappings.
virtual ChainableResult< std::uint16_t > lookup(const std::string &behavior_name) const =0
Looks up the numeric value for a behavior constant name.
A result type that maintains a chainable sequence of errors for debugging and error reporting.
T & value() &
Returns a reference to the contained success value.
bool has_value() const
Checks whether the result contains a success value.
Abstract interface for providing two-way metatile encounter type mappings.
virtual ChainableResult< std::uint8_t > lookup(const std::string &encounter_name) const =0
Looks up the numeric value for an encounter type constant name.
A service for printing file lines with highlighted lines and line numbers.
std::vector< std::string > print(const std::vector< std::string > &lines, const std::vector< std::size_t > &line_indices_to_highlight, std::size_t window_size=9) const
Prints lines with specified lines highlighted and line numbers shown.
A text parameter with associated styling for formatted output.
General-purpose error implementation with formatted message support.
Definition error.hpp:63
Represents the attributes of a single metatile.
static const Style bold
Bold text formatting.
Abstract interface for providing two-way metatile terrain type mappings.
virtual ChainableResult< std::uint8_t > lookup(const std::string &terrain_name) const =0
Looks up the numeric value for a terrain type constant name.
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.
void panic(const StringViewSourceLoc &s)
Unconditionally terminates the program with a panic message.
Definition panic.cpp:43
std::string & trim_line_ending(std::string &line)
Removes line ending characters from a string in-place.
@ split
Split the animation into separate animations for each palette variant (not yet implemented).
void trim(std::string &string)
Removes leading and trailing whitespace from a string in-place.
Utility functions for string manipulation and formatting.