Porytiles
Loading...
Searching...
No Matches
metatile_attribute_schema_reconciler.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <bit>
5#include <cstddef>
6#include <cstdint>
7#include <format>
8#include <optional>
9#include <ranges>
10#include <string>
11#include <unordered_map>
12#include <unordered_set>
13#include <utility>
14#include <vector>
15
19
20namespace porytiles {
21
22namespace {
23
24// Names a field's role for diff prose. Written to read as the object of "has X", so a role-less field says so in
25// words rather than leaving the reader to infer it from the other side of the comparison.
26[[nodiscard]] std::string describe_role(const std::optional<FieldRole> &role)
27{
28 if (!role.has_value()) {
29 return "no role";
30 }
31 return std::format("the {} role", to_string(role.value()));
32}
33
34[[nodiscard]] std::string join_names(const MetatileAttributeFieldDefinitions &fields)
35{
36 std::string joined;
37 bool first = true;
38 for (const auto &field : fields) {
39 if (!first) {
40 joined += ", ";
41 }
42 joined += field.name;
43 first = false;
44 }
45 return joined;
46}
47
55[[nodiscard]] ChainableResult<MetatileAttributeFieldDefinitions> merge_field_overrides(
57 const MetatileAttributeFieldOverrides &overrides,
58 gsl::not_null<const TextFormatter *> format)
59{
60 // A baseline name must be unique so overrides and the schema can address fields unambiguously.
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})};
66 }
67 }
68
69 // Every override must name an existing field.
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})}};
75 }
76 }
77
79 resolved.reserve(fields.size());
80
81 for (const auto &baseline : fields) {
82 MetatileAttributeFieldDefinition merged = baseline;
83
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;
88 }
89 if (override_value.default_value.has_value()) {
90 merged.default_value = override_value.default_value;
91 }
92 if (override_value.role.has_value()) {
93 merged.role = override_value.role.value(); // an inner nullopt encodes `role: null`, clearing it
94 }
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;
99 }
100 else {
101 ProviderDefinition provider = merged.provider.value_or(ProviderDefinition{});
102 if (provider_override.header.has_value()) {
103 provider.header = provider_override.header.value();
104 }
105 if (provider_override.prefix.has_value()) {
106 provider.prefix = provider_override.prefix.value();
107 }
108 if (provider_override.skipped.has_value()) {
109 provider.skipped = provider_override.skipped.value(); // replaces the base skip set wholesale
110 }
111 if (provider_override.format.has_value()) {
112 provider.format = provider_override.format.value();
113 }
114 if (provider.header.empty() || provider.prefix.empty()) {
115 return FormattableError{format->format(
116 "Provider override for field '{}' must supply both a header and a prefix.",
117 FormatParam{baseline.name, Style::bold})};
118 }
119 merged.provider = std::move(provider);
120 }
121 }
122 }
123
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."}};
128 }
129
130 resolved.push_back(std::move(merged));
131 }
132
133 return resolved;
134}
135
136// The width evidence carried by a merged field list: the smallest of 1, 2, or 4 bytes covering every mask, plus a
137// human-readable name for whichever field set the widest bit, so width errors can point at the offending mask.
138struct RequiredWidth {
139 std::size_t bytes{1};
140 std::string widest_source;
141};
142
143[[nodiscard]] RequiredWidth required_width(const MetatileAttributeFieldDefinitions &resolved)
144{
145 std::size_t required_bits = 0;
146 RequiredWidth width;
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());
152 }
153 }
154 width.bytes = required_bits <= 8 ? 1U : (required_bits <= 16 ? 2U : 4U);
155 return width;
156}
157
159[[nodiscard]] ChainableResult<Schema>
160build_schema(const MetatileAttributeFieldDefinitions &resolved, std::size_t attribute_bytes)
161{
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});
167 }
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};
172 }
173 return schema_result;
174}
175
176// Renders a candidate list for selection errors: "{origin} ({required_bytes} bytes), ...".
177[[nodiscard]] std::string describe_candidates(const std::vector<MetatileAttributeCandidateSet> &candidates)
178{
179 std::string described;
180 for (const auto &candidate : candidates) {
181 if (!described.empty()) {
182 described += ", ";
183 }
184 described += std::format("{} ({} bytes)", candidate.origin, candidate.required_bytes);
185 }
186 return described;
187}
188
189// Renders the width provenance of a selected inferred layout. The paths come from the candidate rather than from the
190// fieldmap header, since a layout's masks may have been read from the src/fieldmap.c table instead (pokefirered
191// declares no mask defines at all).
192[[nodiscard]] std::string describe_inferred_size_origin(const MetatileAttributeCandidateSet &selected)
193{
194 if (selected.source.empty()) {
195 return std::format("inferred from {}", selected.origin);
196 }
197 return std::format("inferred from {} ({})", selected.origin, selected.source);
198}
199
200// The candidate sets a pinned width can hold, split into exact width matches and narrower fits. The pointers alias
201// the candidates vector, so it must outlive them.
202struct SizeMatches {
203 std::vector<const MetatileAttributeCandidateSet *> exact;
204 std::vector<const MetatileAttributeCandidateSet *> narrower;
205};
206
207[[nodiscard]] SizeMatches
208match_candidates_to_size(const std::vector<MetatileAttributeCandidateSet> &candidates, std::size_t attribute_size)
209{
210 SizeMatches matches;
211 for (const auto &candidate : candidates) {
212 if (candidate.required_bytes == attribute_size) {
213 matches.exact.push_back(&candidate);
214 }
215 else if (candidate.required_bytes < attribute_size) {
216 matches.narrower.push_back(&candidate);
217 }
218 }
219 return matches;
220}
221
229[[nodiscard]] std::string describe_field_diff(
230 const MetatileAttributeFieldDefinitions &explicit_fields, const MetatileAttributeFieldDefinitions &inferred)
231{
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);
235 }
236 std::unordered_set<std::string> explicit_names;
237 for (const auto &field : explicit_fields) {
238 explicit_names.insert(field.name);
239 }
240
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));
246 continue;
247 }
248 const MetatileAttributeFieldDefinition &other = *it->second;
249 if (field.mask != other.mask) {
250 diffs.push_back(
251 std::format(
252 "'{}' has mask {} in the config but {} in the source",
253 field.name,
255 detail::format_optional_mask(other.mask)));
256 }
257 if (field.role != other.role) {
258 diffs.push_back(
259 std::format(
260 "'{}' has {} in the config but {} in the source",
261 field.name,
262 describe_role(field.role),
263 describe_role(other.role)));
264 }
265 }
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));
269 }
270 }
271
272 std::string joined;
273 for (const auto &diff : diffs) {
274 if (!joined.empty()) {
275 joined += "; ";
276 }
277 joined += diff;
278 }
279 return joined;
280}
281
282// Says what the project's sources failed to state about the declared element width. Each reason names the specific
283// thing that is absent or unusable: the width has no second source, so a user who is told only that Porytiles could
284// not work it out has nowhere to go looking.
285[[nodiscard]] std::string describe_undeclared_width(
286 const AttributeDeclarationScan &scan, const std::string &header_source, const TextFormatter *format)
287{
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.",
293 FormatParam{header_source, Style::bold});
295 return format->format(
296 "'{}' could not be read, so the element type of struct Tileset's metatileAttributes member is unknown.",
297 FormatParam{header_source, Style::bold});
299 return format->format(
300 "'{}' declares no 'struct Tileset', so nothing declares the element type of its metatileAttributes "
301 "member.",
302 FormatParam{header_source, Style::bold});
304 return format->format(
305 "'struct Tileset' in '{}' declares no 'metatileAttributes' pointer member.",
306 FormatParam{header_source, Style::bold});
308 return format->format(
309 "'struct Tileset' in '{}' declares '{}', and the engine reads metatile attribute arrays only as u8, u16, "
310 "or u32.",
311 FormatParam{header_source, Style::bold},
312 FormatParam{to_declaration_string(scan), Style::bold});
313 }
314 panic("unhandled AttributeDeclarationSource value");
315}
316
317// Says what inference could not settle about one field of the selected layout, and names the override that settles
318// it. Each kind names the specific thing that is absent or contradictory: the fact has no other witness in the
319// project, so a user told only that the field could not be resolved has nowhere to go looking.
320[[nodiscard]] std::string describe_field_conflict(
321 const InferredFieldConflict &conflict, const std::string &header_source, const TextFormatter *format)
322{
323 switch (conflict.kind) {
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.",
330 FormatParam{conflict.field_name, Style::bold},
331 FormatParam{conflict.probed, Style::bold});
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.",
338 FormatParam{conflict.field_name, Style::bold},
339 FormatParam{conflict.probed, Style::bold});
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.",
346 FormatParam{conflict.field_name, Style::bold},
347 FormatParam{conflict.probed, Style::bold});
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.",
354 FormatParam{conflict.field_name, Style::bold},
355 FormatParam{conflict.probed, Style::bold},
356 FormatParam{header_source, Style::bold});
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.",
363 FormatParam{conflict.field_name, Style::bold},
364 FormatParam{std::format("0x{:X}", conflict.declared.value()), Style::bold},
365 FormatParam{std::format("0x{:X}", conflict.alternate.value()), Style::bold});
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.",
372 FormatParam{conflict.field_name, Style::bold},
373 FormatParam{conflict.declared.value()},
374 FormatParam{conflict.alternate.value()});
375 }
376 panic("unhandled FieldConflictKind value");
377}
378
379// True when the user's override for the conflicted field speaks to the conflicted fact. The check reads the raw
380// overrides rather than the merged result: only the stated override can distinguish "the user chose raw values"
381// (provider: null) from "inference never found a provider", which is the difference between a settled fact and a
382// guess.
383[[nodiscard]] bool
384override_settles_conflict(const InferredFieldConflict &conflict, const MetatileAttributeFieldOverrides &overrides)
385{
386 const auto it = overrides.find(conflict.field_name);
387 if (it == overrides.end()) {
388 return false;
389 }
390 switch (conflict.kind) {
395 return it->second.provider.has_value();
398 return it->second.mask.has_value();
399 }
400 panic("unhandled FieldConflictKind value");
401}
402
403} // namespace
404
406 const MetatileAttributeInferenceResult &inference,
407 const MetatileAttributeConfigInputs &inputs,
408 gsl::not_null<const TextFormatter *> format,
409 gsl::not_null<const UserDiagnostics *> diag)
410{
411 // More than one inferred mask layout (pokeemerald-expansion holds both build flavors) with no explicit width
412 // setting is fatal before anything else, even when the fields are explicit. No project file records which flavor
413 // the build uses (it is a make argument), the width is the attribute size shared by every tileset, and guessing it
414 // wrong silently corrupts the attributes.
415 if (inference.candidates.size() >= 2 && !inputs.attribute_size.has_value()) {
416 return FormattableError{format->format(
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.",
420 // Every candidate is listed rather than the first two, matching the selection errors further down: the
421 // user needs the full set of widths to know which value to pin.
422 FormatParam{describe_candidates(inference.candidates), Style::bold})};
423 }
424
425 // Decide the field set. Explicit fields are the truth and inference is never consulted for their content;
426 // otherwise the inferred layout must select cleanly or resolution fails with an actionable error.
428 std::string fields_origin;
429 const MetatileAttributeCandidateSet *selected = nullptr;
430 if (!fields.empty()) {
431 fields_origin = std::format("explicit metatile_attribute_fields ({})", inputs.fields_source);
432 // Advisory comparison only: when a usable inferred layout exists and disagrees with the explicit fields,
433 // warn. Overriding what the project's own source declares is legal but worth knowing about.
434 const MetatileAttributeCandidateSet *inferred_layout = nullptr;
435 if (inference.status == AttributeInferenceStatus::valid && inference.candidates.size() == 1) {
436 inferred_layout = &inference.candidates.front();
437 }
438 else if (inference.status == AttributeInferenceStatus::valid && inputs.attribute_size.has_value()) {
439 const auto matches = match_candidates_to_size(inference.candidates, inputs.attribute_size.value());
440 if (matches.exact.size() == 1) {
441 inferred_layout = matches.exact.front();
442 }
443 else if (matches.exact.empty() && matches.narrower.size() == 1) {
444 inferred_layout = matches.narrower.front();
445 }
446 }
447 if (inferred_layout != nullptr) {
448 const std::string diff = describe_field_diff(fields, inferred_layout->fields);
449 if (!diff.empty()) {
450 diag->warning(
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.",
456 FormatParam{inferred_layout->origin, Style::bold},
457 FormatParam{diff});
458 }
459 }
460 }
461 else if (inference.status == AttributeInferenceStatus::invalid) {
462 // The masks are unusable (an undecidable conditional, a declared field with no mask) or could not be read at
463 // all, and there are no explicit fields to fall back on, so resolution cannot proceed even when an explicit
464 // size pinned the width. Inference already phrased the reason, so it passes through verbatim.
465 return FormattableError{inference.error_message};
466 }
467 else if (inference.candidates.empty()) {
468 return FormattableError{std::vector<std::string>{
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."}};
473 }
474 else if (inference.candidates.size() == 1) {
475 selected = &inference.candidates.front();
476 }
477 else {
478 // Two or more candidates; the dual-layout gate above guarantees the width knob is set here. Select by
479 // required width: a unique exact match wins; with none, a unique narrower fit wins (the width step below
480 // warns that the knob is wider than the layout needs); anything else cannot be decided.
481 const std::size_t attribute_size = inputs.attribute_size.value();
482 const auto matches = match_candidates_to_size(inference.candidates, attribute_size);
483 if (matches.exact.size() == 1) {
484 selected = matches.exact.front();
485 }
486 else if (matches.exact.size() > 1) {
487 return FormattableError{format->format(
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.",
491 FormatParam{attribute_size, Style::bold},
492 FormatParam{describe_candidates(inference.candidates), Style::bold})};
493 }
494 else if (matches.narrower.size() == 1) {
495 selected = matches.narrower.front();
496 }
497 else if (matches.narrower.empty()) {
498 return FormattableError{format->format(
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.",
503 FormatParam{describe_candidates(inference.candidates), Style::bold},
504 FormatParam{attribute_size, Style::bold},
506 }
507 else {
508 return FormattableError{format->format(
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.",
512 FormatParam{attribute_size, Style::bold},
513 FormatParam{describe_candidates(inference.candidates), Style::bold})};
514 }
515 }
516
517 if (selected != nullptr) {
518 fields = selected->fields;
519 fields_origin = selected->origin;
520 diag->remark(
522 "Porytiles selected the metatile attribute mask layout inferred from {} ({} bytes).",
523 FormatParam{selected->origin, Style::bold},
524 FormatParam{selected->required_bytes});
525 }
526
527 // Merge the overrides into the baseline fields.
528 auto merged_result = merge_field_overrides(fields, inputs.overrides, format);
529 if (!merged_result.has_value()) {
531 }
532 MetatileAttributeFieldDefinitions resolved = std::move(merged_result).value();
533
534 // Rule on the selected layout's inference conflicts: each fact inference could not settle is fatal unless the
535 // user's override for that field speaks to it (a stated provider, including provider: null, settles a provider
536 // hunt; a stated mask settles a mask or shift dispute). The ruling happens here rather than in inference because
537 // inference runs before the overrides exist, so a fatal raised there would make the escape hatch unreachable.
538 // Explicit metatile_attribute_fields skip this entirely: inference was never consulted for their content, so
539 // nothing it failed to settle is in play (selected is null on that path).
540 if (selected != nullptr) {
541 for (const InferredFieldConflict &conflict : selected->conflicts) {
542 if (!override_settles_conflict(conflict, inputs.overrides)) {
543 return FormattableError{describe_field_conflict(conflict, inputs.fieldmap_header_source, format)};
544 }
545 }
546 }
547
548 // Resolve the width. The explicit knob is authoritative when set; otherwise the merged masks are the sole
549 // evidence, and a wider scanned declaration contradicts them fatally (masks prove a minimum width, never the
550 // width itself, and an attribute entry is never narrower than the element type it is stored in).
551 const RequiredWidth width = required_width(resolved);
552 std::size_t attribute_bytes = 0;
553 std::string size_origin;
554 if (inputs.attribute_size.has_value()) {
555 attribute_bytes = inputs.attribute_size.value();
556 size_origin = inputs.attribute_size_source;
557 if (width.bytes > attribute_bytes) {
558 return FormattableError{format->format(
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.",
564 FormatParam{width.widest_source, Style::bold},
565 FormatParam{width.bytes, Style::bold},
566 FormatParam{attribute_bytes, Style::bold},
568 FormatParam{attribute_bytes, Style::bold})};
569 }
570 // A knob wider than both the merged masks and the scanned declaration width is legal (the unused high bits
571 // simply stay zero) but suspicious enough to flag. When the scanned declaration corroborates the knob there
572 // is nothing to say.
573 if (attribute_bytes > std::max(width.bytes, inference.declaration.size.value_or(0))) {
574 diag->warning(
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.",
579 FormatParam{attribute_bytes, Style::bold},
581 FormatParam{width.bytes, Style::bold},
582 FormatParam{attribute_bytes, Style::bold});
583 }
584 }
585 else {
586 attribute_bytes = width.bytes;
587 size_origin =
588 selected != nullptr
589 ? describe_inferred_size_origin(*selected)
590 : std::format("derived from the explicit metatile_attribute_fields masks ({})", inputs.fields_source);
591 // With no explicit knob, the masks and struct Tileset's declaration must agree exactly. Masks prove a
592 // minimum width, never the width itself, so the declaration is the only corroborating witness: a missing
593 // one leaves the width resting on masks alone, and a disagreeing one (in either direction; expansion's FRLG
594 // build deliberately reads a wider word than it declares) means the project's own sources do not settle
595 // which width the build reads.
596 if (!inference.declaration.size.has_value()) {
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.",
603 describe_undeclared_width(inference.declaration.scan, inputs.fieldmap_header_source, format)});
604 if (!inputs.declaration_size.has_value()) {
605 // The same missing declaration also leaves the generated array declarations without a width, so the
606 // user is told about both knobs in one round trip rather than hitting the second fatal after fixing
607 // the first.
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.";
611 }
612 return FormattableError{std::move(message)};
613 }
614 if (inference.declaration.size.value() != attribute_bytes) {
615 return FormattableError{format->format(
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.",
622 FormatParam{attribute_bytes, Style::bold},
623 FormatParam{inference.declaration.size.value(), Style::bold})};
624 }
625 }
626
627 // Declaration width: the explicit knob when set, otherwise struct Tileset's declaration. There is no third
628 // source. The attribute width above can be derived when the knob is unset because the masks independently bound
629 // it, but no mask says anything about the element type the arrays are declared with, and the two widths are
630 // genuinely different numbers on real projects (expansion's FRLG build declares 'const u16' arrays that are read
631 // as 4-byte words). Defaulting the declaration width to the attribute width would therefore not be a derivation,
632 // it would be a guess, and the value it guesses is written straight into the project's C headers.
633 std::optional<std::size_t> declaration_size = inputs.declaration_size;
634 std::string declaration_origin = "explicit metatile_attribute_declaration_size";
635 if (!declaration_size.has_value()) {
636 if (!inference.declaration.size.has_value()) {
637 return FormattableError{format->format(
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.",
642 describe_undeclared_width(inference.declaration.scan, inputs.fieldmap_header_source, format)})};
643 }
644 declaration_size = inference.declaration.size;
645 declaration_origin =
646 std::format("inferred from struct Tileset's metatileAttributes member ({})", inputs.fieldmap_header_source);
647 }
648 const std::size_t declaration_bytes = declaration_size.value();
649
650 auto schema_result = build_schema(resolved, attribute_bytes);
651 if (!schema_result.has_value()) {
653 }
654
656 std::move(schema_result).value(),
657 std::move(resolved),
658 attribute_bytes,
659 declaration_bytes,
660 std::move(fields_origin),
661 std::move(size_origin),
662 std::move(declaration_origin)};
663
664 // Summarize the resolved schema so the user can see what layout the data-driven resolution landed on.
665 std::string field_names;
666 for (const Field &field : loaded.schema.fields()) {
667 if (!field_names.empty()) {
668 field_names += ", ";
669 }
670 field_names += field.name();
671 }
672 const std::uint32_t resolved_layer_mask = loaded.schema.layer_type_mask();
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);
675 diag->remark(
677 "Porytiles resolved {}-byte metatile attributes with fields: {} ({}).",
678 FormatParam{loaded.attribute_bytes, Style::bold},
679 FormatParam{field_names, Style::bold},
680 FormatParam{layer_type_note, Style::bold});
681
682 return loaded;
683}
684
685} // namespace porytiles
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.
A text parameter with associated styling for formatted output.
General-purpose error implementation with formatted message support.
Definition error.hpp:57
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 name
std::optional< InferredFieldConflict > conflict
std::optional< ProviderDefinition > provider
std::vector< const MetatileAttributeCandidateSet * > exact
std::vector< const MetatileAttributeCandidateSet * > narrower
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.
Definition panic.cpp:43
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
One unsettled fact about one inferred field, carried out of inference for the reconciler to rule on.
The product of reconciling the project's metatile attribute schema.
One complete metatile attribute mask layout a project declares.
The user-stated config inputs to metatile attribute schema reconciliation.
std::vector< MetatileAttributeCandidateSet > candidates