20[[nodiscard]] std::vector<std::string> make_highlighted_details(
23 const std::filesystem::path &file_path,
24 const std::string &message)
29 std::vector<std::string> details;
30 details.push_back(std::format(
"{}:{}:{}: {}", file_path.string(), position.
line, position.
column, message));
31 details.emplace_back();
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());
55 const std::string &
identifier,
const std::string &tileset_shorthand,
bool porytiles_managed)
59 if (porytiles_managed) {
62 prefix += tileset_shorthand +
"_";
65 if (!porytiles_managed) {
74 std::string remainder =
identifier.substr(prefix.size());
77 if (
auto frame_pos = remainder.find(
"_Frame"); frame_pos != std::string::npos) {
82 if (
auto vdests_pos = remainder.find(
"_VDests"); vdests_pos != std::string::npos) {
100[[nodiscard]] std::vector<DynamicCasedName> extract_frame_names(
const std::vector<std::string> &elements)
102 std::vector<DynamicCasedName> frames;
103 frames.reserve(elements.size());
105 for (
const auto &elem : elements) {
107 auto frame_pos = elem.find(
"_Frame");
108 if (frame_pos != std::string::npos) {
109 std::string frame_str = elem.substr(frame_pos + 6);
129extract_tile_offset(
const std::vector<Token> &tokens,
const TextFormatter &format)
131 for (std::size_t i = 0; i + 3 < tokens.size(); ++i) {
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();
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();
149 for (std::size_t i = 0; i < tokens.size(); ++i) {
153 actual += tokens[i].text();
158 "Expected token pattern containing '{}' or '{}'.",
177extract_tile_count(
const std::vector<Token> &tokens,
const TextFormatter &format)
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();
187 for (std::size_t i = 0; i < tokens.size(); ++i) {
191 actual += tokens[i].text();
196 "Expected token pattern containing '{}'.",
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)) {
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();
225 if (body_tokens[j].is(TokenType::semicolon)) {
231 return FormattableError{
"Could not find tileset anim callback assignment in function body."};
237struct TimerCondition {
238 std::size_t frame_factor;
239 std::size_t frame_offset;
240 std::string called_func;
253[[nodiscard]] std::vector<TimerCondition> extract_timer_conditions(
const std::vector<Token> &body_tokens)
255 std::vector<TimerCondition> result;
258 for (std::size_t i = 0; i + 6 < body_tokens.size(); ++i) {
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)) {
264 std::size_t frame_factor = body_tokens[i + 2].int_value();
265 std::size_t frame_offset = body_tokens[i + 4].int_value();
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)) {
272 result.push_back({frame_factor, frame_offset, body_tokens[j].text()});
277 if (body_tokens[j].is(TokenType::kw_if)) {
290struct DiscoveredAnimData {
292 std::size_t tile_offset{};
293 std::size_t tile_count{};
294 std::size_t frame_factor{};
295 std::size_t frame_offset{};
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();
318 for (
const auto &tok : arg_tokens) {
319 if (tok.is(TokenType::identifier)) {
324 return FormattableError{
"No identifier found in first argument of AppendTilesetAnimToBuffer call."};
337struct ParsedFunctions {
338 std::vector<FunctionDefinition> definitions;
339 std::map<std::string, const FunctionDefinition *> by_name;
358 const std::string &callback_func_name,
359 const std::filesystem::path &c_file_path,
362 auto callback_funcs_result = c_parser.
parse_functions(callback_func_name);
363 if (!callback_funcs_result.has_value()) {
367 callback_funcs_result};
370 auto &callback_funcs = callback_funcs_result.value();
372 std::erase_if(callback_funcs, [&](
const FunctionDefinition &func) {
return func.
name() != callback_func_name; });
373 if (callback_funcs.empty()) {
374 return std::string{};
377 if (callback_funcs.size() > 1) {
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()) {
388 driver_func_name_result};
391 return std::move(driver_func_name_result).value();
409 const std::string &driver_func_name,
410 const std::filesystem::path &c_file_path,
414 if (!driver_funcs_result.has_value()) {
417 "'{}': Failed to parse driver function '{}'.",
420 driver_funcs_result};
423 auto &driver_funcs = driver_funcs_result.value();
425 std::erase_if(driver_funcs, [&](
const FunctionDefinition &func) {
return func.
name() != driver_func_name; });
426 if (driver_funcs.empty()) {
430 const auto &driver_func = driver_funcs.front();
431 std::vector<TimerCondition> timer_conditions = extract_timer_conditions(driver_func.body_tokens());
433 if (timer_conditions.empty()) {
438 return timer_conditions;
457 if (!all_funcs_result.has_value()) {
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;
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,
494 std::map<DynamicCasedName, DiscoveredAnimData> discovered_anims;
496 for (
const auto &condition : timer_conditions) {
497 auto it = func_map.find(condition.called_func);
498 if (it == func_map.end()) {
508 if (append_calls.empty()) {
510 "No AppendTilesetAnimToBuffer calls in queue function '{}'.",
516 const auto &call = append_calls.front();
518 if (call.argument_count() < 3) {
520 "AppendTilesetAnimToBuffer call in '{}' has fewer than 3 arguments.",
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 '{}'.",
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 '{}'.",
545 auto tile_offset = extract_tile_offset(call.argument_at(1), *format);
546 if (!tile_offset.has_value()) {
550 "Failed to extract '{}' from second argument of '{}' call in '{}'.",
554 format->
format(
"Full call: '{}'.",
FormatParam{call.reconstruct_call_text(), Style::bold}),
560 auto tile_count = extract_tile_count(call.argument_at(2), *format);
561 if (!tile_count.has_value()) {
565 "Failed to extract '{}' from third argument of '{}' call in '{}'.",
569 format->
format(
"Full call: '{}'.",
FormatParam{call.reconstruct_call_text(), Style::bold}),
574 if (append_calls.size() > 1) {
576 "Queue function '{}' has multiple AppendTilesetAnimToBuffer calls (VDests pattern not yet supported).",
581 discovered_anims[std::move(anim_cased_name).value()] = {
582 tile_offset.value(), tile_count.value(), condition.frame_factor, condition.frame_offset};
585 return discovered_anims;
604 const std::string &pascal_case_tileset,
605 bool porytiles_managed,
606 const std::filesystem::path &c_file_path,
614 if (!anim_frame_arrays_result.has_value()) {
618 anim_frame_arrays_result};
622 if (!porytiles_managed) {
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();
629 merged.end(), std::make_move_iterator(s_arrays.begin()), std::make_move_iterator(s_arrays.end()));
633 return anim_frame_arrays_result;
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)
655 std::map<DynamicCasedName, AnimParams> result;
657 for (
const auto &[cased_name, anim_data] : discovered_anims) {
665 bool found_frames =
false;
666 for (
const auto &arr : frame_arrays) {
668 if (arr.name().find(
"_Frame") != std::string::npos) {
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()) {
678 if (arr_anim_name.value() == cased_name) {
679 auto frame_order = extract_frame_names(arr.elements());
680 if (!frame_order.empty()) {
682 std::vector<DynamicCasedName> frame_names;
683 std::set<DynamicCasedName> seen;
684 for (
const auto &frame : frame_order) {
685 if (!seen.contains(frame)) {
687 frame_names.push_back(frame);
705 result[cased_name] = std::move(params);
716 const std::filesystem::path &c_file_path,
717 const std::string &callback_func_name,
719 bool porytiles_managed)
const
723 const std::string pascal_case_tileset = tileset_cased_name.
to_pascal_case();
725 using ResultType = std::map<DynamicCasedName, AnimParams>;
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()) {
737 step_2_extract_timer_conditions(c_parser, driver_func_name, c_file_path, format_),
741 PT_TRY_ASSIGN_PASS_ERR(parsed_funcs, step_3_build_function_map(c_parser, c_file_path, format_), ResultType);
746 step_4_extract_animation_data(
747 timer_conditions, parsed_funcs.by_name, pascal_case_tileset, porytiles_managed, format_),
753 step_5_parse_frame_arrays(c_parser, pascal_case_tileset, porytiles_managed, c_file_path, format_),
757 return step_6_build_animation_params(discovered_anims, frame_arrays, pascal_case_tileset, porytiles_managed);
#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.
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 > ¶ms) const
Formats a string with styled parameters using fmtlib syntax.
constexpr std::string porytiles_managed_prefix
constexpr std::string g_tileset_anims_prefix
constexpr std::string s_tileset_anims_prefix
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