7#include <unordered_map>
8#include <unordered_set>
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";
28[[nodiscard]] std::string to_lower(std::string text)
30 std::ranges::transform(text, text.begin(), [](
unsigned char c) { return static_cast<char>(std::tolower(c)); });
35[[nodiscard]] std::string normalize_suffix(std::string suffix)
37 if (suffix ==
"LAYER") {
43[[nodiscard]]
bool is_all_digits(
const std::string &text)
45 return !text.empty() && std::all_of(text.begin(), text.end(), [](
unsigned char c) { return std::isdigit(c) != 0; });
48[[nodiscard]]
bool any_enum_member_has_prefix(
const MetatileAttributeScan &scan,
const std::string &prefix)
50 return std::any_of(scan.enum_members.begin(), scan.enum_members.end(), [&](
const InferenceEnumMember &member) {
51 return member.name.starts_with(prefix);
57[[nodiscard]] std::string join_sources(
const std::string &first,
const std::string &second)
65 return first +
", " + second;
87[[nodiscard]] FieldNaming name_field(
const std::string &suffix,
const MetatileAttributeScan &scan)
91 if (suffix ==
"LAYER_TYPE") {
98 if (suffix ==
"BEHAVIOR") {
99 naming.name =
"behavior";
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};
109 naming.conflict = InferredFieldConflict{
113 naming.conflict = InferredFieldConflict{
121 naming.conflict = InferredFieldConflict{
130 else if (is_all_digits(suffix)) {
133 naming.name =
"attribute_" + suffix;
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()) +
"_";
141 if (any_enum_member_has_prefix(scan, probe)) {
145 naming.conflict = InferredFieldConflict{
157[[nodiscard]] InferredAttributeDeclaration interpret_declaration(
const AttributeDeclarationScan &scan)
159 InferredAttributeDeclaration declaration;
160 declaration.scan = scan;
164 if (scan.element_type ==
"u8") {
165 declaration.size = 1;
167 else if (scan.element_type ==
"u16") {
168 declaration.size = 2;
170 else if (scan.element_type ==
"u32") {
171 declaration.size = 4;
178[[nodiscard]] std::size_t required_bytes_for(
const MetatileAttributeCandidateSet &candidate)
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())));
184 return required_bits <= 8 ? 1U : (required_bits <= 16 ? 2U : 4U);
201 text +=
"metatileAttributes";
205MetatileAttributeInferenceResult
217 if (!
name.starts_with(mask_define_prefix)) {
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)) {
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.",
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);
243 if (!member.name.starts_with(attribute_enum_prefix)) {
246 std::string suffix = normalize_suffix(member.name.substr(std::string_view{attribute_enum_prefix}.size()));
247 if (suffix ==
"COUNT") {
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)) {
260 const std::string body = define.name.substr(std::string_view{mask_define_prefix}.size());
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());
268 else if (body.ends_with(mask_define_suffix)) {
269 suffix = body.substr(0, body.size() - std::string_view{mask_define_suffix}.size());
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());
276 else if (body.ends_with(shift_define_suffix)) {
277 suffix = body.substr(0, body.size() - std::string_view{shift_define_suffix}.size());
287 if (suffix.empty()) {
290 suffix = normalize_suffix(suffix);
295 masks[suffix].frlg_shift_define = define.value;
298 masks[suffix].bare_shift_define = define.value;
303 masks[suffix].frlg_define = define.value;
304 any_frlg_define =
true;
307 masks[suffix].bare_define = define.value;
308 any_bare_define =
true;
313 bool any_array_value =
false;
315 if (!entry.index_name.starts_with(attribute_enum_prefix)) {
318 std::string suffix = normalize_suffix(entry.index_name.substr(std::string_view{attribute_enum_prefix}.size()));
319 if (suffix ==
"COUNT") {
322 if (entry.value.has_value()) {
323 masks[suffix].array_value = entry.value;
324 any_array_value =
true;
329 std::unordered_map<std::string, std::uint32_t> shifts;
331 if (!entry.index_name.starts_with(attribute_enum_prefix) || !entry.value.has_value()) {
334 std::string suffix = normalize_suffix(entry.index_name.substr(std::string_view{attribute_enum_prefix}.size()));
335 shifts[suffix] = entry.value.value();
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.",
358 if (ordered_suffixes.empty()) {
360 return no_usable_layout();
365 std::unordered_map<std::string, FieldNaming> namings;
366 for (
const auto &suffix : ordered_suffixes) {
367 namings.emplace(suffix, name_field(suffix, scan));
380 std::vector<SetPlan> plans;
381 if (any_frlg_define) {
384 plans.push_back(SetPlan{
"the bare METATILE_ATTR_*_MASK defines", scan.
header_source,
false});
385 if (any_array_value) {
388 "the METATILE_ATTR_*_MASK_FRLG defines and the sMetatileAttrMasks table",
393 plans.push_back(SetPlan{
"the METATILE_ATTR_*_MASK_FRLG defines", scan.
header_source,
true});
399 if (any_bare_define && any_array_value) {
400 origin =
"the METATILE_ATTR_*_MASK defines and the sMetatileAttrMasks table";
403 else if (any_bare_define) {
404 origin =
"the METATILE_ATTR_*_MASK defines";
408 origin =
"the sMetatileAttrMasks table";
411 plans.push_back(SetPlan{std::move(origin), std::move(source),
false});
416 const auto mask_for = [&](
const SuffixMasks &m,
bool frlg) -> std::optional<std::uint32_t> {
418 return m.frlg_define.has_value() ? m.frlg_define : m.array_value;
420 if (any_frlg_define) {
421 return m.bare_define;
423 return m.bare_define.has_value() ? m.bare_define : m.array_value;
429 const auto collect_conflicts = [&](
const std::string &suffix,
430 const SuffixMasks &m,
431 std::uint32_t resolved_mask,
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());
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()) {
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) {
458 if (frlg == any_frlg_define) {
459 if (
const auto shift_it = shifts.find(suffix); shift_it != shifts.end() && shift_it->second != expected) {
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(); });
477 const std::string define_stem = suffix ==
"LAYER_TYPE" ?
"LAYER" : suffix;
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.",
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()) {
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") {
514 any_value_field =
true;
516 candidate.
fields.push_back(std::move(definition));
518 if (!any_value_field) {
522 result.
candidates.push_back(std::move(candidate));
527 return no_usable_layout();
static const Style bold
Bold text formatting.
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.
std::size_t pointer_depth
AttributeDeclarationSource source
One unsettled fact about one inferred field, carried out of inference for the reconciler to rule on.