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
73
75 int start = 0;
76 int end = 0;
77 int message_limit = -1;
78 std::string policy_warning;
79};
80
82 public:
85 : provider_(provider), previous_(provider.hack_manifest()) {}
86
88 delete;
90 const ScopedHackManifestBindingRestore&) = delete;
91
92 ~ScopedHackManifestBindingRestore() { provider_.SetHackManifest(previous_); }
93
94 private:
96 const core::HackManifest* previous_ = nullptr;
97};
98
99absl::StatusOr<std::filesystem::path> CanonicalExistingPath(
100 const std::string& path, absl::string_view label) {
101 if (path.empty()) {
102 return absl::FailedPreconditionError(
103 absl::StrFormat("%s path is empty", label));
104 }
105
106 std::error_code ec;
107 auto canonical_path = std::filesystem::canonical(path, ec);
108 if (ec) {
109 return absl::FailedPreconditionError(absl::StrFormat(
110 "Cannot resolve %s path '%s': %s", label, path, ec.message()));
111 }
112 return canonical_path;
113}
114
116 project::YazeProject* project, absl::string_view project_path) {
117 if (project->hack_manifest.loaded() || project->hack_manifest_file.empty()) {
118 return absl::OkStatus();
119 }
120
121 const std::string manifest_path =
122 project->GetAbsolutePath(project->hack_manifest_file);
123 const absl::Status manifest_status =
124 project->hack_manifest.LoadFromFile(manifest_path);
125 if (manifest_status.ok()) {
126 return absl::OkStatus();
127 }
128 return absl::Status(
129 manifest_status.code(),
130 absl::StrFormat("Cannot load project '%s': hack manifest '%s': %s",
131 project_path, manifest_path, manifest_status.message()));
132}
133
134std::string FormatManifestConflict(const core::WriteConflict& conflict) {
135 std::string result =
136 absl::StrFormat("address 0x%06X is %s", conflict.address,
138 if (!conflict.module.empty()) {
139 absl::StrAppend(&result, " (Module: ", conflict.module, ")");
140 }
141 return result;
142}
143
144bool IsCanonicalMappedLoRomAddress(uint32_t address) {
145 const uint8_t bank = static_cast<uint8_t>((address >> 16) & 0xFFu);
146 if (bank == 0x7E || bank == 0x7F || (address & 0xFFFFu) < 0x8000u) {
147 return false;
148 }
149 return PcToSnes(SnesToPc(address)) == address;
150}
151
152absl::StatusOr<ProjectMutationContext> LoadProjectMutationContext(
153 const resources::ArgumentParser& parser, const Rom& rom,
154 absl::string_view mutation_scope) {
155 auto project_path = parser.GetString("project");
156 if (!project_path.has_value() || project_path->empty()) {
157 return absl::FailedPreconditionError(absl::StrFormat(
158 "%s requires explicit --project so Yaze can verify ROM ownership and "
159 "write policy",
160 mutation_scope));
161 }
162 if (!rom.is_loaded()) {
163 return absl::FailedPreconditionError("ROM is not loaded");
164 }
165
167 auto& resource_labels = zelda3::GetResourceLabels();
168 absl::Status open_status;
169 {
170 // YazeProject::Open temporarily installs its manifest in the
171 // process-global label provider. Restore the embedding application's prior
172 // non-owning pointer on every path, including malformed project data that
173 // throws from a parser.
174 ScopedHackManifestBindingRestore restore_manifest(resource_labels);
175 try {
176 open_status = context.project.Open(*project_path);
177 } catch (const std::exception& error) {
178 return absl::InvalidArgumentError(absl::StrFormat(
179 "Cannot load project '%s': %s", *project_path, error.what()));
180 } catch (...) {
181 return absl::InvalidArgumentError(absl::StrFormat(
182 "Cannot load project '%s': unknown project parse error",
183 *project_path));
184 }
185 }
186 if (!open_status.ok()) {
187 return absl::Status(open_status.code(),
188 absl::StrFormat("Cannot load project '%s': %s",
189 *project_path, open_status.message()));
190 }
191 if (absl::Status manifest_status =
192 EnsureConfiguredHackManifestLoaded(&context.project, *project_path);
193 !manifest_status.ok()) {
194 return manifest_status;
195 }
196
197 auto active_rom_path_or = CanonicalExistingPath(rom.filename(), "active ROM");
198 if (!active_rom_path_or.ok()) {
199 return active_rom_path_or.status();
200 }
201 auto project_rom_path_or = CanonicalExistingPath(
203 "project ROM");
204 if (!project_rom_path_or.ok()) {
205 return project_rom_path_or.status();
206 }
207 if (*active_rom_path_or != *project_rom_path_or) {
208 return absl::FailedPreconditionError(absl::StrFormat(
209 "Project ROM mismatch: active ROM is '%s' but --project binds to '%s'",
210 active_rom_path_or->string(), project_rom_path_or->string()));
211 }
212 std::error_code file_size_ec;
213 const uintmax_t raw_file_size =
214 std::filesystem::file_size(*active_rom_path_or, file_size_ec);
215 if (file_size_ec) {
216 return absl::FailedPreconditionError(
217 absl::StrFormat("Cannot inspect active ROM size '%s': %s",
218 active_rom_path_or->string(), file_size_ec.message()));
219 }
220 constexpr uintmax_t kSnesBankSize = 0x8000;
221 constexpr uintmax_t kCopierHeaderSize = 0x200;
222 if (raw_file_size >= kCopierHeaderSize &&
223 raw_file_size % kSnesBankSize == kCopierHeaderSize) {
224 return absl::FailedPreconditionError(
225 "Message mutation requires a headerless ROM file; remove the "
226 "512-byte copier header and reopen the ROM");
227 }
228 if (raw_file_size != rom.size()) {
229 return absl::FailedPreconditionError(absl::StrFormat(
230 "Message mutation requires a headerless ROM file: disk size is 0x%zX "
231 "but the loaded ROM is 0x%zX bytes",
232 static_cast<size_t>(raw_file_size), rom.size()));
233 }
234 context.canonical_rom_path = *active_rom_path_or;
235
236 const auto& manifest = context.project.hack_manifest;
237 if (!manifest.loaded()) {
238 return absl::FailedPreconditionError(absl::StrFormat(
239 "Project '%s' has no loaded hack manifest; configure a valid "
240 "hack_manifest_file before mutating messages",
241 *project_path));
242 }
243
244 return context;
245}
246
247absl::StatusOr<ExpandedMutationContext> PreflightExpandedMutation(
248 const project::YazeProject& yaze_project, const Rom& rom) {
250 const auto& manifest = yaze_project.hack_manifest;
251 const auto& layout = manifest.message_layout();
252 if (layout.data_start == 0 || layout.data_end == 0 ||
253 layout.data_end < layout.data_start ||
254 !IsCanonicalMappedLoRomAddress(layout.data_start) ||
255 !IsCanonicalMappedLoRomAddress(layout.data_end)) {
256 return absl::FailedPreconditionError(
257 "Hack manifest does not define a valid LoROM expanded message region");
258 }
259
260 const uint32_t start = SnesToPc(layout.data_start);
261 const uint32_t end = SnesToPc(layout.data_end);
262 if (end < start ||
263 end > static_cast<uint32_t>(std::numeric_limits<int>::max()) ||
264 end >= rom.size()) {
265 return absl::OutOfRangeError(absl::StrFormat(
266 "Configured expanded message region [0x%06X, 0x%06X] is outside the "
267 "active ROM (size=0x%zX)",
268 start, end, rom.size()));
269 }
270 context.start = static_cast<int>(start);
271 context.end = static_cast<int>(end);
272
273 // Even a manifest without ID metadata has a strict physical upper bound:
274 // each message requires at least a 0x7F terminator and the bank needs a final
275 // 0xFF. Keeping this limit finite prevents hostile IDs from driving a huge
276 // sparse-vector allocation.
277 const int region_capacity = context.end - context.start + 1;
278 const int capacity_message_limit = std::max(0, region_capacity - 1);
279 context.message_limit = capacity_message_limit;
280 if (layout.expanded_count > 0) {
281 context.message_limit =
282 std::min(layout.expanded_count, capacity_message_limit);
283 }
284 if (layout.last_expanded_id >= layout.first_expanded_id &&
285 (layout.first_expanded_id != 0 || layout.last_expanded_id != 0)) {
286 const int declared_limit =
287 static_cast<int>(layout.last_expanded_id - layout.first_expanded_id) +
288 1;
289 context.message_limit = std::min(context.message_limit, declared_limit);
290 }
291
292 const std::string lowered_hack_name =
293 absl::AsciiStrToLower(manifest.hack_name());
294 if (yaze_project.rom_metadata.write_policy ==
296 absl::StrContains(lowered_hack_name, "oracle of secrets")) {
297 return absl::PermissionDeniedError(
298 "Oracle of Secrets expanded messages are owned by ASM. No message "
299 "bytes were written; edit Core/message.asm, rebuild Oracle of Secrets, "
300 "and reopen the rebuilt ROM before retrying.");
301 }
302
303 const auto conflicts =
304 manifest.AnalyzePcWriteRanges({{start, static_cast<uint32_t>(end + 1u)}});
305 if (!conflicts.empty()) {
306 const std::string detail = FormatManifestConflict(conflicts.front());
307 if (yaze_project.rom_metadata.write_policy ==
309 return absl::PermissionDeniedError(absl::StrFormat(
310 "Expanded message mutation blocked by hack manifest: %s", detail));
311 }
312 if (yaze_project.rom_metadata.write_policy ==
314 context.policy_warning = absl::StrFormat(
315 "Expanded message region conflicts with hack manifest: %s", detail);
316 }
317 }
318
319 return context;
320}
321
322absl::StatusOr<ExpandedMutationContext> PreflightExpandedMutation(
323 const resources::ArgumentParser& parser, const Rom& rom) {
324 auto project_context_or =
325 LoadProjectMutationContext(parser, rom, "Expanded message mutation");
326 if (!project_context_or.ok()) {
327 return project_context_or.status();
328 }
329 ProjectMutationContext project_context =
330 std::move(project_context_or.value());
331 return PreflightExpandedMutation(project_context.project, rom);
332}
333
334absl::Status VerifyCleanDiskSnapshot(const Rom& rom,
335 std::vector<uint8_t>* baseline) {
336 if (rom.dirty()) {
337 return absl::FailedPreconditionError(
338 "Message bundle apply requires a clean ROM; save or discard existing "
339 "in-memory changes before retrying");
340 }
341
342 Rom disk_rom;
343 Rom::LoadOptions load_options;
344 load_options.load_resource_labels = false;
345 const absl::Status load_status =
346 disk_rom.LoadFromFile(rom.filename(), load_options);
347 if (!load_status.ok()) {
348 return load_status;
349 }
350 if (disk_rom.vector() != rom.vector()) {
351 return absl::FailedPreconditionError(
352 "Active ROM no longer matches the file on disk; reopen it before "
353 "applying a message bundle");
354 }
355 if (baseline != nullptr) {
356 *baseline = disk_rom.vector();
357 }
358 return absl::OkStatus();
359}
360
362 const std::filesystem::path& rom_path,
363 const std::vector<uint8_t>& expected_bytes) {
364 std::error_code file_size_ec;
365 const uintmax_t raw_file_size =
366 std::filesystem::file_size(rom_path, file_size_ec);
367 if (file_size_ec ||
368 raw_file_size != static_cast<uintmax_t>(expected_bytes.size())) {
369 return absl::AbortedError(
370 "ROM size changed on disk after message preflight; no bytes were "
371 "saved. Reopen the ROM and retry");
372 }
373 Rom disk_rom;
374 Rom::LoadOptions load_options;
375 load_options.load_resource_labels = false;
376 const absl::Status load_status =
377 disk_rom.LoadFromFile(rom_path.string(), load_options);
378 if (!load_status.ok()) {
379 return load_status;
380 }
381 if (disk_rom.vector() != expected_bytes) {
382 return absl::AbortedError(
383 "ROM changed on disk after message preflight; no bytes were saved. "
384 "Reopen the ROM and retry");
385 }
386 return absl::OkStatus();
387}
388
389absl::Status ValidateVanillaMutation(const project::YazeProject& yaze_project,
391 std::string* policy_warning) {
392 const auto conflicts =
394 if (conflicts.empty()) {
395 return absl::OkStatus();
396 }
397
398 const std::string detail = FormatManifestConflict(conflicts.front());
399 if (yaze_project.rom_metadata.write_policy ==
401 return absl::PermissionDeniedError(absl::StrFormat(
402 "Vanilla message mutation blocked by hack manifest: %s; no message "
403 "bytes were written",
404 detail));
405 }
406 if (yaze_project.rom_metadata.write_policy ==
408 policy_warning != nullptr) {
409 *policy_warning = absl::StrFormat(
410 "Vanilla message write conflicts with hack manifest: %s", detail);
411 }
412 return absl::OkStatus();
413}
414
415absl::Status VerifySavedRom(
416 const Rom& active_rom, const std::filesystem::path& saved_rom_path,
417 const std::optional<editor::VanillaMessageSavePlan>& vanilla_plan,
418 std::optional<size_t> expected_vanilla_count) {
419 Rom reopened_rom;
420 Rom::LoadOptions load_options;
421 load_options.load_resource_labels = false;
422 const absl::Status reopen_status =
423 reopened_rom.LoadFromFile(saved_rom_path.string(), load_options);
424 if (!reopen_status.ok()) {
425 return absl::DataLossError(absl::StrFormat(
426 "ROM was saved but could not be reopened for verification: %s. "
427 "Restore the required backup before retrying",
428 reopen_status.message()));
429 }
430 if (reopened_rom.vector() != active_rom.vector()) {
431 return absl::DataLossError(
432 "ROM was saved but external readback differs from the planned bytes. "
433 "Restore the required backup before retrying");
434 }
435
436 if (vanilla_plan.has_value()) {
437 if (reopened_rom.size() >
438 static_cast<size_t>(std::numeric_limits<int>::max())) {
439 return absl::DataLossError(
440 "ROM was saved but is too large for bounded vanilla-message "
441 "readback. Restore the required backup before retrying");
442 }
443 const auto reopened_messages =
445 static_cast<int>(reopened_rom.size()));
446 auto readback_plan_or = editor::BuildVanillaMessageSavePlan(
447 reopened_messages, expected_vanilla_count);
448 if (!readback_plan_or.ok() ||
449 readback_plan_or.value() != vanilla_plan.value()) {
450 const std::string detail =
451 readback_plan_or.ok()
452 ? "serialized message bytes differ from the save plan"
453 : std::string(readback_plan_or.status().message());
454 return absl::DataLossError(absl::StrFormat(
455 "ROM was saved but vanilla-message readback failed: %s. Restore the "
456 "required backup before retrying",
457 detail));
458 }
459 }
460
461 return absl::OkStatus();
462}
463
464absl::Status ValidateExpandedMessageId(int id,
465 const ExpandedMutationContext& context) {
466 if (id < 0) {
467 return absl::InvalidArgumentError(
468 absl::StrFormat("Invalid expanded message ID: %d", id));
469 }
470 if (context.message_limit >= 0 && id >= context.message_limit) {
471 return absl::OutOfRangeError(absl::StrFormat(
472 "Expanded message ID %d is outside the manifest range (count=%d)", id,
473 context.message_limit));
474 }
475 return absl::OkStatus();
476}
477
479 size_t message_count, const ExpandedMutationContext& context) {
480 if (context.message_limit >= 0 &&
481 message_count > static_cast<size_t>(context.message_limit)) {
482 return absl::FailedPreconditionError(absl::StrFormat(
483 "Expanded message bank contains %zu messages, exceeding the manifest "
484 "limit of %d; no changes were applied",
485 message_count, context.message_limit));
486 }
487 return absl::OkStatus();
488}
489} // namespace
490
491// ===========================================================================
492// Existing Commands
493// ===========================================================================
494
496 Rom* rom, const resources::ArgumentParser& parser,
497 resources::OutputFormatter& formatter) {
498 auto limit = parser.GetInt("limit").value_or(50);
499 if (limit < 0) {
500 limit = 0;
501 }
502
503 auto messages = editor::ReadAllTextData(const_cast<uint8_t*>(rom->data()),
505 if (limit > static_cast<int>(messages.size())) {
506 limit = static_cast<int>(messages.size());
507 }
508
509 formatter.BeginObject("Message List");
510 formatter.AddField("limit", limit);
511 formatter.AddField("total_messages", static_cast<int>(messages.size()));
512 formatter.AddField("status", "success");
513
514 formatter.BeginArray("messages");
515 for (int i = 0; i < limit; ++i) {
516 const auto& msg = messages[i];
517 formatter.BeginObject();
518 formatter.AddField("id", msg.ID);
519 formatter.AddHexField("address", msg.Address, 6);
520 formatter.AddField("text", msg.ContentsParsed);
521 formatter.AddField("length", static_cast<int>(msg.Data.size()));
522 formatter.EndObject();
523 }
524 formatter.EndArray();
525 formatter.EndObject();
526
527 return absl::OkStatus();
528}
529
531 Rom* rom, const resources::ArgumentParser& parser,
532 resources::OutputFormatter& formatter) {
533 auto message_id_or = parser.GetInt("id");
534 if (!message_id_or.ok()) {
535 return message_id_or.status();
536 }
537 int message_id = message_id_or.value();
538
539 auto messages = editor::ReadAllTextData(const_cast<uint8_t*>(rom->data()),
541 if (message_id < 0 || message_id >= static_cast<int>(messages.size())) {
542 return absl::NotFoundError(
543 absl::StrFormat("Message ID %d not found (max: %d)", message_id,
544 static_cast<int>(messages.size()) - 1));
545 }
546
547 const auto& msg = messages[message_id];
548
549 formatter.BeginObject("Message");
550 formatter.AddField("id", msg.ID);
551 formatter.AddHexField("address", msg.Address, 6);
552 formatter.AddField("text", msg.ContentsParsed);
553 formatter.AddField("length", static_cast<int>(msg.Data.size()));
554 formatter.EndObject();
555
556 return absl::OkStatus();
557}
558
560 Rom* rom, const resources::ArgumentParser& parser,
561 resources::OutputFormatter& formatter) {
562 auto query = parser.GetString("query").value();
563 auto limit = parser.GetInt("limit").value_or(10);
564 if (limit < 0) {
565 limit = 0;
566 }
567
568 auto messages = editor::ReadAllTextData(const_cast<uint8_t*>(rom->data()),
570
571 formatter.BeginObject("Message Search Results");
572 formatter.AddField("query", query);
573 formatter.AddField("limit", limit);
574 formatter.AddField("status", "success");
575
576 std::string lowered_query = absl::AsciiStrToLower(query);
577 std::vector<const editor::MessageData*> matches;
578 for (const auto& msg : messages) {
579 std::string lowered_text = absl::AsciiStrToLower(msg.ContentsParsed);
580 if (lowered_text.find(lowered_query) == std::string::npos) {
581 continue;
582 }
583 matches.push_back(&msg);
584 }
585
586 formatter.AddField("matches_found", static_cast<int>(matches.size()));
587 formatter.BeginArray("matches");
588 int match_count = 0;
589 for (const auto* msg : matches) {
590 if (limit > 0 && match_count >= limit) {
591 break;
592 }
593 formatter.BeginObject();
594 formatter.AddField("id", msg->ID);
595 formatter.AddHexField("address", msg->Address, 6);
596 formatter.AddField("text", msg->ContentsParsed);
597 formatter.EndObject();
598 match_count++;
599 }
600 formatter.EndArray();
601 formatter.EndObject();
602
603 return absl::OkStatus();
604}
605
606// ===========================================================================
607// New: Encode Command
608// ===========================================================================
609
611 Rom* rom, const resources::ArgumentParser& parser,
612 resources::OutputFormatter& formatter) {
613 auto text = parser.GetString("text").value();
614
615 auto parse_result = editor::ParseMessageToDataWithDiagnostics(text);
616 auto bytes = parse_result.bytes;
617
618 // Build hex string
619 std::string hex_str;
620 for (size_t i = 0; i < bytes.size(); ++i) {
621 if (i > 0)
622 hex_str += " ";
623 hex_str += absl::StrFormat("%02X", bytes[i]);
624 }
625
626 formatter.BeginObject("Encoded Message");
627 formatter.AddField("input", text);
628 formatter.AddField("hex", hex_str);
629 formatter.AddField("length", static_cast<int>(bytes.size()));
630
631 // Also output as byte array for JSON consumers
632 formatter.BeginArray("bytes");
633 for (uint8_t byte : bytes) {
634 formatter.AddArrayItem(absl::StrFormat("0x%02X", byte));
635 }
636 formatter.EndArray();
637
638 // Run line width validation
639 auto warnings = editor::ValidateMessageLineWidths(text);
640 if (!warnings.empty()) {
641 formatter.BeginArray("line_width_warnings");
642 for (const auto& warning : warnings) {
643 formatter.AddArrayItem(warning);
644 }
645 formatter.EndArray();
646 }
647 if (!parse_result.warnings.empty()) {
648 formatter.BeginArray("warnings");
649 for (const auto& warning : parse_result.warnings) {
650 formatter.AddArrayItem(warning);
651 }
652 formatter.EndArray();
653 }
654 if (!parse_result.errors.empty()) {
655 formatter.BeginArray("errors");
656 for (const auto& error : parse_result.errors) {
657 formatter.AddArrayItem(error);
658 }
659 formatter.EndArray();
660 }
661
662 formatter.EndObject();
663
664 return absl::OkStatus();
665}
666
667// ===========================================================================
668// New: Decode Command
669// ===========================================================================
670
672 Rom* rom, const resources::ArgumentParser& parser,
673 resources::OutputFormatter& formatter) {
674 auto hex_input = parser.GetString("hex").value();
675
676 // Parse hex string to bytes
677 std::vector<uint8_t> bytes;
678 std::istringstream hex_stream(hex_input);
679 std::string hex_byte;
680 while (hex_stream >> hex_byte) {
681 try {
682 bytes.push_back(static_cast<uint8_t>(std::stoi(hex_byte, nullptr, 16)));
683 } catch (const std::exception&) {
684 return absl::InvalidArgumentError(
685 absl::StrFormat("Invalid hex byte: '%s'", hex_byte));
686 }
687 }
688
689 // Decode bytes to text, handling commands with arguments
690 std::string decoded;
691 for (size_t i = 0; i < bytes.size(); ++i) {
692 uint8_t byte = bytes[i];
693
694 // Check for command (may consume next byte as argument)
695 auto cmd = editor::FindMatchingCommand(byte);
696 if (cmd.has_value()) {
697 if (cmd->HasArgument && i + 1 < bytes.size()) {
698 decoded += cmd->GetParamToken(bytes[++i]);
699 } else {
700 decoded += cmd->GetParamToken();
701 }
702 continue;
703 }
704
705 // Single-byte lookup for chars, specials, dictionary
706 decoded += editor::ParseTextDataByte(byte);
707 }
708
709 formatter.BeginObject("Decoded Message");
710 formatter.AddField("hex", hex_input);
711 formatter.AddField("text", decoded);
712 formatter.AddField("length", static_cast<int>(bytes.size()));
713 formatter.EndObject();
714
715 return absl::OkStatus();
716}
717
718// ===========================================================================
719// New: Import Org Command
720// ===========================================================================
721
723 Rom* rom, const resources::ArgumentParser& parser,
724 resources::OutputFormatter& formatter) {
725 auto file_path = parser.GetString("file").value();
726
727 // Read the .org file
728 std::ifstream file(file_path);
729 if (!file.is_open()) {
730 return absl::NotFoundError(
731 absl::StrFormat("Cannot open file: %s", file_path));
732 }
733
734 std::string content((std::istreambuf_iterator<char>(file)),
735 std::istreambuf_iterator<char>());
736 file.close();
737
738 auto messages = editor::ParseOrgContent(content);
739
740 formatter.BeginObject("Org Import Results");
741 formatter.AddField("file", file_path);
742 formatter.AddField("messages_parsed", static_cast<int>(messages.size()));
743
744 formatter.BeginArray("messages");
745 for (const auto& [msg_id, body] : messages) {
746 // Encode the body text to bytes with diagnostics
747 auto parse_result = editor::ParseMessageToDataWithDiagnostics(body);
748 auto bytes = parse_result.bytes;
749 auto warnings = editor::ValidateMessageLineWidths(body);
750
751 std::string hex_str;
752 for (size_t i = 0; i < bytes.size(); ++i) {
753 if (i > 0)
754 hex_str += " ";
755 hex_str += absl::StrFormat("%02X", bytes[i]);
756 }
757
758 formatter.BeginObject();
759 formatter.AddHexField("id", msg_id, 2);
760 formatter.AddField("text", body);
761 formatter.AddField("hex", hex_str);
762 formatter.AddField("encoded_length", static_cast<int>(bytes.size()));
763 if (!warnings.empty()) {
764 formatter.BeginArray("warnings");
765 for (const auto& warning : warnings) {
766 formatter.AddArrayItem(warning);
767 }
768 formatter.EndArray();
769 }
770 if (!parse_result.warnings.empty()) {
771 formatter.BeginArray("parse_warnings");
772 for (const auto& warning : parse_result.warnings) {
773 formatter.AddArrayItem(warning);
774 }
775 formatter.EndArray();
776 }
777 if (!parse_result.errors.empty()) {
778 formatter.BeginArray("errors");
779 for (const auto& error : parse_result.errors) {
780 formatter.AddArrayItem(error);
781 }
782 formatter.EndArray();
783 }
784 formatter.EndObject();
785 }
786 formatter.EndArray();
787 formatter.EndObject();
788
789 return absl::OkStatus();
790}
791
792// ===========================================================================
793// New: Export Org Command
794// ===========================================================================
795
797 Rom* rom, const resources::ArgumentParser& parser,
798 resources::OutputFormatter& formatter) {
799 auto output_path = parser.GetString("output").value();
800
801 auto messages = editor::ReadAllTextData(const_cast<uint8_t*>(rom->data()),
803
804 // Build message pairs and labels
805 std::vector<std::pair<int, std::string>> msg_pairs;
806 std::vector<std::string> labels;
807 for (const auto& msg : messages) {
808 msg_pairs.push_back({msg.ID, msg.RawString});
809 labels.push_back(absl::StrFormat("Message %02X", msg.ID));
810 }
811
812 std::string org_content = editor::ExportToOrgFormat(msg_pairs, labels);
813
814 // Write to file
815 std::ofstream file(output_path);
816 if (!file.is_open()) {
817 return absl::InternalError(
818 absl::StrFormat("Cannot write to file: %s", output_path));
819 }
820 file << org_content;
821 file.close();
822
823 formatter.BeginObject("Org Export Results");
824 formatter.AddField("output", output_path);
825 formatter.AddField("messages_exported", static_cast<int>(messages.size()));
826 formatter.AddField("status", "success");
827 formatter.EndObject();
828
829 return absl::OkStatus();
830}
831
832// ===========================================================================
833// New: Export Bundle Command
834// ===========================================================================
835
837 Rom* rom, const resources::ArgumentParser& parser,
838 resources::OutputFormatter& formatter) {
839 auto output_path = parser.GetString("output").value();
840 auto range = NormalizeRange(parser.GetString("range").value_or("all"));
841 if (!IncludeVanilla(range) && !IncludeExpanded(range)) {
842 return absl::InvalidArgumentError(
843 absl::StrFormat("Invalid range: %s", range));
844 }
845
846 std::vector<editor::MessageData> vanilla;
847 std::vector<editor::MessageData> expanded;
848
849 if (IncludeVanilla(range)) {
850 vanilla = editor::ReadAllTextData(const_cast<uint8_t*>(rom->data()),
852 }
853
854 if (IncludeExpanded(range)) {
855 expanded = ReadExpandedMessages(*rom);
856 }
857
858 auto status =
859 editor::ExportMessageBundleToJson(output_path, vanilla, expanded);
860 if (!status.ok()) {
861 return status;
862 }
863
864 formatter.BeginObject("Message Bundle Export");
865 formatter.AddField("output", output_path);
866 formatter.AddField("range", range);
867 formatter.AddField("vanilla_count", static_cast<int>(vanilla.size()));
868 formatter.AddField("expanded_count", static_cast<int>(expanded.size()));
869 formatter.AddField("status", "success");
870 formatter.EndObject();
871
872 return absl::OkStatus();
873}
874
875// ===========================================================================
876// New: Import Bundle Command
877// ===========================================================================
878
880 Rom* rom, const resources::ArgumentParser& parser,
881 resources::OutputFormatter& formatter) {
882 auto file_path = parser.GetString("file").value();
883 const bool apply = parser.HasFlag("apply");
884 const bool strict = parser.HasFlag("strict");
885#ifdef __EMSCRIPTEN__
886 if (apply) {
887 return absl::FailedPreconditionError(
888 "message-import-bundle --apply is unavailable in WebAssembly because "
889 "the browser filesystem cannot provide durable backup and readback "
890 "guarantees");
891 }
892#endif
893 auto range = NormalizeRange(parser.GetString("range").value_or("all"));
894 if (!IncludeVanilla(range) && !IncludeExpanded(range)) {
895 return absl::InvalidArgumentError(
896 absl::StrFormat("Invalid range: %s", range));
897 }
898
899 auto entries_or = editor::LoadMessageBundleFromJson(file_path);
900 if (!entries_or.ok()) {
901 return entries_or.status();
902 }
903 auto entries = entries_or.value();
904
905 formatter.BeginObject("Message Bundle Import");
906 formatter.AddField("file", file_path);
907 formatter.AddField("range", range);
908 formatter.AddField("apply", apply);
909 formatter.AddField("strict", strict);
910 formatter.AddField("entries", static_cast<int>(entries.size()));
911
912 bool has_errors = false;
913 int parse_error_count = 0;
914 int error_count = 0;
915 int applied_updates = 0;
916 bool has_vanilla_entries = false;
917 bool has_expanded_entries = false;
918
919 struct ParsedEntry {
922 std::vector<std::string> line_warnings;
923 };
924 std::vector<ParsedEntry> parsed_entries;
925 parsed_entries.reserve(entries.size());
926
927 for (const auto& entry : entries) {
928 if ((entry.bank == editor::MessageBank::kVanilla &&
929 !IncludeVanilla(range)) ||
930 (entry.bank == editor::MessageBank::kExpanded &&
931 !IncludeExpanded(range))) {
932 continue;
933 }
934
935 ParsedEntry parsed{entry,
938 if (!parsed.parse.ok()) {
939 has_errors = true;
940 parse_error_count += static_cast<int>(parsed.parse.errors.size());
941 error_count += static_cast<int>(parsed.parse.errors.size());
942 }
943 if (entry.bank == editor::MessageBank::kVanilla) {
944 has_vanilla_entries = true;
945 } else {
946 has_expanded_entries = true;
947 }
948 parsed_entries.push_back(std::move(parsed));
949 }
950
951 formatter.BeginArray("messages");
952 for (const auto& parsed : parsed_entries) {
953 formatter.BeginObject();
954 formatter.AddField("id", parsed.entry.id);
955 formatter.AddField("bank", BankLabel(parsed.entry.bank));
956 formatter.AddField("text", parsed.entry.text);
957 formatter.AddField("encoded_length",
958 static_cast<int>(parsed.parse.bytes.size()));
959 if (!parsed.line_warnings.empty()) {
960 formatter.BeginArray("line_width_warnings");
961 for (const auto& warning : parsed.line_warnings) {
962 formatter.AddArrayItem(warning);
963 }
964 formatter.EndArray();
965 }
966 if (!parsed.parse.warnings.empty()) {
967 formatter.BeginArray("warnings");
968 for (const auto& warning : parsed.parse.warnings) {
969 formatter.AddArrayItem(warning);
970 }
971 formatter.EndArray();
972 }
973 if (!parsed.parse.errors.empty()) {
974 formatter.BeginArray("errors");
975 for (const auto& error : parsed.parse.errors) {
976 formatter.AddArrayItem(error);
977 }
978 formatter.EndArray();
979 }
980 formatter.EndObject();
981 }
982 formatter.EndArray();
983
984 Rom owned_rom;
985 Rom* active_rom = rom;
986
987 if (apply) {
988 if (active_rom == nullptr || !active_rom->is_loaded()) {
989 auto rom_path = parser.GetString("rom");
990 if (!rom_path.has_value() || rom_path->empty()) {
991 std::string global_rom_path = absl::GetFlag(FLAGS_rom);
992 if (!global_rom_path.empty()) {
993 rom_path = global_rom_path;
994 }
995 }
996 if (!rom_path.has_value() || rom_path->empty()) {
997 error_count++;
998 formatter.AddField("status", "error");
999 formatter.AddField("error",
1000 "ROM not loaded; provide --rom when using --apply");
1001 formatter.AddField("parse_error_count", parse_error_count);
1002 formatter.AddField("error_count", error_count);
1003 formatter.EndObject();
1004 return absl::OkStatus();
1005 }
1006 auto load_status = owned_rom.LoadFromFile(*rom_path);
1007 if (!load_status.ok()) {
1008 error_count++;
1009 formatter.AddField("status", "error");
1010 formatter.AddField("error", std::string(load_status.message()));
1011 formatter.AddField("parse_error_count", parse_error_count);
1012 formatter.AddField("error_count", error_count);
1013 formatter.EndObject();
1014 return absl::OkStatus();
1015 }
1016 active_rom = &owned_rom;
1017 }
1018
1019 if (has_errors) {
1020 formatter.AddField("status", "error");
1021 formatter.AddField("error", "Parse errors present; no changes applied");
1022 formatter.AddField("parse_error_count", parse_error_count);
1023 formatter.AddField("error_count", error_count);
1024 formatter.EndObject();
1025 if (strict && parse_error_count > 0) {
1026 formatter.EndObject();
1027 formatter.Print();
1028 return absl::FailedPreconditionError(
1029 "Strict validation failed due to parse errors");
1030 }
1031 return absl::OkStatus();
1032 }
1033
1034 std::optional<ProjectMutationContext> project_context;
1035 std::vector<uint8_t> disk_baseline;
1036 if (has_vanilla_entries || has_expanded_entries) {
1037 const absl::Status snapshot_status =
1038 VerifyCleanDiskSnapshot(*active_rom, &disk_baseline);
1039 if (!snapshot_status.ok()) {
1040 error_count++;
1041 formatter.AddField("status", "error");
1042 formatter.AddField("error", std::string(snapshot_status.message()));
1043 formatter.AddField("parse_error_count", parse_error_count);
1044 formatter.AddField("error_count", error_count);
1045 formatter.EndObject();
1046 return snapshot_status;
1047 }
1048
1049 auto project_context_or = LoadProjectMutationContext(
1050 parser, *active_rom, "Message bundle apply");
1051 if (!project_context_or.ok()) {
1052 error_count++;
1053 formatter.AddField("status", "error");
1054 formatter.AddField("error",
1055 std::string(project_context_or.status().message()));
1056 formatter.AddField("parse_error_count", parse_error_count);
1057 formatter.AddField("error_count", error_count);
1058 formatter.EndObject();
1059 return project_context_or.status();
1060 }
1061 project_context.emplace(std::move(project_context_or.value()));
1062 }
1063
1064 std::optional<ExpandedMutationContext> expanded_context;
1065 if (IncludeExpanded(range) && has_expanded_entries) {
1066 auto context_or =
1067 PreflightExpandedMutation(project_context->project, *active_rom);
1068 if (!context_or.ok()) {
1069 error_count++;
1070 formatter.AddField("status", "error");
1071 formatter.AddField("error", std::string(context_or.status().message()));
1072 formatter.AddField("parse_error_count", parse_error_count);
1073 formatter.AddField("error_count", error_count);
1074 formatter.EndObject();
1075 return context_or.status();
1076 }
1077 expanded_context.emplace(std::move(context_or.value()));
1078 }
1079
1080 // Build both replacement banks and validate every selected ID before the
1081 // first ROM write. This keeps a bad expanded ID from landing after a
1082 // successful vanilla mutation in a mixed bundle.
1083 std::vector<editor::MessageData> vanilla_messages;
1084 if (IncludeVanilla(range) && has_vanilla_entries) {
1085 if (active_rom->size() >
1086 static_cast<size_t>(std::numeric_limits<int>::max())) {
1087 formatter.EndObject();
1088 return absl::OutOfRangeError(
1089 "ROM is too large for bounded vanilla-message parsing");
1090 }
1091 vanilla_messages = editor::ReadAllTextData(
1092 const_cast<uint8_t*>(active_rom->data()), editor::kTextData,
1093 static_cast<int>(active_rom->size()));
1094 }
1095
1096 std::vector<std::string> expanded_texts;
1097 if (expanded_context.has_value()) {
1098 const auto expanded_messages = ReadExpandedMessages(
1099 *active_rom, expanded_context->start, expanded_context->end);
1100 auto bank_size_status = ValidateExpandedMessageBankSize(
1101 expanded_messages.size(), *expanded_context);
1102 if (!bank_size_status.ok()) {
1103 formatter.AddField("status", "error");
1104 formatter.AddField("error", std::string(bank_size_status.message()));
1105 formatter.EndObject();
1106 return bank_size_status;
1107 }
1108 expanded_texts.reserve(expanded_messages.size());
1109 for (const auto& message : expanded_messages) {
1110 expanded_texts.push_back(message.RawString);
1111 }
1112 }
1113
1114 for (const auto& parsed : parsed_entries) {
1115 if (parsed.entry.bank == editor::MessageBank::kVanilla) {
1116 if (parsed.entry.id < 0 ||
1117 parsed.entry.id >= static_cast<int>(vanilla_messages.size())) {
1118 has_errors = true;
1119 error_count++;
1120 continue;
1121 }
1122 auto& message = vanilla_messages[parsed.entry.id];
1123 message.RawString = parsed.entry.text;
1124 message.ContentsParsed = parsed.entry.text;
1125 message.Data = parsed.parse.bytes;
1126 message.DataParsed = parsed.parse.bytes;
1127 } else {
1128 if (!expanded_context.has_value()) {
1129 continue;
1130 }
1131 const auto id_status =
1132 ValidateExpandedMessageId(parsed.entry.id, *expanded_context);
1133 if (!id_status.ok()) {
1134 has_errors = true;
1135 error_count++;
1136 continue;
1137 }
1138 if (parsed.entry.id >= static_cast<int>(expanded_texts.size())) {
1139 expanded_texts.resize(parsed.entry.id + 1);
1140 }
1141 expanded_texts[parsed.entry.id] = parsed.entry.text;
1142 }
1143 applied_updates++;
1144 }
1145
1146 std::optional<editor::VanillaMessageSavePlan> vanilla_plan;
1147 std::optional<size_t> expected_vanilla_count;
1148 std::string write_policy_warning;
1149 if (!has_errors && IncludeVanilla(range) && has_vanilla_entries) {
1150 const int manifest_count =
1151 project_context->project.hack_manifest.message_layout().vanilla_count;
1152 if (manifest_count <= 0) {
1153 const absl::Status status = absl::FailedPreconditionError(
1154 "Hack manifest must declare a positive messages.vanilla_count "
1155 "before applying vanilla messages");
1156 formatter.AddField("status", "error");
1157 formatter.AddField("error", std::string(status.message()));
1158 formatter.EndObject();
1159 return status;
1160 }
1161 expected_vanilla_count = static_cast<size_t>(manifest_count);
1163 vanilla_messages, expected_vanilla_count);
1164 if (!plan_or.ok()) {
1165 formatter.AddField("status", "error");
1166 formatter.AddField("error", std::string(plan_or.status().message()));
1167 formatter.EndObject();
1168 return plan_or.status();
1169 }
1170 vanilla_plan.emplace(std::move(plan_or.value()));
1171 const absl::Status policy_status = ValidateVanillaMutation(
1172 project_context->project, *vanilla_plan, &write_policy_warning);
1173 if (!policy_status.ok()) {
1174 formatter.AddField("status", "error");
1175 formatter.AddField("error", std::string(policy_status.message()));
1176 formatter.EndObject();
1177 return policy_status;
1178 }
1179 }
1180
1181 if (expanded_context.has_value() &&
1182 !expanded_context->policy_warning.empty()) {
1183 if (!write_policy_warning.empty()) {
1184 absl::StrAppend(&write_policy_warning, "; ");
1185 }
1186 absl::StrAppend(&write_policy_warning, expanded_context->policy_warning);
1187 }
1188 if (!write_policy_warning.empty()) {
1189 formatter.AddField("write_policy_warning", write_policy_warning);
1190 }
1191
1192 if (has_errors) {
1193 formatter.AddField("status", "error");
1194 formatter.AddField("error", "Invalid message IDs; no changes applied");
1195 } else {
1196 ScopedRomTransaction transaction(*active_rom);
1197 if (vanilla_plan.has_value()) {
1198 auto status =
1199 editor::ApplyVanillaMessageSavePlan(active_rom, *vanilla_plan);
1200 if (!status.ok()) {
1201 formatter.AddField("status", "error");
1202 formatter.AddField("error", std::string(status.message()));
1203 formatter.EndObject();
1204 return status;
1205 }
1206 }
1207
1208 if (expanded_context.has_value()) {
1209 auto status = editor::WriteExpandedTextData(
1210 active_rom, expanded_context->start, expanded_context->end,
1211 expanded_texts);
1212 if (!status.ok()) {
1213 formatter.AddField("status", "error");
1214 formatter.AddField("error", std::string(status.message()));
1215 formatter.EndObject();
1216 return status;
1217 }
1218 }
1219
1220 if (active_rom->dirty() && project_context.has_value()) {
1221 const absl::Status unchanged_status = VerifyDiskSnapshotUnchanged(
1222 project_context->canonical_rom_path, disk_baseline);
1223 if (!unchanged_status.ok()) {
1224 formatter.AddField("status", "error");
1225 formatter.AddField("error", std::string(unchanged_status.message()));
1226 formatter.EndObject();
1227 return unchanged_status;
1228 }
1229 Rom::SaveSettings save_settings;
1230 save_settings.save_new = false;
1231 save_settings.require_backup = true;
1232 save_settings.filename = project_context->canonical_rom_path.string();
1233 auto save_status = active_rom->SaveToFile(save_settings);
1234 if (!save_status.ok()) {
1235 formatter.AddField("status", "error");
1236 formatter.AddField("error", std::string(save_status.message()));
1237 formatter.EndObject();
1238 return save_status;
1239 }
1240 }
1241 transaction.Commit();
1242
1243 if (project_context.has_value()) {
1244 const absl::Status readback_status =
1245 VerifySavedRom(*active_rom, project_context->canonical_rom_path,
1246 vanilla_plan, expected_vanilla_count);
1247 if (!readback_status.ok()) {
1248 formatter.AddField("status", "error");
1249 formatter.AddField("error", std::string(readback_status.message()));
1250 formatter.EndObject();
1251 return readback_status;
1252 }
1253 }
1254 formatter.AddField("status", "success");
1255 formatter.AddField("applied_messages", applied_updates);
1256 if (project_context.has_value()) {
1257 formatter.AddField("readback_verified", true);
1258 }
1259 }
1260 } else {
1261 formatter.AddField("status", has_errors ? "error" : "success");
1262 }
1263
1264 formatter.AddField("error_count", error_count);
1265 formatter.AddField("parse_error_count", parse_error_count);
1266 formatter.EndObject();
1267 if (strict && parse_error_count > 0) {
1268 formatter.EndObject();
1269 formatter.Print();
1270 return absl::FailedPreconditionError("Strict validation failed");
1271 }
1272 return absl::OkStatus();
1273}
1274
1275// ===========================================================================
1276// New: Message Write Command
1277// ===========================================================================
1278
1280 Rom* rom, const resources::ArgumentParser& parser,
1281 resources::OutputFormatter& formatter) {
1282 auto id_or = parser.GetInt("id");
1283 if (!id_or.ok())
1284 return id_or.status();
1285 int msg_id = id_or.value();
1286
1287 auto text = parser.GetString("text").value();
1288
1289 // Validate line widths first
1290 auto warnings = editor::ValidateMessageLineWidths(text);
1291
1292 // Encode to bytes
1293 auto bytes = editor::ParseMessageToData(text);
1294 if (bytes.empty() && !text.empty()) {
1295 return absl::InvalidArgumentError("Encoding produced no bytes");
1296 }
1297
1298 auto context_or = PreflightExpandedMutation(parser, *rom);
1299 if (!context_or.ok()) {
1300 return context_or.status();
1301 }
1302 auto context = std::move(context_or.value());
1303 auto id_status = ValidateExpandedMessageId(msg_id, context);
1304 if (!id_status.ok()) {
1305 return id_status;
1306 }
1307
1308 // Read existing expanded messages to find the target
1309 auto expanded = ReadExpandedMessages(*rom, context.start, context.end);
1310 auto bank_size_status =
1311 ValidateExpandedMessageBankSize(expanded.size(), context);
1312 if (!bank_size_status.ok()) {
1313 return bank_size_status;
1314 }
1315
1316 // Build the full message list, inserting/replacing at msg_id
1317 std::vector<std::string> all_texts;
1318 all_texts.reserve(expanded.size());
1319 for (const auto& msg : expanded) {
1320 all_texts.push_back(msg.RawString);
1321 }
1322 if (msg_id >= static_cast<int>(all_texts.size())) {
1323 all_texts.resize(msg_id + 1);
1324 }
1325 all_texts[msg_id] = text;
1326
1327 // Write back
1328 ScopedRomTransaction transaction(*rom);
1329 auto status =
1330 editor::WriteExpandedTextData(rom, context.start, context.end, all_texts);
1331 if (!status.ok())
1332 return status;
1333 transaction.Commit();
1334
1335 formatter.BeginObject("Message Write Result");
1336 formatter.AddField("id", msg_id);
1337 formatter.AddField("text", text);
1338 formatter.AddField("encoded_length", static_cast<int>(bytes.size()));
1339 formatter.AddField("status", "success");
1340 if (!context.policy_warning.empty()) {
1341 formatter.AddField("write_policy_warning", context.policy_warning);
1342 }
1343 if (!warnings.empty()) {
1344 formatter.BeginArray("line_width_warnings");
1345 for (const auto& warning : warnings) {
1346 formatter.AddArrayItem(warning);
1347 }
1348 formatter.EndArray();
1349 }
1350 formatter.EndObject();
1351
1352 return absl::OkStatus();
1353}
1354
1355// ===========================================================================
1356// New: Export BIN Command
1357// ===========================================================================
1358
1360 Rom* rom, const resources::ArgumentParser& parser,
1361 resources::OutputFormatter& formatter) {
1362 auto output_path = parser.GetString("output").value();
1363 auto range = parser.GetString("range").value_or("expanded");
1364
1365 int start, end_addr;
1366 if (range == "expanded") {
1368 end_addr = editor::GetExpandedTextDataEnd();
1369 } else {
1370 start = editor::kTextData;
1371 end_addr = editor::kTextDataEnd;
1372 }
1373
1374 // Find the actual end of data (scan for 0xFF terminator)
1375 const uint8_t* data = rom->data();
1376 int data_end = start;
1377 while (data_end <= end_addr && data[data_end] != 0xFF) {
1378 data_end++;
1379 }
1380 if (data_end <= end_addr) {
1381 data_end++; // Include the 0xFF terminator
1382 }
1383
1384 int size = data_end - start;
1385
1386 std::ofstream file(output_path, std::ios::binary);
1387 if (!file.is_open()) {
1388 return absl::InternalError(
1389 absl::StrFormat("Cannot write to file: %s", output_path));
1390 }
1391 file.write(reinterpret_cast<const char*>(data + start), size);
1392 file.close();
1393
1394 formatter.BeginObject("BIN Export Result");
1395 formatter.AddField("output", output_path);
1396 formatter.AddField("range", range);
1397 formatter.AddHexField("start_address", start, 6);
1398 formatter.AddHexField("end_address", data_end - 1, 6);
1399 formatter.AddField("size_bytes", size);
1400 formatter.AddField("status", "success");
1401 formatter.EndObject();
1402
1403 return absl::OkStatus();
1404}
1405
1406// ===========================================================================
1407// New: Export ASM Command
1408// ===========================================================================
1409
1411 Rom* rom, const resources::ArgumentParser& parser,
1412 resources::OutputFormatter& formatter) {
1413 auto output_path = parser.GetString("output").value();
1414 auto range = parser.GetString("range").value_or("expanded");
1415
1416 int start;
1417 uint32_t snes_addr;
1418 if (range == "expanded") {
1420 snes_addr = 0x2F8000;
1421 } else {
1422 start = editor::kTextData;
1423 snes_addr = 0x1C0000;
1424 }
1425
1426 // Read messages from the specified region
1427 auto messages =
1428 editor::ReadAllTextData(const_cast<uint8_t*>(rom->data()), start);
1429
1430 std::ofstream file(output_path);
1431 if (!file.is_open()) {
1432 return absl::InternalError(
1433 absl::StrFormat("Cannot write to file: %s", output_path));
1434 }
1435
1436 // Write ASM header
1437 file << "; Auto-generated message data\n";
1438 file << absl::StrFormat("; Source: %s region\n", range);
1439 file << absl::StrFormat("; Messages: %d\n\n", messages.size());
1440 file << absl::StrFormat("org $%06X\n\n", snes_addr);
1441
1442 // Write each message as db directives
1443 for (const auto& msg : messages) {
1444 file << absl::StrFormat("; Message $%02X: %s\n", msg.ID,
1445 msg.ContentsParsed.substr(0, 60));
1446 file << "db ";
1447 for (size_t i = 0; i < msg.Data.size(); ++i) {
1448 if (i > 0)
1449 file << ", ";
1450 file << absl::StrFormat("$%02X", msg.Data[i]);
1451 }
1452 file << ", $7F ; terminator\n\n";
1453 }
1454
1455 // End-of-region marker
1456 file << "db $FF ; end of message data\n";
1457 file.close();
1458
1459 formatter.BeginObject("ASM Export Result");
1460 formatter.AddField("output", output_path);
1461 formatter.AddField("range", range);
1462 formatter.AddField("messages_exported", static_cast<int>(messages.size()));
1463 formatter.AddField("status", "success");
1464 formatter.EndObject();
1465
1466 return absl::OkStatus();
1467}
1468
1470 Rom* rom, const resources::ArgumentParser& parser,
1471 resources::OutputFormatter& formatter) {
1472 const std::string project_path = parser.GetString("project").value();
1473 const std::string incoming_path = parser.GetString("file").value();
1474 const bool write = parser.HasFlag("write");
1475
1476 project::YazeProject source_project;
1477 auto& resource_labels = zelda3::GetResourceLabels();
1478 absl::Status open_status;
1479 {
1480 ScopedHackManifestBindingRestore restore_manifest(resource_labels);
1481 try {
1482 open_status = source_project.Open(project_path);
1483 } catch (const std::exception& error) {
1484 return absl::InvalidArgumentError(absl::StrFormat(
1485 "Cannot load project '%s': %s", project_path, error.what()));
1486 } catch (...) {
1487 return absl::InvalidArgumentError(absl::StrFormat(
1488 "Cannot load project '%s': unknown project parse error",
1489 project_path));
1490 }
1491 }
1492 if (!open_status.ok()) {
1493 return absl::Status(open_status.code(),
1494 absl::StrFormat("Cannot load project '%s': %s",
1495 project_path, open_status.message()));
1496 }
1497 if (absl::Status manifest_status =
1498 EnsureConfiguredHackManifestLoaded(&source_project, project_path);
1499 !manifest_status.ok()) {
1500 return manifest_status;
1501 }
1502
1504 .write = write,
1505 .expected_source_sha256 =
1506 parser.GetString("expected-source-sha256").value_or(""),
1507 };
1508 auto result_or =
1509 editor::SyncMessageSource(source_project, incoming_path, options);
1510 if (!result_or.ok()) {
1511 return result_or.status();
1512 }
1513 const auto& result = *result_or;
1514
1515 formatter.BeginObject("Message Source Sync");
1516 formatter.AddField("status", "success");
1517 formatter.AddField("mode", write ? "write" : "dry-run");
1518 formatter.AddField("changed", result.changed);
1519 formatter.AddField("wrote", result.wrote);
1520 formatter.AddField("canonical_bundle", result.canonical_bundle_path.string());
1521 formatter.AddField("generated_asm_include",
1522 result.generated_asm_include_path.string());
1523 formatter.AddHexField("first_expanded_id", result.first_expanded_id, 3);
1524 formatter.AddHexField("last_expanded_id", result.last_expanded_id, 3);
1525 formatter.AddField("expanded_count", result.expanded_count);
1526 formatter.AddField("incoming_updates", result.incoming_updates);
1527 formatter.AddField("encoded_size", static_cast<int>(result.encoded_size));
1528 formatter.AddField("capacity", static_cast<int>(result.capacity));
1529 formatter.AddField("source_sha256_before", result.source_sha256_before);
1530 formatter.AddField("proposed_source_sha256", result.proposed_source_sha256);
1531 formatter.EndObject();
1532 return absl::OkStatus();
1533}
1534
1535} // namespace handlers
1536} // namespace cli
1537} // 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:270
auto filename() const
Definition rom.h:175
auto end()
Definition rom.h:172
auto mutable_data()
Definition rom.h:170
const auto & vector() const
Definition rom.h:173
absl::Status SaveToFile(const SaveSettings &settings)
Definition rom.cc:416
auto data() const
Definition rom.h:169
auto size() const
Definition rom.h:168
bool dirty() const
Definition rom.h:156
bool is_loaded() const
Definition rom.h:155
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.
const MessageLayout & message_layout() const
absl::Status LoadFromFile(const std::string &filepath)
Load manifest from a JSON file path.
std::vector< WriteConflict > AnalyzePcWriteRanges(const std::vector< std::pair< uint32_t, uint32_t > > &pc_ranges) const
Analyze a set of PC-offset ranges for write conflicts.
bool loaded() const
Check if the manifest has been loaded.
std::vector< std::pair< uint32_t, uint32_t > > write_ranges() const
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 VerifyDiskSnapshotUnchanged(const std::filesystem::path &rom_path, const std::vector< uint8_t > &expected_bytes)
absl::Status ValidateExpandedMessageId(int id, const ExpandedMutationContext &context)
absl::StatusOr< std::filesystem::path > CanonicalExistingPath(const std::string &path, absl::string_view label)
absl::StatusOr< ProjectMutationContext > LoadProjectMutationContext(const resources::ArgumentParser &parser, const Rom &rom, absl::string_view mutation_scope)
std::string FormatManifestConflict(const core::WriteConflict &conflict)
absl::StatusOr< ExpandedMutationContext > PreflightExpandedMutation(const project::YazeProject &yaze_project, const Rom &rom)
absl::Status VerifyCleanDiskSnapshot(const Rom &rom, std::vector< uint8_t > *baseline)
absl::Status ValidateVanillaMutation(const project::YazeProject &yaze_project, const editor::VanillaMessageSavePlan &plan, std::string *policy_warning)
absl::Status VerifySavedRom(const Rom &active_rom, const std::filesystem::path &saved_rom_path, const std::optional< editor::VanillaMessageSavePlan > &vanilla_plan, std::optional< size_t > expected_vanilla_count)
std::vector< editor::MessageData > ReadExpandedMessages(const Rom &rom)
std::string AddressOwnershipToString(AddressOwnership ownership)
int GetExpandedTextDataStart()
std::string ParseTextDataByte(uint8_t value)
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)
absl::StatusOr< VanillaMessageSavePlan > BuildVanillaMessageSavePlan(const std::vector< MessageData > &messages, std::optional< size_t > expected_message_count)
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)
absl::Status ApplyVanillaMessageSavePlan(Rom *rom, const VanillaMessageSavePlan &plan)
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
bool load_resource_labels
Definition rom.h:43
std::string filename
Definition rom.h:33
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:1491
absl::Status Open(const std::string &project_path)
Definition project.cc:429