Porytiles
Loading...
Searching...
No Matches
anim_code_parser.cpp
Go to the documentation of this file.
2
3#include <format>
4#include <map>
5#include <set>
6#include <string>
7
15
16namespace {
17
18using namespace porytiles;
19
20[[nodiscard]] std::vector<std::string> make_highlighted_details(
21 const SourcePosition &position,
22 const TextFormatter &format,
23 const std::filesystem::path &file_path,
24 const std::string &message)
25{
26 const FileHighlightPrinter printer{&format};
27
28 // Build error details
29 std::vector<std::string> details;
30 details.push_back(std::format("{}:{}:{}: {}", file_path.string(), position.line, position.column, message));
31 details.emplace_back();
32
33 // Add source context if position is valid
34 if (position.line > 0) {
35 auto context = printer.print(file_path, position.line - 1, position.column - 1);
36 details.insert(details.end(), context.begin(), context.end());
37 }
38
39 return details;
40}
41
54[[nodiscard]] ChainableResult<DynamicCasedName> extract_anim_name_from_array_ref(
55 const std::string &identifier, const std::string &tileset_shorthand, bool porytiles_managed)
56{
57 // Build expected prefix: gTilesetAnims_[PorytilesManaged_]TilesetName_
58 std::string prefix = anim::g_tileset_anims_prefix;
59 if (porytiles_managed) {
61 }
62 prefix += tileset_shorthand + "_";
63
64 if (!identifier.starts_with(prefix)) {
65 if (!porytiles_managed) {
66 prefix = anim::s_tileset_anims_prefix + tileset_shorthand + "_";
67 }
68 if (!identifier.starts_with(prefix)) {
69 return FormattableError{
70 "Could not extract animation name from '{}'.", FormatParam{identifier, Style::bold}};
71 }
72 }
73
74 std::string remainder = identifier.substr(prefix.size());
75
76 // Remove _Frame suffix if present (for individual frame arrays)
77 if (auto frame_pos = remainder.find("_Frame"); frame_pos != std::string::npos) {
78 return DynamicCasedName::from_c_identifier(remainder.substr(0, frame_pos));
79 }
80
81 // Remove _VDests suffix if present
82 if (auto vdests_pos = remainder.find("_VDests"); vdests_pos != std::string::npos) {
83 return DynamicCasedName::from_c_identifier(remainder.substr(0, vdests_pos));
84 }
85
87}
88
100[[nodiscard]] std::vector<DynamicCasedName> extract_frame_names(const std::vector<std::string> &elements)
101{
102 std::vector<DynamicCasedName> frames;
103 frames.reserve(elements.size());
104
105 for (const auto &elem : elements) {
106 // Find "_Frame" suffix and extract the frame name
107 auto frame_pos = elem.find("_Frame");
108 if (frame_pos != std::string::npos) {
109 std::string frame_str = elem.substr(frame_pos + 6); // Skip "_Frame"
110 frames.push_back(DynamicCasedName::from_pascal_case(frame_str));
111 }
112 }
113
114 return frames;
115}
116
129extract_tile_offset(const std::vector<Token> &tokens, const TextFormatter &format)
130{
131 for (std::size_t i = 0; i + 3 < tokens.size(); ++i) {
132 // Primary pattern: TILE_OFFSET_4BPP(<integer>)
133 if (tokens[i].is(TokenType::identifier) && tokens[i].text() == "TILE_OFFSET_4BPP" &&
134 tokens[i + 1].is(TokenType::left_paren) && tokens[i + 2].is(TokenType::integer_literal) &&
135 tokens[i + 3].is(TokenType::right_paren)) {
136 return tokens[i + 2].int_value();
137 }
138
139 // Secondary pattern: TILE_OFFSET_4BPP(NUM_TILES_IN_PRIMARY + <integer>)
140 if (i + 5 < tokens.size() && tokens[i].is(TokenType::identifier) && tokens[i].text() == "TILE_OFFSET_4BPP" &&
141 tokens[i + 1].is(TokenType::left_paren) && tokens[i + 2].is(TokenType::identifier) &&
142 tokens[i + 2].text() == "NUM_TILES_IN_PRIMARY" && tokens[i + 3].is(TokenType::plus) &&
143 tokens[i + 4].is(TokenType::integer_literal) && tokens[i + 5].is(TokenType::right_paren)) {
144 return tokens[i + 4].int_value();
145 }
146 }
147
148 std::string actual;
149 for (std::size_t i = 0; i < tokens.size(); ++i) {
150 if (i > 0) {
151 actual += " ";
152 }
153 actual += tokens[i].text();
154 }
155
156 return FormattableError{std::vector<std::string>{
157 format.format(
158 "Expected token pattern containing '{}' or '{}'.",
159 FormatParam{"TILE_OFFSET_4BPP(<integer>)", Style::bold},
160 FormatParam{"TILE_OFFSET_4BPP(NUM_TILES_IN_PRIMARY + <integer>)", Style::bold}),
161 format.format("Actual tokens: '{}'.", FormatParam{actual, Style::bold}),
162 }};
163}
164
177extract_tile_count(const std::vector<Token> &tokens, const TextFormatter &format)
178{
179 for (std::size_t i = 0; i + 2 < tokens.size(); ++i) {
180 if (tokens[i].is(TokenType::integer_literal) && tokens[i + 1].is(TokenType::star) &&
181 tokens[i + 2].is(TokenType::identifier) && tokens[i + 2].text() == "TILE_SIZE_4BPP") {
182 return tokens[i].int_value();
183 }
184 }
185
186 std::string actual;
187 for (std::size_t i = 0; i < tokens.size(); ++i) {
188 if (i > 0) {
189 actual += " ";
190 }
191 actual += tokens[i].text();
192 }
193
194 return FormattableError{std::vector<std::string>{
195 format.format(
196 "Expected token pattern containing '{}'.",
197 FormatParam{"<tile_count_integer> * TILE_SIZE_4BPP", Style::bold}),
198 format.format("Actual tokens: '{}'.", FormatParam{actual, Style::bold}),
199 }};
200}
201
212[[nodiscard]] ChainableResult<std::string> find_driver_function_from_callback(const std::vector<Token> &body_tokens)
213{
214 for (std::size_t i = 0; i + 2 < body_tokens.size(); ++i) {
215 if (body_tokens[i].is(TokenType::identifier) &&
216 (body_tokens[i].text() == "sPrimaryTilesetAnimCallback" ||
217 body_tokens[i].text() == "sSecondaryTilesetAnimCallback") &&
218 body_tokens[i + 1].is(TokenType::equal)) {
219 // Find the identifier after the equals sign
220 for (std::size_t j = i + 2; j < body_tokens.size(); ++j) {
221 if (body_tokens[j].is(TokenType::identifier)) {
222 return body_tokens[j].text();
223 }
224 // Stop if we hit a semicolon
225 if (body_tokens[j].is(TokenType::semicolon)) {
226 break;
227 }
228 }
229 }
230 }
231 return FormattableError{"Could not find tileset anim callback assignment in function body."};
232}
233
237struct TimerCondition {
238 std::size_t frame_factor; // The X in timer % X
239 std::size_t frame_offset; // The Y in timer % X == Y
240 std::string called_func; // The function called inside the condition block
241};
242
253[[nodiscard]] std::vector<TimerCondition> extract_timer_conditions(const std::vector<Token> &body_tokens)
254{
255 std::vector<TimerCondition> result;
256
257 // Look for: timer % X == Y patterns followed by function calls
258 for (std::size_t i = 0; i + 6 < body_tokens.size(); ++i) {
259 // Check for: timer % X == Y
260 if (body_tokens[i].is(TokenType::identifier) && body_tokens[i].text() == "timer" &&
261 body_tokens[i + 1].is(TokenType::percent) && body_tokens[i + 2].is(TokenType::integer_literal) &&
262 body_tokens[i + 3].is(TokenType::equal_equal) && body_tokens[i + 4].is(TokenType::integer_literal)) {
263
264 std::size_t frame_factor = body_tokens[i + 2].int_value();
265 std::size_t frame_offset = body_tokens[i + 4].int_value();
266
267 // Search ahead for function call (identifier followed by lparen)
268 for (std::size_t j = i + 5; j < body_tokens.size() && j < i + 50; ++j) {
269 if (body_tokens[j].is(TokenType::identifier) && j + 1 < body_tokens.size() &&
270 body_tokens[j + 1].is(TokenType::left_paren)) {
271 // Found a function call
272 result.push_back({frame_factor, frame_offset, body_tokens[j].text()});
273 break;
274 }
275
276 // Stop if we hit another 'if' - we've gone past the relevant block
277 if (body_tokens[j].is(TokenType::kw_if)) {
278 break;
279 }
280 }
281 }
282 }
283
284 return result;
285}
286
290struct DiscoveredAnimData {
291 // std::string anim_name_pascal; // PascalCase animation name
292 std::size_t tile_offset{};
293 std::size_t tile_count{};
294 std::size_t frame_factor{};
295 std::size_t frame_offset{};
296};
297
308[[nodiscard]] ChainableResult<std::string> extract_array_name_from_first_arg(const std::vector<Token> &arg_tokens)
309{
310 // Look for an identifier followed by left_bracket
311 for (std::size_t i = 0; i + 1 < arg_tokens.size(); ++i) {
312 if (arg_tokens[i].is(TokenType::identifier) && arg_tokens[i + 1].is(TokenType::left_bracket)) {
313 return arg_tokens[i].text();
314 }
315 }
316
317 // If no bracket found, just return the first identifier
318 for (const auto &tok : arg_tokens) {
319 if (tok.is(TokenType::identifier)) {
320 return tok.text();
321 }
322 }
323
324 return FormattableError{"No identifier found in first argument of AppendTilesetAnimToBuffer call."};
325}
326
337struct ParsedFunctions {
338 std::vector<FunctionDefinition> definitions;
339 std::map<std::string, const FunctionDefinition *> by_name;
340};
341
356[[nodiscard]] ChainableResult<std::string> step_1_find_driver_function(
357 CParserFacade &c_parser,
358 const std::string &callback_func_name,
359 const std::filesystem::path &c_file_path,
360 const TextFormatter *format)
361{
362 auto callback_funcs_result = c_parser.parse_functions(callback_func_name);
363 if (!callback_funcs_result.has_value()) {
365 FormattableError{format->format(
366 "'{}': Failed to parse callback function.", FormatParam{c_file_path.string(), Style::bold})},
367 callback_funcs_result};
368 }
369
370 auto &callback_funcs = callback_funcs_result.value();
371 // Narrow from prefix match to exact name match (parse_functions uses starts_with)
372 std::erase_if(callback_funcs, [&](const FunctionDefinition &func) { return func.name() != callback_func_name; });
373 if (callback_funcs.empty()) {
374 return std::string{};
375 }
376
377 if (callback_funcs.size() > 1) {
378 return FormattableError{
379 "Found multiple callback functions matching '{}'.", FormatParam{callback_func_name, Style::bold}};
380 }
381
382 const auto &callback_func = callback_funcs.front();
383 auto driver_func_name_result = find_driver_function_from_callback(callback_func.body_tokens());
384 if (!driver_func_name_result.has_value()) {
387 "Could not find driver function assignment in '{}'.", FormatParam{callback_func_name, Style::bold}},
388 driver_func_name_result};
389 }
390
391 return std::move(driver_func_name_result).value();
392}
393
407[[nodiscard]] ChainableResult<std::vector<TimerCondition>> step_2_extract_timer_conditions(
408 CParserFacade &c_parser,
409 const std::string &driver_func_name,
410 const std::filesystem::path &c_file_path,
411 const TextFormatter *format)
412{
413 auto driver_funcs_result = c_parser.parse_functions(driver_func_name);
414 if (!driver_funcs_result.has_value()) {
416 FormattableError{format->format(
417 "'{}': Failed to parse driver function '{}'.",
418 FormatParam{c_file_path.string(), Style::bold},
419 FormatParam{driver_func_name, Style::bold})},
420 driver_funcs_result};
421 }
422
423 auto &driver_funcs = driver_funcs_result.value();
424 // Narrow from prefix match to exact name match (parse_functions uses starts_with)
425 std::erase_if(driver_funcs, [&](const FunctionDefinition &func) { return func.name() != driver_func_name; });
426 if (driver_funcs.empty()) {
427 return FormattableError{"Driver function '{}' not found in file.", FormatParam{driver_func_name, Style::bold}};
428 }
429
430 const auto &driver_func = driver_funcs.front();
431 std::vector<TimerCondition> timer_conditions = extract_timer_conditions(driver_func.body_tokens());
432
433 if (timer_conditions.empty()) {
434 return FormattableError{
435 "No timer conditions found in driver function '{}'.", FormatParam{driver_func_name, Style::bold}};
436 }
437
438 return timer_conditions;
439}
440
453[[nodiscard]] ChainableResult<ParsedFunctions> step_3_build_function_map(
454 CParserFacade &c_parser, const std::filesystem::path &c_file_path, const TextFormatter *format)
455{
456 auto all_funcs_result = c_parser.parse_functions();
457 if (!all_funcs_result.has_value()) {
460 format->format("'{}': Failed to parse functions.", FormatParam{c_file_path.string(), Style::bold})},
461 all_funcs_result};
462 }
463
464 ParsedFunctions parsed;
465 parsed.definitions = std::move(all_funcs_result).value();
466 for (const auto &func : parsed.definitions) {
467 parsed.by_name[func.name()] = &func;
468 }
469
470 return parsed;
471}
472
487[[nodiscard]] ChainableResult<std::map<DynamicCasedName, DiscoveredAnimData>> step_4_extract_animation_data(
488 const std::vector<TimerCondition> &timer_conditions,
489 const std::map<std::string, const FunctionDefinition *> &func_map,
490 const std::string &pascal_case_tileset,
491 bool porytiles_managed,
492 const TextFormatter *format)
493{
494 std::map<DynamicCasedName, DiscoveredAnimData> discovered_anims;
495
496 for (const auto &condition : timer_conditions) {
497 auto it = func_map.find(condition.called_func);
498 if (it == func_map.end()) {
499 return FormattableError{
500 "Queue function '{}' not found in file.", FormatParam{condition.called_func, Style::bold}};
501 }
502
503 const FunctionDefinition *queue_func = it->second;
504
505 // Find AppendTilesetAnimToBuffer calls in the queue function
506 auto append_calls = find_function_calls(queue_func->body_tokens(), "AppendTilesetAnimToBuffer");
507
508 if (append_calls.empty()) {
509 return FormattableError{
510 "No AppendTilesetAnimToBuffer calls in queue function '{}'.",
511 FormatParam{condition.called_func, Style::bold}};
512 }
513
514 // Process the first AppendTilesetAnimToBuffer call
515 // Note: For VDests patterns, there may be multiple calls - we defer full handling per design decision
516 const auto &call = append_calls.front();
517
518 if (call.argument_count() < 3) {
519 return FormattableError{
520 "AppendTilesetAnimToBuffer call in '{}' has fewer than 3 arguments.",
521 FormatParam{condition.called_func, Style::bold}};
522 }
523
524 // Extract animation name from first argument (e.g., gTilesetAnims_General_Flower[i])
525 auto array_name_result = extract_array_name_from_first_arg(call.argument_at(0));
526 if (!array_name_result.has_value()) {
529 "Failed to parse animation data from queue function '{}'.",
530 FormatParam{condition.called_func, Style::bold}},
531 array_name_result};
532 }
533
534 auto anim_cased_name =
535 extract_anim_name_from_array_ref(array_name_result.value(), pascal_case_tileset, porytiles_managed);
536 if (!anim_cased_name.has_value()) {
539 "Failed to parse animation data from queue function '{}'.",
540 FormatParam{condition.called_func, Style::bold}},
541 anim_cased_name};
542 }
543
544 // Extract tile_offset from second argument
545 auto tile_offset = extract_tile_offset(call.argument_at(1), *format);
546 if (!tile_offset.has_value()) {
548 FormattableError{std::vector{
549 format->format(
550 "Failed to extract '{}' from second argument of '{}' call in '{}'.",
551 FormatParam{"TILE_OFFSET_4BPP", Style::bold},
552 FormatParam{"AppendTilesetAnimToBuffer", Style::bold},
553 FormatParam{condition.called_func, Style::bold}),
554 format->format("Full call: '{}'.", FormatParam{call.reconstruct_call_text(), Style::bold}),
555 }},
556 tile_offset};
557 }
558
559 // Extract tile_count from third argument
560 auto tile_count = extract_tile_count(call.argument_at(2), *format);
561 if (!tile_count.has_value()) {
563 FormattableError{std::vector{
564 format->format(
565 "Failed to extract '{}' from third argument of '{}' call in '{}'.",
566 FormatParam{"TILE_SIZE_4BPP", Style::bold},
567 FormatParam{"AppendTilesetAnimToBuffer", Style::bold},
568 FormatParam{condition.called_func, Style::bold}),
569 format->format("Full call: '{}'.", FormatParam{call.reconstruct_call_text(), Style::bold}),
570 }},
571 tile_count};
572 }
573
574 if (append_calls.size() > 1) {
575 return FormattableError{
576 "Queue function '{}' has multiple AppendTilesetAnimToBuffer calls (VDests pattern not yet supported).",
577 FormatParam{condition.called_func, Style::bold}};
578 }
579
580 // Store discovered animation data
581 discovered_anims[std::move(anim_cased_name).value()] = {
582 tile_offset.value(), tile_count.value(), condition.frame_factor, condition.frame_offset};
583 }
584
585 return discovered_anims;
586}
587
602[[nodiscard]] ChainableResult<std::vector<ArrayDeclaration>> step_5_parse_frame_arrays(
603 CParserFacade &c_parser,
604 const std::string &pascal_case_tileset,
605 bool porytiles_managed,
606 const std::filesystem::path &c_file_path,
607 const TextFormatter *format)
608{
609 const auto frame_array_prefix = anim::g_tileset_anims_prefix +
610 (porytiles_managed ? anim::porytiles_managed_prefix : std::string{}) +
611 pascal_case_tileset;
612
613 auto anim_frame_arrays_result = c_parser.parse_pointer_arrays(frame_array_prefix);
614 if (!anim_frame_arrays_result.has_value()) {
616 FormattableError{format->format(
617 "{}: Failed to parse animation frame arrays.", FormatParam{c_file_path.string(), Style::bold})},
618 anim_frame_arrays_result};
619 }
620
621 // Also search with sTilesetAnims_ prefix
622 if (!porytiles_managed) {
623 const auto s_frame_array_prefix = anim::s_tileset_anims_prefix + pascal_case_tileset;
624 auto s_anim_frame_arrays_result = c_parser.parse_pointer_arrays(s_frame_array_prefix);
625 if (s_anim_frame_arrays_result.has_value()) {
626 auto &merged = anim_frame_arrays_result.value();
627 auto s_arrays = std::move(s_anim_frame_arrays_result).value();
628 merged.insert(
629 merged.end(), std::make_move_iterator(s_arrays.begin()), std::make_move_iterator(s_arrays.end()));
630 }
631 }
632
633 return anim_frame_arrays_result;
634}
635
649[[nodiscard]] ChainableResult<std::map<DynamicCasedName, AnimParams>> step_6_build_animation_params(
650 const std::map<DynamicCasedName, DiscoveredAnimData> &discovered_anims,
651 const std::vector<ArrayDeclaration> &frame_arrays,
652 const std::string &pascal_case_tileset,
653 bool porytiles_managed)
654{
655 std::map<DynamicCasedName, AnimParams> result;
656
657 for (const auto &[cased_name, anim_data] : discovered_anims) {
658 AnimParams params;
659 params.tile_offset(anim_data.tile_offset);
660 params.tile_count(anim_data.tile_count);
661 params.frame_factor(anim_data.frame_factor);
662 params.frame_offset(anim_data.frame_offset);
663
664 // Find matching frame array
665 bool found_frames = false;
666 for (const auto &arr : frame_arrays) {
667 // Skip individual frame arrays (gTilesetAnims_..._Frame0), we want the main pointer array
668 if (arr.name().find("_Frame") != std::string::npos) {
669 continue;
670 }
671
672 auto arr_anim_name = extract_anim_name_from_array_ref(arr.name(), pascal_case_tileset, porytiles_managed);
673 if (!arr_anim_name.has_value()) {
674 continue;
675 }
676
677 // DynamicCasedName canonical equality handles inconsistencies like TVTurnedOn vs TvTurnedOn
678 if (arr_anim_name.value() == cased_name) {
679 auto frame_order = extract_frame_names(arr.elements());
680 if (!frame_order.empty()) {
681 // Derive unique frame_names from frame_order (preserving first occurrence order)
682 std::vector<DynamicCasedName> frame_names;
683 std::set<DynamicCasedName> seen;
684 for (const auto &frame : frame_order) {
685 if (!seen.contains(frame)) {
686 seen.insert(frame);
687 frame_names.push_back(frame);
688 }
689 }
690 params.frame_names(std::move(frame_names));
691 params.frame_order(std::move(frame_order));
692 found_frames = true;
693 }
694 break;
695 }
696 }
697
698 if (!found_frames) {
699 return FormattableError{
700 "Could not find frame array for animation '{}'.", FormatParam{cased_name.to_snake_case(), Style::bold}};
701 }
702
703 params.cased_name(cased_name);
704
705 result[cased_name] = std::move(params);
706 }
707
708 return result;
709}
710
711} // namespace
712
713namespace porytiles {
714
716 const std::filesystem::path &c_file_path,
717 const std::string &callback_func_name,
718 const DynamicCasedName &tileset_cased_name,
719 bool porytiles_managed) const
720{
721 CParserFacade c_parser{c_file_path, format_};
722
723 const std::string pascal_case_tileset = tileset_cased_name.to_pascal_case();
724
725 using ResultType = std::map<DynamicCasedName, AnimParams>;
726
727 // Step 1: Parse callback function -> find driver function name
729 driver_func_name, step_1_find_driver_function(c_parser, callback_func_name, c_file_path, format_), ResultType);
730 if (driver_func_name.empty()) {
731 return ResultType{};
732 }
733
734 // Step 2: Parse driver function -> extract timer conditions
736 timer_conditions,
737 step_2_extract_timer_conditions(c_parser, driver_func_name, c_file_path, format_),
738 ResultType);
739
740 // Step 3: Parse all functions -> build lookup map
741 PT_TRY_ASSIGN_PASS_ERR(parsed_funcs, step_3_build_function_map(c_parser, c_file_path, format_), ResultType);
742
743 // Step 4: Extract animation data from queue functions
745 discovered_anims,
746 step_4_extract_animation_data(
747 timer_conditions, parsed_funcs.by_name, pascal_case_tileset, porytiles_managed, format_),
748 ResultType);
749
750 // Step 5: Parse frame pointer arrays
752 frame_arrays,
753 step_5_parse_frame_arrays(c_parser, pascal_case_tileset, porytiles_managed, c_file_path, format_),
754 ResultType);
755
756 // Step 6: Build final AnimParams map
757 return step_6_build_animation_params(discovered_anims, frame_arrays, pascal_case_tileset, porytiles_managed);
758}
759
760} // namespace porytiles
#define PT_TRY_ASSIGN_PASS_ERR(var, expr, return_type)
Unwraps a ChainableResult, passing through the error chain with an empty FormattableError when types ...
ChainableResult< std::map< DynamicCasedName, AnimParams > > parse_from_callback(const std::filesystem::path &c_file_path, const std::string &callback_func_name, const DynamicCasedName &tileset_cased_name, bool porytiles_managed) const
Parses animation parameters by following the callback chain.
Configuration parameters for a single tileset animation.
const DynamicCasedName & cased_name() const
Returns the structured name for this animation, preserving case format information.
const std::vector< DynamicCasedName > & frame_order() const
Returns the playback sequence.
std::size_t frame_offset() const
Returns the frame offset (remainder value for timer modulo check).
std::size_t tile_offset() const
Returns the VRAM tile offset for this animation.
const std::vector< DynamicCasedName > & frame_names() const
Returns the unique frame definitions.
std::size_t frame_factor() const
Returns the frame factor (modulus divisor for timer).
std::size_t tile_count() const
Returns the number of tiles per animation frame.
High-level facade for parsing C/C++ source files.
ChainableResult< std::vector< FunctionDefinition > > parse_functions(const std::optional< std::string > &name_prefix=std::nullopt)
Parses function definitions from the file.
ChainableResult< std::vector< ArrayDeclaration > > parse_pointer_arrays(const std::optional< std::string > &name_prefix=std::nullopt)
Parses all pointer array declarations from the file.
A result type that maintains a chainable sequence of errors for debugging and error reporting.
A smart string wrapper that preserves word structure for lossless case format conversion.
static DynamicCasedName from_c_identifier(const std::string &input)
Constructs from a C identifier format string (PascalCase segments joined by underscores).
std::string to_pascal_case() const
Outputs all words flattened and joined in PascalCase (each word capitalized, no separators).
static DynamicCasedName from_pascal_case(const std::string &input)
Constructs from a PascalCase input string.
A service for printing file lines with highlighted lines and line numbers.
A text parameter with associated styling for formatted output.
General-purpose error implementation with formatted message support.
Definition error.hpp:63
Represents a parsed C function definition.
const std::string & name() const
Returns the function name.
const std::vector< Token > & body_tokens() const
Returns the function body tokens.
static const Style bold
Bold text formatting.
Abstract base class for applying text styling with context-aware 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.
constexpr std::string porytiles_managed_prefix
Definition animation.hpp:22
constexpr std::string g_tileset_anims_prefix
Definition animation.hpp:20
constexpr std::string s_tileset_anims_prefix
Definition animation.hpp:21
std::vector< FunctionCallInfo > find_function_calls(const std::vector< Token > &tokens, const std::string &target_function_name)
Finds all calls to a specific function within a token stream.
Represents a position within source content.
std::size_t line
1-based line number
std::size_t column
1-based column number