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"
33 return absl::AsciiStrToLower(range);
37 return range ==
"all" || range ==
"vanilla";
41 return range ==
"all" || range ==
"expanded";
50 if (start < 0 ||
static_cast<size_t>(start) >= rom.
size()) {
54 static_cast<int>(rom.
size()) - 1);
61 if (start < 0 || end < start ||
static_cast<size_t>(start) >= rom.
size()) {
64 end = std::min(end,
static_cast<int>(rom.
size()) - 1);
77 int message_limit = -1;
85 : provider_(provider), previous_(provider.hack_manifest()) {}
100 const std::string& path, absl::string_view label) {
102 return absl::FailedPreconditionError(
103 absl::StrFormat(
"%s path is empty", label));
107 auto canonical_path = std::filesystem::canonical(path, ec);
109 return absl::FailedPreconditionError(absl::StrFormat(
110 "Cannot resolve %s path '%s': %s", label, path, ec.message()));
112 return canonical_path;
118 return absl::OkStatus();
121 const std::string manifest_path =
123 const absl::Status manifest_status =
125 if (manifest_status.ok()) {
126 return absl::OkStatus();
129 manifest_status.code(),
130 absl::StrFormat(
"Cannot load project '%s': hack manifest '%s': %s",
131 project_path, manifest_path, manifest_status.message()));
136 absl::StrFormat(
"address 0x%06X is %s", conflict.
address,
138 if (!conflict.module.empty()) {
139 absl::StrAppend(&result,
" (Module: ", conflict.module,
")");
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) {
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 "
163 return absl::FailedPreconditionError(
"ROM is not loaded");
168 absl::Status open_status;
177 }
catch (
const std::exception& error) {
178 return absl::InvalidArgumentError(absl::StrFormat(
179 "Cannot load project '%s': %s", *project_path, error.what()));
181 return absl::InvalidArgumentError(absl::StrFormat(
182 "Cannot load project '%s': unknown project parse error",
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()));
191 if (absl::Status manifest_status =
193 !manifest_status.ok()) {
194 return manifest_status;
198 if (!active_rom_path_or.ok()) {
199 return active_rom_path_or.status();
204 if (!project_rom_path_or.ok()) {
205 return project_rom_path_or.status();
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()));
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);
216 return absl::FailedPreconditionError(
217 absl::StrFormat(
"Cannot inspect active ROM size '%s': %s",
218 active_rom_path_or->string(), file_size_ec.message()));
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");
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()));
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",
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");
260 const uint32_t start =
SnesToPc(layout.data_start);
261 const uint32_t end =
SnesToPc(layout.data_end);
263 end >
static_cast<uint32_t
>(std::numeric_limits<int>::max()) ||
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()));
270 context.
start =
static_cast<int>(start);
271 context.
end =
static_cast<int>(end);
277 const int region_capacity = context.
end - context.
start + 1;
278 const int capacity_message_limit = std::max(0, region_capacity - 1);
280 if (layout.expanded_count > 0) {
282 std::min(layout.expanded_count, capacity_message_limit);
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) +
292 const std::string lowered_hack_name =
293 absl::AsciiStrToLower(manifest.hack_name());
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.");
303 const auto conflicts =
304 manifest.AnalyzePcWriteRanges({{start,
static_cast<uint32_t
>(end + 1u)}});
305 if (!conflicts.empty()) {
309 return absl::PermissionDeniedError(absl::StrFormat(
310 "Expanded message mutation blocked by hack manifest: %s", detail));
315 "Expanded message region conflicts with hack manifest: %s", detail);
324 auto project_context_or =
326 if (!project_context_or.ok()) {
327 return project_context_or.status();
330 std::move(project_context_or.value());
335 std::vector<uint8_t>* baseline) {
337 return absl::FailedPreconditionError(
338 "Message bundle apply requires a clean ROM; save or discard existing "
339 "in-memory changes before retrying");
345 const absl::Status load_status =
347 if (!load_status.ok()) {
351 return absl::FailedPreconditionError(
352 "Active ROM no longer matches the file on disk; reopen it before "
353 "applying a message bundle");
355 if (baseline !=
nullptr) {
356 *baseline = disk_rom.
vector();
358 return absl::OkStatus();
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);
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");
376 const absl::Status load_status =
378 if (!load_status.ok()) {
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");
386 return absl::OkStatus();
391 std::string* policy_warning) {
392 const auto conflicts =
394 if (conflicts.empty()) {
395 return absl::OkStatus();
401 return absl::PermissionDeniedError(absl::StrFormat(
402 "Vanilla message mutation blocked by hack manifest: %s; no message "
403 "bytes were written",
408 policy_warning !=
nullptr) {
409 *policy_warning = absl::StrFormat(
410 "Vanilla message write conflicts with hack manifest: %s", detail);
412 return absl::OkStatus();
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) {
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()));
431 return absl::DataLossError(
432 "ROM was saved but external readback differs from the planned bytes. "
433 "Restore the required backup before retrying");
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");
443 const auto reopened_messages =
445 static_cast<int>(reopened_rom.
size()));
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",
461 return absl::OkStatus();
467 return absl::InvalidArgumentError(
468 absl::StrFormat(
"Invalid expanded message ID: %d",
id));
471 return absl::OutOfRangeError(absl::StrFormat(
472 "Expanded message ID %d is outside the manifest range (count=%d)",
id,
475 return absl::OkStatus();
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",
487 return absl::OkStatus();
498 auto limit = parser.
GetInt(
"limit").value_or(50);
505 if (limit >
static_cast<int>(messages.size())) {
506 limit =
static_cast<int>(messages.size());
511 formatter.
AddField(
"total_messages",
static_cast<int>(messages.size()));
512 formatter.
AddField(
"status",
"success");
515 for (
int i = 0; i < limit; ++i) {
516 const auto& msg = messages[i];
520 formatter.
AddField(
"text", msg.ContentsParsed);
521 formatter.
AddField(
"length",
static_cast<int>(msg.Data.size()));
527 return absl::OkStatus();
533 auto message_id_or = parser.
GetInt(
"id");
534 if (!message_id_or.ok()) {
535 return message_id_or.status();
537 int message_id = message_id_or.value();
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));
547 const auto& msg = messages[message_id];
552 formatter.
AddField(
"text", msg.ContentsParsed);
553 formatter.
AddField(
"length",
static_cast<int>(msg.Data.size()));
556 return absl::OkStatus();
562 auto query = parser.
GetString(
"query").value();
563 auto limit = parser.
GetInt(
"limit").value_or(10);
574 formatter.
AddField(
"status",
"success");
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) {
583 matches.push_back(&msg);
586 formatter.
AddField(
"matches_found",
static_cast<int>(matches.size()));
589 for (
const auto* msg : matches) {
590 if (limit > 0 && match_count >= limit) {
596 formatter.
AddField(
"text", msg->ContentsParsed);
603 return absl::OkStatus();
613 auto text = parser.
GetString(
"text").value();
616 auto bytes = parse_result.bytes;
620 for (
size_t i = 0; i < bytes.size(); ++i) {
623 hex_str += absl::StrFormat(
"%02X", bytes[i]);
629 formatter.
AddField(
"length",
static_cast<int>(bytes.size()));
633 for (uint8_t
byte : bytes) {
634 formatter.
AddArrayItem(absl::StrFormat(
"0x%02X",
byte));
640 if (!warnings.empty()) {
642 for (
const auto& warning : warnings) {
647 if (!parse_result.warnings.empty()) {
649 for (
const auto& warning : parse_result.warnings) {
654 if (!parse_result.errors.empty()) {
656 for (
const auto& error : parse_result.errors) {
664 return absl::OkStatus();
674 auto hex_input = parser.
GetString(
"hex").value();
677 std::vector<uint8_t> bytes;
678 std::istringstream hex_stream(hex_input);
679 std::string hex_byte;
680 while (hex_stream >> hex_byte) {
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));
691 for (
size_t i = 0; i < bytes.size(); ++i) {
692 uint8_t
byte = bytes[i];
696 if (cmd.has_value()) {
697 if (cmd->HasArgument && i + 1 < bytes.size()) {
698 decoded += cmd->GetParamToken(bytes[++i]);
700 decoded += cmd->GetParamToken();
710 formatter.
AddField(
"hex", hex_input);
711 formatter.
AddField(
"text", decoded);
712 formatter.
AddField(
"length",
static_cast<int>(bytes.size()));
715 return absl::OkStatus();
725 auto file_path = parser.
GetString(
"file").value();
728 std::ifstream file(file_path);
729 if (!file.is_open()) {
730 return absl::NotFoundError(
731 absl::StrFormat(
"Cannot open file: %s", file_path));
734 std::string content((std::istreambuf_iterator<char>(file)),
735 std::istreambuf_iterator<char>());
741 formatter.
AddField(
"file", file_path);
742 formatter.
AddField(
"messages_parsed",
static_cast<int>(messages.size()));
745 for (
const auto& [msg_id, body] : messages) {
748 auto bytes = parse_result.bytes;
752 for (
size_t i = 0; i < bytes.size(); ++i) {
755 hex_str += absl::StrFormat(
"%02X", bytes[i]);
762 formatter.
AddField(
"encoded_length",
static_cast<int>(bytes.size()));
763 if (!warnings.empty()) {
765 for (
const auto& warning : warnings) {
770 if (!parse_result.warnings.empty()) {
772 for (
const auto& warning : parse_result.warnings) {
777 if (!parse_result.errors.empty()) {
779 for (
const auto& error : parse_result.errors) {
789 return absl::OkStatus();
799 auto output_path = parser.
GetString(
"output").value();
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));
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));
824 formatter.
AddField(
"output", output_path);
825 formatter.
AddField(
"messages_exported",
static_cast<int>(messages.size()));
826 formatter.
AddField(
"status",
"success");
829 return absl::OkStatus();
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));
846 std::vector<editor::MessageData> vanilla;
847 std::vector<editor::MessageData> expanded;
849 if (IncludeVanilla(range)) {
854 if (IncludeExpanded(range)) {
855 expanded = ReadExpandedMessages(*rom);
865 formatter.
AddField(
"output", output_path);
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");
872 return absl::OkStatus();
882 auto file_path = parser.
GetString(
"file").value();
883 const bool apply = parser.
HasFlag(
"apply");
884 const bool strict = parser.
HasFlag(
"strict");
887 return absl::FailedPreconditionError(
888 "message-import-bundle --apply is unavailable in WebAssembly because "
889 "the browser filesystem cannot provide durable backup and readback "
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));
900 if (!entries_or.ok()) {
901 return entries_or.status();
903 auto entries = entries_or.value();
906 formatter.
AddField(
"file", file_path);
909 formatter.
AddField(
"strict", strict);
910 formatter.
AddField(
"entries",
static_cast<int>(entries.size()));
912 bool has_errors =
false;
913 int parse_error_count = 0;
915 int applied_updates = 0;
916 bool has_vanilla_entries =
false;
917 bool has_expanded_entries =
false;
922 std::vector<std::string> line_warnings;
924 std::vector<ParsedEntry> parsed_entries;
925 parsed_entries.reserve(entries.size());
927 for (
const auto& entry : entries) {
929 !IncludeVanilla(range)) ||
931 !IncludeExpanded(range))) {
935 ParsedEntry parsed{entry,
938 if (!parsed.parse.ok()) {
940 parse_error_count +=
static_cast<int>(parsed.parse.errors.size());
941 error_count +=
static_cast<int>(parsed.parse.errors.size());
944 has_vanilla_entries =
true;
946 has_expanded_entries =
true;
948 parsed_entries.push_back(std::move(parsed));
952 for (
const auto& parsed : parsed_entries) {
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()) {
961 for (
const auto& warning : parsed.line_warnings) {
966 if (!parsed.parse.warnings.empty()) {
968 for (
const auto& warning : parsed.parse.warnings) {
973 if (!parsed.parse.errors.empty()) {
975 for (
const auto& error : parsed.parse.errors) {
985 Rom* active_rom = rom;
988 if (active_rom ==
nullptr || !active_rom->
is_loaded()) {
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;
996 if (!rom_path.has_value() || rom_path->empty()) {
998 formatter.
AddField(
"status",
"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);
1004 return absl::OkStatus();
1007 if (!load_status.ok()) {
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);
1014 return absl::OkStatus();
1016 active_rom = &owned_rom;
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);
1025 if (strict && parse_error_count > 0) {
1028 return absl::FailedPreconditionError(
1029 "Strict validation failed due to parse errors");
1031 return absl::OkStatus();
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()) {
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);
1046 return snapshot_status;
1049 auto project_context_or = LoadProjectMutationContext(
1050 parser, *active_rom,
"Message bundle apply");
1051 if (!project_context_or.ok()) {
1053 formatter.
AddField(
"status",
"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);
1059 return project_context_or.status();
1061 project_context.emplace(std::move(project_context_or.value()));
1064 std::optional<ExpandedMutationContext> expanded_context;
1065 if (IncludeExpanded(range) && has_expanded_entries) {
1067 PreflightExpandedMutation(project_context->project, *active_rom);
1068 if (!context_or.ok()) {
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);
1075 return context_or.status();
1077 expanded_context.emplace(std::move(context_or.value()));
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())) {
1088 return absl::OutOfRangeError(
1089 "ROM is too large for bounded vanilla-message parsing");
1093 static_cast<int>(active_rom->
size()));
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()));
1106 return bank_size_status;
1108 expanded_texts.reserve(expanded_messages.size());
1109 for (
const auto& message : expanded_messages) {
1110 expanded_texts.push_back(message.RawString);
1114 for (
const auto& parsed : parsed_entries) {
1116 if (parsed.entry.id < 0 ||
1117 parsed.entry.id >=
static_cast<int>(vanilla_messages.size())) {
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;
1128 if (!expanded_context.has_value()) {
1131 const auto id_status =
1132 ValidateExpandedMessageId(parsed.entry.id, *expanded_context);
1133 if (!id_status.ok()) {
1138 if (parsed.entry.id >=
static_cast<int>(expanded_texts.size())) {
1139 expanded_texts.resize(parsed.entry.id + 1);
1141 expanded_texts[parsed.entry.id] = parsed.entry.text;
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()));
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()));
1168 return plan_or.status();
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()));
1177 return policy_status;
1181 if (expanded_context.has_value() &&
1182 !expanded_context->policy_warning.empty()) {
1183 if (!write_policy_warning.empty()) {
1184 absl::StrAppend(&write_policy_warning,
"; ");
1186 absl::StrAppend(&write_policy_warning, expanded_context->policy_warning);
1188 if (!write_policy_warning.empty()) {
1189 formatter.
AddField(
"write_policy_warning", write_policy_warning);
1193 formatter.
AddField(
"status",
"error");
1194 formatter.
AddField(
"error",
"Invalid message IDs; no changes applied");
1197 if (vanilla_plan.has_value()) {
1201 formatter.
AddField(
"status",
"error");
1202 formatter.
AddField(
"error", std::string(status.message()));
1208 if (expanded_context.has_value()) {
1210 active_rom, expanded_context->start, expanded_context->
end,
1213 formatter.
AddField(
"status",
"error");
1214 formatter.
AddField(
"error", std::string(status.message()));
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()));
1227 return unchanged_status;
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()));
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()));
1251 return readback_status;
1254 formatter.
AddField(
"status",
"success");
1255 formatter.
AddField(
"applied_messages", applied_updates);
1256 if (project_context.has_value()) {
1257 formatter.
AddField(
"readback_verified",
true);
1261 formatter.
AddField(
"status", has_errors ?
"error" :
"success");
1264 formatter.
AddField(
"error_count", error_count);
1265 formatter.
AddField(
"parse_error_count", parse_error_count);
1267 if (strict && parse_error_count > 0) {
1270 return absl::FailedPreconditionError(
"Strict validation failed");
1272 return absl::OkStatus();
1282 auto id_or = parser.
GetInt(
"id");
1284 return id_or.status();
1285 int msg_id = id_or.value();
1287 auto text = parser.
GetString(
"text").value();
1294 if (bytes.empty() && !text.empty()) {
1295 return absl::InvalidArgumentError(
"Encoding produced no bytes");
1298 auto context_or = PreflightExpandedMutation(parser, *rom);
1299 if (!context_or.ok()) {
1300 return context_or.status();
1302 auto context = std::move(context_or.value());
1303 auto id_status = ValidateExpandedMessageId(msg_id, context);
1304 if (!id_status.ok()) {
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;
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);
1322 if (msg_id >=
static_cast<int>(all_texts.size())) {
1323 all_texts.resize(msg_id + 1);
1325 all_texts[msg_id] = 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);
1343 if (!warnings.empty()) {
1345 for (
const auto& warning : warnings) {
1352 return absl::OkStatus();
1362 auto output_path = parser.
GetString(
"output").value();
1363 auto range = parser.
GetString(
"range").value_or(
"expanded");
1365 int start, end_addr;
1366 if (range ==
"expanded") {
1375 const uint8_t* data = rom->
data();
1376 int data_end = start;
1377 while (data_end <= end_addr && data[data_end] != 0xFF) {
1380 if (data_end <= end_addr) {
1384 int size = data_end - start;
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));
1391 file.write(
reinterpret_cast<const char*
>(data + start), size);
1395 formatter.
AddField(
"output", output_path);
1396 formatter.
AddField(
"range", range);
1398 formatter.
AddHexField(
"end_address", data_end - 1, 6);
1399 formatter.
AddField(
"size_bytes", size);
1400 formatter.
AddField(
"status",
"success");
1403 return absl::OkStatus();
1413 auto output_path = parser.
GetString(
"output").value();
1414 auto range = parser.
GetString(
"range").value_or(
"expanded");
1418 if (range ==
"expanded") {
1420 snes_addr = 0x2F8000;
1423 snes_addr = 0x1C0000;
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));
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);
1443 for (
const auto& msg : messages) {
1444 file << absl::StrFormat(
"; Message $%02X: %s\n", msg.ID,
1445 msg.ContentsParsed.substr(0, 60));
1447 for (
size_t i = 0; i < msg.Data.size(); ++i) {
1450 file << absl::StrFormat(
"$%02X", msg.Data[i]);
1452 file <<
", $7F ; terminator\n\n";
1456 file <<
"db $FF ; end of message data\n";
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");
1466 return absl::OkStatus();
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");
1478 absl::Status open_status;
1480 ScopedHackManifestBindingRestore restore_manifest(resource_labels);
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()));
1487 return absl::InvalidArgumentError(absl::StrFormat(
1488 "Cannot load project '%s': unknown project parse error",
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()));
1497 if (absl::Status manifest_status =
1498 EnsureConfiguredHackManifestLoaded(&source_project, project_path);
1499 !manifest_status.ok()) {
1500 return manifest_status;
1505 .expected_source_sha256 =
1506 parser.
GetString(
"expected-source-sha256").value_or(
""),
1510 if (!result_or.ok()) {
1511 return result_or.status();
1513 const auto& result = *result_or;
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);
1532 return absl::OkStatus();
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
absl::Status LoadFromFile(const std::string &filename, const LoadOptions &options=LoadOptions::Defaults())
const auto & vector() const
absl::Status SaveToFile(const SaveSettings &settings)
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()
zelda3::ResourceLabelProvider & provider_
ScopedHackManifestBindingRestore(const ScopedHackManifestBindingRestore &)=delete
ScopedHackManifestBindingRestore & operator=(const ScopedHackManifestBindingRestore &)=delete
ScopedHackManifestBindingRestore(zelda3::ResourceLabelProvider &provider)
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)
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)
std::string BankLabel(editor::MessageBank bank)
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)
bool IncludeVanilla(const std::string &range)
absl::Status ValidateExpandedMessageId(int id, const ExpandedMutationContext &context)
absl::StatusOr< std::filesystem::path > CanonicalExistingPath(const std::string &path, absl::string_view label)
bool IncludeExpanded(const std::string &range)
std::string NormalizeRange(const std::string &range)
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)
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)
uint32_t SnesToPc(uint32_t addr) noexcept
bool load_resource_labels
std::string policy_warning
project::YazeProject project
std::filesystem::path canonical_rom_path
A conflict detected when yaze wants to write to an ASM-owned address.
AddressOwnership ownership
Modern project structure with comprehensive settings consolidation.
core::HackManifest hack_manifest
std::string hack_manifest_file
std::string GetAbsolutePath(const std::string &relative_path) const
absl::Status Open(const std::string &project_path)