yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
message_commands.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cstddef>
5#include <exception>
6#include <filesystem>
7#include <fstream>
8#include <limits>
9#include <optional>
10#include <sstream>
11
12#include "absl/flags/declare.h"
13#include "absl/flags/flag.h"
14#include "absl/strings/ascii.h"
15#include "absl/strings/match.h"
16#include "absl/strings/str_cat.h"
17#include "absl/strings/str_format.h"
18
21#include "rom/snes.h"
22#include "rom/transaction.h"
24
25ABSL_DECLARE_FLAG(std::string, rom);
26
27namespace yaze {
28namespace cli {
29namespace handlers {
30
31namespace {
32std::string NormalizeRange(const std::string& range) {
33 return absl::AsciiStrToLower(range);
34}
35
36bool IncludeVanilla(const std::string& range) {
37 return range == "all" || range == "vanilla";
38}
39
40bool IncludeExpanded(const std::string& range) {
41 return range == "all" || range == "expanded";
42}
43
44std::string BankLabel(editor::MessageBank bank) {
45 return editor::MessageBankToString(bank);
46}
47
48std::vector<editor::MessageData> ReadExpandedMessages(const Rom& rom) {
49 const int start = editor::GetExpandedTextDataStart();
50 if (start < 0 || static_cast<size_t>(start) >= rom.size()) {
51 return {};
52 }
53 const int end = std::min(editor::GetExpandedTextDataEnd(),
54 static_cast<int>(rom.size()) - 1);
55 return editor::ReadExpandedTextData(const_cast<uint8_t*>(rom.data()), start,
56 end);
57}
58
59std::vector<editor::MessageData> ReadExpandedMessages(const Rom& rom, int start,
60 int end) {
61 if (start < 0 || end < start || static_cast<size_t>(start) >= rom.size()) {
62 return {};
63 }
64 end = std::min(end, static_cast<int>(rom.size()) - 1);
65 return editor::ReadExpandedTextData(const_cast<uint8_t*>(rom.data()), start,
66 end);
67}
68
71 int start = 0;
72 int end = 0;
73 int message_limit = -1;
74 std::string policy_warning;
75};
76
78 public:
81 : provider_(provider), previous_(provider.hack_manifest()) {}
82
84 delete;
86 const ScopedHackManifestBindingRestore&) = delete;
87
88 ~ScopedHackManifestBindingRestore() { provider_.SetHackManifest(previous_); }
89
90 private:
92 const core::HackManifest* previous_ = nullptr;
93};
94
95absl::StatusOr<std::filesystem::path> CanonicalExistingPath(
96 const std::string& path, absl::string_view label) {
97 if (path.empty()) {
98 return absl::FailedPreconditionError(
99 absl::StrFormat("%s path is empty", label));
100 }
101
102 std::error_code ec;
103 auto canonical_path = std::filesystem::canonical(path, ec);
104 if (ec) {
105 return absl::FailedPreconditionError(absl::StrFormat(
106 "Cannot resolve %s path '%s': %s", label, path, ec.message()));
107 }
108 return canonical_path;
109}
110
112 project::YazeProject* project, absl::string_view project_path) {
113 if (project->hack_manifest.loaded() || project->hack_manifest_file.empty()) {
114 return absl::OkStatus();
115 }
116
117 const std::string manifest_path =
118 project->GetAbsolutePath(project->hack_manifest_file);
119 const absl::Status manifest_status =
120 project->hack_manifest.LoadFromFile(manifest_path);
121 if (manifest_status.ok()) {
122 return absl::OkStatus();
123 }
124 return absl::Status(
125 manifest_status.code(),
126 absl::StrFormat("Cannot load project '%s': hack manifest '%s': %s",
127 project_path, manifest_path, manifest_status.message()));
128}
129
130std::string FormatManifestConflict(const core::WriteConflict& conflict) {
131 std::string result =
132 absl::StrFormat("address 0x%06X is %s", conflict.address,
134 if (!conflict.module.empty()) {
135 absl::StrAppend(&result, " (Module: ", conflict.module, ")");
136 }
137 return result;
138}
139
140bool IsCanonicalMappedLoRomAddress(uint32_t address) {
141 const uint8_t bank = static_cast<uint8_t>((address >> 16) & 0xFFu);
142 if (bank == 0x7E || bank == 0x7F || (address & 0xFFFFu) < 0x8000u) {
143 return false;
144 }
145 return PcToSnes(SnesToPc(address)) == address;
146}
147
148absl::StatusOr<ExpandedMutationContext> PreflightExpandedMutation(
149 const resources::ArgumentParser& parser, const Rom& rom) {
150 auto project_path = parser.GetString("project");
151 if (!project_path.has_value() || project_path->empty()) {
152 return absl::FailedPreconditionError(
153 "Expanded message mutation requires explicit --project so Yaze can "
154 "verify ROM ownership and write policy");
155 }
156 if (!rom.is_loaded()) {
157 return absl::FailedPreconditionError("ROM is not loaded");
158 }
159
161 auto& resource_labels = zelda3::GetResourceLabels();
162 absl::Status open_status;
163 {
164 // YazeProject::Open temporarily installs its manifest in the
165 // process-global label provider. Restore the embedding application's prior
166 // non-owning pointer on every path, including malformed project data that
167 // throws from a parser.
168 ScopedHackManifestBindingRestore restore_manifest(resource_labels);
169 try {
170 open_status = context.project.Open(*project_path);
171 } catch (const std::exception& error) {
172 return absl::InvalidArgumentError(absl::StrFormat(
173 "Cannot load project '%s': %s", *project_path, error.what()));
174 } catch (...) {
175 return absl::InvalidArgumentError(absl::StrFormat(
176 "Cannot load project '%s': unknown project parse error",
177 *project_path));
178 }
179 }
180 if (!open_status.ok()) {
181 return absl::Status(open_status.code(),
182 absl::StrFormat("Cannot load project '%s': %s",
183 *project_path, open_status.message()));
184 }
185 if (absl::Status manifest_status =
186 EnsureConfiguredHackManifestLoaded(&context.project, *project_path);
187 !manifest_status.ok()) {
188 return manifest_status;
189 }
190
191 auto active_rom_path_or = CanonicalExistingPath(rom.filename(), "active ROM");
192 if (!active_rom_path_or.ok()) {
193 return active_rom_path_or.status();
194 }
195 auto project_rom_path_or = CanonicalExistingPath(
197 "project ROM");
198 if (!project_rom_path_or.ok()) {
199 return project_rom_path_or.status();
200 }
201 if (*active_rom_path_or != *project_rom_path_or) {
202 return absl::FailedPreconditionError(absl::StrFormat(
203 "Project ROM mismatch: active ROM is '%s' but --project binds to '%s'",
204 active_rom_path_or->string(), project_rom_path_or->string()));
205 }
206
207 const auto& manifest = context.project.hack_manifest;
208 if (!manifest.loaded()) {
209 return absl::FailedPreconditionError(absl::StrFormat(
210 "Project '%s' has no loaded hack manifest; configure a valid "
211 "hack_manifest_file before mutating expanded messages",
212 *project_path));
213 }
214
215 const auto& layout = manifest.message_layout();
216 if (layout.data_start == 0 || layout.data_end == 0 ||
217 layout.data_end < layout.data_start ||
218 !IsCanonicalMappedLoRomAddress(layout.data_start) ||
219 !IsCanonicalMappedLoRomAddress(layout.data_end)) {
220 return absl::FailedPreconditionError(
221 "Hack manifest does not define a valid LoROM expanded message region");
222 }
223
224 const uint32_t start = SnesToPc(layout.data_start);
225 const uint32_t end = SnesToPc(layout.data_end);
226 if (end < start ||
227 end > static_cast<uint32_t>(std::numeric_limits<int>::max()) ||
228 end >= rom.size()) {
229 return absl::OutOfRangeError(absl::StrFormat(
230 "Configured expanded message region [0x%06X, 0x%06X] is outside the "
231 "active ROM (size=0x%zX)",
232 start, end, rom.size()));
233 }
234 context.start = static_cast<int>(start);
235 context.end = static_cast<int>(end);
236
237 // Even a manifest without ID metadata has a strict physical upper bound:
238 // each message requires at least a 0x7F terminator and the bank needs a final
239 // 0xFF. Keeping this limit finite prevents hostile IDs from driving a huge
240 // sparse-vector allocation.
241 const int region_capacity = context.end - context.start + 1;
242 const int capacity_message_limit = std::max(0, region_capacity - 1);
243 context.message_limit = capacity_message_limit;
244 if (layout.expanded_count > 0) {
245 context.message_limit =
246 std::min(layout.expanded_count, capacity_message_limit);
247 }
248 if (layout.last_expanded_id >= layout.first_expanded_id &&
249 (layout.first_expanded_id != 0 || layout.last_expanded_id != 0)) {
250 const int declared_limit =
251 static_cast<int>(layout.last_expanded_id - layout.first_expanded_id) +
252 1;
253 context.message_limit = std::min(context.message_limit, declared_limit);
254 }
255
256 const std::string lowered_hack_name =
257 absl::AsciiStrToLower(manifest.hack_name());
258 if (context.project.rom_metadata.write_policy ==
260 absl::StrContains(lowered_hack_name, "oracle of secrets")) {
261 return absl::PermissionDeniedError(
262 "Oracle of Secrets expanded messages are owned by ASM. No message "
263 "bytes were written; edit Core/message.asm, rebuild Oracle of Secrets, "
264 "and reopen the rebuilt ROM before retrying.");
265 }
266
267 const auto conflicts =
268 manifest.AnalyzePcWriteRanges({{start, static_cast<uint32_t>(end + 1u)}});
269 if (!conflicts.empty()) {
270 const std::string detail = FormatManifestConflict(conflicts.front());
271 if (context.project.rom_metadata.write_policy ==
273 return absl::PermissionDeniedError(absl::StrFormat(
274 "Expanded message mutation blocked by hack manifest: %s", detail));
275 }
276 if (context.project.rom_metadata.write_policy ==
278 context.policy_warning = absl::StrFormat(
279 "Expanded message region conflicts with hack manifest: %s", detail);
280 }
281 }
282
283 return context;
284}
285
286absl::Status ValidateExpandedMessageId(int id,
287 const ExpandedMutationContext& context) {
288 if (id < 0) {
289 return absl::InvalidArgumentError(
290 absl::StrFormat("Invalid expanded message ID: %d", id));
291 }
292 if (context.message_limit >= 0 && id >= context.message_limit) {
293 return absl::OutOfRangeError(absl::StrFormat(
294 "Expanded message ID %d is outside the manifest range (count=%d)", id,
295 context.message_limit));
296 }
297 return absl::OkStatus();
298}
299
301 size_t message_count, const ExpandedMutationContext& context) {
302 if (context.message_limit >= 0 &&
303 message_count > static_cast<size_t>(context.message_limit)) {
304 return absl::FailedPreconditionError(absl::StrFormat(
305 "Expanded message bank contains %zu messages, exceeding the manifest "
306 "limit of %d; no changes were applied",
307 message_count, context.message_limit));
308 }
309 return absl::OkStatus();
310}
311} // namespace
312
313// ===========================================================================
314// Existing Commands
315// ===========================================================================
316
318 Rom* rom, const resources::ArgumentParser& parser,
319 resources::OutputFormatter& formatter) {
320 auto limit = parser.GetInt("limit").value_or(50);
321 if (limit < 0) {
322 limit = 0;
323 }
324
325 auto messages = editor::ReadAllTextData(const_cast<uint8_t*>(rom->data()),
327 if (limit > static_cast<int>(messages.size())) {
328 limit = static_cast<int>(messages.size());
329 }
330
331 formatter.BeginObject("Message List");
332 formatter.AddField("limit", limit);
333 formatter.AddField("total_messages", static_cast<int>(messages.size()));
334 formatter.AddField("status", "success");
335
336 formatter.BeginArray("messages");
337 for (int i = 0; i < limit; ++i) {
338 const auto& msg = messages[i];
339 formatter.BeginObject();
340 formatter.AddField("id", msg.ID);
341 formatter.AddHexField("address", msg.Address, 6);
342 formatter.AddField("text", msg.ContentsParsed);
343 formatter.AddField("length", static_cast<int>(msg.Data.size()));
344 formatter.EndObject();
345 }
346 formatter.EndArray();
347 formatter.EndObject();
348
349 return absl::OkStatus();
350}
351
353 Rom* rom, const resources::ArgumentParser& parser,
354 resources::OutputFormatter& formatter) {
355 auto message_id_or = parser.GetInt("id");
356 if (!message_id_or.ok()) {
357 return message_id_or.status();
358 }
359 int message_id = message_id_or.value();
360
361 auto messages = editor::ReadAllTextData(const_cast<uint8_t*>(rom->data()),
363 if (message_id < 0 || message_id >= static_cast<int>(messages.size())) {
364 return absl::NotFoundError(
365 absl::StrFormat("Message ID %d not found (max: %d)", message_id,
366 static_cast<int>(messages.size()) - 1));
367 }
368
369 const auto& msg = messages[message_id];
370
371 formatter.BeginObject("Message");
372 formatter.AddField("id", msg.ID);
373 formatter.AddHexField("address", msg.Address, 6);
374 formatter.AddField("text", msg.ContentsParsed);
375 formatter.AddField("length", static_cast<int>(msg.Data.size()));
376 formatter.EndObject();
377
378 return absl::OkStatus();
379}
380
382 Rom* rom, const resources::ArgumentParser& parser,
383 resources::OutputFormatter& formatter) {
384 auto query = parser.GetString("query").value();
385 auto limit = parser.GetInt("limit").value_or(10);
386 if (limit < 0) {
387 limit = 0;
388 }
389
390 auto messages = editor::ReadAllTextData(const_cast<uint8_t*>(rom->data()),
392
393 formatter.BeginObject("Message Search Results");
394 formatter.AddField("query", query);
395 formatter.AddField("limit", limit);
396 formatter.AddField("status", "success");
397
398 std::string lowered_query = absl::AsciiStrToLower(query);
399 std::vector<const editor::MessageData*> matches;
400 for (const auto& msg : messages) {
401 std::string lowered_text = absl::AsciiStrToLower(msg.ContentsParsed);
402 if (lowered_text.find(lowered_query) == std::string::npos) {
403 continue;
404 }
405 matches.push_back(&msg);
406 }
407
408 formatter.AddField("matches_found", static_cast<int>(matches.size()));
409 formatter.BeginArray("matches");
410 int match_count = 0;
411 for (const auto* msg : matches) {
412 if (limit > 0 && match_count >= limit) {
413 break;
414 }
415 formatter.BeginObject();
416 formatter.AddField("id", msg->ID);
417 formatter.AddHexField("address", msg->Address, 6);
418 formatter.AddField("text", msg->ContentsParsed);
419 formatter.EndObject();
420 match_count++;
421 }
422 formatter.EndArray();
423 formatter.EndObject();
424
425 return absl::OkStatus();
426}
427
428// ===========================================================================
429// New: Encode Command
430// ===========================================================================
431
433 Rom* rom, const resources::ArgumentParser& parser,
434 resources::OutputFormatter& formatter) {
435 auto text = parser.GetString("text").value();
436
437 auto parse_result = editor::ParseMessageToDataWithDiagnostics(text);
438 auto bytes = parse_result.bytes;
439
440 // Build hex string
441 std::string hex_str;
442 for (size_t i = 0; i < bytes.size(); ++i) {
443 if (i > 0)
444 hex_str += " ";
445 hex_str += absl::StrFormat("%02X", bytes[i]);
446 }
447
448 formatter.BeginObject("Encoded Message");
449 formatter.AddField("input", text);
450 formatter.AddField("hex", hex_str);
451 formatter.AddField("length", static_cast<int>(bytes.size()));
452
453 // Also output as byte array for JSON consumers
454 formatter.BeginArray("bytes");
455 for (uint8_t byte : bytes) {
456 formatter.AddArrayItem(absl::StrFormat("0x%02X", byte));
457 }
458 formatter.EndArray();
459
460 // Run line width validation
461 auto warnings = editor::ValidateMessageLineWidths(text);
462 if (!warnings.empty()) {
463 formatter.BeginArray("line_width_warnings");
464 for (const auto& warning : warnings) {
465 formatter.AddArrayItem(warning);
466 }
467 formatter.EndArray();
468 }
469 if (!parse_result.warnings.empty()) {
470 formatter.BeginArray("warnings");
471 for (const auto& warning : parse_result.warnings) {
472 formatter.AddArrayItem(warning);
473 }
474 formatter.EndArray();
475 }
476 if (!parse_result.errors.empty()) {
477 formatter.BeginArray("errors");
478 for (const auto& error : parse_result.errors) {
479 formatter.AddArrayItem(error);
480 }
481 formatter.EndArray();
482 }
483
484 formatter.EndObject();
485
486 return absl::OkStatus();
487}
488
489// ===========================================================================
490// New: Decode Command
491// ===========================================================================
492
494 Rom* rom, const resources::ArgumentParser& parser,
495 resources::OutputFormatter& formatter) {
496 auto hex_input = parser.GetString("hex").value();
497
498 // Parse hex string to bytes
499 std::vector<uint8_t> bytes;
500 std::istringstream hex_stream(hex_input);
501 std::string hex_byte;
502 while (hex_stream >> hex_byte) {
503 try {
504 bytes.push_back(static_cast<uint8_t>(std::stoi(hex_byte, nullptr, 16)));
505 } catch (const std::exception&) {
506 return absl::InvalidArgumentError(
507 absl::StrFormat("Invalid hex byte: '%s'", hex_byte));
508 }
509 }
510
511 // Decode bytes to text, handling commands with arguments
512 std::string decoded;
513 for (size_t i = 0; i < bytes.size(); ++i) {
514 uint8_t byte = bytes[i];
515
516 // Check for command (may consume next byte as argument)
517 auto cmd = editor::FindMatchingCommand(byte);
518 if (cmd.has_value()) {
519 if (cmd->HasArgument && i + 1 < bytes.size()) {
520 decoded += cmd->GetParamToken(bytes[++i]);
521 } else {
522 decoded += cmd->GetParamToken();
523 }
524 continue;
525 }
526
527 // Single-byte lookup for chars, specials, dictionary
528 decoded += editor::ParseTextDataByte(byte);
529 }
530
531 formatter.BeginObject("Decoded Message");
532 formatter.AddField("hex", hex_input);
533 formatter.AddField("text", decoded);
534 formatter.AddField("length", static_cast<int>(bytes.size()));
535 formatter.EndObject();
536
537 return absl::OkStatus();
538}
539
540// ===========================================================================
541// New: Import Org Command
542// ===========================================================================
543
545 Rom* rom, const resources::ArgumentParser& parser,
546 resources::OutputFormatter& formatter) {
547 auto file_path = parser.GetString("file").value();
548
549 // Read the .org file
550 std::ifstream file(file_path);
551 if (!file.is_open()) {
552 return absl::NotFoundError(
553 absl::StrFormat("Cannot open file: %s", file_path));
554 }
555
556 std::string content((std::istreambuf_iterator<char>(file)),
557 std::istreambuf_iterator<char>());
558 file.close();
559
560 auto messages = editor::ParseOrgContent(content);
561
562 formatter.BeginObject("Org Import Results");
563 formatter.AddField("file", file_path);
564 formatter.AddField("messages_parsed", static_cast<int>(messages.size()));
565
566 formatter.BeginArray("messages");
567 for (const auto& [msg_id, body] : messages) {
568 // Encode the body text to bytes with diagnostics
569 auto parse_result = editor::ParseMessageToDataWithDiagnostics(body);
570 auto bytes = parse_result.bytes;
571 auto warnings = editor::ValidateMessageLineWidths(body);
572
573 std::string hex_str;
574 for (size_t i = 0; i < bytes.size(); ++i) {
575 if (i > 0)
576 hex_str += " ";
577 hex_str += absl::StrFormat("%02X", bytes[i]);
578 }
579
580 formatter.BeginObject();
581 formatter.AddHexField("id", msg_id, 2);
582 formatter.AddField("text", body);
583 formatter.AddField("hex", hex_str);
584 formatter.AddField("encoded_length", static_cast<int>(bytes.size()));
585 if (!warnings.empty()) {
586 formatter.BeginArray("warnings");
587 for (const auto& warning : warnings) {
588 formatter.AddArrayItem(warning);
589 }
590 formatter.EndArray();
591 }
592 if (!parse_result.warnings.empty()) {
593 formatter.BeginArray("parse_warnings");
594 for (const auto& warning : parse_result.warnings) {
595 formatter.AddArrayItem(warning);
596 }
597 formatter.EndArray();
598 }
599 if (!parse_result.errors.empty()) {
600 formatter.BeginArray("errors");
601 for (const auto& error : parse_result.errors) {
602 formatter.AddArrayItem(error);
603 }
604 formatter.EndArray();
605 }
606 formatter.EndObject();
607 }
608 formatter.EndArray();
609 formatter.EndObject();
610
611 return absl::OkStatus();
612}
613
614// ===========================================================================
615// New: Export Org Command
616// ===========================================================================
617
619 Rom* rom, const resources::ArgumentParser& parser,
620 resources::OutputFormatter& formatter) {
621 auto output_path = parser.GetString("output").value();
622
623 auto messages = editor::ReadAllTextData(const_cast<uint8_t*>(rom->data()),
625
626 // Build message pairs and labels
627 std::vector<std::pair<int, std::string>> msg_pairs;
628 std::vector<std::string> labels;
629 for (const auto& msg : messages) {
630 msg_pairs.push_back({msg.ID, msg.RawString});
631 labels.push_back(absl::StrFormat("Message %02X", msg.ID));
632 }
633
634 std::string org_content = editor::ExportToOrgFormat(msg_pairs, labels);
635
636 // Write to file
637 std::ofstream file(output_path);
638 if (!file.is_open()) {
639 return absl::InternalError(
640 absl::StrFormat("Cannot write to file: %s", output_path));
641 }
642 file << org_content;
643 file.close();
644
645 formatter.BeginObject("Org Export Results");
646 formatter.AddField("output", output_path);
647 formatter.AddField("messages_exported", static_cast<int>(messages.size()));
648 formatter.AddField("status", "success");
649 formatter.EndObject();
650
651 return absl::OkStatus();
652}
653
654// ===========================================================================
655// New: Export Bundle Command
656// ===========================================================================
657
659 Rom* rom, const resources::ArgumentParser& parser,
660 resources::OutputFormatter& formatter) {
661 auto output_path = parser.GetString("output").value();
662 auto range = NormalizeRange(parser.GetString("range").value_or("all"));
663 if (!IncludeVanilla(range) && !IncludeExpanded(range)) {
664 return absl::InvalidArgumentError(
665 absl::StrFormat("Invalid range: %s", range));
666 }
667
668 std::vector<editor::MessageData> vanilla;
669 std::vector<editor::MessageData> expanded;
670
671 if (IncludeVanilla(range)) {
672 vanilla = editor::ReadAllTextData(const_cast<uint8_t*>(rom->data()),
674 }
675
676 if (IncludeExpanded(range)) {
677 expanded = ReadExpandedMessages(*rom);
678 }
679
680 auto status =
681 editor::ExportMessageBundleToJson(output_path, vanilla, expanded);
682 if (!status.ok()) {
683 return status;
684 }
685
686 formatter.BeginObject("Message Bundle Export");
687 formatter.AddField("output", output_path);
688 formatter.AddField("range", range);
689 formatter.AddField("vanilla_count", static_cast<int>(vanilla.size()));
690 formatter.AddField("expanded_count", static_cast<int>(expanded.size()));
691 formatter.AddField("status", "success");
692 formatter.EndObject();
693
694 return absl::OkStatus();
695}
696
697// ===========================================================================
698// New: Import Bundle Command
699// ===========================================================================
700
702 Rom* rom, const resources::ArgumentParser& parser,
703 resources::OutputFormatter& formatter) {
704 auto file_path = parser.GetString("file").value();
705 const bool apply = parser.HasFlag("apply");
706 const bool strict = parser.HasFlag("strict");
707 auto range = NormalizeRange(parser.GetString("range").value_or("all"));
708 if (!IncludeVanilla(range) && !IncludeExpanded(range)) {
709 return absl::InvalidArgumentError(
710 absl::StrFormat("Invalid range: %s", range));
711 }
712
713 auto entries_or = editor::LoadMessageBundleFromJson(file_path);
714 if (!entries_or.ok()) {
715 return entries_or.status();
716 }
717 auto entries = entries_or.value();
718
719 formatter.BeginObject("Message Bundle Import");
720 formatter.AddField("file", file_path);
721 formatter.AddField("range", range);
722 formatter.AddField("apply", apply);
723 formatter.AddField("strict", strict);
724 formatter.AddField("entries", static_cast<int>(entries.size()));
725
726 bool has_errors = false;
727 int parse_error_count = 0;
728 int error_count = 0;
729 int applied_updates = 0;
730 bool has_vanilla_entries = false;
731 bool has_expanded_entries = false;
732
733 struct ParsedEntry {
736 std::vector<std::string> line_warnings;
737 };
738 std::vector<ParsedEntry> parsed_entries;
739 parsed_entries.reserve(entries.size());
740
741 for (const auto& entry : entries) {
742 if ((entry.bank == editor::MessageBank::kVanilla &&
743 !IncludeVanilla(range)) ||
744 (entry.bank == editor::MessageBank::kExpanded &&
745 !IncludeExpanded(range))) {
746 continue;
747 }
748
749 ParsedEntry parsed{entry,
752 if (!parsed.parse.ok()) {
753 has_errors = true;
754 parse_error_count += static_cast<int>(parsed.parse.errors.size());
755 error_count += static_cast<int>(parsed.parse.errors.size());
756 }
757 if (entry.bank == editor::MessageBank::kVanilla) {
758 has_vanilla_entries = true;
759 } else {
760 has_expanded_entries = true;
761 }
762 parsed_entries.push_back(std::move(parsed));
763 }
764
765 formatter.BeginArray("messages");
766 for (const auto& parsed : parsed_entries) {
767 formatter.BeginObject();
768 formatter.AddField("id", parsed.entry.id);
769 formatter.AddField("bank", BankLabel(parsed.entry.bank));
770 formatter.AddField("text", parsed.entry.text);
771 formatter.AddField("encoded_length",
772 static_cast<int>(parsed.parse.bytes.size()));
773 if (!parsed.line_warnings.empty()) {
774 formatter.BeginArray("line_width_warnings");
775 for (const auto& warning : parsed.line_warnings) {
776 formatter.AddArrayItem(warning);
777 }
778 formatter.EndArray();
779 }
780 if (!parsed.parse.warnings.empty()) {
781 formatter.BeginArray("warnings");
782 for (const auto& warning : parsed.parse.warnings) {
783 formatter.AddArrayItem(warning);
784 }
785 formatter.EndArray();
786 }
787 if (!parsed.parse.errors.empty()) {
788 formatter.BeginArray("errors");
789 for (const auto& error : parsed.parse.errors) {
790 formatter.AddArrayItem(error);
791 }
792 formatter.EndArray();
793 }
794 formatter.EndObject();
795 }
796 formatter.EndArray();
797
798 Rom owned_rom;
799 Rom* active_rom = rom;
800
801 if (apply) {
802 if (active_rom == nullptr || !active_rom->is_loaded()) {
803 auto rom_path = parser.GetString("rom");
804 if (!rom_path.has_value() || rom_path->empty()) {
805 std::string global_rom_path = absl::GetFlag(FLAGS_rom);
806 if (!global_rom_path.empty()) {
807 rom_path = global_rom_path;
808 }
809 }
810 if (!rom_path.has_value() || rom_path->empty()) {
811 error_count++;
812 formatter.AddField("status", "error");
813 formatter.AddField("error",
814 "ROM not loaded; provide --rom when using --apply");
815 formatter.AddField("parse_error_count", parse_error_count);
816 formatter.AddField("error_count", error_count);
817 formatter.EndObject();
818 return absl::OkStatus();
819 }
820 auto load_status = owned_rom.LoadFromFile(*rom_path);
821 if (!load_status.ok()) {
822 error_count++;
823 formatter.AddField("status", "error");
824 formatter.AddField("error", std::string(load_status.message()));
825 formatter.AddField("parse_error_count", parse_error_count);
826 formatter.AddField("error_count", error_count);
827 formatter.EndObject();
828 return absl::OkStatus();
829 }
830 active_rom = &owned_rom;
831 }
832
833 if (has_errors) {
834 formatter.AddField("status", "error");
835 formatter.AddField("error", "Parse errors present; no changes applied");
836 formatter.AddField("parse_error_count", parse_error_count);
837 formatter.AddField("error_count", error_count);
838 formatter.EndObject();
839 if (strict && parse_error_count > 0) {
840 formatter.EndObject();
841 formatter.Print();
842 return absl::FailedPreconditionError(
843 "Strict validation failed due to parse errors");
844 }
845 return absl::OkStatus();
846 }
847
848 std::optional<ExpandedMutationContext> expanded_context;
849 if (IncludeExpanded(range) && has_expanded_entries) {
850 auto context_or = PreflightExpandedMutation(parser, *active_rom);
851 if (!context_or.ok()) {
852 error_count++;
853 formatter.AddField("status", "error");
854 formatter.AddField("error", std::string(context_or.status().message()));
855 formatter.AddField("parse_error_count", parse_error_count);
856 formatter.AddField("error_count", error_count);
857 formatter.EndObject();
858 return context_or.status();
859 }
860 expanded_context.emplace(std::move(context_or.value()));
861 if (!expanded_context->policy_warning.empty()) {
862 formatter.AddField("write_policy_warning",
863 expanded_context->policy_warning);
864 }
865 }
866
867 // Build both replacement banks and validate every selected ID before the
868 // first ROM write. This keeps a bad expanded ID from landing after a
869 // successful vanilla mutation in a mixed bundle.
870 std::vector<editor::MessageData> vanilla_messages;
871 if (IncludeVanilla(range) && has_vanilla_entries) {
872 vanilla_messages = editor::ReadAllTextData(
873 const_cast<uint8_t*>(active_rom->data()), editor::kTextData);
874 }
875
876 std::vector<std::string> expanded_texts;
877 if (expanded_context.has_value()) {
878 const auto expanded_messages = ReadExpandedMessages(
879 *active_rom, expanded_context->start, expanded_context->end);
880 auto bank_size_status = ValidateExpandedMessageBankSize(
881 expanded_messages.size(), *expanded_context);
882 if (!bank_size_status.ok()) {
883 formatter.AddField("status", "error");
884 formatter.AddField("error", std::string(bank_size_status.message()));
885 formatter.EndObject();
886 return bank_size_status;
887 }
888 expanded_texts.reserve(expanded_messages.size());
889 for (const auto& message : expanded_messages) {
890 expanded_texts.push_back(message.RawString);
891 }
892 }
893
894 for (const auto& parsed : parsed_entries) {
895 if (parsed.entry.bank == editor::MessageBank::kVanilla) {
896 if (parsed.entry.id < 0 ||
897 parsed.entry.id >= static_cast<int>(vanilla_messages.size())) {
898 has_errors = true;
899 error_count++;
900 continue;
901 }
902 auto& message = vanilla_messages[parsed.entry.id];
903 message.RawString = parsed.entry.text;
904 message.ContentsParsed = parsed.entry.text;
905 message.Data = parsed.parse.bytes;
906 message.DataParsed = parsed.parse.bytes;
907 } else {
908 if (!expanded_context.has_value()) {
909 continue;
910 }
911 const auto id_status =
912 ValidateExpandedMessageId(parsed.entry.id, *expanded_context);
913 if (!id_status.ok()) {
914 has_errors = true;
915 error_count++;
916 continue;
917 }
918 if (parsed.entry.id >= static_cast<int>(expanded_texts.size())) {
919 expanded_texts.resize(parsed.entry.id + 1);
920 }
921 expanded_texts[parsed.entry.id] = parsed.entry.text;
922 }
923 applied_updates++;
924 }
925
926 if (has_errors) {
927 formatter.AddField("status", "error");
928 formatter.AddField("error", "Invalid message IDs; no changes applied");
929 } else {
930 ScopedRomTransaction transaction(*active_rom);
931 if (IncludeVanilla(range) && has_vanilla_entries) {
932 auto status = editor::WriteAllTextData(active_rom, vanilla_messages);
933 if (!status.ok()) {
934 formatter.AddField("status", "error");
935 formatter.AddField("error", std::string(status.message()));
936 formatter.EndObject();
937 return status;
938 }
939 }
940
941 if (expanded_context.has_value()) {
942 auto status = editor::WriteExpandedTextData(
943 active_rom, expanded_context->start, expanded_context->end,
944 expanded_texts);
945 if (!status.ok()) {
946 formatter.AddField("status", "error");
947 formatter.AddField("error", std::string(status.message()));
948 formatter.EndObject();
949 return status;
950 }
951 }
952
953 if (active_rom->dirty()) {
954 auto save_status = active_rom->SaveToFile({.save_new = false});
955 if (!save_status.ok()) {
956 formatter.AddField("status", "error");
957 formatter.AddField("error", std::string(save_status.message()));
958 formatter.EndObject();
959 return save_status;
960 }
961 }
962 transaction.Commit();
963 formatter.AddField("status", "success");
964 formatter.AddField("applied_messages", applied_updates);
965 }
966 } else {
967 formatter.AddField("status", has_errors ? "error" : "success");
968 }
969
970 formatter.AddField("error_count", error_count);
971 formatter.AddField("parse_error_count", parse_error_count);
972 formatter.EndObject();
973 if (strict && parse_error_count > 0) {
974 formatter.EndObject();
975 formatter.Print();
976 return absl::FailedPreconditionError("Strict validation failed");
977 }
978 return absl::OkStatus();
979}
980
981// ===========================================================================
982// New: Message Write Command
983// ===========================================================================
984
986 Rom* rom, const resources::ArgumentParser& parser,
987 resources::OutputFormatter& formatter) {
988 auto id_or = parser.GetInt("id");
989 if (!id_or.ok())
990 return id_or.status();
991 int msg_id = id_or.value();
992
993 auto text = parser.GetString("text").value();
994
995 // Validate line widths first
996 auto warnings = editor::ValidateMessageLineWidths(text);
997
998 // Encode to bytes
999 auto bytes = editor::ParseMessageToData(text);
1000 if (bytes.empty() && !text.empty()) {
1001 return absl::InvalidArgumentError("Encoding produced no bytes");
1002 }
1003
1004 auto context_or = PreflightExpandedMutation(parser, *rom);
1005 if (!context_or.ok()) {
1006 return context_or.status();
1007 }
1008 auto context = std::move(context_or.value());
1009 auto id_status = ValidateExpandedMessageId(msg_id, context);
1010 if (!id_status.ok()) {
1011 return id_status;
1012 }
1013
1014 // Read existing expanded messages to find the target
1015 auto expanded = ReadExpandedMessages(*rom, context.start, context.end);
1016 auto bank_size_status =
1017 ValidateExpandedMessageBankSize(expanded.size(), context);
1018 if (!bank_size_status.ok()) {
1019 return bank_size_status;
1020 }
1021
1022 // Build the full message list, inserting/replacing at msg_id
1023 std::vector<std::string> all_texts;
1024 all_texts.reserve(expanded.size());
1025 for (const auto& msg : expanded) {
1026 all_texts.push_back(msg.RawString);
1027 }
1028 if (msg_id >= static_cast<int>(all_texts.size())) {
1029 all_texts.resize(msg_id + 1);
1030 }
1031 all_texts[msg_id] = text;
1032
1033 // Write back
1034 ScopedRomTransaction transaction(*rom);
1035 auto status =
1036 editor::WriteExpandedTextData(rom, context.start, context.end, all_texts);
1037 if (!status.ok())
1038 return status;
1039 transaction.Commit();
1040
1041 formatter.BeginObject("Message Write Result");
1042 formatter.AddField("id", msg_id);
1043 formatter.AddField("text", text);
1044 formatter.AddField("encoded_length", static_cast<int>(bytes.size()));
1045 formatter.AddField("status", "success");
1046 if (!context.policy_warning.empty()) {
1047 formatter.AddField("write_policy_warning", context.policy_warning);
1048 }
1049 if (!warnings.empty()) {
1050 formatter.BeginArray("line_width_warnings");
1051 for (const auto& warning : warnings) {
1052 formatter.AddArrayItem(warning);
1053 }
1054 formatter.EndArray();
1055 }
1056 formatter.EndObject();
1057
1058 return absl::OkStatus();
1059}
1060
1061// ===========================================================================
1062// New: Export BIN Command
1063// ===========================================================================
1064
1066 Rom* rom, const resources::ArgumentParser& parser,
1067 resources::OutputFormatter& formatter) {
1068 auto output_path = parser.GetString("output").value();
1069 auto range = parser.GetString("range").value_or("expanded");
1070
1071 int start, end_addr;
1072 if (range == "expanded") {
1074 end_addr = editor::GetExpandedTextDataEnd();
1075 } else {
1076 start = editor::kTextData;
1077 end_addr = editor::kTextDataEnd;
1078 }
1079
1080 // Find the actual end of data (scan for 0xFF terminator)
1081 const uint8_t* data = rom->data();
1082 int data_end = start;
1083 while (data_end <= end_addr && data[data_end] != 0xFF) {
1084 data_end++;
1085 }
1086 if (data_end <= end_addr) {
1087 data_end++; // Include the 0xFF terminator
1088 }
1089
1090 int size = data_end - start;
1091
1092 std::ofstream file(output_path, std::ios::binary);
1093 if (!file.is_open()) {
1094 return absl::InternalError(
1095 absl::StrFormat("Cannot write to file: %s", output_path));
1096 }
1097 file.write(reinterpret_cast<const char*>(data + start), size);
1098 file.close();
1099
1100 formatter.BeginObject("BIN Export Result");
1101 formatter.AddField("output", output_path);
1102 formatter.AddField("range", range);
1103 formatter.AddHexField("start_address", start, 6);
1104 formatter.AddHexField("end_address", data_end - 1, 6);
1105 formatter.AddField("size_bytes", size);
1106 formatter.AddField("status", "success");
1107 formatter.EndObject();
1108
1109 return absl::OkStatus();
1110}
1111
1112// ===========================================================================
1113// New: Export ASM Command
1114// ===========================================================================
1115
1117 Rom* rom, const resources::ArgumentParser& parser,
1118 resources::OutputFormatter& formatter) {
1119 auto output_path = parser.GetString("output").value();
1120 auto range = parser.GetString("range").value_or("expanded");
1121
1122 int start;
1123 uint32_t snes_addr;
1124 if (range == "expanded") {
1126 snes_addr = 0x2F8000;
1127 } else {
1128 start = editor::kTextData;
1129 snes_addr = 0x1C0000;
1130 }
1131
1132 // Read messages from the specified region
1133 auto messages =
1134 editor::ReadAllTextData(const_cast<uint8_t*>(rom->data()), start);
1135
1136 std::ofstream file(output_path);
1137 if (!file.is_open()) {
1138 return absl::InternalError(
1139 absl::StrFormat("Cannot write to file: %s", output_path));
1140 }
1141
1142 // Write ASM header
1143 file << "; Auto-generated message data\n";
1144 file << absl::StrFormat("; Source: %s region\n", range);
1145 file << absl::StrFormat("; Messages: %d\n\n", messages.size());
1146 file << absl::StrFormat("org $%06X\n\n", snes_addr);
1147
1148 // Write each message as db directives
1149 for (const auto& msg : messages) {
1150 file << absl::StrFormat("; Message $%02X: %s\n", msg.ID,
1151 msg.ContentsParsed.substr(0, 60));
1152 file << "db ";
1153 for (size_t i = 0; i < msg.Data.size(); ++i) {
1154 if (i > 0)
1155 file << ", ";
1156 file << absl::StrFormat("$%02X", msg.Data[i]);
1157 }
1158 file << ", $7F ; terminator\n\n";
1159 }
1160
1161 // End-of-region marker
1162 file << "db $FF ; end of message data\n";
1163 file.close();
1164
1165 formatter.BeginObject("ASM Export Result");
1166 formatter.AddField("output", output_path);
1167 formatter.AddField("range", range);
1168 formatter.AddField("messages_exported", static_cast<int>(messages.size()));
1169 formatter.AddField("status", "success");
1170 formatter.EndObject();
1171
1172 return absl::OkStatus();
1173}
1174
1176 Rom* rom, const resources::ArgumentParser& parser,
1177 resources::OutputFormatter& formatter) {
1178 const std::string project_path = parser.GetString("project").value();
1179 const std::string incoming_path = parser.GetString("file").value();
1180 const bool write = parser.HasFlag("write");
1181
1182 project::YazeProject source_project;
1183 auto& resource_labels = zelda3::GetResourceLabels();
1184 absl::Status open_status;
1185 {
1186 ScopedHackManifestBindingRestore restore_manifest(resource_labels);
1187 try {
1188 open_status = source_project.Open(project_path);
1189 } catch (const std::exception& error) {
1190 return absl::InvalidArgumentError(absl::StrFormat(
1191 "Cannot load project '%s': %s", project_path, error.what()));
1192 } catch (...) {
1193 return absl::InvalidArgumentError(absl::StrFormat(
1194 "Cannot load project '%s': unknown project parse error",
1195 project_path));
1196 }
1197 }
1198 if (!open_status.ok()) {
1199 return absl::Status(open_status.code(),
1200 absl::StrFormat("Cannot load project '%s': %s",
1201 project_path, open_status.message()));
1202 }
1203 if (absl::Status manifest_status =
1204 EnsureConfiguredHackManifestLoaded(&source_project, project_path);
1205 !manifest_status.ok()) {
1206 return manifest_status;
1207 }
1208
1210 .write = write,
1211 .expected_source_sha256 =
1212 parser.GetString("expected-source-sha256").value_or(""),
1213 };
1214 auto result_or =
1215 editor::SyncMessageSource(source_project, incoming_path, options);
1216 if (!result_or.ok()) {
1217 return result_or.status();
1218 }
1219 const auto& result = *result_or;
1220
1221 formatter.BeginObject("Message Source Sync");
1222 formatter.AddField("status", "success");
1223 formatter.AddField("mode", write ? "write" : "dry-run");
1224 formatter.AddField("changed", result.changed);
1225 formatter.AddField("wrote", result.wrote);
1226 formatter.AddField("canonical_bundle", result.canonical_bundle_path.string());
1227 formatter.AddField("generated_asm_include",
1228 result.generated_asm_include_path.string());
1229 formatter.AddHexField("first_expanded_id", result.first_expanded_id, 3);
1230 formatter.AddHexField("last_expanded_id", result.last_expanded_id, 3);
1231 formatter.AddField("expanded_count", result.expanded_count);
1232 formatter.AddField("incoming_updates", result.incoming_updates);
1233 formatter.AddField("encoded_size", static_cast<int>(result.encoded_size));
1234 formatter.AddField("capacity", static_cast<int>(result.capacity));
1235 formatter.AddField("source_sha256_before", result.source_sha256_before);
1236 formatter.AddField("proposed_source_sha256", result.proposed_source_sha256);
1237 formatter.EndObject();
1238 return absl::OkStatus();
1239}
1240
1241} // namespace handlers
1242} // namespace cli
1243} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
absl::Status LoadFromFile(const std::string &filename, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:227
auto filename() const
Definition rom.h:157
auto end()
Definition rom.h:154
absl::Status SaveToFile(const SaveSettings &settings)
Definition rom.cc:371
auto data() const
Definition rom.h:151
auto size() const
Definition rom.h:150
bool dirty() const
Definition rom.h:145
bool is_loaded() const
Definition rom.h:144
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
ScopedHackManifestBindingRestore & operator=(const ScopedHackManifestBindingRestore &)=delete
Utility for parsing common CLI argument patterns.
std::optional< std::string > GetString(const std::string &name) const
Parse a named argument (e.g., –format=json or –format json)
bool HasFlag(const std::string &name) const
Check if a flag is present.
absl::StatusOr< int > GetInt(const std::string &name) const
Parse an integer argument (supports hex with 0x prefix)
Utility for consistent output formatting across commands.
void BeginArray(const std::string &key)
Begin an array.
void AddArrayItem(const std::string &item)
Add an item to current array.
void BeginObject(const std::string &title="")
Start a JSON object or text section.
void EndObject()
End a JSON object or text section.
void AddField(const std::string &key, const std::string &value)
Add a key-value pair.
void AddHexField(const std::string &key, uint64_t value, int width=2)
Add a hex-formatted field.
void Print() const
Print the formatted output to stdout.
Loads and queries the hack manifest JSON for yaze-ASM integration.
absl::Status LoadFromFile(const std::string &filepath)
Load manifest from a JSON file path.
bool loaded() const
Check if the manifest has been loaded.
Unified interface for accessing resource labels with project overrides.
ABSL_DECLARE_FLAG(std::string, rom)
absl::Status EnsureConfiguredHackManifestLoaded(project::YazeProject *project, absl::string_view project_path)
absl::Status ValidateExpandedMessageBankSize(size_t message_count, const ExpandedMutationContext &context)
absl::Status ValidateExpandedMessageId(int id, const ExpandedMutationContext &context)
absl::StatusOr< std::filesystem::path > CanonicalExistingPath(const std::string &path, absl::string_view label)
std::string FormatManifestConflict(const core::WriteConflict &conflict)
std::vector< editor::MessageData > ReadExpandedMessages(const Rom &rom)
absl::StatusOr< ExpandedMutationContext > PreflightExpandedMutation(const resources::ArgumentParser &parser, const Rom &rom)
std::string AddressOwnershipToString(AddressOwnership ownership)
int GetExpandedTextDataStart()
std::string ParseTextDataByte(uint8_t value)
absl::Status WriteAllTextData(Rom *rom, const std::vector< MessageData > &messages)
constexpr int kTextData
std::string MessageBankToString(MessageBank bank)
absl::Status WriteExpandedTextData(Rom *rom, int start, int end, const std::vector< std::string > &messages)
absl::StatusOr< std::vector< MessageBundleEntry > > LoadMessageBundleFromJson(const std::string &path)
std::vector< MessageData > ReadAllTextData(uint8_t *rom, int pos, int max_pos, bool allow_bank_switch)
std::vector< uint8_t > ParseMessageToData(std::string str)
absl::Status ExportMessageBundleToJson(const std::string &path, const std::vector< MessageData > &vanilla, const std::vector< MessageData > &expanded)
std::string ExportToOrgFormat(const std::vector< std::pair< int, std::string > > &messages, const std::vector< std::string > &labels)
std::vector< MessageData > ReadExpandedTextData(uint8_t *rom, int pos)
std::optional< TextElement > FindMatchingCommand(uint8_t b)
MessageParseResult ParseMessageToDataWithDiagnostics(std::string_view str)
int GetExpandedTextDataEnd()
absl::StatusOr< MessageSourceSyncResult > SyncMessageSource(const project::YazeProject &project, const fs::path &incoming_bundle_path, const MessageSourceSyncOptions &options)
std::vector< std::string > ValidateMessageLineWidths(const std::string &message)
std::vector< std::pair< int, std::string > > ParseOrgContent(const std::string &content)
constexpr int kTextDataEnd
ResourceLabelProvider & GetResourceLabels()
Get the global ResourceLabelProvider instance.
uint32_t PcToSnes(uint32_t addr)
Definition snes.h:17
uint32_t SnesToPc(uint32_t addr) noexcept
Definition snes.h:8
A conflict detected when yaze wants to write to an ASM-owned address.
AddressOwnership ownership
RomWritePolicy write_policy
Definition project.h:110
Modern project structure with comprehensive settings consolidation.
Definition project.h:172
core::HackManifest hack_manifest
Definition project.h:212
std::string hack_manifest_file
Definition project.h:194
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:1486
absl::Status Open(const std::string &project_path)
Definition project.cc:429