11#include <unordered_map>
12#include <unordered_set>
26[[nodiscard]] std::string describe_role(
const std::optional<FieldRole> &role)
28 if (!role.has_value()) {
31 return std::format(
"the {} role",
to_string(role.value()));
38 for (
const auto &field : fields) {
55[[nodiscard]] ChainableResult<MetatileAttributeFieldDefinitions> merge_field_overrides(
58 gsl::not_null<const TextFormatter *> format)
61 std::unordered_set<std::string> names;
62 for (
const auto &field : fields) {
63 if (!names.insert(field.name).second) {
64 return FormattableError{
65 format->format(
"Field '{}' is defined more than once.", FormatParam{field.name,
Style::bold})};
70 for (
const auto &
name : overrides | std::views::keys) {
71 if (!names.contains(
name)) {
72 return FormattableError{std::vector<std::string>{
73 format->format(
"Override names unknown field '{}'.", FormatParam{
name,
Style::bold}),
74 format->format(
"Available fields: {}.", FormatParam{join_names(fields), Style::bold})}};
79 resolved.reserve(fields.size());
81 for (
const auto &baseline : fields) {
82 MetatileAttributeFieldDefinition merged = baseline;
84 if (
const auto it = overrides.find(baseline.name); it != overrides.end()) {
85 const MetatileAttributeFieldOverride &override_value = it->second;
86 if (override_value.mask.has_value()) {
87 merged.mask = override_value.mask;
89 if (override_value.default_value.has_value()) {
90 merged.default_value = override_value.default_value;
92 if (override_value.role.has_value()) {
93 merged.role = override_value.role.value();
95 if (override_value.provider.has_value()) {
96 const ProviderDefinitionOverride &provider_override = override_value.provider.value();
97 if (provider_override.remove) {
98 merged.provider = std::nullopt;
101 ProviderDefinition
provider = merged.provider.value_or(ProviderDefinition{});
102 if (provider_override.header.has_value()) {
103 provider.header = provider_override.header.value();
105 if (provider_override.prefix.has_value()) {
106 provider.prefix = provider_override.prefix.value();
108 if (provider_override.skipped.has_value()) {
109 provider.skipped = provider_override.skipped.value();
111 if (provider_override.format.has_value()) {
112 provider.format = provider_override.format.value();
115 return FormattableError{format->format(
116 "Provider override for field '{}' must supply both a header and a prefix.",
119 merged.provider = std::move(
provider);
124 if (!merged.mask.has_value()) {
125 return FormattableError{std::vector<std::string>{
126 format->format(
"Field '{}' has no mask.", FormatParam{merged.name,
Style::bold}),
127 "Every field must define one."}};
130 resolved.push_back(std::move(merged));
138struct RequiredWidth {
145 std::size_t required_bits = 0;
147 for (
const auto &field : resolved) {
148 const auto bits =
static_cast<std::size_t
>(std::bit_width(field.mask.value()));
149 if (bits > required_bits) {
150 required_bits = bits;
151 width.widest_source = std::format(
"Field '{}' (mask 0x{:X})", field.name, field.mask.value());
154 width.bytes = required_bits <= 8 ? 1U : (required_bits <= 16 ? 2U : 4U);
159[[nodiscard]] ChainableResult<Schema>
162 std::vector<Field> schema_fields;
163 schema_fields.reserve(resolved.size());
164 for (
const auto &merged : resolved) {
165 schema_fields.push_back(
166 Field{merged.name, merged.mask.value(), merged.default_value.value_or(0), merged.provider, merged.role});
168 auto schema_result =
Schema::create(std::move(schema_fields), attribute_bytes);
169 if (!schema_result.has_value()) {
170 return ChainableResult<Schema>{
171 FormattableError{
"The configured metatile attribute fields do not form a valid layout."}, schema_result};
173 return schema_result;
177[[nodiscard]] std::string describe_candidates(
const std::vector<MetatileAttributeCandidateSet> &candidates)
179 std::string described;
180 for (
const auto &candidate : candidates) {
181 if (!described.empty()) {
184 described += std::format(
"{} ({} bytes)", candidate.origin, candidate.required_bytes);
192[[nodiscard]] std::string describe_inferred_size_origin(
const MetatileAttributeCandidateSet &selected)
194 if (selected.source.empty()) {
195 return std::format(
"inferred from {}", selected.origin);
197 return std::format(
"inferred from {} ({})", selected.origin, selected.source);
203 std::vector<const MetatileAttributeCandidateSet *>
exact;
204 std::vector<const MetatileAttributeCandidateSet *>
narrower;
207[[nodiscard]] SizeMatches
208match_candidates_to_size(
const std::vector<MetatileAttributeCandidateSet> &candidates, std::size_t attribute_size)
211 for (
const auto &candidate : candidates) {
212 if (candidate.required_bytes == attribute_size) {
213 matches.exact.push_back(&candidate);
215 else if (candidate.required_bytes < attribute_size) {
216 matches.narrower.push_back(&candidate);
229[[nodiscard]] std::string describe_field_diff(
232 std::unordered_map<std::string, const MetatileAttributeFieldDefinition *> inferred_by_name;
233 for (
const auto &field : inferred) {
234 inferred_by_name.emplace(field.name, &field);
236 std::unordered_set<std::string> explicit_names;
237 for (
const auto &field : explicit_fields) {
238 explicit_names.insert(field.name);
241 std::vector<std::string> diffs;
242 for (
const auto &field : explicit_fields) {
243 const auto it = inferred_by_name.find(field.name);
244 if (it == inferred_by_name.end()) {
245 diffs.push_back(std::format(
"'{}' is only in the config", field.name));
248 const MetatileAttributeFieldDefinition &other = *it->second;
249 if (field.mask != other.mask) {
252 "'{}' has mask {} in the config but {} in the source",
257 if (field.role != other.role) {
260 "'{}' has {} in the config but {} in the source",
262 describe_role(field.role),
263 describe_role(other.role)));
266 for (
const auto &field : inferred) {
267 if (!explicit_names.contains(field.name)) {
268 diffs.push_back(std::format(
"'{}' is only in the source", field.name));
273 for (
const auto &diff : diffs) {
274 if (!joined.empty()) {
285[[nodiscard]] std::string describe_undeclared_width(
286 const AttributeDeclarationScan &scan,
const std::string &header_source,
const TextFormatter *format)
288 switch (scan.source) {
290 return format->format(
291 "This project has no '{}', so nothing in it declares the element type of struct Tileset's "
292 "metatileAttributes member.",
295 return format->format(
296 "'{}' could not be read, so the element type of struct Tileset's metatileAttributes member is unknown.",
299 return format->format(
300 "'{}' declares no 'struct Tileset', so nothing declares the element type of its metatileAttributes "
304 return format->format(
305 "'struct Tileset' in '{}' declares no 'metatileAttributes' pointer member.",
308 return format->format(
309 "'struct Tileset' in '{}' declares '{}', and the engine reads metatile attribute arrays only as u8, u16, "
314 panic(
"unhandled AttributeDeclarationSource value");
320[[nodiscard]] std::string describe_field_conflict(
321 const InferredFieldConflict &
conflict,
const std::string &header_source,
const TextFormatter *format)
325 return format->format(
326 "Metatile attribute field '{}' takes its value names from the behavior constants header, but this "
327 "project has no '{}'. Porytiles will not silently fall back to raw numeric values. Restore the header, "
328 "or state the field's provider (or 'provider: null' to use raw values deliberately) via "
329 "metatile_attribute_field_overrides in your Porytiles config.",
333 return format->format(
334 "Metatile attribute field '{}' takes its value names from the behavior constants header, but '{}' could "
335 "not be read, so whatever it declares is unknown. Fix the header so Porytiles can scan it, or state the "
336 "field's provider (or 'provider: null' to use raw values deliberately) via "
337 "metatile_attribute_field_overrides in your Porytiles config.",
341 return format->format(
342 "Metatile attribute field '{}' takes its value names from the behavior constants header, but '{}' "
343 "declares no MB_ name. Porytiles will not silently fall back to raw numeric values. Declare the "
344 "constants there, or state the field's provider (or 'provider: null' to use raw values deliberately) "
345 "via metatile_attribute_field_overrides in your Porytiles config.",
349 return format->format(
350 "Metatile attribute field '{}' should take its value names from a '{}' enum, but '{}' declares no enum "
351 "member with that prefix. Porytiles will not silently fall back to raw numeric values. Declare the enum, "
352 "or state the field's provider (or 'provider: null' to use raw values deliberately) via "
353 "metatile_attribute_field_overrides in your Porytiles config.",
358 return format->format(
359 "Metatile attribute field '{}' has mask {} from its METATILE_ATTR define but {} from the "
360 "sMetatileAttrMasks table. The project states two different masks for the same field, so it does not in "
361 "fact state one, and packing with the wrong mask silently corrupts every attribute word. Make the two "
362 "sources agree, or set the field's mask via metatile_attribute_field_overrides in your Porytiles config.",
367 return format->format(
368 "Metatile attribute field '{}' declares shift {}, but its mask places the field at bit offset {}. The "
369 "engine unpacks attribute values with the declared shift while Porytiles packs them at the mask's "
370 "offset, so a value written with this layout would not read back as itself. Make the shift match the "
371 "mask, or set the field's mask via metatile_attribute_field_overrides in your Porytiles config.",
373 FormatParam{
conflict.declared.value()},
374 FormatParam{
conflict.alternate.value()});
376 panic(
"unhandled FieldConflictKind value");
386 const auto it = overrides.find(
conflict.field_name);
387 if (it == overrides.end()) {
395 return it->second.provider.has_value();
398 return it->second.mask.has_value();
400 panic(
"unhandled FieldConflictKind value");
408 gsl::not_null<const TextFormatter *> format,
409 gsl::not_null<const UserDiagnostics *> diag)
417 "Porytiles found more than one metatile attribute mask layout in this project ({}), so it cannot infer "
418 "the attribute size. Set 'fieldmap.metatile_attribute_size' in porytiles/config.yaml (or pass "
419 "--metatile-attribute-size) to choose the layout this build uses.",
428 std::string fields_origin;
430 if (!fields.empty()) {
431 fields_origin = std::format(
"explicit metatile_attribute_fields ({})", inputs.
fields_source);
436 inferred_layout = &inference.
candidates.front();
440 if (matches.exact.size() == 1) {
441 inferred_layout = matches.exact.front();
443 else if (matches.exact.empty() && matches.narrower.size() == 1) {
444 inferred_layout = matches.narrower.front();
447 if (inferred_layout !=
nullptr) {
448 const std::string diff = describe_field_diff(fields, inferred_layout->
fields);
452 "The explicit metatile_attribute_fields ({}) do not match the metatile attribute layout "
453 "Porytiles inferred from {}: {}. The explicit fields are used as declared; this warning "
454 "is only a heads-up that they disagree with the project's source.",
469 "No metatile attribute fields are configured, and Porytiles found no mask layout to infer them from.",
470 "It infers the layout from the attribute masks the base game declares: the METATILE_ATTR_*_MASK "
471 "defines in 'include/global.fieldmap.h' or the sMetatileAttrMasks table in 'src/fieldmap.c'.",
472 "Make sure those masks exist, or add a metatile_attribute_fields list to your Porytiles config."}};
482 const auto matches = match_candidates_to_size(inference.
candidates, attribute_size);
483 if (matches.exact.size() == 1) {
484 selected = matches.exact.front();
486 else if (matches.exact.size() > 1) {
488 "Porytiles inferred more than one metatile attribute mask layout matching the configured "
489 "attribute size of {} bytes: {}. It cannot choose between them. Declare "
490 "metatile_attribute_fields in your Porytiles config to define the layout explicitly.",
494 else if (matches.narrower.size() == 1) {
495 selected = matches.narrower.front();
497 else if (matches.narrower.empty()) {
499 "Porytiles inferred these metatile attribute mask layouts from the project: {}. None of them "
500 "fits the configured attribute size of {} bytes (from {}). Change "
501 "'fieldmap.metatile_attribute_size' (or --metatile-attribute-size) to one of the listed widths, "
502 "or declare metatile_attribute_fields in your Porytiles config to define the layout explicitly.",
509 "Porytiles inferred more than one metatile attribute mask layout that fits within the configured "
510 "attribute size of {} bytes: {}. It cannot choose between them. Declare "
511 "metatile_attribute_fields in your Porytiles config to define the layout explicitly.",
517 if (selected !=
nullptr) {
518 fields = selected->
fields;
519 fields_origin = selected->
origin;
522 "Porytiles selected the metatile attribute mask layout inferred from {} ({} bytes).",
528 auto merged_result = merge_field_overrides(fields, inputs.
overrides, format);
529 if (!merged_result.has_value()) {
540 if (selected !=
nullptr) {
551 const RequiredWidth width = required_width(resolved);
552 std::size_t attribute_bytes = 0;
553 std::string size_origin;
557 if (width.bytes > attribute_bytes) {
559 "{} needs a {}-byte attribute word, but the metatile attribute size is set to {} bytes (from "
560 "{}). That size is the project's read stride, fixed for every tileset, so Porytiles cannot "
561 "widen it to fit this mask. Narrow the mask to fit {} bytes, or raise "
562 "'fieldmap.metatile_attribute_size' (or --metatile-attribute-size) if the project really uses a "
563 "wider attribute word.",
573 if (attribute_bytes > std::max(width.bytes, inference.
declaration.
size.value_or(0))) {
576 "The metatile attribute size is set to {} bytes (from {}), which is wider than anything the "
577 "project declares: the resolved field masks need only {} bytes. Porytiles will use {}-byte "
578 "attributes; the unused high bits stay zero.",
586 attribute_bytes = width.bytes;
589 ? describe_inferred_size_origin(*selected)
590 : std::format(
"derived from the explicit metatile_attribute_fields masks ({})", inputs.
fields_source);
597 std::string message = format->format(
598 "{} Without an explicit metatile attribute size, that declaration is the only fact that can "
599 "corroborate the width the resolved field masks imply, so Porytiles cannot confirm the width this "
600 "project reads. Set 'fieldmap.metatile_attribute_size' in porytiles/config.yaml (or pass "
601 "--metatile-attribute-size) to state it.",
608 message +=
" Without a usable declaration, 'fieldmap.metatile_attribute_declaration_size' "
609 "(--metatile-attribute-declaration-size) is also needed for the generated "
610 "gMetatileAttributes_* declarations.";
616 "The resolved metatile attribute field masks need {}-byte attributes, but struct Tileset declares "
617 "its metatileAttributes member with a {}-byte element type. Masks prove a minimum width, never the "
618 "width itself, and a project may legitimately read a different word size than it declares "
619 "(pokeemerald-expansion's FRLG build reads 4-byte words from 'const u16' arrays), so Porytiles "
620 "cannot tell which width this project reads. Set 'fieldmap.metatile_attribute_size' in "
621 "porytiles/config.yaml (or pass --metatile-attribute-size) to pin the width.",
634 std::string declaration_origin =
"explicit metatile_attribute_declaration_size";
635 if (!declaration_size.has_value()) {
638 "{} Porytiles needs that width to declare the generated gMetatileAttributes_* arrays, and nothing "
639 "else in the project implies it. Set 'fieldmap.metatile_attribute_declaration_size' in "
640 "porytiles/config.yaml (or pass --metatile-attribute-declaration-size) to state it.",
646 std::format(
"inferred from struct Tileset's metatileAttributes member ({})", inputs.
fieldmap_header_source);
648 const std::size_t declaration_bytes = declaration_size.value();
650 auto schema_result = build_schema(resolved, attribute_bytes);
651 if (!schema_result.has_value()) {
656 std::move(schema_result).value(),
660 std::move(fields_origin),
661 std::move(size_origin),
662 std::move(declaration_origin)};
665 std::string field_names;
666 for (
const Field &field : loaded.schema.fields()) {
667 if (!field_names.empty()) {
670 field_names += field.name();
673 const std::string layer_type_note =
674 resolved_layer_mask == 0 ?
"layer type disabled" : std::format(
"layer type mask 0x{:X}", resolved_layer_mask);
677 "Porytiles resolved {}-byte metatile attributes with fields: {} ({}).",
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.
One named bit-field within a metatile attribute layout.
static ChainableResult< Schema > create(std::vector< Field > fields, std::size_t attribute_bytes)
Validates a set of fields against an attribute size and builds a Schema.
std::uint32_t layer_type_mask() const
Returns the mask of the layer_type bits within the packed attribute word.
static const Style bold
Bold text formatting.
std::string format_optional_mask(const std::optional< std::uint32_t > &mask)
constexpr auto metatile_attr_schema_tag
The diagnostic tag the reconciler emits its remarks and warnings under.
std::map< std::string, MetatileAttributeFieldOverride > MetatileAttributeFieldOverrides
A map from field name to its override; applied at schema load time.
void panic(const StringViewSourceLoc &s)
Unconditionally terminates the program with a panic message.
std::vector< MetatileAttributeFieldDefinition > MetatileAttributeFieldDefinitions
An ordered list of metatile attribute field definitions; order is display/declaration order.
@ no_attributes_member
struct Tileset declares no metatileAttributes member
@ header_unreadable
the fieldmap header exists but could not be scanned
@ no_tileset_struct
the header was scanned and declares no struct Tileset
@ declared
the member is declared, and the declarator fields describe how
@ no_fieldmap_header
the project has no include/global.fieldmap.h
@ valid
one or more usable candidate sets were inferred
@ invalid
no layout could be determined from what the project declares (fatal at resolution time)
std::string to_declaration_string(const AttributeDeclarationScan &scan)
Renders a declared metatileAttributes member the way the project wrote it.
ChainableResult< LoadedMetatileAttributeSchema > reconcile_metatile_attribute_schema(const MetatileAttributeInferenceResult &inference, const MetatileAttributeConfigInputs &inputs, gsl::not_null< const TextFormatter * > format, gsl::not_null< const UserDiagnostics * > diag)
Reconciles the inferred metatile attribute facts with the user's config, returning a LoadedMetatileAt...
std::string to_string(const PrimaryPairingMode m)
Converts a PrimaryPairingMode to its canonical string representation.
@ provider_behaviors_unreadable
the behavior constants header exists but could not be scanned
@ provider_behaviors_absent
the behavior constants header does not exist
@ shift_vs_mask
the field's declared shift and its mask's bit offset disagree
@ provider_no_matching_enum
no enum member with the probed prefix exists in the fieldmap header
@ provider_behaviors_no_constants
the header was scanned and declares no MB_ name
@ mask_define_vs_table
the field's mask define and mask table entry disagree
std::optional< std::size_t > size
AttributeDeclarationScan scan
One unsettled fact about one inferred field, carried out of inference for the reconciler to rule on.