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
40[[nodiscard]] std::string generate_include_directive(const std::string &snake_dir)
41{
42 return fmt::format(
43 "#include \"{}/{}/generated_anim_code.h\"", porytiles_generated_include_base.string(), snake_dir);
44}
45
49[[nodiscard]] std::string generate_porytiles_include_comment()
50{
51 return "// [Porytiles] Auto-generated include. Do not remove.";
52}
53
57[[nodiscard]] std::string generate_porytiles_declaration_comment()
58{
59 return "// [Porytiles] Auto-generated declaration. Do not remove.";
60}
61
71[[nodiscard]] std::string generate_declaration(const std::string &shorthand)
72{
73 return fmt::format("void InitTilesetAnim_PorytilesManaged_{}(void);", shorthand);
74}
75
85[[nodiscard]] std::string generate_declaration_pattern(const std::string &shorthand)
86{
87 return fmt::format("InitTilesetAnim_PorytilesManaged_{}", shorthand);
88}
89
99[[nodiscard]] std::string generate_include_pattern(const std::string &snake_dir)
100{
101 return fmt::format("{}/generated_anim_code.h", snake_dir);
102}
103
109[[nodiscard]] std::optional<std::size_t>
110find_existing_include(const std::vector<std::string> &lines, const std::string &snake_dir)
111{
112 const std::string pattern = generate_include_pattern(snake_dir);
113
114 for (std::size_t i = 0; i < lines.size(); ++i) {
115 if (lines[i].find(pattern) != std::string::npos) {
116 return i;
117 }
118 }
119 return std::nullopt;
120}
121
127[[nodiscard]] std::optional<std::size_t>
128find_existing_declaration(const std::vector<std::string> &lines, const std::string &shorthand)
129{
130 const std::string pattern = generate_declaration_pattern(shorthand);
131
132 for (std::size_t i = 0; i < lines.size(); ++i) {
133 if (lines[i].find(pattern) != std::string::npos) {
134 return i;
135 }
136 }
137 return std::nullopt;
138}
139
145[[nodiscard]] std::optional<std::size_t> find_endif_guard(const std::vector<std::string> &lines)
146{
147 for (std::size_t i = lines.size(); i > 0; --i) {
148 const auto &line = lines[i - 1];
149 if (line.find("#endif") != std::string::npos) {
150 return i - 1;
151 }
152 }
153 return std::nullopt;
154}
155
159[[nodiscard]] bool is_porytiles_comment(const std::string &line)
160{
161 return line.find("[Porytiles]") != std::string::npos;
162}
163
168read_file_lines(const std::filesystem::path &path, const UserDiagnostics *diag)
169{
170 std::ifstream in{path};
171 if (!in.is_open()) {
172 return FormattableError{
173 diag->formatter().format("{}: Failed to open for reading.", FormatParam{path.string(), Style::bold})};
174 }
175
176 std::vector<std::string> lines;
177 std::string line;
178 while (std::getline(in, line)) {
179 lines.push_back(line);
180 }
181 return lines;
182}
183
187[[nodiscard]] ChainableResult<void>
188write_file_lines(const std::filesystem::path &path, const std::vector<std::string> &lines, const UserDiagnostics *diag)
189{
190 std::ofstream out{path};
191 if (!out.is_open()) {
192 return FormattableError{
193 diag->formatter().format("{}: Failed to open for writing.", FormatParam{path.string(), Style::bold})};
194 }
195
196 for (const auto &line : lines) {
197 out << line << '\n';
198 }
199 out.flush();
200
201 if (out.fail()) {
202 return FormattableError{
203 diag->formatter().format("{}: Failed to write file.", FormatParam{path.string(), Style::bold})};
204 }
205
206 return {};
207}
208
209} // namespace
210
211namespace porytiles {
212
214 std::filesystem::path project_root,
215 gsl::not_null<const InfraConfig *> config,
216 gsl::not_null<const UserDiagnostics *> diagnostics)
217 : project_root_{std::move(project_root)}, config_{config}, diagnostics_{diagnostics}
218{
219}
220
222ProjectTilesetAnimsModifier::wire_include_for_tileset(const std::string &tileset_name, bool is_secondary) const
223{
224 // Step 1: Validate tileset name starts with expected prefix
225 constexpr std::string_view tileset_prefix = "gTileset_";
226 if (!tileset_name.starts_with(tileset_prefix)) {
227 return FormattableError{diagnostics_->formatter().format(
228 "Tileset name '{}' does not start with 'gTileset_'.", FormatParam{tileset_name, Style::bold})};
229 }
230
231 // Step 2: Extract shorthand and convert to snake_case for directory name
232 const auto tileset_cased = extract_tileset_cased_name(tileset_name);
233 const std::string shorthand = tileset_cased.to_pascal_case();
234 const std::string snake_dir = tileset_cased.to_snake_case();
235
236 // Step 3: Read .c file
237 const auto anims_c_path = project_root_ / tileset_anims_c_rel_path;
239 c_lines,
240 read_file_lines(anims_c_path, diagnostics_),
241 void,
242 "Failed to read tileset_anims.c for tileset '{}'.",
243 FormatParam(tileset_name, Style::bold));
244
245 // Step 4: Read .h file
246 const auto anims_h_path = project_root_ / tileset_anims_h_rel_path;
248 h_lines,
249 read_file_lines(anims_h_path, diagnostics_),
250 void,
251 "Failed to read tileset_anims.h for tileset '{}'.",
252 FormatParam(tileset_name, Style::bold));
253
254 // Step 5: Check idempotency for .c file - warn and skip if already present
255 const bool include_exists = find_existing_include(c_lines, snake_dir).has_value();
256 const bool declaration_exists = find_existing_declaration(h_lines, shorthand).has_value();
257
258 if (include_exists && declaration_exists) {
259 diagnostics_->remark(
260 "wire-tileset-animation",
261 "Animation include and declaration for '{}' already exist, skipping wire operation.",
262 FormatParam{tileset_name, Style::bold});
263 return {};
264 }
265
266 // Step 6: Add include to .c file if not present
267 if (!include_exists) {
268 const std::string include_comment = generate_porytiles_include_comment();
269 const std::string include_line = generate_include_directive(snake_dir);
270
271 c_lines.emplace_back("");
272 c_lines.push_back(include_comment);
273 c_lines.push_back(include_line);
274 }
275
276 // Step 7: Add declaration to .h file if not present
277 if (!declaration_exists) {
278 // Find #endif to insert before it
279 const auto endif_index = find_endif_guard(h_lines);
280 if (!endif_index.has_value()) {
281 return FormattableError{diagnostics_->formatter().format(
282 "Failed to find #endif guard in tileset_anims.h for tileset '{}'.",
283 FormatParam{tileset_name, Style::bold})};
284 }
285
286 const std::string decl_comment = generate_porytiles_declaration_comment();
287 const std::string decl_line = generate_declaration(shorthand);
288
289 // Insert blank line (if needed), comment, declaration, and trailing blank before #endif
290 auto insert_pos = h_lines.begin() + static_cast<std::ptrdiff_t>(endif_index.value());
291
292 // Only insert leading blank if the line before #endif is not already blank
293 const std::size_t idx = endif_index.value();
294 if (idx == 0 || !h_lines[idx - 1].empty()) {
295 insert_pos = h_lines.insert(insert_pos, "");
296 ++insert_pos;
297 }
298
299 insert_pos = h_lines.insert(insert_pos, decl_comment);
300 ++insert_pos;
301 insert_pos = h_lines.insert(insert_pos, decl_line);
302 ++insert_pos;
303 // Add trailing blank line to ensure spacing before #endif
304 h_lines.insert(insert_pos, "");
305 }
306
307 // Step 8: Write both files back
308 auto c_write_result = write_file_lines(anims_c_path, c_lines, diagnostics_);
309 if (!c_write_result.has_value()) {
310 return c_write_result;
311 }
312
313 return write_file_lines(anims_h_path, h_lines, diagnostics_);
314}
315
317ProjectTilesetAnimsModifier::remove_include_for_tileset(const std::string &tileset_name, bool is_secondary) const
318{
319 // Note: is_secondary is not used in removal since the pattern matching is based solely on
320 // the tileset directory name, not the tier. This allows removal to work regardless of which
321 // tier the include was originally added for.
322 (void)is_secondary;
323
324 // Step 1: Validate tileset name starts with expected prefix
325 constexpr std::string_view tileset_prefix = "gTileset_";
326 if (!tileset_name.starts_with(tileset_prefix)) {
327 return FormattableError{diagnostics_->formatter().format(
328 "Tileset name '{}' does not start with 'gTileset_'.", FormatParam{tileset_name, Style::bold})};
329 }
330
331 // Step 2: Extract shorthand and convert to snake_case for directory name
332 const auto tileset_cased = extract_tileset_cased_name(tileset_name);
333 const std::string shorthand = tileset_cased.to_pascal_case();
334 const std::string snake_dir = tileset_cased.to_snake_case();
335
336 // Step 3: Read .c file
337 const auto anims_c_path = project_root_ / tileset_anims_c_rel_path;
339 c_lines,
340 read_file_lines(anims_c_path, diagnostics_),
341 void,
342 "Failed to read tileset_anims.c for tileset '{}'.",
343 FormatParam(tileset_name, Style::bold));
344
345 // Step 4: Read .h file
346 const auto anims_h_path = project_root_ / tileset_anims_h_rel_path;
348 h_lines,
349 read_file_lines(anims_h_path, diagnostics_),
350 void,
351 "Failed to read tileset_anims.h for tileset '{}'.",
352 FormatParam(tileset_name, Style::bold));
353
354 // Step 5: Check if anything exists to remove
355 const auto include_index = find_existing_include(c_lines, snake_dir);
356 const auto decl_index = find_existing_declaration(h_lines, shorthand);
357
358 if (!include_index.has_value() && !decl_index.has_value()) {
359 diagnostics_->remark(
360 "wire-tileset-animation",
361 "Animation include and declaration for '{}' not found, skipping remove operation.",
362 FormatParam{tileset_name, Style::bold});
363 return {};
364 }
365
366 // Step 6: Remove the include line from .c file if present
367 if (include_index.has_value()) {
368 std::size_t idx = include_index.value();
369 c_lines.erase(c_lines.begin() + static_cast<std::ptrdiff_t>(idx));
370
371 // Remove the preceding Porytiles comment if present
372 if (idx > 0 && is_porytiles_comment(c_lines[idx - 1])) {
373 c_lines.erase(c_lines.begin() + static_cast<std::ptrdiff_t>(idx - 1));
374 --idx;
375
376 // Also remove the blank line before the comment if present
377 if (idx > 0 && c_lines[idx - 1].empty()) {
378 c_lines.erase(c_lines.begin() + static_cast<std::ptrdiff_t>(idx - 1));
379 }
380 }
381 }
382
383 // Step 7: Remove the declaration line from .h file if present
384 if (decl_index.has_value()) {
385 std::size_t idx = decl_index.value();
386 h_lines.erase(h_lines.begin() + static_cast<std::ptrdiff_t>(idx));
387
388 // Remove the preceding Porytiles comment if present
389 if (idx > 0 && is_porytiles_comment(h_lines[idx - 1])) {
390 h_lines.erase(h_lines.begin() + static_cast<std::ptrdiff_t>(idx - 1));
391 --idx;
392
393 // Also remove the blank line before the comment if present
394 if (idx > 0 && h_lines[idx - 1].empty()) {
395 h_lines.erase(h_lines.begin() + static_cast<std::ptrdiff_t>(idx - 1));
396 }
397 }
398 }
399
400 // Step 8: Write both files back
401 auto c_write_result = write_file_lines(anims_c_path, c_lines, diagnostics_);
402 if (!c_write_result.has_value()) {
403 return c_write_result;
404 }
405
406 return write_file_lines(anims_h_path, h_lines, diagnostics_);
407}
408
409} // 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:63
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.