Porytiles
Loading...
Searching...
No Matches
metatile_attribute_scanner.cpp
Go to the documentation of this file.
2
3#include <cstdint>
4#include <optional>
5#include <unordered_map>
6#include <utility>
7
10
11namespace porytiles {
12
13namespace {
14
15constexpr auto masks_array_name = "sMetatileAttrMasks";
16constexpr auto shifts_array_name = "sMetatileAttrShifts";
17constexpr auto tileset_struct_name = "Tileset";
18constexpr auto attributes_member_name = "metatileAttributes";
19
20[[nodiscard]] std::vector<InferenceArrayEntry> to_inference_entries(const std::vector<IndexedArrayEntry> &entries)
21{
22 std::vector<InferenceArrayEntry> out;
23 out.reserve(entries.size());
24 for (const auto &entry : entries) {
25 std::optional<std::uint32_t> value;
26 if (entry.value.has_value()) {
27 value = static_cast<std::uint32_t>(entry.value.value());
28 }
29 out.push_back(InferenceArrayEntry{entry.index_name, value});
30 }
31 return out;
32}
33
34// The behaviors header may declare its MB_ constants as #defines (pokefirered) or as enum members (pokeemerald), so
35// both forms are scanned. The scan reports what it found, never what it means: an absent header, one that failed to
36// scan, one with no MB_ name, and one declaring at least one are four distinct facts, and the domain decides what
37// each of them implies for the behavior field's value provider.
38[[nodiscard]] BehaviorsHeaderScan scan_behaviors_header(const std::filesystem::path &path, const TextFormatter *format)
39{
40 BehaviorsHeaderScan scan;
41 scan.path = path.string();
42 if (!std::filesystem::exists(path)) {
44 return scan;
45 }
46 CParserFacade facade{path, format};
47
48 auto defines = facade.parse_defines_tolerant();
49 if (defines.has_value()) {
50 for (const auto &define : defines.value().defines) {
51 if (define.name().starts_with("MB_")) {
53 return scan;
54 }
55 }
56 }
57
58 auto enums = facade.parse_enums_tolerant();
59 if (enums.has_value()) {
60 for (const auto &decl : enums.value().enums) {
61 for (const auto &member : decl.members) {
62 if (member.name.starts_with("MB_")) {
64 return scan;
65 }
66 }
67 }
68 }
69
70 // Both tolerant scans fail only on a load or lex failure. A header that could not be scanned at all may declare
71 // any number of MB_ names, so it must not be flattened into "declares none".
72 scan.source = (!defines.has_value() && !enums.has_value()) ? BehaviorsHeaderSource::unreadable
74 return scan;
75}
76
77// Locates struct Tileset's metatileAttributes member and records the declarator it was written with. Every base game
78// declares the member as 'const uN *metatileAttributes', but this reports whatever is actually there, including a type
79// nobody recognizes: what the declarator means is the domain's call, not the scanner's. A member whose declarator the
80// parser cannot pattern-match at all never reaches here, so it reads as no member.
81[[nodiscard]] AttributeDeclarationScan declaration_from(const std::vector<StructDefinition> &defs)
82{
83 bool saw_tileset_struct = false;
84 for (const auto &def : defs) {
85 if (def.name != tileset_struct_name) {
86 continue;
87 }
88 saw_tileset_struct = true;
89 for (const auto &member : def.members) {
90 if (member.member_name == attributes_member_name) {
91 return AttributeDeclarationScan{
92 AttributeDeclarationSource::declared, member.type_name, member.pointer_depth, member.is_const};
93 }
94 }
95 }
96 return AttributeDeclarationScan{
99}
100
101} // namespace
102
104 std::filesystem::path project_root,
105 gsl::not_null<const TextFormatter *> format,
106 gsl::not_null<const UserDiagnostics *> diag)
107 : project_root_{std::move(project_root)}, format_{format}, diag_{diag}
108{
109}
110
112{
113 const std::filesystem::path fieldmap_header_rel = std::filesystem::path{"include"} / "global.fieldmap.h";
114 const std::filesystem::path fieldmap_source_rel = std::filesystem::path{"src"} / "fieldmap.c";
115 const std::filesystem::path behaviors_header_rel =
116 std::filesystem::path{"include"} / "constants" / "metatile_behaviors.h";
117
119
120 const auto fieldmap_header = project_root_ / fieldmap_header_rel;
121 scan.header_source = fieldmap_header.string();
122 if (!std::filesystem::exists(fieldmap_header)) {
123 // No fieldmap header: the project states nothing about its attribute layout.
125 return scan;
126 }
127
128 CParserFacade header_facade{fieldmap_header, format_};
129 auto defines_result = header_facade.parse_defines_tolerant();
130 if (!defines_result.has_value()) {
131 diag_->warning(
133 "Could not scan '{}' for attribute masks; skipping schema inference.",
134 FormatParam{fieldmap_header.string(), Style::bold});
135 scan.unreadable_sources.push_back(fieldmap_header.string());
137 return scan;
138 }
139 // Both tolerant scans fail only on a load or lex failure, and both run over the same cached content, so the enum
140 // scan cannot fail once the define scan has succeeded on this facade. Asserted rather than handled: a graceful
141 // branch here would be untestable, and if the facade ever grows a failure mode the two scans do not share, a
142 // silent "the project declares no attribute enum" is the worst way to find out.
143 auto enums_result = header_facade.parse_enums_tolerant();
145 enums_result.has_value(), "enum scan failed on a fieldmap header whose define scan already succeeded");
146
147 const auto &header_defines = defines_result.value().defines;
148 const auto &header_enums = enums_result.value().enums;
149
150 scan.ambiguous_defines = defines_result.value().ambiguous_values;
151 std::unordered_map<std::string, std::int64_t> seeds;
152
153 for (const auto &define : header_defines) {
154 if (define.has_int_value()) {
155 scan.defines.push_back(InferenceDefine{define.name(), static_cast<std::uint32_t>(define.int_value())});
156 seeds[define.name()] = define.int_value();
157 }
158 }
159 for (const auto &decl : header_enums) {
160 for (const auto &member : decl.members) {
161 scan.enum_members.push_back(InferenceEnumMember{member.name, member.value});
162 if (member.value.has_value()) {
163 seeds[member.name] = member.value.value();
164 }
165 }
166 }
167
168 // Read the exact-name mask/shift tables from the source, seeded with the header symbols so their FRLG-macro values
169 // resolve. A missing file or missing table simply leaves the tables empty.
170 const auto fieldmap_source = project_root_ / fieldmap_source_rel;
171 if (std::filesystem::exists(fieldmap_source)) {
172 CParserFacade source_facade{fieldmap_source, format_, seeds};
173 if (auto arrays = source_facade.parse_indexed_arrays(); arrays.has_value()) {
174 for (const auto &array : arrays.value()) {
175 if (array.name == masks_array_name) {
176 scan.masks_array = to_inference_entries(array.entries);
177 // Recorded only when the table is actually found, so nothing downstream can point a user at a
178 // file that contributed no masks.
179 scan.masks_table_source = fieldmap_source.string();
180 }
181 else if (array.name == shifts_array_name) {
182 scan.shifts_array = to_inference_entries(array.entries);
183 }
184 }
185 }
186 else {
187 // The table file is present but could not be lexed or parsed. Unlike a missing table (which states
188 // nothing about the layout), a present-but-unparseable one is worth surfacing: without it the header
189 // defines alone drive inference, and a table-driven project would otherwise be told its masks are
190 // missing and advised to restore a table that already exists.
191 diag_->warning(
193 "Could not scan '{}' for the metatile attribute mask and shift tables; continuing from the header "
194 "defines only.",
195 FormatParam{fieldmap_source.string(), Style::bold});
196 scan.unreadable_sources.push_back(fieldmap_source.string());
197 }
198 // Surface the source file's scan warnings too, matching how the header facade's warnings are emitted below.
199 for (const auto &warning : source_facade.scan_warnings()) {
201 }
202 }
203
204 scan.behaviors_header = scan_behaviors_header(project_root_ / behaviors_header_rel, format_);
206 // Matches the fieldmap source handling above: a file that exists but could not be scanned is warned about and
207 // listed as unreadable, so the missing facts are never mistaken for facts the project failed to state.
208 diag_->warning(
210 "Could not scan '{}' for behavior constants.",
212 scan.unreadable_sources.push_back(scan.behaviors_header.path);
213 }
214
215 // Surface the recoverable scan warnings (conflicting redefinitions in undecidable regions, etc.).
216 for (const auto &warning : header_facade.scan_warnings()) {
218 }
219
220 // struct Tileset's metatileAttributes declaration, from the same header. The struct scan fails only on a load or
221 // lex failure, and both already succeeded on this facade's cached content, so it cannot fail here. Asserted rather
222 // than handled for the same reason as the enum scan above, and with more at stake: the declaration width has no
223 // second source, so quietly recording "this project declares no struct Tileset" would send the user hunting
224 // through a header that declares one perfectly well.
225 auto struct_defs = header_facade.parse_struct_definitions(std::string{tileset_struct_name});
227 struct_defs.has_value(), "struct scan failed on a fieldmap header whose define scan already succeeded");
228 scan.declaration = declaration_from(struct_defs.value());
229
230 return scan;
231}
232
233} // namespace porytiles
High-level facade for parsing C/C++ source files.
A text parameter with associated styling for formatted output.
MetatileAttributeScan scan_project() const
Scans the project's fieldmap sources and returns the gathered facts.
MetatileAttributeScanner(std::filesystem::path project_root, gsl::not_null< const TextFormatter * > format, gsl::not_null< const UserDiagnostics * > diag)
Constructs a MetatileAttributeScanner.
static const Style bold
Bold text formatting.
virtual void warning(const std::string &tag, const std::vector< std::string > &lines) const =0
Display a tagged warning message.
void assert_or_panic(bool condition, const StringViewSourceLoc &s)
Conditionally panics if the given condition is false.
Definition panic.cpp:53
@ 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
BehaviorsHeaderSource
What the scan found where the behavior constants header should be.
@ 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
@ warning
Emit a warning and continue decompilation.
constexpr auto metatile_attr_inference_tag
The diagnostic tag the infra-layer scanner emits its warnings under.
std::string path
the path looked at, so a diagnostic can name it
One integer #define gathered from a base game's fieldmap header.
One enum member gathered from a base game's fieldmap header.
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
BehaviorsHeaderScan behaviors_header
the behavior constants header, or why there is none
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