yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
oracle_menu_registry.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cctype>
5#include <fstream>
6#include <map>
7#include <regex>
8#include <set>
9#include <sstream>
10#include <string>
11#include <unordered_map>
12#include <vector>
13
14#include "absl/status/status.h"
15#include "absl/strings/ascii.h"
16#include "absl/strings/str_format.h"
17#include "absl/strings/strip.h"
18#include "util/macro.h"
19
20namespace yaze::core {
21
22namespace {
23
24std::string NormalizePathString(const std::filesystem::path& path) {
25 return path.lexically_normal().generic_string();
26}
27
28std::string RelativePathString(const std::filesystem::path& base,
29 const std::filesystem::path& path) {
30 std::error_code ec;
31 auto rel = std::filesystem::relative(path, base, ec);
32 if (ec) {
33 return NormalizePathString(path);
34 }
35 return NormalizePathString(rel);
36}
37
38std::string Lowercase(std::string text) {
39 std::transform(text.begin(), text.end(), text.begin(),
40 [](unsigned char c) { return std::tolower(c); });
41 return text;
42}
43
44bool IsPathWithinRoot(const std::filesystem::path& root,
45 const std::filesystem::path& candidate) {
46 std::error_code ec;
47 const std::filesystem::path canonical_root =
48 std::filesystem::weakly_canonical(root, ec);
49 if (ec) {
50 return false;
51 }
52
53 const std::filesystem::path canonical_candidate =
54 std::filesystem::weakly_canonical(candidate, ec);
55 if (ec) {
56 return false;
57 }
58
59 const std::filesystem::path rel =
60 canonical_candidate.lexically_relative(canonical_root);
61 if (rel.empty() || rel.is_absolute()) {
62 return false;
63 }
64 for (const auto& part : rel) {
65 if (part == "..") {
66 return false;
67 }
68 }
69 return true;
70}
71
72bool ContainsDrawToken(const std::string& value) {
73 return Lowercase(value).find("draw") != std::string::npos;
74}
75
76bool ParseGlobalLabel(const std::string& line, std::string* label_out) {
77 static const std::regex kPattern(
78 R"(^\s*([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(?:;.*)?$)");
79 std::smatch match;
80 if (!std::regex_match(line, match, kPattern)) {
81 return false;
82 }
83 *label_out = match[1].str();
84 return true;
85}
86
87bool ParseLocalLabel(const std::string& line, std::string* label_out) {
88 static const std::regex kPattern(
89 R"(^\s*(\.[A-Za-z_][A-Za-z0-9_]*)\s*(?::\s*)?(?:;.*)?$)");
90 std::smatch match;
91 if (!std::regex_match(line, match, kPattern)) {
92 return false;
93 }
94 *label_out = match[1].str();
95 return true;
96}
97
98bool ParseIncbinLine(const std::string& line, std::string* inline_label_out,
99 std::string* bin_path_out) {
100 static const std::regex kPattern(
101 R"(^\s*(?:([A-Za-z_][A-Za-z0-9_]*)\s*:\s*)?incbin\s+\"([^\"]+)\".*$)",
102 std::regex::icase);
103 std::smatch match;
104 if (!std::regex_match(line, match, kPattern)) {
105 return false;
106 }
107 *inline_label_out = match[1].str();
108 *bin_path_out = match[2].str();
109 return true;
110}
111
112bool ParseMenuOffsetLine(const std::string& line, int* row_out, int* col_out,
113 std::string* note_out) {
114 static const std::regex kPattern(
115 R"(^\s*dw\s+menu_offset\‍(\s*([0-9]+)\s*,\s*([0-9]+)\s*\)(.*)$)",
116 std::regex::icase);
117 std::smatch match;
118 if (!std::regex_match(line, match, kPattern)) {
119 return false;
120 }
121
122 *row_out = std::stoi(match[1].str());
123 *col_out = std::stoi(match[2].str());
124
125 std::string trailing = match[3].str();
126 const size_t semicolon = trailing.find(';');
127 if (semicolon != std::string::npos) {
128 *note_out =
129 std::string(absl::StripAsciiWhitespace(trailing.substr(semicolon + 1)));
130 } else {
131 note_out->clear();
132 }
133 return true;
134}
135
136bool ParseDrawReference(const std::string& line, std::string* target_out) {
137 static const std::regex kPattern(
138 R"(\b(?:JSR|JSL|JMP)\s+([A-Za-z_][A-Za-z0-9_\.]*))", std::regex::icase);
139 std::smatch match;
140 if (!std::regex_search(line, match, kPattern)) {
141 return false;
142 }
143
144 const std::string target = match[1].str();
145 if (!ContainsDrawToken(target)) {
146 return false;
147 }
148 *target_out = target;
149 return true;
150}
151
152absl::StatusOr<std::vector<std::string>> ReadLines(
153 const std::filesystem::path& path, std::string* newline_out,
154 bool* trailing_newline_out) {
155 std::ifstream in(path, std::ios::binary);
156 if (!in.is_open()) {
157 return absl::NotFoundError(
158 absl::StrFormat("Cannot open file: %s", path.string()));
159 }
160
161 std::stringstream buffer;
162 buffer << in.rdbuf();
163 if (!in.good() && !in.eof()) {
164 return absl::InternalError(
165 absl::StrFormat("Failed reading file: %s", path.string()));
166 }
167
168 const std::string content = buffer.str();
169 *newline_out = content.find("\r\n") != std::string::npos ? "\r\n" : "\n";
170 *trailing_newline_out =
171 !content.empty() &&
172 (content.back() == '\n' ||
173 (content.size() >= 2 &&
174 content.substr(content.size() - 2) == std::string("\r\n")));
175
176 std::vector<std::string> lines;
177 lines.reserve(512);
178
179 size_t cursor = 0;
180 while (cursor < content.size()) {
181 size_t end = content.find('\n', cursor);
182 if (end == std::string::npos) {
183 std::string line = content.substr(cursor);
184 if (!line.empty() && line.back() == '\r') {
185 line.pop_back();
186 }
187 lines.push_back(std::move(line));
188 break;
189 }
190 std::string line = content.substr(cursor, end - cursor);
191 if (!line.empty() && line.back() == '\r') {
192 line.pop_back();
193 }
194 lines.push_back(std::move(line));
195 cursor = end + 1;
196 }
197
198 if (lines.empty()) {
199 lines.emplace_back();
200 }
201
202 return lines;
203}
204
205absl::Status WriteLines(const std::filesystem::path& path,
206 const std::vector<std::string>& lines,
207 absl::string_view newline, bool trailing_newline) {
208 std::ofstream out(path, std::ios::binary | std::ios::trunc);
209 if (!out.is_open()) {
210 return absl::PermissionDeniedError(
211 absl::StrFormat("Cannot open file for writing: %s", path.string()));
212 }
213
214 for (size_t i = 0; i < lines.size(); ++i) {
215 out << lines[i];
216 if (i + 1 < lines.size()) {
217 out << newline;
218 }
219 }
220 if (trailing_newline) {
221 out << newline;
222 }
223
224 if (!out.good()) {
225 return absl::InternalError(
226 absl::StrFormat("Failed writing file: %s", path.string()));
227 }
228 return absl::OkStatus();
229}
230
233 absl::string_view code, const std::string& message,
234 const std::string& asm_path = std::string(),
235 int line = 0) {
237 issue.severity = severity;
238 issue.code = std::string(code);
239 issue.message = message;
240 issue.asm_path = asm_path;
241 issue.line = line;
242 report->issues.push_back(std::move(issue));
243 if (severity == OracleMenuValidationSeverity::kError) {
244 report->errors++;
245 } else {
246 report->warnings++;
247 }
248}
249
250} // namespace
251
252absl::StatusOr<std::filesystem::path> ResolveOracleProjectRoot(
253 const std::filesystem::path& start_path) {
254 namespace fs = std::filesystem;
255
256 std::error_code ec;
257 fs::path current = start_path;
258 if (current.empty()) {
259 current = fs::current_path(ec);
260 if (ec) {
261 return absl::InternalError(absl::StrFormat(
262 "Failed to read current directory: %s", ec.message()));
263 }
264 }
265 current = fs::absolute(current, ec);
266 if (ec) {
267 return absl::InvalidArgumentError(
268 absl::StrFormat("Invalid start path: %s", start_path.string()));
269 }
270 if (fs::is_regular_file(current, ec) && !ec) {
271 current = current.parent_path();
272 }
273
274 while (!current.empty()) {
275 fs::path menu_entry = current / "Menu" / "menu.asm";
276 if (fs::exists(menu_entry, ec) && !ec) {
277 fs::path canonical = fs::weakly_canonical(current, ec);
278 if (!ec) {
279 return canonical;
280 }
281 return current;
282 }
283 fs::path parent = current.parent_path();
284 if (parent == current) {
285 break;
286 }
287 current = parent;
288 }
289
290 return absl::NotFoundError(
291 "Could not find Oracle project root (expected Menu/menu.asm)");
292}
293
294absl::StatusOr<OracleMenuRegistry> BuildOracleMenuRegistry(
295 const std::filesystem::path& project_root) {
296 namespace fs = std::filesystem;
297
298 ASSIGN_OR_RETURN(const fs::path root, ResolveOracleProjectRoot(project_root));
299 const fs::path menu_dir = root / "Menu";
300 std::error_code ec;
301 if (!fs::exists(menu_dir, ec) || ec || !fs::is_directory(menu_dir, ec)) {
302 return absl::NotFoundError(
303 absl::StrFormat("Missing Menu directory: %s", menu_dir.string()));
304 }
305
306 OracleMenuRegistry registry;
307 registry.project_root = root;
308
309 std::vector<fs::path> asm_paths;
310 fs::recursive_directory_iterator it(
311 menu_dir, fs::directory_options::skip_permission_denied, ec);
312 fs::recursive_directory_iterator end;
313 if (ec) {
314 return absl::InternalError(absl::StrFormat(
315 "Unable to enumerate Menu directory: %s", ec.message()));
316 }
317 for (; it != end; it.increment(ec)) {
318 if (ec) {
319 ec.clear();
320 continue;
321 }
322 if (!it->is_regular_file()) {
323 continue;
324 }
325 const std::string ext = Lowercase(it->path().extension().string());
326 if (ext == ".asm") {
327 asm_paths.push_back(it->path());
328 }
329 }
330
331 std::sort(asm_paths.begin(), asm_paths.end(),
332 [](const fs::path& a, const fs::path& b) {
333 return a.generic_string() < b.generic_string();
334 });
335
336 std::unordered_map<std::string, int> draw_references;
337 std::unordered_map<std::string, size_t> draw_indices;
338
339 for (const auto& asm_path : asm_paths) {
340 const std::string asm_rel = RelativePathString(root, asm_path);
341 registry.asm_files.push_back(asm_rel);
342
343 std::ifstream file(asm_path);
344 if (!file.is_open()) {
345 registry.warnings.push_back(
346 absl::StrFormat("Failed to open %s", asm_rel));
347 continue;
348 }
349
350 std::unordered_map<std::string, int> component_indices;
351 std::string current_global_label;
352 std::string line;
353 int line_no = 0;
354 while (std::getline(file, line)) {
355 ++line_no;
356
357 std::string global_label;
358 if (ParseGlobalLabel(line, &global_label)) {
359 current_global_label = global_label;
360 component_indices[current_global_label] = 0;
361 if (ContainsDrawToken(global_label)) {
362 const std::string key = asm_rel + ":" + global_label;
363 if (draw_indices.find(key) == draw_indices.end()) {
364 draw_indices[key] = registry.draw_routines.size();
365 registry.draw_routines.push_back(
366 {.label = global_label, .asm_path = asm_rel, .line = line_no});
367 }
368 }
369 }
370
371 std::string local_label;
372 if (ParseLocalLabel(line, &local_label) &&
373 ContainsDrawToken(local_label)) {
374 std::string full_label = local_label;
375 if (!current_global_label.empty()) {
376 full_label = current_global_label + local_label;
377 }
378 const std::string key = asm_rel + ":" + full_label;
379 if (draw_indices.find(key) == draw_indices.end()) {
380 draw_indices[key] = registry.draw_routines.size();
381 registry.draw_routines.push_back({.label = full_label,
382 .asm_path = asm_rel,
383 .line = line_no,
384 .local = true});
385 }
386 }
387
388 std::string inline_label;
389 std::string bin_path;
390 if (ParseIncbinLine(line, &inline_label, &bin_path)) {
391 const std::string label =
392 !inline_label.empty() ? inline_label : current_global_label;
393 fs::path resolved =
394 (asm_path.parent_path() / bin_path).lexically_normal();
395 std::error_code stat_ec;
396 const bool exists = fs::exists(resolved, stat_ec) && !stat_ec;
397 uintmax_t size_bytes = 0;
398 if (exists) {
399 size_bytes = fs::file_size(resolved, stat_ec);
400 if (stat_ec) {
401 size_bytes = 0;
402 }
403 }
404 registry.bins.push_back(
405 {.label = label,
406 .asm_path = asm_rel,
407 .line = line_no,
408 .bin_path = bin_path,
409 .resolved_bin_path = RelativePathString(root, resolved),
410 .exists = exists,
411 .size_bytes = size_bytes});
412 }
413
414 int row = 0;
415 int col = 0;
416 std::string note;
417 if (ParseMenuOffsetLine(line, &row, &col, &note) &&
418 !current_global_label.empty()) {
419 const int index = component_indices[current_global_label]++;
420 registry.components.push_back({.table_label = current_global_label,
421 .index = index,
422 .row = row,
423 .col = col,
424 .note = note,
425 .asm_path = asm_rel,
426 .line = line_no});
427 }
428
429 std::string draw_target;
430 if (ParseDrawReference(line, &draw_target)) {
431 draw_references[draw_target]++;
432 }
433 }
434 }
435
436 for (auto& routine : registry.draw_routines) {
437 auto it_ref = draw_references.find(routine.label);
438 if (it_ref != draw_references.end()) {
439 routine.references = it_ref->second;
440 continue;
441 }
442
443 // Fallback for local labels represented as "Global.local"
444 const size_t dot = routine.label.find('.');
445 if (dot != std::string::npos) {
446 const std::string local = routine.label.substr(dot);
447 it_ref = draw_references.find(local);
448 if (it_ref != draw_references.end()) {
449 routine.references = it_ref->second;
450 }
451 }
452 }
453
454 std::sort(registry.bins.begin(), registry.bins.end(),
455 [](const OracleMenuBinEntry& a, const OracleMenuBinEntry& b) {
456 if (a.asm_path != b.asm_path) {
457 return a.asm_path < b.asm_path;
458 }
459 return a.line < b.line;
460 });
461 std::sort(registry.draw_routines.begin(), registry.draw_routines.end(),
462 [](const OracleMenuDrawRoutine& a, const OracleMenuDrawRoutine& b) {
463 if (a.asm_path != b.asm_path) {
464 return a.asm_path < b.asm_path;
465 }
466 return a.line < b.line;
467 });
468 std::sort(registry.components.begin(), registry.components.end(),
469 [](const OracleMenuComponent& a, const OracleMenuComponent& b) {
470 if (a.asm_path != b.asm_path) {
471 return a.asm_path < b.asm_path;
472 }
473 return a.line < b.line;
474 });
475
476 return registry;
477}
478
480 const OracleMenuRegistry& registry, int max_row, int max_col) {
482
483 if (registry.asm_files.empty()) {
484 AddValidationIssue(
485 &report, OracleMenuValidationSeverity::kError, "no_menu_asm",
486 "No Menu/*.asm files were discovered under the project root.");
487 }
488
489 for (const auto& warning : registry.warnings) {
490 AddValidationIssue(&report, OracleMenuValidationSeverity::kWarning,
491 "registry_warning", warning);
492 }
493
494 for (const auto& entry : registry.bins) {
495 if (!entry.exists) {
496 AddValidationIssue(
497 &report, OracleMenuValidationSeverity::kError, "missing_bin",
498 absl::StrFormat("Missing incbin asset \"%s\" for label %s",
499 entry.bin_path,
500 entry.label.empty() ? "(unlabeled)" : entry.label),
501 entry.asm_path, entry.line);
502 continue;
503 }
504 if (entry.size_bytes == 0) {
505 AddValidationIssue(
506 &report, OracleMenuValidationSeverity::kWarning, "empty_bin",
507 absl::StrFormat("Incbin asset \"%s\" resolves to 0 bytes",
508 entry.resolved_bin_path),
509 entry.asm_path, entry.line);
510 }
511 }
512
513 struct TableState {
514 int component_count = 0;
515 std::set<int> indices;
516 std::map<std::string, int> coordinate_counts;
517 };
518 std::map<std::string, TableState> table_states;
519
520 for (const auto& component : registry.components) {
521 if (component.table_label.empty()) {
522 AddValidationIssue(
523 &report, OracleMenuValidationSeverity::kError, "empty_table_label",
524 "menu_offset entry was parsed without an owning table label",
525 component.asm_path, component.line);
526 continue;
527 }
528
529 if (component.row < 0 || component.col < 0 || component.row > max_row ||
530 component.col > max_col) {
531 AddValidationIssue(
532 &report, OracleMenuValidationSeverity::kError,
533 "component_out_of_bounds",
534 absl::StrFormat(
535 "%s[%d] has out-of-bounds menu_offset(%d,%d); allowed row=0..%d "
536 "col=0..%d",
537 component.table_label, component.index, component.row,
538 component.col, max_row, max_col),
539 component.asm_path, component.line);
540 }
541
542 TableState& state = table_states[component.table_label];
543 state.component_count++;
544
545 if (!state.indices.insert(component.index).second) {
546 AddValidationIssue(
547 &report, OracleMenuValidationSeverity::kError,
548 "duplicate_component_index",
549 absl::StrFormat("%s contains duplicate index %d",
550 component.table_label, component.index),
551 component.asm_path, component.line);
552 }
553
554 const std::string coord_key =
555 absl::StrFormat("%d,%d", component.row, component.col);
556 int& coordinate_count = state.coordinate_counts[coord_key];
557 coordinate_count++;
558 if (coordinate_count == 2) {
559 AddValidationIssue(
560 &report, OracleMenuValidationSeverity::kWarning,
561 "duplicate_component_coordinate",
562 absl::StrFormat(
563 "%s reuses menu_offset(%d,%d) for multiple component entries",
564 component.table_label, component.row, component.col),
565 component.asm_path, component.line);
566 }
567 }
568
569 for (const auto& [table_label, state] : table_states) {
570 if (state.component_count <= 0) {
571 continue;
572 }
573
574 std::vector<int> missing_indices;
575 missing_indices.reserve(8);
576 for (int expected = 0; expected < state.component_count; ++expected) {
577 if (state.indices.count(expected) == 0) {
578 missing_indices.push_back(expected);
579 }
580 }
581
582 if (!missing_indices.empty()) {
583 std::string missing_list;
584 const int preview_count =
585 std::min<int>(static_cast<int>(missing_indices.size()), 6);
586 for (int i = 0; i < preview_count; ++i) {
587 if (i > 0) {
588 missing_list.append(", ");
589 }
590 missing_list.append(std::to_string(missing_indices[i]));
591 }
592 if (static_cast<int>(missing_indices.size()) > preview_count) {
593 missing_list.append(", ...");
594 }
595
596 AddValidationIssue(
597 &report, OracleMenuValidationSeverity::kError,
598 "component_index_gap",
599 absl::StrFormat(
600 "%s component indices are not contiguous from 0..%d "
601 "(missing: %s)",
602 table_label, state.component_count - 1, missing_list));
603 }
604 }
605
606 return report;
607}
608
609absl::StatusOr<OracleMenuComponentEditResult> SetOracleMenuComponentOffset(
610 const std::filesystem::path& project_root,
611 const std::string& asm_relative_path, const std::string& table_label,
612 int index, int row, int col, bool write_changes) {
613 namespace fs = std::filesystem;
614
615 if (asm_relative_path.empty()) {
616 return absl::InvalidArgumentError("--asm path is required");
617 }
618 if (table_label.empty()) {
619 return absl::InvalidArgumentError("--table is required");
620 }
621 if (index < 0) {
622 return absl::InvalidArgumentError("--index must be >= 0");
623 }
624 if (row < 0 || col < 0) {
625 return absl::InvalidArgumentError("--row and --col must be >= 0");
626 }
627
628 ASSIGN_OR_RETURN(const fs::path root, ResolveOracleProjectRoot(project_root));
629
630 fs::path asm_path = fs::path(asm_relative_path);
631 if (!asm_path.is_absolute()) {
632 asm_path = root / asm_path;
633 }
634 asm_path = asm_path.lexically_normal();
635
636 std::error_code ec;
637 if (!fs::exists(asm_path, ec) || ec || !fs::is_regular_file(asm_path, ec)) {
638 return absl::NotFoundError(
639 absl::StrFormat("ASM file not found: %s", asm_path.string()));
640 }
641 if (!IsPathWithinRoot(root, asm_path)) {
642 return absl::PermissionDeniedError(absl::StrFormat(
643 "ASM path escapes project root: %s", asm_path.string()));
644 }
645
646 std::string newline;
647 bool trailing_newline = false;
648 ASSIGN_OR_RETURN(auto lines,
649 ReadLines(asm_path, &newline, &trailing_newline));
650
651 bool in_table = false;
652 int current_index = 0;
653 int target_line = -1;
654 int old_row = -1;
655 int old_col = -1;
656 std::string old_line;
657 std::string new_line;
658 static const std::regex kRewritePattern(
659 R"(^(\s*dw\s+menu_offset\‍(\s*)([0-9]+)(\s*,\s*)([0-9]+)(\s*\).*)$)",
660 std::regex::icase);
661
662 for (size_t i = 0; i < lines.size(); ++i) {
663 const std::string& line = lines[i];
664
665 std::string global_label;
666 if (ParseGlobalLabel(line, &global_label)) {
667 if (global_label == table_label) {
668 in_table = true;
669 current_index = 0;
670 } else if (in_table) {
671 break;
672 }
673 }
674
675 if (!in_table) {
676 continue;
677 }
678
679 int parsed_row = 0;
680 int parsed_col = 0;
681 std::string note;
682 if (!ParseMenuOffsetLine(line, &parsed_row, &parsed_col, &note)) {
683 continue;
684 }
685
686 if (current_index == index) {
687 std::smatch match;
688 if (!std::regex_match(line, match, kRewritePattern)) {
689 return absl::InternalError(absl::StrFormat(
690 "Matched menu_offset but rewrite failed at %s:%d",
691 RelativePathString(root, asm_path), static_cast<int>(i + 1)));
692 }
693
694 target_line = static_cast<int>(i + 1);
695 old_row = parsed_row;
696 old_col = parsed_col;
697 old_line = line;
698 new_line = absl::StrFormat("%s%d%s%d%s", match[1].str(), row,
699 match[3].str(), col, match[5].str());
700 break;
701 }
702 ++current_index;
703 }
704
705 if (target_line < 0) {
706 return absl::NotFoundError(
707 absl::StrFormat("Could not find %s[%d] in %s", table_label, index,
708 RelativePathString(root, asm_path)));
709 }
710
712 result.asm_path = RelativePathString(root, asm_path);
713 result.line = target_line;
714 result.table_label = table_label;
715 result.index = index;
716 result.old_row = old_row;
717 result.old_col = old_col;
718 result.new_row = row;
719 result.new_col = col;
720 result.old_line = old_line;
721 result.new_line = new_line;
722 result.changed = (old_line != new_line);
723 result.write_applied = false;
724
725 if (!write_changes) {
726 return result;
727 }
728
729 if (result.changed) {
730 lines[static_cast<size_t>(target_line - 1)] = new_line;
731 RETURN_IF_ERROR(WriteLines(asm_path, lines, newline, trailing_newline));
732 }
733
734 result.write_applied = true;
735 return result;
736}
737
738} // namespace yaze::core
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
absl::StatusOr< std::vector< std::string > > ReadLines(const std::filesystem::path &path, std::string *newline_out, bool *trailing_newline_out)
bool ParseIncbinLine(const std::string &line, std::string *inline_label_out, std::string *bin_path_out)
std::string NormalizePathString(const std::filesystem::path &path)
bool ParseDrawReference(const std::string &line, std::string *target_out)
absl::Status WriteLines(const std::filesystem::path &path, const std::vector< std::string > &lines, absl::string_view newline, bool trailing_newline)
std::string RelativePathString(const std::filesystem::path &base, const std::filesystem::path &path)
bool ParseGlobalLabel(const std::string &line, std::string *label_out)
bool IsPathWithinRoot(const std::filesystem::path &root, const std::filesystem::path &candidate)
bool ParseLocalLabel(const std::string &line, std::string *label_out)
bool ParseMenuOffsetLine(const std::string &line, int *row_out, int *col_out, std::string *note_out)
void AddValidationIssue(OracleMenuValidationReport *report, OracleMenuValidationSeverity severity, absl::string_view code, const std::string &message, const std::string &asm_path=std::string(), int line=0)
absl::StatusOr< std::filesystem::path > ResolveOracleProjectRoot(const std::filesystem::path &start_path)
absl::StatusOr< OracleMenuRegistry > BuildOracleMenuRegistry(const std::filesystem::path &project_root)
absl::StatusOr< OracleMenuComponentEditResult > SetOracleMenuComponentOffset(const std::filesystem::path &project_root, const std::string &asm_relative_path, const std::string &table_label, int index, int row, int col, bool write_changes)
OracleMenuValidationReport ValidateOracleMenuRegistry(const OracleMenuRegistry &registry, int max_row, int max_col)
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
std::vector< OracleMenuComponent > components
std::vector< OracleMenuDrawRoutine > draw_routines
std::vector< std::string > warnings
std::vector< std::string > asm_files
std::vector< OracleMenuBinEntry > bins
OracleMenuValidationSeverity severity
std::vector< OracleMenuValidationIssue > issues