Porytiles
Loading...
Searching...
No Matches
project_tileset_anims_modifier.cpp
Go to the documentation of this file.
2
3#include <cstddef>
4#include <filesystem>
5#include <fstream>
6#include <string>
7#include <utility>
8#include <vector>
9
10#include "fmt/format.h"
11
16
17namespace {
18
19using namespace porytiles;
20
21// Paths relative to project root
22const std::filesystem::path tileset_anims_c_rel_path = std::filesystem::path{"src"} / "tileset_anims.c";
23const std::filesystem::path tileset_anims_h_rel_path = std::filesystem::path{"include"} / "tileset_anims.h";
24// Note: "include/" is omitted because pokeemerald's makefile adds "include" to the include path
25const std::filesystem::path porytiles_generated_include_base =
26 std::filesystem::path{"porytiles_generated"} / "tilesets";
27
38[[nodiscard]] std::string generate_include_directive(const std::string &snake_dir)
39{
40 return fmt::format(
41 "#include \"{}/{}/generated_anim_code.h\"", porytiles_generated_include_base.string(), snake_dir);
42}
43
45[[nodiscard]] std::string generate_porytiles_include_comment()
46{
47 return "// [Porytiles] Auto-generated include. Do not remove.";
48}
49
51[[nodiscard]] std::string generate_porytiles_declaration_comment()
52{
53 return "// [Porytiles] Auto-generated declaration. Do not remove.";
54}
55
63[[nodiscard]] std::string generate_declaration(const std::string &shorthand)
64{
65 return fmt::format("void InitTilesetAnim_PorytilesManaged_{}(void);", shorthand);
66}
67
75[[nodiscard]] std::string generate_declaration_pattern(const std::string &shorthand)
76{
77 return fmt::format("InitTilesetAnim_PorytilesManaged_{}", shorthand);
78}
79
87[[nodiscard]] std::string generate_include_pattern(const std::string &snake_dir)
88{
89 return fmt::format("{}/generated_anim_code.h", snake_dir);
90}
91
95[[nodiscard]] std::optional<std::size_t>
96find_existing_include(const std::vector<std::string> &lines, const std::string &snake_dir)
97{
98 const std::string pattern = generate_include_pattern(snake_dir);
99
100 for (std::size_t i = 0; i < lines.size(); ++i) {
101 if (lines[i].find(pattern) != std::string::npos) {
102 return i;
103 }
104 }
105 return std::nullopt;
106}
107
111[[nodiscard]] std::optional<std::size_t>
112find_existing_declaration(const std::vector<std::string> &lines, const std::string &shorthand)
113{
114 const std::string pattern = generate_declaration_pattern(shorthand);
115
116 for (std::size_t i = 0; i < lines.size(); ++i) {
117 if (lines[i].find(pattern) != std::string::npos) {
118 return i;
119 }
120 }
121 return std::nullopt;
122}
123
127[[nodiscard]] std::optional<std::size_t> find_endif_guard(const std::vector<std::string> &lines)
128{
129 for (std::size_t i = lines.size(); i > 0; --i) {
130 const auto &line = lines[i - 1];
131 if (line.find("#endif") != std::string::npos) {
132 return i - 1;
133 }
134 }
135 return std::nullopt;
136}
137
139[[nodiscard]] bool is_porytiles_comment(const std::string &line)
140{
141 return line.find("[Porytiles]") != std::string::npos;
142}
143
146read_file_lines(const std::filesystem::path &path, const UserDiagnostics *diag)
147{
148 std::ifstream in{path};
149 if (!in.is_open()) {
150 return FormattableError{
151 diag->formatter().format("{}: Failed to open for reading.", FormatParam{path.string(), Style::bold})};
152 }
153
154 std::vector<std::string> lines;
155 std::string line;
156 while (std::getline(in, line)) {
157 lines.push_back(line);
158 }
159 return lines;
160}
161
163[[nodiscard]] ChainableResult<void>
164write_file_lines(const std::filesystem::path &path, const std::vector<std::string> &lines, const UserDiagnostics *diag)
165{
166 std::ofstream out{path};
167 if (!out.is_open()) {
168 return FormattableError{
169 diag->formatter().format("{}: Failed to open for writing.", FormatParam{path.string(), Style::bold})};
170 }
171
172 for (const auto &line : lines) {
173 out << line << '\n';
174 }
175 out.flush();
176
177 if (out.fail()) {
178 return FormattableError{
179 diag->formatter().format("{}: Failed to write file.", FormatParam{path.string(), Style::bold})};
180 }
181
182 return {};
183}
184
185} // namespace
186
187namespace porytiles {
188
190 std::filesystem::path project_root,
191 gsl::not_null<const InfraConfig *> config,
192 gsl::not_null<const UserDiagnostics *> diagnostics)
193 : project_root_{std::move(project_root)}, config_{config}, diagnostics_{diagnostics}
194{
195}
196
198ProjectTilesetAnimsModifier::wire_include_for_tileset(const std::string &tileset_name, bool is_secondary) const
199{
200 // Step 1: Validate tileset name starts with expected prefix
201 constexpr std::string_view tileset_prefix = "gTileset_";
202 if (!tileset_name.starts_with(tileset_prefix)) {
203 return FormattableError{diagnostics_->formatter().format(
204 "Tileset name '{}' does not start with 'gTileset_'.", FormatParam{tileset_name, Style::bold})};
205 }
206
207 // Step 2: Extract shorthand and convert to snake_case for directory name
208 const auto tileset_cased = extract_tileset_cased_name(tileset_name);
209 const std::string shorthand = tileset_cased.to_pascal_case();
210 const std::string snake_dir = tileset_cased.to_snake_case();
211
212 // Step 3: Read .c file
213 const auto anims_c_path = project_root_ / tileset_anims_c_rel_path;
215 c_lines,
216 read_file_lines(anims_c_path, diagnostics_),
217 void,
218 "Failed to read tileset_anims.c for tileset '{}'.",
219 FormatParam(tileset_name, Style::bold));
220
221 // Step 4: Read .h file
222 const auto anims_h_path = project_root_ / tileset_anims_h_rel_path;
224 h_lines,
225 read_file_lines(anims_h_path, diagnostics_),
226 void,
227 "Failed to read tileset_anims.h for tileset '{}'.",
228 FormatParam(tileset_name, Style::bold));
229
230 // Step 5: Check idempotency for .c file - warn and skip if already present
231 const bool include_exists = find_existing_include(c_lines, snake_dir).has_value();
232 const bool declaration_exists = find_existing_declaration(h_lines, shorthand).has_value();
233
234 if (include_exists && declaration_exists) {
235 diagnostics_->remark(
236 "wire-tileset-animation",
237 "Animation include and declaration for '{}' already exist, skipping wire operation.",
238 FormatParam{tileset_name, Style::bold});
239 return {};
240 }
241
242 // Step 6: Add include to .c file if not present
243 if (!include_exists) {
244 const std::string include_comment = generate_porytiles_include_comment();
245 const std::string include_line = generate_include_directive(snake_dir);
246
247 c_lines.emplace_back("");
248 c_lines.push_back(include_comment);
249 c_lines.push_back(include_line);
250 }
251
252 // Step 7: Add declaration to .h file if not present
253 if (!declaration_exists) {
254 // Find #endif to insert before it
255 const auto endif_index = find_endif_guard(h_lines);
256 if (!endif_index.has_value()) {
257 return FormattableError{diagnostics_->formatter().format(
258 "Failed to find #endif guard in tileset_anims.h for tileset '{}'.",
259 FormatParam{tileset_name, Style::bold})};
260 }
261
262 const std::string decl_comment = generate_porytiles_declaration_comment();
263 const std::string decl_line = generate_declaration(shorthand);
264
265 // Insert blank line (if needed), comment, declaration, and trailing blank before #endif
266 auto insert_pos = h_lines.begin() + static_cast<std::ptrdiff_t>(endif_index.value());
267
268 // Only insert leading blank if the line before #endif is not already blank
269 const std::size_t idx = endif_index.value();
270 if (idx == 0 || !h_lines[idx - 1].empty()) {
271 insert_pos = h_lines.insert(insert_pos, "");
272 ++insert_pos;
273 }
274
275 insert_pos = h_lines.insert(insert_pos, decl_comment);
276 ++insert_pos;
277 insert_pos = h_lines.insert(insert_pos, decl_line);
278 ++insert_pos;
279 // Add trailing blank line to ensure spacing before #endif
280 h_lines.insert(insert_pos, "");
281 }
282
283 // Step 8: Write both files back
284 auto c_write_result = write_file_lines(anims_c_path, c_lines, diagnostics_);
285 if (!c_write_result.has_value()) {
286 return c_write_result;
287 }
288
289 return write_file_lines(anims_h_path, h_lines, diagnostics_);
290}
291
293ProjectTilesetAnimsModifier::remove_include_for_tileset(const std::string &tileset_name, bool is_secondary) const
294{
295 // Note: is_secondary is not used in removal since the pattern matching is based solely on
296 // the tileset directory name, not the tier. This allows removal to work regardless of which
297 // tier the include was originally added for.
298 (void)is_secondary;
299
300 // Step 1: Validate tileset name starts with expected prefix
301 constexpr std::string_view tileset_prefix = "gTileset_";
302 if (!tileset_name.starts_with(tileset_prefix)) {
303 return FormattableError{diagnostics_->formatter().format(
304 "Tileset name '{}' does not start with 'gTileset_'.", FormatParam{tileset_name, Style::bold})};
305 }
306
307 // Step 2: Extract shorthand and convert to snake_case for directory name
308 const auto tileset_cased = extract_tileset_cased_name(tileset_name);
309 const std::string shorthand = tileset_cased.to_pascal_case();
310 const std::string snake_dir = tileset_cased.to_snake_case();
311
312 // Step 3: Read .c file
313 const auto anims_c_path = project_root_ / tileset_anims_c_rel_path;
315 c_lines,
316 read_file_lines(anims_c_path, diagnostics_),
317 void,
318 "Failed to read tileset_anims.c for tileset '{}'.",
319 FormatParam(tileset_name, Style::bold));
320
321 // Step 4: Read .h file
322 const auto anims_h_path = project_root_ / tileset_anims_h_rel_path;
324 h_lines,
325 read_file_lines(anims_h_path, diagnostics_),
326 void,
327 "Failed to read tileset_anims.h for tileset '{}'.",
328 FormatParam(tileset_name, Style::bold));
329
330 // Step 5: Check if anything exists to remove
331 const auto include_index = find_existing_include(c_lines, snake_dir);
332 const auto decl_index = find_existing_declaration(h_lines, shorthand);
333
334 if (!include_index.has_value() && !decl_index.has_value()) {
335 diagnostics_->remark(
336 "wire-tileset-animation",
337 "Animation include and declaration for '{}' not found, skipping remove operation.",
338 FormatParam{tileset_name, Style::bold});
339 return {};
340 }
341
342 // Step 6: Remove the include line from .c file if present
343 if (include_index.has_value()) {
344 std::size_t idx = include_index.value();
345 c_lines.erase(c_lines.begin() + static_cast<std::ptrdiff_t>(idx));
346
347 // Remove the preceding Porytiles comment if present
348 if (idx > 0 && is_porytiles_comment(c_lines[idx - 1])) {
349 c_lines.erase(c_lines.begin() + static_cast<std::ptrdiff_t>(idx - 1));
350 --idx;
351
352 // Also remove the blank line before the comment if present
353 if (idx > 0 && c_lines[idx - 1].empty()) {
354 c_lines.erase(c_lines.begin() + static_cast<std::ptrdiff_t>(idx - 1));
355 }
356 }
357 }
358
359 // Step 7: Remove the declaration line from .h file if present
360 if (decl_index.has_value()) {
361 std::size_t idx = decl_index.value();
362 h_lines.erase(h_lines.begin() + static_cast<std::ptrdiff_t>(idx));
363
364 // Remove the preceding Porytiles comment if present
365 if (idx > 0 && is_porytiles_comment(h_lines[idx - 1])) {
366 h_lines.erase(h_lines.begin() + static_cast<std::ptrdiff_t>(idx - 1));
367 --idx;
368
369 // Also remove the blank line before the comment if present
370 if (idx > 0 && h_lines[idx - 1].empty()) {
371 h_lines.erase(h_lines.begin() + static_cast<std::ptrdiff_t>(idx - 1));
372 }
373 }
374 }
375
376 // Step 8: Write both files back
377 auto c_write_result = write_file_lines(anims_c_path, c_lines, diagnostics_);
378 if (!c_write_result.has_value()) {
379 return c_write_result;
380 }
381
382 return write_file_lines(anims_h_path, h_lines, diagnostics_);
383}
384
385} // namespace porytiles
#define PT_TRY_ASSIGN_CHAIN_ERR(var, expr, return_type,...)
Unwraps a ChainableResult, chaining a new error message on failure.
A result type that maintains a chainable sequence of errors for debugging and error reporting.
A text parameter with associated styling for formatted output.
General-purpose error implementation with formatted message support.
Definition error.hpp:57
ChainableResult< void > wire_include_for_tileset(const std::string &tileset_name, bool is_secondary) const
Adds an #include directive for the tileset's generated animation code.
ProjectTilesetAnimsModifier(std::filesystem::path project_root, gsl::not_null< const InfraConfig * > config, gsl::not_null< const UserDiagnostics * > diagnostics)
Constructs a TilesetAnimsModifier with required dependencies.
ChainableResult< void > remove_include_for_tileset(const std::string &tileset_name, bool is_secondary) const
Removes the #include directive for the tileset's generated animation code.
static const Style bold
Bold text formatting.
virtual std::string format(const std::string &format_str, const std::vector< FormatParam > &params) const
Formats a string with styled parameters using fmtlib syntax.
Abstract class for structured error reporting and diagnostic output.
virtual void remark(const std::string &tag, const std::vector< std::string > &lines) const =0
Display a tagged remark message.
const TextFormatter & formatter() const
DynamicCasedName extract_tileset_cased_name(const std::string &tileset_name)
Extracts the tileset short name and wraps it in a DynamicCasedName.
Utility functions for string manipulation and formatting.