22[[nodiscard]] std::vector<std::string> make_highlighted_details(
25 const std::filesystem::path &file_path,
26 const std::string &message)
31 std::vector<std::string> details;
32 details.push_back(std::format(
"{}:{}:{}: {}", file_path.string(), position.
line, position.
column, message));
33 details.emplace_back();
36 if (position.
line > 0) {
37 auto context = printer.print(file_path, position.
line - 1, position.
column - 1);
38 details.insert(details.end(), context.begin(), context.end());
45constexpr const char *anim_code_parse_tag =
"anim-code-parse";
54struct AnimArrayPrefixCandidates {
55 std::vector<std::string> strict;
56 std::vector<std::string> fallback;
72[[nodiscard]] AnimArrayPrefixCandidates
73build_anim_array_prefix_candidates(
const DynamicCasedName &tileset_cased_name,
bool porytiles_managed)
75 AnimArrayPrefixCandidates candidates;
77 if (porytiles_managed) {
78 candidates.strict.push_back(
88 if (pascal != c_identifier) {
93 std::string shortened = c_identifier;
94 for (
auto underscore_pos = shortened.rfind(
'_'); underscore_pos != std::string::npos;
95 underscore_pos = shortened.rfind(
'_')) {
96 shortened = shortened.substr(0, underscore_pos);
105struct ResolvedAnimArrayRef {
107 std::string matched_prefix;
108 bool used_shorthand_fallback{};
127 bool porytiles_managed,
130 const auto candidates = build_anim_array_prefix_candidates(tileset_cased_name, porytiles_managed);
132 auto try_prefixes = [&](
const std::vector<std::string> &prefixes,
133 bool is_fallback) -> std::optional<ResolvedAnimArrayRef> {
134 for (
const auto &prefix : prefixes) {
139 std::string remainder =
identifier.substr(prefix.size());
142 if (
auto frame_pos = remainder.find(
"_Frame"); frame_pos != std::string::npos) {
143 remainder = remainder.substr(0, frame_pos);
145 else if (
auto vdests_pos = remainder.find(
"_VDests"); vdests_pos != std::string::npos) {
146 remainder = remainder.substr(0, vdests_pos);
149 if (remainder.empty()) {
158 if (
auto resolved = try_prefixes(candidates.strict,
false); resolved.has_value()) {
159 return std::move(resolved).value();
161 if (
auto resolved = try_prefixes(candidates.fallback,
true); resolved.has_value()) {
162 return std::move(resolved).value();
165 auto quote_join = [&format](
const std::vector<std::string> &prefixes) {
167 for (
const auto &prefix : prefixes) {
168 if (!joined.empty()) {
176 std::vector<std::string> lines;
177 lines.push_back(format.
format(
"Could not extract animation name from '{}'.",
FormatParam{identifier, Style::bold}));
179 format.
format(
"Expected the array name to start with one of: {}.",
FormatParam{quote_join(candidates.strict)}));
180 if (!candidates.fallback.empty()) {
181 lines.push_back(format.
format(
182 "Also tried shortened tileset shorthand prefixes: {}.",
FormatParam{quote_join(candidates.fallback)}));
184 lines.push_back(format.
format(
185 "This usually means the animation arrays are named with a tileset shorthand that does not match '{}'.",
186 FormatParam{tileset_cased_name.to_c_identifier(), Style::bold}));
199[[nodiscard]] std::vector<DynamicCasedName> extract_frame_names(
const std::vector<std::string> &elements)
201 std::vector<DynamicCasedName> frames;
202 frames.reserve(elements.size());
204 for (
const auto &elem : elements) {
206 auto frame_pos = elem.find(
"_Frame");
207 if (frame_pos != std::string::npos) {
208 std::string frame_str = elem.substr(frame_pos + 6);
226extract_tile_offset(
const std::vector<Token> &tokens,
const TextFormatter &format)
228 for (std::size_t i = 0; i + 3 < tokens.size(); ++i) {
230 if (tokens[i].is(TokenType::identifier) && tokens[i].text() ==
"TILE_OFFSET_4BPP" &&
231 tokens[i + 1].is(TokenType::left_paren) && tokens[i + 2].is(TokenType::integer_literal) &&
232 tokens[i + 3].is(TokenType::right_paren)) {
233 return tokens[i + 2].int_value();
237 if (i + 5 < tokens.size() && tokens[i].is(TokenType::identifier) && tokens[i].text() ==
"TILE_OFFSET_4BPP" &&
238 tokens[i + 1].is(TokenType::left_paren) && tokens[i + 2].is(TokenType::identifier) &&
239 tokens[i + 2].text() ==
"NUM_TILES_IN_PRIMARY" && tokens[i + 3].is(TokenType::plus) &&
240 tokens[i + 4].is(TokenType::integer_literal) && tokens[i + 5].is(TokenType::right_paren)) {
241 return tokens[i + 4].int_value();
246 for (std::size_t i = 0; i < tokens.size(); ++i) {
250 actual += tokens[i].text();
255 "Expected token pattern containing '{}' or '{}'.",
272extract_tile_count(
const std::vector<Token> &tokens,
const TextFormatter &format)
274 for (std::size_t i = 0; i + 2 < tokens.size(); ++i) {
275 if (tokens[i].is(TokenType::integer_literal) && tokens[i + 1].is(TokenType::star) &&
276 tokens[i + 2].is(TokenType::identifier) && tokens[i + 2].text() ==
"TILE_SIZE_4BPP") {
277 return tokens[i].int_value();
282 for (std::size_t i = 0; i < tokens.size(); ++i) {
286 actual += tokens[i].text();
291 "Expected token pattern containing '{}'.",
307 for (std::size_t i = 0; i + 2 < body_tokens.size(); ++i) {
308 if (body_tokens[i].is(TokenType::identifier) &&
309 (body_tokens[i].text() ==
"sPrimaryTilesetAnimCallback" ||
310 body_tokens[i].text() ==
"sSecondaryTilesetAnimCallback") &&
311 body_tokens[i + 1].is(TokenType::equal)) {
313 for (std::size_t j = i + 2; j < body_tokens.size(); ++j) {
314 if (body_tokens[j].is(TokenType::identifier)) {
315 return body_tokens[j].text();
318 if (body_tokens[j].is(TokenType::semicolon)) {
324 return FormattableError{
"Could not find tileset anim callback assignment in function body."};
328struct TimerCondition {
329 std::size_t frame_factor;
330 std::size_t frame_offset;
331 std::string called_func;
342[[nodiscard]] std::vector<TimerCondition> extract_timer_conditions(
const std::vector<Token> &body_tokens)
344 std::vector<TimerCondition> result;
347 for (std::size_t i = 0; i + 6 < body_tokens.size(); ++i) {
349 if (body_tokens[i].is(TokenType::identifier) && body_tokens[i].text() ==
"timer" &&
350 body_tokens[i + 1].is(TokenType::percent) && body_tokens[i + 2].is(TokenType::integer_literal) &&
351 body_tokens[i + 3].is(TokenType::equal_equal) && body_tokens[i + 4].is(TokenType::integer_literal)) {
353 std::size_t frame_factor = body_tokens[i + 2].int_value();
354 std::size_t frame_offset = body_tokens[i + 4].int_value();
357 for (std::size_t j = i + 5; j < body_tokens.size() && j < i + 50; ++j) {
358 if (body_tokens[j].is(TokenType::identifier) && j + 1 < body_tokens.size() &&
359 body_tokens[j + 1].is(TokenType::left_paren)) {
361 result.push_back({frame_factor, frame_offset, body_tokens[j].text()});
366 if (body_tokens[j].is(TokenType::kw_if)) {
377struct DiscoveredAnimData {
378 std::string array_identifier;
379 std::size_t tile_offset{};
380 std::size_t tile_count{};
381 std::size_t frame_factor{};
382 std::size_t frame_offset{};
396 for (std::size_t i = 0; i + 1 < arg_tokens.size(); ++i) {
397 if (arg_tokens[i].is(TokenType::identifier) && arg_tokens[i + 1].is(TokenType::left_bracket)) {
398 return arg_tokens[i].text();
403 for (
const auto &tok : arg_tokens) {
404 if (tok.is(TokenType::identifier)) {
409 return FormattableError{
"No identifier found in first argument of AppendTilesetAnimToBuffer call."};
421struct ParsedFunctions {
422 std::vector<FunctionDefinition> definitions;
423 std::map<std::string, const FunctionDefinition *> by_name;
440 const std::string &callback_func_name,
441 const std::filesystem::path &c_file_path,
444 auto callback_funcs_result = c_parser.
parse_functions(callback_func_name);
445 if (!callback_funcs_result.has_value()) {
449 callback_funcs_result};
452 auto &callback_funcs = callback_funcs_result.value();
454 std::erase_if(callback_funcs, [&](
const FunctionDefinition &func) {
return func.
name() != callback_func_name; });
455 if (callback_funcs.empty()) {
456 return std::string{};
459 if (callback_funcs.size() > 1) {
464 const auto &callback_func = callback_funcs.front();
465 auto driver_func_name_result = find_driver_function_from_callback(callback_func.body_tokens());
466 if (!driver_func_name_result.has_value()) {
470 driver_func_name_result};
473 return std::move(driver_func_name_result).value();
489 const std::string &driver_func_name,
490 const std::filesystem::path &c_file_path,
494 if (!driver_funcs_result.has_value()) {
497 "'{}': Failed to parse driver function '{}'.",
500 driver_funcs_result};
503 auto &driver_funcs = driver_funcs_result.value();
505 std::erase_if(driver_funcs, [&](
const FunctionDefinition &func) {
return func.
name() != driver_func_name; });
506 if (driver_funcs.empty()) {
510 const auto &driver_func = driver_funcs.front();
511 std::vector<TimerCondition> timer_conditions = extract_timer_conditions(driver_func.body_tokens());
513 if (timer_conditions.empty()) {
518 return timer_conditions;
535 if (!all_funcs_result.has_value()) {
542 ParsedFunctions parsed;
543 parsed.definitions = std::move(all_funcs_result).value();
544 for (
const auto &func : parsed.definitions) {
545 parsed.by_name[func.
name()] = &func;
565 const std::vector<TimerCondition> &timer_conditions,
566 const std::map<std::string, const FunctionDefinition *> &func_map,
568 bool porytiles_managed,
572 std::map<DynamicCasedName, DiscoveredAnimData> discovered_anims;
574 for (
const auto &condition : timer_conditions) {
575 auto it = func_map.find(condition.called_func);
576 if (it == func_map.end()) {
586 if (append_calls.empty()) {
588 "No AppendTilesetAnimToBuffer calls in queue function '{}'.",
594 const auto &call = append_calls.front();
596 if (call.argument_count() < 3) {
598 "AppendTilesetAnimToBuffer call in '{}' has fewer than 3 arguments.",
603 auto array_name_result = extract_array_name_from_first_arg(call.argument_at(0));
604 if (!array_name_result.has_value()) {
607 "Failed to parse animation data from queue function '{}'.",
612 auto resolved_result =
613 resolve_anim_name_from_array_ref(array_name_result.value(), tileset_cased_name, porytiles_managed, *format);
614 if (!resolved_result.has_value()) {
617 "Failed to parse animation data from queue function '{}'.",
621 auto resolved = std::move(resolved_result).value();
623 if (resolved.used_shorthand_fallback) {
626 std::vector<std::string>{
628 "Animation array '{}' is not named with the tileset shorthand '{}'.",
632 "Resolved animation '{}' using the shortened shorthand prefix '{}'.",
633 FormatParam{resolved.anim_name.to_snake_case(), Style::bold},
634 FormatParam{resolved.matched_prefix, Style::bold}),
639 auto tile_offset = extract_tile_offset(call.argument_at(1), *format);
640 if (!tile_offset.has_value()) {
644 "Failed to extract '{}' from second argument of '{}' call in '{}'.",
648 format->
format(
"Full call: '{}'.",
FormatParam{call.reconstruct_call_text(), Style::bold}),
654 auto tile_count = extract_tile_count(call.argument_at(2), *format);
655 if (!tile_count.has_value()) {
659 "Failed to extract '{}' from third argument of '{}' call in '{}'.",
663 format->
format(
"Full call: '{}'.",
FormatParam{call.reconstruct_call_text(), Style::bold}),
668 if (append_calls.size() > 1) {
670 "Queue function '{}' has multiple AppendTilesetAnimToBuffer calls (VDests pattern not yet supported).",
675 discovered_anims[resolved.anim_name] = {
676 array_name_result.value(),
679 condition.frame_factor,
680 condition.frame_offset};
683 return discovered_anims;
700 if (!anim_frame_arrays_result.has_value()) {
704 anim_frame_arrays_result};
707 return anim_frame_arrays_result;
722 const std::map<DynamicCasedName, DiscoveredAnimData> &discovered_anims,
723 const std::vector<ArrayDeclaration> &frame_arrays,
726 std::map<DynamicCasedName, AnimParams> result;
728 for (
const auto &[cased_name, anim_data] : discovered_anims) {
736 const auto array_it = std::ranges::find_if(
737 frame_arrays, [&](
const ArrayDeclaration &arr) {
return arr.
name() == anim_data.array_identifier; });
738 if (array_it == frame_arrays.end()) {
741 "Could not find frame array '{}' for animation '{}'.",
744 "A queue function references this array, but the file contains no pointer array declaration with "
749 auto frame_order = extract_frame_names(array_it->elements());
750 if (frame_order.empty()) {
752 "Frame array '{}' for animation '{}' has no elements with a '{}' suffix.",
759 std::vector<DynamicCasedName> frame_names;
760 std::set<DynamicCasedName> seen;
761 for (
const auto &frame : frame_order) {
762 if (!seen.contains(frame)) {
764 frame_names.push_back(frame);
772 result[cased_name] = std::move(params);
783 const std::filesystem::path &c_file_path,
784 const std::string &callback_func_name,
786 bool porytiles_managed)
const
790 using ResultType = std::map<DynamicCasedName, AnimParams>;
794 driver_func_name, step_1_find_driver_function(c_parser, callback_func_name, c_file_path, format_), ResultType);
795 if (driver_func_name.empty()) {
802 step_2_extract_timer_conditions(c_parser, driver_func_name, c_file_path, format_),
806 PT_TRY_ASSIGN_PASS_ERR(parsed_funcs, step_3_build_function_map(c_parser, c_file_path, format_), ResultType);
811 step_4_extract_animation_data(
812 timer_conditions, parsed_funcs.by_name, tileset_cased_name, porytiles_managed, format_, diag_),
816 PT_TRY_ASSIGN_PASS_ERR(frame_arrays, step_5_parse_frame_arrays(c_parser, c_file_path, format_), ResultType);
819 return step_6_build_animation_params(discovered_anims, frame_arrays, format_);
#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 std::string & frame_array_identifier() const
Returns the C identifier of the frame pointer array backing this 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.
Represents a parsed C pointer array declaration.
const std::string & name() const
Returns the array variable name.
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).
std::string to_c_identifier() const
Outputs PascalCase within each segment, with segments joined by underscores.
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.
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.
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