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