8#include <unordered_map>
9#include <unordered_set>
24 std::size_t metatile_id;
25 std::vector<std::string> field_cells;
26 std::optional<std::string> layer_type_token;
34[[nodiscard]] std::string expected_header_string(
const Schema &schema)
36 std::string header =
"id";
40 for (
const Field &field : schema.value_fields()) {
41 header +=
"," + field.name();
52[[nodiscard]] std::optional<std::string>
53extract_layer_type_token(
const std::vector<std::string> &columns, std::optional<std::size_t> index)
55 if (!index.has_value() || columns.size() <= index.value()) {
58 std::string token = columns[index.value()];
72 const std::string &line,
73 std::size_t line_index,
74 const std::filesystem::path &path,
75 const std::vector<std::string> &all_lines,
79 std::size_t max_columns,
80 std::optional<std::size_t> layer_type_token_index)
82 const std::size_t field_count = schema.
value_fields().size();
83 auto columns =
split(line,
",");
85 if (columns.size() < 1 + field_count) {
86 std::vector<std::string> err_lines{};
87 err_lines.push_back(format.
format(
88 "{}:{}: expected at least {} columns ({}), found {}",
94 err_lines.emplace_back();
95 err_lines.append_range(file_printer.
print(all_lines, std::vector{line_index}));
102 if (columns.size() > max_columns) {
103 std::vector<std::string> err_lines{};
104 err_lines.push_back(format.
format(
105 "{}:{}: expected at most {} columns, found {}",
110 err_lines.emplace_back();
111 err_lines.append_range(file_printer.
print(all_lines, std::vector{line_index}));
115 for (std::size_t i = 0; i <= field_count; ++i) {
119 auto id_result = parse_int<int>(columns[0], 0);
120 if (!id_result.has_value()) {
121 std::vector<std::string> err_lines{};
122 err_lines.push_back(format.
format(
123 "{}:{}: invalid metatile id '{}': {}",
128 err_lines.emplace_back();
129 err_lines.append_range(file_printer.
print(all_lines, std::vector{line_index}));
133 if (id_result.value() < 0) {
134 std::vector<std::string> err_lines{};
135 err_lines.push_back(format.
format(
136 "{}:{}: metatile id '{}' cannot be negative",
140 err_lines.emplace_back();
141 err_lines.append_range(file_printer.
print(all_lines, std::vector{line_index}));
145 std::vector<std::string> field_cells{};
146 field_cells.reserve(field_count);
147 for (std::size_t i = 0; i < field_count; ++i) {
148 field_cells.push_back(columns[1 + i]);
154 static_cast<std::size_t
>(id_result.value()),
155 std::move(field_cells),
156 extract_layer_type_token(columns, layer_type_token_index)};
160 const std::filesystem::path &path,
166 std::map<FieldRole, bool> &active_pin_column_present,
173 const std::string expected_header = expected_header_string(schema);
176 std::vector<std::string> lines{};
178 std::ifstream stream{path};
179 std::string line_buf{};
180 while (std::getline(stream, line_buf)) {
182 lines.push_back(line_buf);
188 "{}: file is empty, expected header '{}'",
196 auto header_columns =
split(lines[0],
",");
197 for (
auto &col : header_columns) {
201 const std::size_t field_count = schema.
value_fields().size();
202 auto make_header_error = [&](
const std::string &message) ->
FormattableError {
203 std::vector<std::string> err_lines{};
204 err_lines.push_back(message);
205 err_lines.emplace_back();
206 err_lines.push_back(format.
format(
207 "Based on resolved attribute schema, expected header: '{}'",
FormatParam{expected_header, Style::bold}));
208 err_lines.emplace_back();
209 err_lines.append_range(file_printer.
print(lines, std::vector<std::size_t>{0}));
213 for (std::size_t i = 0; i <= field_count; ++i) {
214 const std::string &expected_column = i == 0 ?
"id" : schema.
value_fields()[i - 1].name();
215 if (i >= header_columns.size()) {
216 return make_header_error(format.
format(
217 "{}:{}: invalid header: missing column '{}' at position {}",
223 if (header_columns[i] != expected_column) {
224 return make_header_error(format.
format(
225 "{}:{}: invalid header: expected column {} to be '{}' but found '{}'",
237 std::optional<std::size_t> layer_type_apply_index;
238 std::optional<std::string> ignored_role_pin_column;
239 std::unordered_set<std::string> seen_trailing;
240 for (std::size_t j = 1 + field_count; j < header_columns.size(); ++j) {
241 const std::string &column = header_columns[j];
243 if (!seen_trailing.insert(column).second) {
244 return make_header_error(format.
format(
245 "{}:{}: invalid header: duplicate column '{}' at position {}",
255 return make_header_error(format.
format(
256 "{}:{}: invalid header: unexpected column '{}' at position {}",
264 if (!role.has_value()) {
265 return make_header_error(format.
format(
266 "{}:{}: invalid header: pin column '{}' at position {} names no known role; the only role is '{}'",
271 FormatParam{to_string(FieldRole::layer_type), Style::bold}));
277 if (!ignored_role_pin_column.has_value()) {
278 ignored_role_pin_column = column;
283 switch (role.value()) {
284 case FieldRole::layer_type:
285 layer_type_apply_index = j;
290 const std::size_t max_columns = header_columns.size();
296 active_pin_column_present[pin.role] = (pin.role == FieldRole::layer_type) && layer_type_apply_index.has_value();
300 if (ignored_role_pin_column.has_value()) {
301 std::vector<std::string> warn_lines{};
302 warn_lines.push_back(format.
format(
303 "{}: a '{}' column is present but no active role pin uses it. Its values are ignored and the layer type "
306 FormatParam{ignored_role_pin_column.value(), Style::bold}));
307 warn_lines.push_back(format.
format(
308 "Add a {} entry for the layer_type role to apply the column, or remove the column.",
310 diag.
warning(
"role-pin-column", warn_lines);
314 std::map<std::size_t, MetatileAttribute> result{};
315 std::unordered_map<std::size_t, std::size_t> id_to_line_index{};
320 auto apply_explicit_layer_type =
322 if (!row.layer_type_token.has_value()) {
327 std::vector<std::string> err_lines{};
328 err_lines.push_back(format.
format(
329 "{}:{}: invalid layer_type '{}'",
332 FormatParam{row.layer_type_token.value(), Style::bold}));
333 err_lines.emplace_back();
334 err_lines.append_range(file_printer.
print(lines, std::vector{line_index}));
344 auto resolve_field_cell =
347 const auto provider_it = providers.find(field.
name());
348 if (provider_it == providers.end()) {
351 "parse_attributes_csv: field '{}' has a provider spec but no provider was built for it",
354 auto lookup_result = provider_it->second->lookup(cell);
355 if (!lookup_result.has_value()) {
356 std::vector<std::string> err_lines{};
357 err_lines.push_back(format.
format(
358 "{}:{}: unknown {} '{}'",
363 err_lines.emplace_back();
364 err_lines.append_range(file_printer.
print(lines, std::vector{line_index}));
367 return lookup_result.value();
370 auto int_result = parse_int<long long>(cell, 0);
371 if (!int_result.has_value() || int_result.value() < 0) {
372 std::vector<std::string> err_lines{};
373 err_lines.push_back(format.
format(
374 "{}:{}: invalid {} value '{}': expected an unsigned integer",
379 err_lines.emplace_back();
380 err_lines.append_range(file_printer.
print(lines, std::vector{line_index}));
383 if (int_result.value() >
static_cast<long long>(field.
max_value())) {
384 std::vector<std::string> err_lines{};
385 err_lines.push_back(format.
format(
386 "{}:{}: {} value '{}' exceeds the field's maximum of {}",
392 err_lines.emplace_back();
393 err_lines.append_range(file_printer.
print(lines, std::vector{line_index}));
396 return static_cast<std::uint32_t
>(int_result.value());
399 for (std::size_t line_index = 1; line_index < lines.size(); ++line_index) {
400 const auto &line = lines[line_index];
407 line, line_index, path, lines, schema, format, file_printer, max_columns, layer_type_apply_index);
414 const auto &row = row_result.
value();
416 if (id_to_line_index.contains(row.metatile_id)) {
417 const std::size_t original_line_index = id_to_line_index.at(row.metatile_id);
418 std::vector<std::string> err_lines{};
421 err_lines.push_back(format.
format(
422 "{}:{}: duplicate metatile id '{}'",
426 err_lines.emplace_back();
429 err_lines.append_range(file_printer.
print(lines, std::vector{line_index}));
430 err_lines.emplace_back();
433 err_lines.push_back(format.
format(
434 "{} originally defined at line {}:",
439 err_lines.append_range(file_printer.
print(lines, std::vector{original_line_index}));
443 id_to_line_index.emplace(row.metatile_id, line_index);
447 for (std::size_t i = 0; i < field_count; ++i) {
449 auto value_result = resolve_field_cell(field, row.field_cells[i], line_index);
450 if (!value_result.has_value()) {
454 attribute.
field(field.
name(), value_result.value());
456 if (
const auto applied = apply_explicit_layer_type(attribute, row, line_index); !applied.has_value()) {
460 result.emplace(row.metatile_id, std::move(attribute));
471 const std::filesystem::path &path,
474 const std::string &tileset_name)
const
480 if (!role_pins_cv.has_value()) {
485 std::map<FieldRole, bool> active_pin_column_present;
486 auto parsed = parse_attributes_csv(
487 path, schema, providers, *format_, *file_printer_, role_pins, active_pin_column_present, *diag_);
488 if (!parsed.has_value()) {
ChainableResult< AttributesCsvLoadResult > load(const std::filesystem::path &path, const Schema &schema, const ProviderMap &providers, const std::string &tileset_name) const
Loads metatile attributes from a CSV file.
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.
One named bit-field within a metatile attribute layout.
const std::string & name() const
bool has_provider() const
std::uint32_t max_value() const
Returns the largest value the field can hold.
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.
ChainableResult< ConfigValue< RolePinDefinitions > > role_pins(ConfigScopeType type, const std::string &scope) const
A validated metatile attribute layout: an ordered set of non-overlapping fields.
const std::vector< Field > & value_fields() const
Returns the fields that hold plain per-metatile values, excluding the layer_type-role field.
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 > ¶ms) const
Formats a string with styled parameters using fmtlib syntax.
Abstract class for structured error reporting and diagnostic output.
virtual void warning(const std::string &tag, const std::vector< std::string > &lines) const =0
Display a tagged warning message.
bool is_pin_column_name(const std::string &name)
Reports whether a column name sits in the reserved pin namespace.
const RolePinDefinition * find_role_pin(const RolePinDefinitions &definitions, FieldRole role)
Finds the role pin definition for a given role, or nullptr when the role is not pinned.
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.
std::string & trim_line_ending(std::string &line)
Removes line ending characters from a string in-place.
ChainableResult< LayerType > layer_type_from_csv_token(const std::string &token)
Parses a CSV layer_type token into a LayerType.
std::map< std::string, std::unique_ptr< EnumMapProvider >, std::less<> > ProviderMap
Maps schema field names to the provider that names that field's values.
std::optional< FieldRole > role_from_pin_column_name(const std::string &name)
Parses the role out of a pin column name.
@ tileset
Configuration scoped to a specific tileset.
@ 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.
The result of loading an attributes CSV: the per-metatile attributes plus the per-role pin-column.
A user request to emit a trailing pin column for one schema role in attributes.csv.