Porytiles
Loading...
Searching...
No Matches
metatile_attribute_inference.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <bit>
5#include <cctype>
6#include <string_view>
7#include <unordered_map>
8#include <unordered_set>
9#include <utility>
10
14
15namespace porytiles {
16
17namespace {
18
19constexpr const char *attribute_enum_prefix = "METATILE_ATTRIBUTE_";
20constexpr const char *mask_define_prefix = "METATILE_ATTR_";
21constexpr const char *mask_define_suffix = "_MASK";
22constexpr const char *frlg_mask_define_suffix = "_MASK_FRLG";
23constexpr const char *shift_define_suffix = "_SHIFT";
24constexpr const char *frlg_shift_define_suffix = "_SHIFT_FRLG";
25constexpr const char *behaviors_header = "include/constants/metatile_behaviors.h";
26constexpr const char *fieldmap_header = "include/global.fieldmap.h";
27
28[[nodiscard]] std::string to_lower(std::string text)
29{
30 std::ranges::transform(text, text.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
31 return text;
32}
33
34// A base game may spell the layer field's enum member LAYER or LAYER_TYPE; both mean the same field.
35[[nodiscard]] std::string normalize_suffix(std::string suffix)
36{
37 if (suffix == "LAYER") {
38 return "LAYER_TYPE";
39 }
40 return suffix;
41}
42
43[[nodiscard]] bool is_all_digits(const std::string &text)
44{
45 return !text.empty() && std::all_of(text.begin(), text.end(), [](unsigned char c) { return std::isdigit(c) != 0; });
46}
47
48[[nodiscard]] bool any_enum_member_has_prefix(const MetatileAttributeScan &scan, const std::string &prefix)
49{
50 return std::any_of(scan.enum_members.begin(), scan.enum_members.end(), [&](const InferenceEnumMember &member) {
51 return member.name.starts_with(prefix);
52 });
53}
54
55// Joins the file paths that fed one candidate set, skipping any the scan did not record. A set names only the files
56// its origin prose names, so the two stay in agreement.
57[[nodiscard]] std::string join_sources(const std::string &first, const std::string &second)
58{
59 if (first.empty()) {
60 return second;
61 }
62 if (second.empty()) {
63 return first;
64 }
65 return first + ", " + second;
66}
67
68// Per-suffix mask and shift facts collected in Phase A before the sets are grouped.
69struct SuffixMasks {
70 std::optional<std::uint32_t> bare_define;
71 std::optional<std::uint32_t> frlg_define;
72 std::optional<std::uint32_t> array_value;
73 std::optional<std::uint32_t> bare_shift_define;
74 std::optional<std::uint32_t> frlg_shift_define;
75};
76
77// Phase B naming: the display name, optional value-name provider, and optional provider conflict for one suffix.
78// Layout-independent, so it is computed once per suffix and shared by every candidate set that includes the field.
79// A conflict is recorded rather than diagnosed: inference runs before the user's overrides are merged, so whether a
80// missing provider is fatal is the reconciler's call, not this function's.
81struct FieldNaming {
82 std::string name;
83 std::optional<ProviderDefinition> provider;
84 std::optional<InferredFieldConflict> conflict;
85};
86
87[[nodiscard]] FieldNaming name_field(const std::string &suffix, const MetatileAttributeScan &scan)
88{
89 FieldNaming naming;
90
91 if (suffix == "LAYER_TYPE") {
92 // The layer_type field's values are managed by Porytiles (FieldRole::layer_type), so its name is hardcoded
93 // rather than derived and it never gets a value provider.
94 naming.name = std::string{attribute::field_layer_type};
95 return naming;
96 }
97
98 if (suffix == "BEHAVIOR") {
99 naming.name = "behavior";
100 // Diagnostics name the path the scan actually looked at; the canonical relative path stands in when the scan
101 // recorded none (a unit-test scan, or a scan that never reached the behaviors header).
102 const std::string behaviors_path =
103 scan.behaviors_header.path.empty() ? std::string{behaviors_header} : scan.behaviors_header.path;
104 switch (scan.behaviors_header.source) {
106 naming.provider = ProviderDefinition{behaviors_header, "MB_", {"MB_INVALID"}, HeaderFormat::either};
107 break;
109 naming.conflict = InferredFieldConflict{
110 naming.name, FieldConflictKind::provider_behaviors_absent, behaviors_path, std::nullopt, std::nullopt};
111 break;
113 naming.conflict = InferredFieldConflict{
114 naming.name,
116 behaviors_path,
117 std::nullopt,
118 std::nullopt};
119 break;
121 naming.conflict = InferredFieldConflict{
122 naming.name,
124 behaviors_path,
125 std::nullopt,
126 std::nullopt};
127 break;
128 }
129 }
130 else if (is_all_digits(suffix)) {
131 // A purely numeric suffix (METATILE_ATTRIBUTE_2) is the project stating the field has no name and no
132 // constants. Nothing was inferred, so nothing failed: no provider and no conflict.
133 naming.name = "attribute_" + suffix;
134 }
135 else {
136 naming.name = to_lower(suffix);
137 std::string probe = "TILE_" + suffix + "_";
138 if (!any_enum_member_has_prefix(scan, probe) && suffix.ends_with("_TYPE")) {
139 probe = "TILE_" + suffix.substr(0, suffix.size() - std::string_view{"_TYPE"}.size()) + "_";
140 }
141 if (any_enum_member_has_prefix(scan, probe)) {
142 naming.provider = ProviderDefinition{fieldmap_header, probe, {}, HeaderFormat::enums_only};
143 }
144 else {
145 naming.conflict = InferredFieldConflict{
146 naming.name, FieldConflictKind::provider_no_matching_enum, probe, std::nullopt, std::nullopt};
147 }
148 }
149
150 return naming;
151}
152
153// Maps struct Tileset's metatileAttributes declarator to a declared element width. Every base game declares the member
154// as 'const uN *metatileAttributes' (u16 on pokeemerald and expansion, u32 on pokefirered). A declarator that is not a
155// single pointer to u8, u16, or u32 names no width the engine can read, so the width stays unset and reconciliation
156// rules on what to do about it. The scan travels along either way, so the ruling can quote the declaration.
157[[nodiscard]] InferredAttributeDeclaration interpret_declaration(const AttributeDeclarationScan &scan)
158{
159 InferredAttributeDeclaration declaration;
160 declaration.scan = scan;
161 if (scan.source != AttributeDeclarationSource::declared || scan.pointer_depth != 1) {
162 return declaration;
163 }
164 if (scan.element_type == "u8") {
165 declaration.size = 1;
166 }
167 else if (scan.element_type == "u16") {
168 declaration.size = 2;
169 }
170 else if (scan.element_type == "u32") {
171 declaration.size = 4;
172 }
173 return declaration;
174}
175
176// The smallest of 1, 2, or 4 bytes that covers every mask in a candidate set. The layer-type field is an
177// ordinary member of the set, so its mask participates like any other.
178[[nodiscard]] std::size_t required_bytes_for(const MetatileAttributeCandidateSet &candidate)
179{
180 std::size_t required_bits = 0;
181 for (const auto &field : candidate.fields) {
182 required_bits = std::max(required_bits, static_cast<std::size_t>(std::bit_width(field.mask.value())));
183 }
184 return required_bits <= 8 ? 1U : (required_bits <= 16 ? 2U : 4U);
185}
186
187} // namespace
188
190{
192 return {};
193 }
194 std::string text;
195 if (scan.is_const) {
196 text += "const ";
197 }
198 text += scan.element_type;
199 text += ' ';
200 text.append(scan.pointer_depth, '*');
201 text += "metatileAttributes";
202 return text;
203}
204
205MetatileAttributeInferenceResult
206infer_metatile_attribute_candidates(const MetatileAttributeScan &scan, gsl::not_null<const TextFormatter *> format)
207{
209
210 // The declaration width is a project fact independent of the mask layout, so it is mapped up front and survives
211 // every status, including the early invalid returns below.
212 result.declaration = interpret_declaration(scan.declaration);
213
214 // --- Phase A: gather suffixes and masks ---
215
216 for (const std::string &name : scan.ambiguous_defines) {
217 if (!name.starts_with(mask_define_prefix)) {
218 continue;
219 }
220 const std::string_view body = std::string_view{name}.substr(std::string_view{mask_define_prefix}.size());
221 if (!body.ends_with(mask_define_suffix) && !body.ends_with(frlg_mask_define_suffix)) {
222 continue;
223 }
225 result.error_message = format->format(
226 "Metatile attribute mask define '{}' has conflicting values in a conditional Porytiles could not "
227 "evaluate. Porytiles cannot infer a safe attribute layout from that definition. Set the mask explicitly "
228 "with metatile_attribute_field_overrides or declare metatile_attribute_fields in your Porytiles config.",
230 return result;
231 }
232
233 // Ordered, de-duplicated suffix list: enum suffixes first (declaration order), then define-only suffixes.
234 std::vector<std::string> ordered_suffixes;
235 std::unordered_set<std::string> seen_suffixes;
236 const auto add_suffix = [&](const std::string &suffix) {
237 if (seen_suffixes.insert(suffix).second) {
238 ordered_suffixes.push_back(suffix);
239 }
240 };
241
242 for (const auto &member : scan.enum_members) {
243 if (!member.name.starts_with(attribute_enum_prefix)) {
244 continue;
245 }
246 std::string suffix = normalize_suffix(member.name.substr(std::string_view{attribute_enum_prefix}.size()));
247 if (suffix == "COUNT") {
248 continue; // the count sentinel is not a field
249 }
250 add_suffix(suffix);
251 }
252
253 std::unordered_map<std::string, SuffixMasks> masks;
254 bool any_bare_define = false;
255 bool any_frlg_define = false;
256 for (const auto &define : scan.defines) {
257 if (!define.name.starts_with(mask_define_prefix)) {
258 continue;
259 }
260 const std::string body = define.name.substr(std::string_view{mask_define_prefix}.size());
261 std::string suffix;
262 bool is_frlg = false;
263 bool is_shift = false;
264 if (body.ends_with(frlg_mask_define_suffix)) {
265 suffix = body.substr(0, body.size() - std::string_view{frlg_mask_define_suffix}.size());
266 is_frlg = true;
267 }
268 else if (body.ends_with(mask_define_suffix)) {
269 suffix = body.substr(0, body.size() - std::string_view{mask_define_suffix}.size());
270 }
271 else if (body.ends_with(frlg_shift_define_suffix)) {
272 suffix = body.substr(0, body.size() - std::string_view{frlg_shift_define_suffix}.size());
273 is_frlg = true;
274 is_shift = true;
275 }
276 else if (body.ends_with(shift_define_suffix)) {
277 suffix = body.substr(0, body.size() - std::string_view{shift_define_suffix}.size());
278 is_shift = true;
279 }
280 else {
281 // A define carrying the attribute prefix with a suffix none of the four branches above read. Recorded so
282 // the caller can say it was ignored: a user who spells a mask METATILE_ATTR_BEHAVIOR_MASK_RSE gets a
283 // layout built without it, and dropping it in silence reads as Porytiles having accepted it.
284 result.unrecognized_defines.push_back(define.name);
285 continue;
286 }
287 if (suffix.empty()) {
288 continue;
289 }
290 suffix = normalize_suffix(suffix);
291 if (is_shift) {
292 // A shift define is a cross-check on its mask, never a field source: it is recorded for the conflict
293 // checks below but does not introduce a suffix.
294 if (is_frlg) {
295 masks[suffix].frlg_shift_define = define.value;
296 }
297 else {
298 masks[suffix].bare_shift_define = define.value;
299 }
300 continue;
301 }
302 if (is_frlg) {
303 masks[suffix].frlg_define = define.value;
304 any_frlg_define = true;
305 }
306 else {
307 masks[suffix].bare_define = define.value;
308 any_bare_define = true;
309 }
310 add_suffix(suffix);
311 }
312
313 bool any_array_value = false;
314 for (const auto &entry : scan.masks_array) {
315 if (!entry.index_name.starts_with(attribute_enum_prefix)) {
316 continue;
317 }
318 std::string suffix = normalize_suffix(entry.index_name.substr(std::string_view{attribute_enum_prefix}.size()));
319 if (suffix == "COUNT") {
320 continue;
321 }
322 if (entry.value.has_value()) {
323 masks[suffix].array_value = entry.value;
324 any_array_value = true;
325 }
326 // Array entries only fill in suffixes already known from the enum; they do not introduce new fields.
327 }
328
329 std::unordered_map<std::string, std::uint32_t> shifts;
330 for (const auto &entry : scan.shifts_array) {
331 if (!entry.index_name.starts_with(attribute_enum_prefix) || !entry.value.has_value()) {
332 continue;
333 }
334 std::string suffix = normalize_suffix(entry.index_name.substr(std::string_view{attribute_enum_prefix}.size()));
335 shifts[suffix] = entry.value.value();
336 }
337
338 // A file the scan could not read is the likely reason nothing usable turned up, so it is reported as such rather
339 // than as "this project declares no masks": the masks may well be sitting in the file, unread. Both no-candidate
340 // exits below route through here. The message stands on its own rather than deferring to the scanner's warning
341 // about the same file, since that warning is subject to the user's diagnostic filters and this is not.
342 const auto no_usable_layout = [&]() -> MetatileAttributeInferenceResult & {
343 if (scan.unreadable_sources.empty()) {
345 return result;
346 }
348 result.error_message = format->format(
349 "Porytiles could not read {}, so it has no metatile attribute masks to infer a layout from, and no "
350 "metatile_attribute_fields list is configured to stand in for them. Fix those files so Porytiles can "
351 "scan them, or declare the layout explicitly with metatile_attribute_fields in your Porytiles config.",
352 // The list is short (at most the fieldmap header, its source file, and the behaviors header), so every
353 // path is named rather than summarized.
355 return result;
356 };
357
358 if (ordered_suffixes.empty()) {
359 // Nothing attribute-related was found anywhere; defer to other providers.
360 return no_usable_layout();
361 }
362
363 // --- Phase B: name each suffix and attach providers (layout-independent, shared by all sets) ---
364
365 std::unordered_map<std::string, FieldNaming> namings;
366 for (const auto &suffix : ordered_suffixes) {
367 namings.emplace(suffix, name_field(suffix, scan));
368 }
369
370 // --- Phase C: group masks into candidate sets, rejecting fields with no mask in any set ---
371
372 // Describes one candidate set being assembled: which per-suffix mask slot feeds it, what to call it, and which
373 // files it was read from. The masks live in two different files (the header defines and the src/fieldmap.c table),
374 // so the paths are decided here, next to the prose naming those same sources.
375 struct SetPlan {
376 std::string origin;
377 std::string source;
378 bool frlg; // true selects frlg_define ?? array_value, false selects the bare define (?? array when single)
379 };
380 std::vector<SetPlan> plans;
381 if (any_frlg_define) {
382 // Dual layout: bare defines are one set; FRLG defines (plus the table, which describes the FRLG layout in
383 // these projects) are the other.
384 plans.push_back(SetPlan{"the bare METATILE_ATTR_*_MASK defines", scan.header_source, false});
385 if (any_array_value) {
386 plans.push_back(
387 SetPlan{
388 "the METATILE_ATTR_*_MASK_FRLG defines and the sMetatileAttrMasks table",
389 join_sources(scan.header_source, scan.masks_table_source),
390 true});
391 }
392 else {
393 plans.push_back(SetPlan{"the METATILE_ATTR_*_MASK_FRLG defines", scan.header_source, true});
394 }
395 }
396 else {
397 std::string origin;
398 std::string source;
399 if (any_bare_define && any_array_value) {
400 origin = "the METATILE_ATTR_*_MASK defines and the sMetatileAttrMasks table";
401 source = join_sources(scan.header_source, scan.masks_table_source);
402 }
403 else if (any_bare_define) {
404 origin = "the METATILE_ATTR_*_MASK defines";
405 source = scan.header_source;
406 }
407 else {
408 origin = "the sMetatileAttrMasks table";
409 source = scan.masks_table_source;
410 }
411 plans.push_back(SetPlan{std::move(origin), std::move(source), false});
412 }
413
414 // Resolve the mask one set selects for one suffix. In a dual-layout project the bare define alone is the primary
415 // mask; in a single-layout project the define wins over a disagreeing table entry.
416 const auto mask_for = [&](const SuffixMasks &m, bool frlg) -> std::optional<std::uint32_t> {
417 if (frlg) {
418 return m.frlg_define.has_value() ? m.frlg_define : m.array_value;
419 }
420 if (any_frlg_define) {
421 return m.bare_define;
422 }
423 return m.bare_define.has_value() ? m.bare_define : m.array_value;
424 };
425
426 // Records the facts inference could not settle for one field of one set. The pairing rules mirror how the masks
427 // themselves are resolved: in a dual layout the mask/shift tables and the _FRLG defines describe the FRLG set
428 // while the bare defines describe the bare set; in a single layout everything pairs with the one merged set.
429 const auto collect_conflicts = [&](const std::string &suffix,
430 const SuffixMasks &m,
431 std::uint32_t resolved_mask,
432 bool frlg,
433 std::vector<InferredFieldConflict> &conflicts) {
434 const FieldNaming &naming = namings.at(suffix);
435 if (naming.conflict.has_value()) {
436 conflicts.push_back(naming.conflict.value());
437 }
438
439 // A mask define disagreeing with the mask table: the project states two different masks for the same field,
440 // so it does not in fact state one. Only the set the table feeds can carry this conflict.
441 const auto &paired_define = frlg ? m.frlg_define : m.bare_define;
442 if (frlg == any_frlg_define && paired_define.has_value() && m.array_value.has_value() &&
443 paired_define.value() != m.array_value.value()) {
444 conflicts.push_back(
446 naming.name, FieldConflictKind::mask_define_vs_table, {}, paired_define, m.array_value});
447 }
448
449 // Declared shifts must equal the resolved mask's low-bit offset: the engine unpacks with the shift while
450 // Porytiles packs at the mask offset, so a disagreement means every written value reads back wrong.
451 const auto expected = static_cast<std::uint32_t>(std::countr_zero(resolved_mask));
452 const auto &shift_define = frlg ? m.frlg_shift_define : m.bare_shift_define;
453 if (shift_define.has_value() && shift_define.value() != expected) {
454 conflicts.push_back(
455 InferredFieldConflict{naming.name, FieldConflictKind::shift_vs_mask, {}, shift_define, expected});
456 }
457 // The shift table pairs with whichever set the mask table feeds (the FRLG set in a dual layout).
458 if (frlg == any_frlg_define) {
459 if (const auto shift_it = shifts.find(suffix); shift_it != shifts.end() && shift_it->second != expected) {
460 conflicts.push_back(
462 naming.name, FieldConflictKind::shift_vs_mask, {}, shift_it->second, expected});
463 }
464 }
465 };
466
467 // Assemble the sets. A suffix missing a mask in one set is simply excluded from that set, but a suffix missing a
468 // mask in every set means the project declares a field it exposes no mask for, which is fatal. The layer_type
469 // field is covered like any other; its suggested define spelling follows the emerald family's
470 // METATILE_ATTR_LAYER_MASK rather than the normalized LAYER_TYPE suffix.
471 for (const auto &suffix : ordered_suffixes) {
472 const auto it = masks.find(suffix);
473 const SuffixMasks m = (it != masks.end()) ? it->second : SuffixMasks{};
474 const bool covered = std::any_of(
475 plans.begin(), plans.end(), [&](const SetPlan &plan) { return mask_for(m, plan.frlg).has_value(); });
476 if (!covered) {
477 const std::string define_stem = suffix == "LAYER_TYPE" ? "LAYER" : suffix;
479 result.error_message = format->format(
480 "Could not determine a bit mask for metatile attribute field '{}'. The base game declares this "
481 "field but exposes no mask for it. Provide one of: restore the sMetatileAttrMasks[] table under its "
482 "exact name in src/fieldmap.c; add a METATILE_ATTR_{}_MASK #define in {}; or set the mask "
483 "explicitly via metatile_attribute_field_overrides (or a full metatile_attribute_fields list) in "
484 "your Porytiles config.",
485 FormatParam{namings.at(suffix).name, Style::bold},
486 FormatParam{define_stem},
487 FormatParam{fieldmap_header, Style::bold});
488 return result;
489 }
490 }
491
492 for (const auto &plan : plans) {
494 candidate.origin = plan.origin;
495 candidate.source = plan.source;
496 bool any_value_field = false;
497 for (const auto &suffix : ordered_suffixes) {
498 const auto it = masks.find(suffix);
499 const SuffixMasks m = (it != masks.end()) ? it->second : SuffixMasks{};
500 const auto mask = mask_for(m, plan.frlg);
501 if (!mask.has_value()) {
502 continue; // excluded from this set; another set covers it
503 }
504 collect_conflicts(suffix, m, mask.value(), plan.frlg, candidate.conflicts);
505 const FieldNaming &naming = namings.at(suffix);
507 definition.name = naming.name;
508 definition.mask = mask;
509 definition.provider = naming.provider;
510 if (suffix == "LAYER_TYPE") {
511 definition.role = FieldRole::layer_type;
512 }
513 else {
514 any_value_field = true;
515 }
516 candidate.fields.push_back(std::move(definition));
517 }
518 if (!any_value_field) {
519 continue; // a set with nothing beyond the layer-type role field is not a usable layout
520 }
521 candidate.required_bytes = required_bytes_for(candidate);
522 result.candidates.push_back(std::move(candidate));
523 }
524
525 if (result.candidates.empty()) {
526 // Every discovered suffix was the managed layer_type; nothing usable to provide.
527 return no_usable_layout();
528 }
529
531 return result;
532}
533
534} // namespace porytiles
A text parameter with associated styling for formatted output.
static const Style bold
Bold text formatting.
std::optional< std::uint32_t > frlg_define
std::optional< std::uint32_t > bare_shift_define
std::optional< std::uint32_t > frlg_shift_define
std::string name
std::optional< InferredFieldConflict > conflict
std::optional< ProviderDefinition > provider
std::optional< std::uint32_t > array_value
std::optional< std::uint32_t > bare_define
constexpr std::string_view field_layer_type
MetatileAttributeInferenceResult infer_metatile_attribute_candidates(const MetatileAttributeScan &scan, gsl::not_null< const TextFormatter * > format)
Infers the metatile attribute mask candidate sets from a project's raw fieldmap facts.
@ declared
the member is declared, and the declarator fields describe how
@ unreadable
the header exists but could not be scanned
@ no_constants
the header was scanned and declares no MB_ name
@ declared
the header declares at least one MB_ name
@ absent
the project has no include/constants/metatile_behaviors.h
@ not_provided
nothing attribute-related was found; other providers should be consulted
@ 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.
@ 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::string join_quoted(const std::vector< std::string > &values, const std::string &delimiter=", ")
Joins strings into a delimited list, wrapping each element in single quotes.
Utility functions for string manipulation and formatting.
struct Tileset's metatileAttributes declaration, as the project wrote it.
One unsettled fact about one inferred field, carried out of inference for the reconciler to rule on.
One complete metatile attribute mask layout a project declares.
A user-authored (or inferred) description of one metatile attribute field.
std::vector< MetatileAttributeCandidateSet > candidates
The raw facts a project exposes about its metatile attribute layout.
std::vector< std::string > unreadable_sources
files that exist but could not be read, so whatever they declare is missing above
std::vector< InferenceArrayEntry > masks_array
entries of the exact-name sMetatileAttrMasks table (may be empty)
std::string header_source
path of the fieldmap header the defines, enum members, and struct came from
std::vector< InferenceArrayEntry > shifts_array
entries of the exact-name sMetatileAttrShifts table (may be empty)
std::vector< InferenceEnumMember > enum_members
all enum members from the fieldmap header, in declaration order
AttributeDeclarationScan declaration
struct Tileset's metatileAttributes declaration, or why there is none
std::string masks_table_source
path of the file the mask table came from, empty when no table was read
std::unordered_set< std::string > ambiguous_defines
mask defines with conflicting values in an undecidable conditional branch
std::vector< InferenceDefine > defines
all integer defines from the fieldmap header