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#include "absl/strings/str_join.h"
19
22#include "rom/snes.h"
23#include "rom/transaction.h"
24#include "util/macro.h"
26
27ABSL_DECLARE_FLAG(std::string, rom);
28
29namespace yaze {
30namespace cli {
31namespace handlers {
32
33namespace {
34std::string NormalizeRange(const std::string& range) {
35 return absl::AsciiStrToLower(range);
36}
37
38bool IncludeVanilla(const std::string& range) {
39 return range == "all" || range == "vanilla";
40}
41
42bool IncludeExpanded(const std::string& range) {
43 return range == "all" || range == "expanded";
44}
45
46std::string BankLabel(editor::MessageBank bank) {
47 return editor::MessageBankToString(bank);
48}
49
50std::vector<editor::MessageData> ReadExpandedMessages(const Rom& rom) {
51 const int start = editor::GetExpandedTextDataStart();
52 if (start < 0 || static_cast<size_t>(start) >= rom.size()) {
53 return {};
54 }
55 const int end = std::min(editor::GetExpandedTextDataEnd(),
56 static_cast<int>(rom.size()) - 1);
57 return editor::ReadExpandedTextData(const_cast<uint8_t*>(rom.data()), start,
58 end);
59}
60
61std::vector<editor::MessageData> ReadExpandedMessages(const Rom& rom, int start,
62 int end) {
63 if (start < 0 || end < start || static_cast<size_t>(start) >= rom.size()) {
64 return {};
65 }
66 end = std::min(end, static_cast<int>(rom.size()) - 1);
67 return editor::ReadExpandedTextData(const_cast<uint8_t*>(rom.data()), start,
68 end);
69}
70
75
77 int start = 0;
78 int end = 0;
79 int message_limit = -1;
80 std::string policy_warning;
81};
82
84 public:
87 : provider_(provider), previous_(provider.hack_manifest()) {}
88
90 delete;
92 const ScopedHackManifestBindingRestore&) = delete;
93
94 ~ScopedHackManifestBindingRestore() { provider_.SetHackManifest(previous_); }
95
96 private:
98 const core::HackManifest* previous_ = nullptr;
99};
100
101absl::StatusOr<std::filesystem::path> CanonicalExistingPath(
102 const std::string& path, absl::string_view label) {
103 if (path.empty()) {
104 return absl::FailedPreconditionError(
105 absl::StrFormat("%s path is empty", label));
106 }
107
108 std::error_code ec;
109 auto canonical_path = std::filesystem::canonical(path, ec);
110 if (ec) {
111 return absl::FailedPreconditionError(absl::StrFormat(
112 "Cannot resolve %s path '%s': %s", label, path, ec.message()));
113 }
114 return canonical_path;
115}
116
118 project::YazeProject* project, absl::string_view project_path) {
119 if (project->hack_manifest.loaded() || project->hack_manifest_file.empty()) {
120 return absl::OkStatus();
121 }
122
123 const std::string manifest_path =
124 project->GetAbsolutePath(project->hack_manifest_file);
125 const absl::Status manifest_status =
126 project->hack_manifest.LoadFromFile(manifest_path);
127 if (manifest_status.ok()) {
128 return absl::OkStatus();
129 }
130 return absl::Status(
131 manifest_status.code(),
132 absl::StrFormat("Cannot load project '%s': hack manifest '%s': %s",
133 project_path, manifest_path, manifest_status.message()));
134}
135
136std::string FormatManifestConflict(const core::WriteConflict& conflict) {
137 std::string result =
138 absl::StrFormat("address 0x%06X is %s", conflict.address,
140 if (!conflict.module.empty()) {
141 absl::StrAppend(&result, " (Module: ", conflict.module, ")");
142 }
143 return result;
144}
145
146bool IsCanonicalMappedLoRomAddress(uint32_t address) {
147 const uint8_t bank = static_cast<uint8_t>((address >> 16) & 0xFFu);
148 if (bank == 0x7E || bank == 0x7F || (address & 0xFFFFu) < 0x8000u) {
149 return false;
150 }
151 return PcToSnes(SnesToPc(address)) == address;
152}
153
154absl::StatusOr<ProjectMutationContext> LoadProjectMutationContext(
155 const resources::ArgumentParser& parser, const Rom& rom,
156 absl::string_view mutation_scope) {
157 auto project_path = parser.GetString("project");
158 if (!project_path.has_value() || project_path->empty()) {
159 return absl::FailedPreconditionError(absl::StrFormat(
160 "%s requires explicit --project so Yaze can verify ROM ownership and "
161 "write policy",
162 mutation_scope));
163 }
164 if (!rom.is_loaded()) {
165 return absl::FailedPreconditionError("ROM is not loaded");
166 }
167
169 auto& resource_labels = zelda3::GetResourceLabels();
170 absl::Status open_status;
171 {
172 // YazeProject::Open temporarily installs its manifest in the
173 // process-global label provider. Restore the embedding application's prior
174 // non-owning pointer on every path, including malformed project data that
175 // throws from a parser.
176 ScopedHackManifestBindingRestore restore_manifest(resource_labels);
177 try {
178 open_status = context.project.Open(*project_path);
179 } catch (const std::exception& error) {
180 return absl::InvalidArgumentError(absl::StrFormat(
181 "Cannot load project '%s': %s", *project_path, error.what()));
182 } catch (...) {
183 return absl::InvalidArgumentError(absl::StrFormat(
184 "Cannot load project '%s': unknown project parse error",
185 *project_path));
186 }
187 }
188 if (!open_status.ok()) {
189 return absl::Status(open_status.code(),
190 absl::StrFormat("Cannot load project '%s': %s",
191 *project_path, open_status.message()));
192 }
193 if (context.project.rom_metadata.write_policy ==
195 return absl::PermissionDeniedError(
196 "Message mutation is disabled by the project's read-only policy");
197 }
198 if (absl::Status manifest_status =
199 EnsureConfiguredHackManifestLoaded(&context.project, *project_path);
200 !manifest_status.ok()) {
201 return manifest_status;
202 }
203
204 auto active_rom_path_or = CanonicalExistingPath(rom.filename(), "active ROM");
205 if (!active_rom_path_or.ok()) {
206 return active_rom_path_or.status();
207 }
208 auto project_rom_path_or = CanonicalExistingPath(
210 "project ROM");
211 if (!project_rom_path_or.ok()) {
212 return project_rom_path_or.status();
213 }
214 if (*active_rom_path_or != *project_rom_path_or) {
215 return absl::FailedPreconditionError(absl::StrFormat(
216 "Project ROM mismatch: active ROM is '%s' but --project binds to '%s'",
217 active_rom_path_or->string(), project_rom_path_or->string()));
218 }
219 std::error_code file_size_ec;
220 const uintmax_t raw_file_size =
221 std::filesystem::file_size(*active_rom_path_or, file_size_ec);
222 if (file_size_ec) {
223 return absl::FailedPreconditionError(
224 absl::StrFormat("Cannot inspect active ROM size '%s': %s",
225 active_rom_path_or->string(), file_size_ec.message()));
226 }
227 constexpr uintmax_t kSnesBankSize = 0x8000;
228 constexpr uintmax_t kCopierHeaderSize = 0x200;
229 if (raw_file_size >= kCopierHeaderSize &&
230 raw_file_size % kSnesBankSize == kCopierHeaderSize) {
231 return absl::FailedPreconditionError(
232 "Message mutation requires a headerless ROM file; remove the "
233 "512-byte copier header and reopen the ROM");
234 }
235 if (raw_file_size != rom.size()) {
236 return absl::FailedPreconditionError(absl::StrFormat(
237 "Message mutation requires a headerless ROM file: disk size is 0x%zX "
238 "but the loaded ROM is 0x%zX bytes",
239 static_cast<size_t>(raw_file_size), rom.size()));
240 }
241 context.canonical_rom_path = *active_rom_path_or;
242
243 const auto& manifest = context.project.hack_manifest;
244 if (!manifest.loaded()) {
245 return absl::FailedPreconditionError(absl::StrFormat(
246 "Project '%s' has no loaded hack manifest; configure a valid "
247 "hack_manifest_file before mutating messages",
248 *project_path));
249 }
250
251 return context;
252}
253
254absl::StatusOr<ExpandedMutationContext> PreflightExpandedMutation(
255 const project::YazeProject& yaze_project, const Rom& rom) {
257 const auto& manifest = yaze_project.hack_manifest;
258 const auto& layout = manifest.message_layout();
259 if (layout.data_start == 0 || layout.data_end == 0 ||
260 layout.data_end < layout.data_start ||
261 !IsCanonicalMappedLoRomAddress(layout.data_start) ||
262 !IsCanonicalMappedLoRomAddress(layout.data_end)) {
263 return absl::FailedPreconditionError(
264 "Hack manifest does not define a valid LoROM expanded message region");
265 }
266
267 const uint32_t start = SnesToPc(layout.data_start);
268 const uint32_t end = SnesToPc(layout.data_end);
269 if (end < start ||
270 end > static_cast<uint32_t>(std::numeric_limits<int>::max()) ||
271 end >= rom.size()) {
272 return absl::OutOfRangeError(absl::StrFormat(
273 "Configured expanded message region [0x%06X, 0x%06X] is outside the "
274 "active ROM (size=0x%zX)",
275 start, end, rom.size()));
276 }
277 context.start = static_cast<int>(start);
278 context.end = static_cast<int>(end);
279
280 // Even a manifest without ID metadata has a strict physical upper bound:
281 // each message requires at least a 0x7F terminator and the bank needs a final
282 // 0xFF. Keeping this limit finite prevents hostile IDs from driving a huge
283 // sparse-vector allocation.
284 const int region_capacity = context.end - context.start + 1;
285 const int capacity_message_limit = std::max(0, region_capacity - 1);
286 context.message_limit = capacity_message_limit;
287 if (layout.expanded_count > 0) {
288 context.message_limit =
289 std::min(layout.expanded_count, capacity_message_limit);
290 }
291 if (layout.last_expanded_id >= layout.first_expanded_id &&
292 (layout.first_expanded_id != 0 || layout.last_expanded_id != 0)) {
293 const int declared_limit =
294 static_cast<int>(layout.last_expanded_id - layout.first_expanded_id) +
295 1;
296 context.message_limit = std::min(context.message_limit, declared_limit);
297 }
298
299 const std::string lowered_hack_name =
300 absl::AsciiStrToLower(manifest.hack_name());
301 if (yaze_project.rom_metadata.write_policy ==
303 absl::StrContains(lowered_hack_name, "oracle of secrets")) {
304 return absl::PermissionDeniedError(editor::kOracleExpandedRomWriteRefusal);
305 }
306
307 const auto conflicts =
308 manifest.AnalyzePcWriteRanges({{start, static_cast<uint32_t>(end + 1u)}});
309 if (!conflicts.empty()) {
310 const std::string detail = FormatManifestConflict(conflicts.front());
311 if (yaze_project.rom_metadata.write_policy ==
313 return absl::PermissionDeniedError(absl::StrFormat(
314 "Expanded message mutation blocked by hack manifest: %s", detail));
315 }
316 if (yaze_project.rom_metadata.write_policy ==
318 context.policy_warning = absl::StrFormat(
319 "Expanded message region conflicts with hack manifest: %s", detail);
320 }
321 }
322
323 return context;
324}
325
326absl::Status VerifyCleanDiskSnapshot(const Rom& rom,
327 std::vector<uint8_t>* baseline) {
328 if (rom.dirty()) {
329 return absl::FailedPreconditionError(
330 "Message bundle apply requires a clean ROM; save or discard existing "
331 "in-memory changes before retrying");
332 }
333
334 Rom disk_rom;
335 Rom::LoadOptions load_options;
336 load_options.load_resource_labels = false;
337 const absl::Status load_status =
338 disk_rom.LoadFromFile(rom.filename(), load_options);
339 if (!load_status.ok()) {
340 return load_status;
341 }
342 if (disk_rom.vector() != rom.vector()) {
343 return absl::FailedPreconditionError(
344 "Active ROM no longer matches the file on disk; reopen it before "
345 "applying a message bundle");
346 }
347 if (baseline != nullptr) {
348 *baseline = disk_rom.vector();
349 }
350 return absl::OkStatus();
351}
352
354 const std::filesystem::path& rom_path,
355 const std::vector<uint8_t>& expected_bytes) {
356 std::error_code file_size_ec;
357 const uintmax_t raw_file_size =
358 std::filesystem::file_size(rom_path, file_size_ec);
359 if (file_size_ec ||
360 raw_file_size != static_cast<uintmax_t>(expected_bytes.size())) {
361 return absl::AbortedError(
362 "ROM size changed on disk after message preflight; no bytes were "
363 "saved. Reopen the ROM and retry");
364 }
365 Rom disk_rom;
366 Rom::LoadOptions load_options;
367 load_options.load_resource_labels = false;
368 const absl::Status load_status =
369 disk_rom.LoadFromFile(rom_path.string(), load_options);
370 if (!load_status.ok()) {
371 return load_status;
372 }
373 if (disk_rom.vector() != expected_bytes) {
374 return absl::AbortedError(
375 "ROM changed on disk after message preflight; no bytes were saved. "
376 "Reopen the ROM and retry");
377 }
378 return absl::OkStatus();
379}
380
382 const project::YazeProject& yaze_project,
383 const std::vector<std::pair<uint32_t, uint32_t>>& changed_ranges,
384 std::string* policy_warning) {
385 const auto conflicts =
386 yaze_project.hack_manifest.AnalyzePcWriteRanges(changed_ranges);
387 if (conflicts.empty()) {
388 return absl::OkStatus();
389 }
390
391 const std::string detail = FormatManifestConflict(conflicts.front());
392 if (yaze_project.rom_metadata.write_policy ==
394 return absl::PermissionDeniedError(absl::StrFormat(
395 "Vanilla message mutation blocked by hack manifest: %s; no message "
396 "bytes were written",
397 detail));
398 }
399 if (yaze_project.rom_metadata.write_policy ==
401 policy_warning != nullptr) {
402 *policy_warning = absl::StrFormat(
403 "Vanilla message write conflicts with hack manifest: %s", detail);
404 }
405 return absl::OkStatus();
406}
407
408// The two vanilla text regions in stream order: CreateMessagePointers
409// ($0E:D3EB) walks $1C:8000 and moves to $0E:DF40 at the first [BANK] byte.
411 const char* name;
412 uint32_t start;
413 uint32_t capacity_end; // exclusive
414};
416 {"primary", static_cast<uint32_t>(editor::kTextData),
417 static_cast<uint32_t>(editor::kTextDataEnd) + 1},
418 {"secondary", static_cast<uint32_t>(editor::kTextData2),
419 static_cast<uint32_t>(editor::kTextData2End) + 1},
420};
421
423 int id = 0;
424 size_t previous_length = 0; // bytes before the terminator
425 size_t encoded_length = 0;
426 bool unchanged = false; // same text as the ROM; bytes kept as they are
427};
428
431 editor::VanillaMessageSavePlan current; // the ROM's stream as it is
432 std::vector<std::pair<uint32_t, uint32_t>> changed_ranges;
433 size_t expected_count = 0;
434 bool secondary_length_pinned = false;
435 std::vector<EditedVanillaMessage> edited;
436};
437
438// Places `edits` (message ID, encoded bytes) into the ROM's vanilla stream and
439// checks the result before anything is written:
440// - edited messages are re-encoded with the ROM's word dictionary;
441// - the stream must hold the manifest's vanilla_count messages;
442// - only bytes that change are checked against the manifest, so rewriting a
443// protected byte with its current value is not a conflict;
444// - when the manifest declares a region-switch hook (messages.hook_address),
445// the expanded messages are chained after the vanilla stream: Oracle of
446// Secrets hooks the switch at $0E:D436, and its build turns the byte after
447// the last vanilla message into a second [BANK]. The secondary region then
448// keeps its length. A shorter stream would end the game's pointer walk
449// before the expanded messages; a longer one would run into that byte.
450absl::StatusOr<VanillaEditPlan> PlanVanillaEdits(
451 const project::YazeProject& yaze_project, const Rom& rom,
452 const std::vector<std::pair<int, std::vector<uint8_t>>>& edits,
453 std::string* policy_warning) {
454 const auto& layout = yaze_project.hack_manifest.message_layout();
455 if (layout.vanilla_count <= 0) {
456 return absl::FailedPreconditionError(
457 "Hack manifest must declare a positive messages.vanilla_count "
458 "before applying vanilla messages");
459 }
460 if (rom.size() > static_cast<size_t>(std::numeric_limits<int>::max())) {
461 return absl::OutOfRangeError(
462 "ROM is too large for bounded vanilla-message parsing");
463 }
464 const size_t expected_count = static_cast<size_t>(layout.vanilla_count);
465 auto messages =
466 editor::ReadAllTextData(const_cast<uint8_t*>(rom.data()),
467 editor::kTextData, static_cast<int>(rom.size()));
468 auto current_or =
469 editor::BuildVanillaMessageSavePlan(messages, expected_count);
470 if (!current_or.ok()) {
471 return current_or.status();
472 }
473
474 const auto dictionary =
476 if (dictionary.empty()) {
477 return absl::FailedPreconditionError(absl::StrFormat(
478 "Word dictionary table at PC 0x%06X is outside the ROM or malformed; "
479 "no message bytes were written",
481 }
482 std::vector<EditedVanillaMessage> edited;
483 for (const auto& [id, bytes] : edits) {
484 if (id < 0 || id >= static_cast<int>(messages.size())) {
485 return absl::InvalidArgumentError(
486 absl::StrFormat("Vanilla message ID 0x%03X is outside 0x000-0x%03X",
487 id, static_cast<int>(messages.size()) - 1));
488 }
489 editor::MessageData& message = messages[id];
490 std::vector<uint8_t> encoded = bytes;
491 // ReadAllTextData attaches the region switch to the first message of the
492 // secondary region. It is stream layout, not text; keep it when the new
493 // text leaves it out.
494 if (!message.Data.empty() &&
495 message.Data.front() == editor::kBankSwitchCommand &&
496 (encoded.empty() || encoded.front() != editor::kBankSwitchCommand)) {
497 encoded.insert(encoded.begin(), editor::kBankSwitchCommand);
498 }
499 const size_t previous_length = message.Data.size();
500 // A message whose text did not change keeps its bytes. Re-encoding it
501 // would still shrink text stored without the dictionary (the editor's
502 // save and older imports), which trips the region-2 length pin when a
503 // whole exported bundle is imported back.
504 if (editor::ExpandMessageDictionary(encoded, dictionary) ==
505 editor::ExpandMessageDictionary(message.Data, dictionary)) {
506 edited.push_back({id, previous_length, previous_length, true});
507 continue;
508 }
509 message.Data = editor::CompressMessageWithDictionary(encoded, dictionary);
510 message.DataParsed = message.Data;
511 edited.push_back({id, previous_length, message.Data.size(), false});
512 }
513
514 auto plan_or = editor::BuildVanillaMessageSavePlan(messages, expected_count);
515 if (!plan_or.ok()) {
516 return plan_or.status();
517 }
518 VanillaEditPlan edit{std::move(plan_or.value()),
519 std::move(current_or.value()),
520 {},
521 expected_count,
522 layout.hook_address != 0,
523 std::move(edited)};
524 edit.changed_ranges = edit.plan.ChangedRanges(rom.data(), rom.size());
525
526 const auto& new_writes = edit.plan.writes();
527 const auto& old_writes = edit.current.writes();
528 if (edit.secondary_length_pinned && new_writes.size() > 1 &&
529 old_writes.size() > 1 && new_writes[1].end() != old_writes[1].end()) {
530 return absl::FailedPreconditionError(absl::StrFormat(
531 "Vanilla text region 2 (PC 0x%06X) must stay %zu bytes because the "
532 "manifest chains expanded messages from its end (the byte at PC "
533 "0x%06X becomes the switch into the expanded bank); this edit makes "
534 "it %zu bytes. Keep messages in region 2 the same total length. No "
535 "message bytes were written",
536 editor::kTextData2, old_writes[1].bytes().size() - 1,
537 old_writes[1].end() - 1, new_writes[1].bytes().size() - 1));
538 }
539
540 RETURN_IF_ERROR(ValidateVanillaMutation(yaze_project, edit.changed_ranges,
541 policy_warning));
542 return edit;
543}
544
545// Per-region usage of the planned stream (and of the ROM as it is), plus the
546// byte ranges a save would change.
548 const VanillaEditPlan& edit) {
549 formatter.BeginArray("vanilla_regions");
550 const auto& new_writes = edit.plan.writes();
551 const auto& old_writes = edit.current.writes();
552 for (size_t index = 0; index < std::size(kVanillaTextRegions); ++index) {
553 const VanillaTextRegion& region = kVanillaTextRegions[index];
554 const uint64_t capacity = region.capacity_end - region.start;
555 const uint64_t used =
556 index < new_writes.size() ? new_writes[index].bytes().size() : 0;
557 const uint64_t current_used =
558 index < old_writes.size() ? old_writes[index].bytes().size() : 0;
559 formatter.BeginObject();
560 formatter.AddField("region", region.name);
561 formatter.AddHexField("start", region.start, 6);
562 formatter.AddHexField("capacity_end", region.capacity_end, 6);
563 formatter.AddField("capacity", capacity);
564 formatter.AddField("current_used", current_used);
565 formatter.AddField("used", used);
566 // A pinned region cannot grow, so it has no free bytes to offer.
567 const bool pinned = index == 1 && edit.secondary_length_pinned;
568 formatter.AddField(
569 "free", pinned || used > capacity ? uint64_t{0} : capacity - used);
570 if (pinned) {
571 formatter.AddField("length_pinned", true);
572 }
573 formatter.EndObject();
574 }
575 formatter.EndArray();
576
577 uint64_t changed_bytes = 0;
578 formatter.BeginArray("changed_ranges");
579 for (const auto& [start, end] : edit.changed_ranges) {
580 changed_bytes += end - start;
581 formatter.AddArrayItem(absl::StrFormat("0x%06X-0x%06X", start, end));
582 }
583 formatter.EndArray();
584 formatter.AddField("changed_bytes", changed_bytes);
585
586 formatter.BeginArray("encoded_messages");
587 for (const EditedVanillaMessage& message : edit.edited) {
588 formatter.BeginObject();
589 formatter.AddHexField("id", static_cast<uint64_t>(message.id), 3);
590 formatter.AddField("previous_length",
591 static_cast<uint64_t>(message.previous_length));
592 formatter.AddField("encoded_length",
593 static_cast<uint64_t>(message.encoded_length));
594 if (message.unchanged) {
595 formatter.AddField("unchanged", true);
596 }
597 formatter.EndObject();
598 }
599 formatter.EndArray();
600}
601
602// Dry run of a vanilla apply: with a ROM (--rom) and a project, builds the
603// plan and reports it without writing. Reports why it was skipped otherwise.
605 const resources::ArgumentParser& parser, Rom* rom,
606 const std::vector<std::pair<int, std::vector<uint8_t>>>& edits,
607 resources::OutputFormatter& formatter) {
608 Rom owned_rom;
609 Rom* active_rom = rom;
610 if (active_rom == nullptr || !active_rom->is_loaded()) {
611 auto rom_path = parser.GetString("rom");
612 if (!rom_path.has_value() || rom_path->empty()) {
613 const std::string global_rom_path = absl::GetFlag(FLAGS_rom);
614 if (!global_rom_path.empty()) {
615 rom_path = global_rom_path;
616 }
617 }
618 if (!rom_path.has_value() || rom_path->empty()) {
619 formatter.AddField("apply_preflight", "skipped: no --rom");
620 return;
621 }
622 Rom::LoadOptions load_options;
623 load_options.load_resource_labels = false;
624 const absl::Status load_status =
625 owned_rom.LoadFromFile(*rom_path, load_options);
626 if (!load_status.ok()) {
627 formatter.AddField("apply_preflight",
628 absl::StrCat("error: ", load_status.message()));
629 return;
630 }
631 active_rom = &owned_rom;
632 }
633 auto project_context_or = LoadProjectMutationContext(
634 parser, *active_rom, "Message bundle preflight");
635 if (!project_context_or.ok()) {
636 formatter.AddField(
637 "apply_preflight",
638 absl::StrCat("skipped: ", project_context_or.status().message()));
639 return;
640 }
641 std::string policy_warning;
642 auto edit_or = PlanVanillaEdits(project_context_or->project, *active_rom,
643 edits, &policy_warning);
644 if (!edit_or.ok()) {
645 formatter.AddField("apply_preflight",
646 absl::StrCat("blocked: ", edit_or.status().message()));
647 return;
648 }
649 formatter.AddField("apply_preflight", "ok");
650 if (!policy_warning.empty()) {
651 formatter.AddField("write_policy_warning", policy_warning);
652 }
653 AddVanillaPlanReport(formatter, *edit_or);
654}
655
656absl::Status VerifySavedRom(
657 const Rom& active_rom, const std::filesystem::path& saved_rom_path,
658 const std::optional<editor::VanillaMessageSavePlan>& vanilla_plan,
659 std::optional<size_t> expected_vanilla_count) {
660 Rom reopened_rom;
661 Rom::LoadOptions load_options;
662 load_options.load_resource_labels = false;
663 const absl::Status reopen_status =
664 reopened_rom.LoadFromFile(saved_rom_path.string(), load_options);
665 if (!reopen_status.ok()) {
666 return absl::DataLossError(absl::StrFormat(
667 "ROM was saved but could not be reopened for verification: %s. "
668 "Restore the required backup before retrying",
669 reopen_status.message()));
670 }
671 if (reopened_rom.vector() != active_rom.vector()) {
672 return absl::DataLossError(
673 "ROM was saved but external readback differs from the planned bytes. "
674 "Restore the required backup before retrying");
675 }
676
677 if (vanilla_plan.has_value()) {
678 if (reopened_rom.size() >
679 static_cast<size_t>(std::numeric_limits<int>::max())) {
680 return absl::DataLossError(
681 "ROM was saved but is too large for bounded vanilla-message "
682 "readback. Restore the required backup before retrying");
683 }
684 const auto reopened_messages =
686 static_cast<int>(reopened_rom.size()));
687 auto readback_plan_or = editor::BuildVanillaMessageSavePlan(
688 reopened_messages, expected_vanilla_count);
689 if (!readback_plan_or.ok() ||
690 readback_plan_or.value() != vanilla_plan.value()) {
691 const std::string detail =
692 readback_plan_or.ok()
693 ? "serialized message bytes differ from the save plan"
694 : std::string(readback_plan_or.status().message());
695 return absl::DataLossError(absl::StrFormat(
696 "ROM was saved but vanilla-message readback failed: %s. Restore the "
697 "required backup before retrying",
698 detail));
699 }
700 }
701
702 return absl::OkStatus();
703}
704
705absl::Status ValidateExpandedMessageId(int id,
706 const ExpandedMutationContext& context) {
707 if (id < 0) {
708 return absl::InvalidArgumentError(
709 absl::StrFormat("Invalid expanded message ID: %d", id));
710 }
711 if (context.message_limit >= 0 && id >= context.message_limit) {
712 return absl::OutOfRangeError(absl::StrFormat(
713 "Expanded message ID %d is outside the manifest range (count=%d)", id,
714 context.message_limit));
715 }
716 return absl::OkStatus();
717}
718
720 size_t message_count, const ExpandedMutationContext& context) {
721 if (context.message_limit >= 0 &&
722 message_count > static_cast<size_t>(context.message_limit)) {
723 return absl::FailedPreconditionError(absl::StrFormat(
724 "Expanded message bank contains %zu messages, exceeding the manifest "
725 "limit of %d; no changes were applied",
726 message_count, context.message_limit));
727 }
728 return absl::OkStatus();
729}
730} // namespace
731
732// ===========================================================================
733// Existing Commands
734// ===========================================================================
735
737 Rom* rom, const resources::ArgumentParser& parser,
738 resources::OutputFormatter& formatter) {
739 auto limit = parser.GetInt("limit").value_or(50);
740 if (limit < 0) {
741 limit = 0;
742 }
743
744 auto messages = editor::ReadAllTextData(const_cast<uint8_t*>(rom->data()),
746 if (limit > static_cast<int>(messages.size())) {
747 limit = static_cast<int>(messages.size());
748 }
749
750 formatter.BeginObject("Message List");
751 formatter.AddField("limit", limit);
752 formatter.AddField("total_messages", static_cast<int>(messages.size()));
753 formatter.AddField("status", "success");
754
755 formatter.BeginArray("messages");
756 for (int i = 0; i < limit; ++i) {
757 const auto& msg = messages[i];
758 formatter.BeginObject();
759 formatter.AddField("id", msg.ID);
760 formatter.AddHexField("address", msg.Address, 6);
761 formatter.AddField("text", msg.ContentsParsed);
762 formatter.AddField("length", static_cast<int>(msg.Data.size()));
763 formatter.EndObject();
764 }
765 formatter.EndArray();
766 formatter.EndObject();
767
768 return absl::OkStatus();
769}
770
772 Rom* rom, const resources::ArgumentParser& parser,
773 resources::OutputFormatter& formatter) {
774 auto message_id_or = parser.GetInt("id");
775 if (!message_id_or.ok()) {
776 return message_id_or.status();
777 }
778 int message_id = message_id_or.value();
779
780 auto messages = editor::ReadAllTextData(const_cast<uint8_t*>(rom->data()),
782 if (message_id < 0 || message_id >= static_cast<int>(messages.size())) {
783 return absl::NotFoundError(
784 absl::StrFormat("Message ID %d not found (max: %d)", message_id,
785 static_cast<int>(messages.size()) - 1));
786 }
787
788 const auto& msg = messages[message_id];
789
790 formatter.BeginObject("Message");
791 formatter.AddField("id", msg.ID);
792 formatter.AddHexField("address", msg.Address, 6);
793 formatter.AddField("text", msg.ContentsParsed);
794 formatter.AddField("length", static_cast<int>(msg.Data.size()));
795 formatter.EndObject();
796
797 return absl::OkStatus();
798}
799
801 Rom* rom, const resources::ArgumentParser& parser,
802 resources::OutputFormatter& formatter) {
803 auto query = parser.GetString("query").value();
804 auto limit = parser.GetInt("limit").value_or(10);
805 if (limit < 0) {
806 limit = 0;
807 }
808
809 auto messages = editor::ReadAllTextData(const_cast<uint8_t*>(rom->data()),
811
812 formatter.BeginObject("Message Search Results");
813 formatter.AddField("query", query);
814 formatter.AddField("limit", limit);
815 formatter.AddField("status", "success");
816
817 std::string lowered_query = absl::AsciiStrToLower(query);
818 std::vector<const editor::MessageData*> matches;
819 for (const auto& msg : messages) {
820 std::string lowered_text = absl::AsciiStrToLower(msg.ContentsParsed);
821 if (lowered_text.find(lowered_query) == std::string::npos) {
822 continue;
823 }
824 matches.push_back(&msg);
825 }
826
827 formatter.AddField("matches_found", static_cast<int>(matches.size()));
828 formatter.BeginArray("matches");
829 int match_count = 0;
830 for (const auto* msg : matches) {
831 if (limit > 0 && match_count >= limit) {
832 break;
833 }
834 formatter.BeginObject();
835 formatter.AddField("id", msg->ID);
836 formatter.AddHexField("address", msg->Address, 6);
837 formatter.AddField("text", msg->ContentsParsed);
838 formatter.EndObject();
839 match_count++;
840 }
841 formatter.EndArray();
842 formatter.EndObject();
843
844 return absl::OkStatus();
845}
846
847// ===========================================================================
848// New: Encode Command
849// ===========================================================================
850
852 Rom* rom, const resources::ArgumentParser& parser,
853 resources::OutputFormatter& formatter) {
854 auto text = parser.GetString("text").value();
855
856 auto parse_result = editor::ParseMessageToDataWithDiagnostics(text);
857 auto bytes = parse_result.bytes;
858
859 // Build hex string
860 std::string hex_str;
861 for (size_t i = 0; i < bytes.size(); ++i) {
862 if (i > 0)
863 hex_str += " ";
864 hex_str += absl::StrFormat("%02X", bytes[i]);
865 }
866
867 formatter.BeginObject("Encoded Message");
868 formatter.AddField("input", text);
869 formatter.AddField("hex", hex_str);
870 formatter.AddField("length", static_cast<int>(bytes.size()));
871
872 // Also output as byte array for JSON consumers
873 formatter.BeginArray("bytes");
874 for (uint8_t byte : bytes) {
875 formatter.AddArrayItem(absl::StrFormat("0x%02X", byte));
876 }
877 formatter.EndArray();
878
879 // Run line width validation
880 auto warnings = editor::ValidateMessageLineWidths(text);
881 if (!warnings.empty()) {
882 formatter.BeginArray("line_width_warnings");
883 for (const auto& warning : warnings) {
884 formatter.AddArrayItem(warning);
885 }
886 formatter.EndArray();
887 }
888 if (!parse_result.warnings.empty()) {
889 formatter.BeginArray("warnings");
890 for (const auto& warning : parse_result.warnings) {
891 formatter.AddArrayItem(warning);
892 }
893 formatter.EndArray();
894 }
895 if (!parse_result.errors.empty()) {
896 formatter.BeginArray("errors");
897 for (const auto& error : parse_result.errors) {
898 formatter.AddArrayItem(error);
899 }
900 formatter.EndArray();
901 }
902
903 formatter.EndObject();
904
905 return absl::OkStatus();
906}
907
908// ===========================================================================
909// New: Decode Command
910// ===========================================================================
911
913 Rom* rom, const resources::ArgumentParser& parser,
914 resources::OutputFormatter& formatter) {
915 auto hex_input = parser.GetString("hex").value();
916
917 // Parse hex string to bytes
918 std::vector<uint8_t> bytes;
919 std::istringstream hex_stream(hex_input);
920 std::string hex_byte;
921 while (hex_stream >> hex_byte) {
922 try {
923 bytes.push_back(static_cast<uint8_t>(std::stoi(hex_byte, nullptr, 16)));
924 } catch (const std::exception&) {
925 return absl::InvalidArgumentError(
926 absl::StrFormat("Invalid hex byte: '%s'", hex_byte));
927 }
928 }
929
930 // Decode bytes to text, handling commands with arguments
931 std::string decoded;
932 for (size_t i = 0; i < bytes.size(); ++i) {
933 uint8_t byte = bytes[i];
934
935 // Check for command (may consume next byte as argument)
936 auto cmd = editor::FindMatchingCommand(byte);
937 if (cmd.has_value()) {
938 if (cmd->HasArgument && i + 1 < bytes.size()) {
939 decoded += cmd->GetParamToken(bytes[++i]);
940 } else {
941 decoded += cmd->GetParamToken();
942 }
943 continue;
944 }
945
946 // Single-byte lookup for chars, specials, dictionary
947 decoded += editor::ParseTextDataByte(byte);
948 }
949
950 formatter.BeginObject("Decoded Message");
951 formatter.AddField("hex", hex_input);
952 formatter.AddField("text", decoded);
953 formatter.AddField("length", static_cast<int>(bytes.size()));
954 formatter.EndObject();
955
956 return absl::OkStatus();
957}
958
959// ===========================================================================
960// New: Import Org Command
961// ===========================================================================
962
964 Rom* rom, const resources::ArgumentParser& parser,
965 resources::OutputFormatter& formatter) {
966 auto file_path = parser.GetString("file").value();
967
968 // Read the .org file
969 std::ifstream file(file_path);
970 if (!file.is_open()) {
971 return absl::NotFoundError(
972 absl::StrFormat("Cannot open file: %s", file_path));
973 }
974
975 std::string content((std::istreambuf_iterator<char>(file)),
976 std::istreambuf_iterator<char>());
977 file.close();
978
979 auto messages = editor::ParseOrgContent(content);
980
981 formatter.BeginObject("Org Import Results");
982 formatter.AddField("file", file_path);
983 formatter.AddField("messages_parsed", static_cast<int>(messages.size()));
984
985 formatter.BeginArray("messages");
986 for (const auto& [msg_id, body] : messages) {
987 // Encode the body text to bytes with diagnostics
988 auto parse_result = editor::ParseMessageToDataWithDiagnostics(body);
989 auto bytes = parse_result.bytes;
990 auto warnings = editor::ValidateMessageLineWidths(body);
991
992 std::string hex_str;
993 for (size_t i = 0; i < bytes.size(); ++i) {
994 if (i > 0)
995 hex_str += " ";
996 hex_str += absl::StrFormat("%02X", bytes[i]);
997 }
998
999 formatter.BeginObject();
1000 formatter.AddHexField("id", msg_id, 2);
1001 formatter.AddField("text", body);
1002 formatter.AddField("hex", hex_str);
1003 formatter.AddField("encoded_length", static_cast<int>(bytes.size()));
1004 if (!warnings.empty()) {
1005 formatter.BeginArray("warnings");
1006 for (const auto& warning : warnings) {
1007 formatter.AddArrayItem(warning);
1008 }
1009 formatter.EndArray();
1010 }
1011 if (!parse_result.warnings.empty()) {
1012 formatter.BeginArray("parse_warnings");
1013 for (const auto& warning : parse_result.warnings) {
1014 formatter.AddArrayItem(warning);
1015 }
1016 formatter.EndArray();
1017 }
1018 if (!parse_result.errors.empty()) {
1019 formatter.BeginArray("errors");
1020 for (const auto& error : parse_result.errors) {
1021 formatter.AddArrayItem(error);
1022 }
1023 formatter.EndArray();
1024 }
1025 formatter.EndObject();
1026 }
1027 formatter.EndArray();
1028 formatter.EndObject();
1029
1030 return absl::OkStatus();
1031}
1032
1033// ===========================================================================
1034// New: Export Org Command
1035// ===========================================================================
1036
1038 Rom* rom, const resources::ArgumentParser& parser,
1039 resources::OutputFormatter& formatter) {
1040 auto output_path = parser.GetString("output").value();
1041
1042 auto messages = editor::ReadAllTextData(const_cast<uint8_t*>(rom->data()),
1044
1045 // Build message pairs and labels
1046 std::vector<std::pair<int, std::string>> msg_pairs;
1047 std::vector<std::string> labels;
1048 for (const auto& msg : messages) {
1049 msg_pairs.push_back({msg.ID, msg.RawString});
1050 labels.push_back(absl::StrFormat("Message %02X", msg.ID));
1051 }
1052
1053 std::string org_content = editor::ExportToOrgFormat(msg_pairs, labels);
1054
1055 // Write to file
1056 std::ofstream file(output_path);
1057 if (!file.is_open()) {
1058 return absl::InternalError(
1059 absl::StrFormat("Cannot write to file: %s", output_path));
1060 }
1061 file << org_content;
1062 file.close();
1063
1064 formatter.BeginObject("Org Export Results");
1065 formatter.AddField("output", output_path);
1066 formatter.AddField("messages_exported", static_cast<int>(messages.size()));
1067 formatter.AddField("status", "success");
1068 formatter.EndObject();
1069
1070 return absl::OkStatus();
1071}
1072
1073// ===========================================================================
1074// New: Export Bundle Command
1075// ===========================================================================
1076
1078 Rom* rom, const resources::ArgumentParser& parser,
1079 resources::OutputFormatter& formatter) {
1080 auto output_path = parser.GetString("output").value();
1081 auto range = NormalizeRange(parser.GetString("range").value_or("all"));
1082 if (!IncludeVanilla(range) && !IncludeExpanded(range)) {
1083 return absl::InvalidArgumentError(
1084 absl::StrFormat("Invalid range: %s", range));
1085 }
1086
1087 std::vector<editor::MessageData> vanilla;
1088 std::vector<editor::MessageData> expanded;
1089
1090 if (IncludeVanilla(range)) {
1091 vanilla = editor::ReadAllTextData(const_cast<uint8_t*>(rom->data()),
1093 }
1094
1095 if (IncludeExpanded(range)) {
1096 expanded = ReadExpandedMessages(*rom);
1097 }
1098
1099 auto status =
1100 editor::ExportMessageBundleToJson(output_path, vanilla, expanded);
1101 if (!status.ok()) {
1102 return status;
1103 }
1104
1105 formatter.BeginObject("Message Bundle Export");
1106 formatter.AddField("output", output_path);
1107 formatter.AddField("range", range);
1108 formatter.AddField("vanilla_count", static_cast<int>(vanilla.size()));
1109 formatter.AddField("expanded_count", static_cast<int>(expanded.size()));
1110 formatter.AddField("status", "success");
1111 formatter.EndObject();
1112
1113 return absl::OkStatus();
1114}
1115
1116// ===========================================================================
1117// New: Import Bundle Command
1118// ===========================================================================
1119
1121 Rom* rom, const resources::ArgumentParser& parser,
1122 resources::OutputFormatter& formatter) {
1123 auto file_path = parser.GetString("file").value();
1124 const bool apply = parser.HasFlag("apply");
1125 const bool strict = parser.HasFlag("strict");
1126#ifdef __EMSCRIPTEN__
1127 if (apply) {
1128 return absl::FailedPreconditionError(
1129 "message-import-bundle --apply is unavailable in WebAssembly because "
1130 "the browser filesystem cannot provide durable backup and readback "
1131 "guarantees");
1132 }
1133#endif
1134 auto range = NormalizeRange(parser.GetString("range").value_or("all"));
1135 if (!IncludeVanilla(range) && !IncludeExpanded(range)) {
1136 return absl::InvalidArgumentError(
1137 absl::StrFormat("Invalid range: %s", range));
1138 }
1139
1140 auto entries_or = editor::LoadMessageBundleFromJson(file_path);
1141 if (!entries_or.ok()) {
1142 return entries_or.status();
1143 }
1144 auto entries = entries_or.value();
1145
1146 formatter.BeginObject("Message Bundle Import");
1147 formatter.AddField("file", file_path);
1148 formatter.AddField("range", range);
1149 formatter.AddField("apply", apply);
1150 formatter.AddField("strict", strict);
1151 formatter.AddField("entries", static_cast<int>(entries.size()));
1152
1153 bool has_errors = false;
1154 int parse_error_count = 0;
1155 int error_count = 0;
1156 int applied_updates = 0;
1157 bool has_vanilla_entries = false;
1158 bool has_expanded_entries = false;
1159
1160 struct ParsedEntry {
1163 std::vector<std::string> line_warnings;
1164 };
1165 std::vector<ParsedEntry> parsed_entries;
1166 parsed_entries.reserve(entries.size());
1167
1168 for (const auto& entry : entries) {
1169 if ((entry.bank == editor::MessageBank::kVanilla &&
1170 !IncludeVanilla(range)) ||
1171 (entry.bank == editor::MessageBank::kExpanded &&
1172 !IncludeExpanded(range))) {
1173 continue;
1174 }
1175
1176 ParsedEntry parsed{entry,
1179 if (!parsed.parse.ok()) {
1180 has_errors = true;
1181 parse_error_count += static_cast<int>(parsed.parse.errors.size());
1182 error_count += static_cast<int>(parsed.parse.errors.size());
1183 }
1184 if (entry.bank == editor::MessageBank::kVanilla) {
1185 has_vanilla_entries = true;
1186 } else {
1187 has_expanded_entries = true;
1188 }
1189 parsed_entries.push_back(std::move(parsed));
1190 }
1191
1192 formatter.BeginArray("messages");
1193 for (const auto& parsed : parsed_entries) {
1194 formatter.BeginObject();
1195 formatter.AddField("id", parsed.entry.id);
1196 formatter.AddField("bank", BankLabel(parsed.entry.bank));
1197 formatter.AddField("text", parsed.entry.text);
1198 formatter.AddField("encoded_length",
1199 static_cast<int>(parsed.parse.bytes.size()));
1200 if (!parsed.line_warnings.empty()) {
1201 formatter.BeginArray("line_width_warnings");
1202 for (const auto& warning : parsed.line_warnings) {
1203 formatter.AddArrayItem(warning);
1204 }
1205 formatter.EndArray();
1206 }
1207 if (!parsed.parse.warnings.empty()) {
1208 formatter.BeginArray("warnings");
1209 for (const auto& warning : parsed.parse.warnings) {
1210 formatter.AddArrayItem(warning);
1211 }
1212 formatter.EndArray();
1213 }
1214 if (!parsed.parse.errors.empty()) {
1215 formatter.BeginArray("errors");
1216 for (const auto& error : parsed.parse.errors) {
1217 formatter.AddArrayItem(error);
1218 }
1219 formatter.EndArray();
1220 }
1221 formatter.EndObject();
1222 }
1223 formatter.EndArray();
1224
1225 Rom owned_rom;
1226 Rom* active_rom = rom;
1227
1228 if (apply) {
1229 if (active_rom == nullptr || !active_rom->is_loaded()) {
1230 auto rom_path = parser.GetString("rom");
1231 if (!rom_path.has_value() || rom_path->empty()) {
1232 std::string global_rom_path = absl::GetFlag(FLAGS_rom);
1233 if (!global_rom_path.empty()) {
1234 rom_path = global_rom_path;
1235 }
1236 }
1237 if (!rom_path.has_value() || rom_path->empty()) {
1238 error_count++;
1239 formatter.AddField("status", "error");
1240 formatter.AddField("error",
1241 "ROM not loaded; provide --rom when using --apply");
1242 formatter.AddField("parse_error_count", parse_error_count);
1243 formatter.AddField("error_count", error_count);
1244 formatter.EndObject();
1245 return absl::OkStatus();
1246 }
1247 auto load_status = owned_rom.LoadFromFile(*rom_path);
1248 if (!load_status.ok()) {
1249 error_count++;
1250 formatter.AddField("status", "error");
1251 formatter.AddField("error", std::string(load_status.message()));
1252 formatter.AddField("parse_error_count", parse_error_count);
1253 formatter.AddField("error_count", error_count);
1254 formatter.EndObject();
1255 return absl::OkStatus();
1256 }
1257 active_rom = &owned_rom;
1258 }
1259
1260 if (has_errors) {
1261 formatter.AddField("status", "error");
1262 formatter.AddField("error", "Parse errors present; no changes applied");
1263 formatter.AddField("parse_error_count", parse_error_count);
1264 formatter.AddField("error_count", error_count);
1265 formatter.EndObject();
1266 if (strict && parse_error_count > 0) {
1267 formatter.EndObject();
1268 formatter.Print();
1269 return absl::FailedPreconditionError(
1270 "Strict validation failed due to parse errors");
1271 }
1272 return absl::OkStatus();
1273 }
1274
1275 std::optional<ProjectMutationContext> project_context;
1276 std::vector<uint8_t> disk_baseline;
1277 if (has_vanilla_entries || has_expanded_entries) {
1278 const absl::Status snapshot_status =
1279 VerifyCleanDiskSnapshot(*active_rom, &disk_baseline);
1280 if (!snapshot_status.ok()) {
1281 error_count++;
1282 formatter.AddField("status", "error");
1283 formatter.AddField("error", std::string(snapshot_status.message()));
1284 formatter.AddField("parse_error_count", parse_error_count);
1285 formatter.AddField("error_count", error_count);
1286 formatter.EndObject();
1287 return snapshot_status;
1288 }
1289
1290 auto project_context_or = LoadProjectMutationContext(
1291 parser, *active_rom, "Message bundle apply");
1292 if (!project_context_or.ok()) {
1293 error_count++;
1294 formatter.AddField("status", "error");
1295 formatter.AddField("error",
1296 std::string(project_context_or.status().message()));
1297 formatter.AddField("parse_error_count", parse_error_count);
1298 formatter.AddField("error_count", error_count);
1299 formatter.EndObject();
1300 return project_context_or.status();
1301 }
1302 project_context.emplace(std::move(project_context_or.value()));
1303 }
1304
1305 std::optional<ExpandedMutationContext> expanded_context;
1306 if (IncludeExpanded(range) && has_expanded_entries) {
1307 auto context_or =
1308 PreflightExpandedMutation(project_context->project, *active_rom);
1309 if (!context_or.ok()) {
1310 error_count++;
1311 formatter.AddField("status", "error");
1312 formatter.AddField("error", std::string(context_or.status().message()));
1313 formatter.AddField("parse_error_count", parse_error_count);
1314 formatter.AddField("error_count", error_count);
1315 formatter.EndObject();
1316 return context_or.status();
1317 }
1318 expanded_context.emplace(std::move(context_or.value()));
1319 }
1320
1321 // Build both replacement banks and validate every selected ID before the
1322 // first ROM write. This keeps a bad expanded ID from landing after a
1323 // successful vanilla mutation in a mixed bundle.
1324 std::vector<editor::MessageData> vanilla_messages;
1325 std::vector<std::pair<int, std::vector<uint8_t>>> vanilla_edits;
1326 if (IncludeVanilla(range) && has_vanilla_entries) {
1327 if (active_rom->size() >
1328 static_cast<size_t>(std::numeric_limits<int>::max())) {
1329 formatter.EndObject();
1330 return absl::OutOfRangeError(
1331 "ROM is too large for bounded vanilla-message parsing");
1332 }
1333 vanilla_messages = editor::ReadAllTextData(
1334 const_cast<uint8_t*>(active_rom->data()), editor::kTextData,
1335 static_cast<int>(active_rom->size()));
1336 }
1337
1338 std::vector<std::string> expanded_texts;
1339 if (expanded_context.has_value()) {
1340 const auto expanded_messages = ReadExpandedMessages(
1341 *active_rom, expanded_context->start, expanded_context->end);
1342 auto bank_size_status = ValidateExpandedMessageBankSize(
1343 expanded_messages.size(), *expanded_context);
1344 if (!bank_size_status.ok()) {
1345 formatter.AddField("status", "error");
1346 formatter.AddField("error", std::string(bank_size_status.message()));
1347 formatter.EndObject();
1348 return bank_size_status;
1349 }
1350 expanded_texts.reserve(expanded_messages.size());
1351 for (const auto& message : expanded_messages) {
1352 expanded_texts.push_back(message.RawString);
1353 }
1354 }
1355
1356 for (const auto& parsed : parsed_entries) {
1357 if (parsed.entry.bank == editor::MessageBank::kVanilla) {
1358 if (parsed.entry.id < 0 ||
1359 parsed.entry.id >= static_cast<int>(vanilla_messages.size())) {
1360 has_errors = true;
1361 error_count++;
1362 continue;
1363 }
1364 vanilla_edits.emplace_back(parsed.entry.id, parsed.parse.bytes);
1365 } else {
1366 if (!expanded_context.has_value()) {
1367 continue;
1368 }
1369 const auto id_status =
1370 ValidateExpandedMessageId(parsed.entry.id, *expanded_context);
1371 if (!id_status.ok()) {
1372 has_errors = true;
1373 error_count++;
1374 continue;
1375 }
1376 if (parsed.entry.id >= static_cast<int>(expanded_texts.size())) {
1377 expanded_texts.resize(parsed.entry.id + 1);
1378 }
1379 expanded_texts[parsed.entry.id] = parsed.entry.text;
1380 }
1381 applied_updates++;
1382 }
1383
1384 std::optional<editor::VanillaMessageSavePlan> vanilla_plan;
1385 std::optional<size_t> expected_vanilla_count;
1386 std::string write_policy_warning;
1387 if (!has_errors && IncludeVanilla(range) && has_vanilla_entries) {
1388 auto edit_or = PlanVanillaEdits(project_context->project, *active_rom,
1389 vanilla_edits, &write_policy_warning);
1390 if (!edit_or.ok()) {
1391 formatter.AddField("status", "error");
1392 formatter.AddField("error", std::string(edit_or.status().message()));
1393 formatter.EndObject();
1394 return edit_or.status();
1395 }
1396 AddVanillaPlanReport(formatter, *edit_or);
1397 expected_vanilla_count = edit_or->expected_count;
1398 vanilla_plan.emplace(std::move(edit_or->plan));
1399 }
1400
1401 if (expanded_context.has_value() &&
1402 !expanded_context->policy_warning.empty()) {
1403 if (!write_policy_warning.empty()) {
1404 absl::StrAppend(&write_policy_warning, "; ");
1405 }
1406 absl::StrAppend(&write_policy_warning, expanded_context->policy_warning);
1407 }
1408 if (!write_policy_warning.empty()) {
1409 formatter.AddField("write_policy_warning", write_policy_warning);
1410 }
1411
1412 if (has_errors) {
1413 formatter.AddField("status", "error");
1414 formatter.AddField("error", "Invalid message IDs; no changes applied");
1415 } else {
1416 ScopedRomTransaction transaction(*active_rom);
1417 if (vanilla_plan.has_value()) {
1418 auto status =
1419 editor::ApplyVanillaMessageSavePlan(active_rom, *vanilla_plan);
1420 if (!status.ok()) {
1421 formatter.AddField("status", "error");
1422 formatter.AddField("error", std::string(status.message()));
1423 formatter.EndObject();
1424 return status;
1425 }
1426 }
1427
1428 if (expanded_context.has_value()) {
1429 auto status = editor::WriteExpandedTextData(
1430 active_rom, expanded_context->start, expanded_context->end,
1431 expanded_texts);
1432 if (!status.ok()) {
1433 formatter.AddField("status", "error");
1434 formatter.AddField("error", std::string(status.message()));
1435 formatter.EndObject();
1436 return status;
1437 }
1438 }
1439
1440 if (active_rom->dirty() && project_context.has_value()) {
1441 const absl::Status unchanged_status = VerifyDiskSnapshotUnchanged(
1442 project_context->canonical_rom_path, disk_baseline);
1443 if (!unchanged_status.ok()) {
1444 formatter.AddField("status", "error");
1445 formatter.AddField("error", std::string(unchanged_status.message()));
1446 formatter.EndObject();
1447 return unchanged_status;
1448 }
1449 Rom::SaveSettings save_settings;
1450 save_settings.save_new = false;
1451 save_settings.require_backup = true;
1452 save_settings.filename = project_context->canonical_rom_path.string();
1453 auto save_status = active_rom->SaveToFile(save_settings);
1454 if (!save_status.ok()) {
1455 formatter.AddField("status", "error");
1456 formatter.AddField("error", std::string(save_status.message()));
1457 formatter.EndObject();
1458 return save_status;
1459 }
1460 }
1461 transaction.Commit();
1462
1463 if (project_context.has_value()) {
1464 const absl::Status readback_status =
1465 VerifySavedRom(*active_rom, project_context->canonical_rom_path,
1466 vanilla_plan, expected_vanilla_count);
1467 if (!readback_status.ok()) {
1468 formatter.AddField("status", "error");
1469 formatter.AddField("error", std::string(readback_status.message()));
1470 formatter.EndObject();
1471 return readback_status;
1472 }
1473 }
1474 formatter.AddField("status", "success");
1475 formatter.AddField("applied_messages", applied_updates);
1476 if (project_context.has_value()) {
1477 formatter.AddField("readback_verified", true);
1478 }
1479 }
1480 } else {
1481 if (!has_errors && IncludeVanilla(range) && has_vanilla_entries) {
1482 std::vector<std::pair<int, std::vector<uint8_t>>> vanilla_edits;
1483 for (const auto& parsed : parsed_entries) {
1484 if (parsed.entry.bank == editor::MessageBank::kVanilla) {
1485 vanilla_edits.emplace_back(parsed.entry.id, parsed.parse.bytes);
1486 }
1487 }
1488 AddVanillaApplyPreflight(parser, rom, vanilla_edits, formatter);
1489 }
1490 formatter.AddField("status", has_errors ? "error" : "success");
1491 }
1492
1493 formatter.AddField("error_count", error_count);
1494 formatter.AddField("parse_error_count", parse_error_count);
1495 formatter.EndObject();
1496 if (strict && parse_error_count > 0) {
1497 formatter.EndObject();
1498 formatter.Print();
1499 return absl::FailedPreconditionError("Strict validation failed");
1500 }
1501 return absl::OkStatus();
1502}
1503
1504// ===========================================================================
1505// New: Message Write Command
1506// ===========================================================================
1507
1508namespace {
1509
1510// message-write --range vanilla: replaces one vanilla message through the
1511// same checks as message-import-bundle --apply (clean disk snapshot, project
1512// and manifest policy, dictionary encoding, backup, readback).
1513absl::Status WriteVanillaMessage(Rom* rom,
1514 const resources::ArgumentParser& parser,
1515 resources::OutputFormatter& formatter, int id,
1516 const std::string& text) {
1517#ifdef __EMSCRIPTEN__
1518 return absl::FailedPreconditionError(
1519 "message-write --range vanilla is unavailable in WebAssembly because "
1520 "the browser filesystem cannot provide durable backup and readback "
1521 "guarantees");
1522#endif
1523 if (rom == nullptr || !rom->is_loaded()) {
1524 return absl::FailedPreconditionError("ROM not loaded; provide --rom");
1525 }
1526 const editor::MessageParseResult parse =
1528 if (!parse.ok()) {
1529 return absl::InvalidArgumentError(absl::StrCat(
1530 "Message text does not encode: ", absl::StrJoin(parse.errors, "; ")));
1531 }
1532
1533 std::vector<uint8_t> disk_baseline;
1534 RETURN_IF_ERROR(VerifyCleanDiskSnapshot(*rom, &disk_baseline));
1536 ProjectMutationContext project_context,
1537 LoadProjectMutationContext(parser, *rom, "Vanilla message write"));
1538 std::vector<std::pair<int, std::vector<uint8_t>>> edits;
1539 edits.emplace_back(id, parse.bytes);
1540 std::string policy_warning;
1542 VanillaEditPlan edit,
1543 PlanVanillaEdits(project_context.project, *rom, edits, &policy_warning));
1544
1545 formatter.BeginObject("Message Write Result");
1546 formatter.AddHexField("id", static_cast<uint64_t>(id), 3);
1547 formatter.AddField("range", "vanilla");
1548 formatter.AddField("text", text);
1549 AddVanillaPlanReport(formatter, edit);
1550 if (!policy_warning.empty()) {
1551 formatter.AddField("write_policy_warning", policy_warning);
1552 }
1553 const auto line_warnings = editor::ValidateMessageLineWidths(text);
1554 if (!line_warnings.empty()) {
1555 formatter.BeginArray("line_width_warnings");
1556 for (const auto& warning : line_warnings) {
1557 formatter.AddArrayItem(warning);
1558 }
1559 formatter.EndArray();
1560 }
1561
1562 // Close the result object on every exit, so a failure still prints valid
1563 // JSON with its error (a DataLoss error names the backup to restore).
1564 auto finish = [&formatter](absl::Status status) {
1565 if (status.ok()) {
1566 formatter.AddField("status", "success");
1567 formatter.AddField("readback_verified", true);
1568 } else {
1569 formatter.AddField("status", "error");
1570 formatter.AddField("error", std::string(status.message()));
1571 }
1572 formatter.EndObject();
1573 return status;
1574 };
1575
1576 ScopedRomTransaction transaction(*rom);
1577 absl::Status status = editor::ApplyVanillaMessageSavePlan(rom, edit.plan);
1578 if (status.ok() && rom->dirty()) {
1579 status = VerifyDiskSnapshotUnchanged(project_context.canonical_rom_path,
1580 disk_baseline);
1581 if (status.ok()) {
1582 Rom::SaveSettings save_settings;
1583 save_settings.save_new = false;
1584 save_settings.require_backup = true;
1585 save_settings.filename = project_context.canonical_rom_path.string();
1586 status = rom->SaveToFile(save_settings);
1587 }
1588 }
1589 if (!status.ok()) {
1590 return finish(status); // the transaction rolls the ROM back
1591 }
1592 transaction.Commit();
1593 return finish(VerifySavedRom(*rom, project_context.canonical_rom_path,
1594 edit.plan, edit.expected_count));
1595}
1596
1597// message-write --range expanded: replaces one expanded message and saves the
1598// bound project ROM with the same clean-snapshot, backup and readback checks
1599// as the vanilla range. Success is reported only after the bytes are on disk.
1600absl::Status WriteExpandedMessage(Rom* rom,
1601 const resources::ArgumentParser& parser,
1602 resources::OutputFormatter& formatter, int id,
1603 const std::string& text) {
1604#ifdef __EMSCRIPTEN__
1605 return absl::FailedPreconditionError(
1606 "message-write --range expanded is unavailable in WebAssembly because "
1607 "the browser filesystem cannot provide durable backup and readback "
1608 "guarantees");
1609#endif
1610 if (rom == nullptr || !rom->is_loaded()) {
1611 return absl::FailedPreconditionError("ROM not loaded; provide --rom");
1612 }
1613 const auto warnings = editor::ValidateMessageLineWidths(text);
1614 const auto bytes = editor::ParseMessageToData(text);
1615 if (bytes.empty() && !text.empty()) {
1616 return absl::InvalidArgumentError("Encoding produced no bytes");
1617 }
1618
1620 ProjectMutationContext project_context,
1621 LoadProjectMutationContext(parser, *rom, "Expanded message mutation"));
1623 PreflightExpandedMutation(project_context.project, *rom));
1625 std::vector<uint8_t> disk_baseline;
1626 RETURN_IF_ERROR(VerifyCleanDiskSnapshot(*rom, &disk_baseline));
1627
1628 const auto expanded = ReadExpandedMessages(*rom, context.start, context.end);
1629 RETURN_IF_ERROR(ValidateExpandedMessageBankSize(expanded.size(), context));
1630 std::vector<std::string> all_texts;
1631 all_texts.reserve(expanded.size());
1632 for (const auto& msg : expanded) {
1633 all_texts.push_back(msg.RawString);
1634 }
1635 if (id >= static_cast<int>(all_texts.size())) {
1636 all_texts.resize(id + 1);
1637 }
1638 all_texts[id] = text;
1639
1640 formatter.BeginObject("Message Write Result");
1641 formatter.AddField("id", id);
1642 formatter.AddField("range", "expanded");
1643 formatter.AddField("text", text);
1644 formatter.AddField("encoded_length", static_cast<int>(bytes.size()));
1645 if (!context.policy_warning.empty()) {
1646 formatter.AddField("write_policy_warning", context.policy_warning);
1647 }
1648 if (!warnings.empty()) {
1649 formatter.BeginArray("line_width_warnings");
1650 for (const auto& warning : warnings) {
1651 formatter.AddArrayItem(warning);
1652 }
1653 formatter.EndArray();
1654 }
1655
1656 // Close the result object on every exit, so a failure still prints valid
1657 // JSON with its error (a DataLoss error names the backup to restore).
1658 auto finish = [&formatter](absl::Status status) {
1659 if (status.ok()) {
1660 formatter.AddField("status", "success");
1661 formatter.AddField("readback_verified", true);
1662 } else {
1663 formatter.AddField("status", "error");
1664 formatter.AddField("error", std::string(status.message()));
1665 }
1666 formatter.EndObject();
1667 return status;
1668 };
1669
1670 ScopedRomTransaction transaction(*rom);
1671 absl::Status status =
1672 editor::WriteExpandedTextData(rom, context.start, context.end, all_texts);
1673 if (status.ok() && rom->dirty()) {
1674 status = VerifyDiskSnapshotUnchanged(project_context.canonical_rom_path,
1675 disk_baseline);
1676 if (status.ok()) {
1677 Rom::SaveSettings save_settings;
1678 save_settings.save_new = false;
1679 save_settings.require_backup = true;
1680 save_settings.filename = project_context.canonical_rom_path.string();
1681 status = rom->SaveToFile(save_settings);
1682 }
1683 }
1684 if (!status.ok()) {
1685 return finish(status); // the transaction rolls the ROM back
1686 }
1687 transaction.Commit();
1688 return finish(VerifySavedRom(*rom, project_context.canonical_rom_path,
1689 std::nullopt, std::nullopt));
1690}
1691
1692} // namespace
1693
1695 Rom* rom, const resources::ArgumentParser& parser,
1696 resources::OutputFormatter& formatter) {
1697 auto id_or = parser.GetInt("id");
1698 if (!id_or.ok())
1699 return id_or.status();
1700 int msg_id = id_or.value();
1701
1702 auto text = parser.GetString("text").value();
1703
1704 const std::string range =
1705 NormalizeRange(parser.GetString("range").value_or("expanded"));
1706 if (range == "vanilla") {
1707 return WriteVanillaMessage(rom, parser, formatter, msg_id, text);
1708 }
1709 if (range != "expanded") {
1710 return absl::InvalidArgumentError(absl::StrFormat(
1711 "message-write --range must be vanilla or expanded, got %s", range));
1712 }
1713 return WriteExpandedMessage(rom, parser, formatter, msg_id, text);
1714}
1715
1716// ===========================================================================
1717// New: Export BIN Command
1718// ===========================================================================
1719
1721 Rom* rom, const resources::ArgumentParser& parser,
1722 resources::OutputFormatter& formatter) {
1723 auto output_path = parser.GetString("output").value();
1724 auto range = parser.GetString("range").value_or("expanded");
1725
1726 int start, end_addr;
1727 if (range == "expanded") {
1729 end_addr = editor::GetExpandedTextDataEnd();
1730 } else {
1731 start = editor::kTextData;
1732 end_addr = editor::kTextDataEnd;
1733 }
1734
1735 // Find the actual end of data (scan for 0xFF terminator)
1736 const uint8_t* data = rom->data();
1737 int data_end = start;
1738 while (data_end <= end_addr && data[data_end] != 0xFF) {
1739 data_end++;
1740 }
1741 if (data_end <= end_addr) {
1742 data_end++; // Include the 0xFF terminator
1743 }
1744
1745 int size = data_end - start;
1746
1747 std::ofstream file(output_path, std::ios::binary);
1748 if (!file.is_open()) {
1749 return absl::InternalError(
1750 absl::StrFormat("Cannot write to file: %s", output_path));
1751 }
1752 file.write(reinterpret_cast<const char*>(data + start), size);
1753 file.close();
1754
1755 formatter.BeginObject("BIN Export Result");
1756 formatter.AddField("output", output_path);
1757 formatter.AddField("range", range);
1758 formatter.AddHexField("start_address", start, 6);
1759 formatter.AddHexField("end_address", data_end - 1, 6);
1760 formatter.AddField("size_bytes", size);
1761 formatter.AddField("status", "success");
1762 formatter.EndObject();
1763
1764 return absl::OkStatus();
1765}
1766
1767// ===========================================================================
1768// New: Export ASM Command
1769// ===========================================================================
1770
1772 Rom* rom, const resources::ArgumentParser& parser,
1773 resources::OutputFormatter& formatter) {
1774 auto output_path = parser.GetString("output").value();
1775 auto range = parser.GetString("range").value_or("expanded");
1776
1777 int start;
1778 uint32_t snes_addr;
1779 if (range == "expanded") {
1781 snes_addr = 0x2F8000;
1782 } else {
1783 start = editor::kTextData;
1784 snes_addr = 0x1C0000;
1785 }
1786
1787 // Read messages from the specified region
1788 auto messages =
1789 editor::ReadAllTextData(const_cast<uint8_t*>(rom->data()), start);
1790
1791 std::ofstream file(output_path);
1792 if (!file.is_open()) {
1793 return absl::InternalError(
1794 absl::StrFormat("Cannot write to file: %s", output_path));
1795 }
1796
1797 // Write ASM header
1798 file << "; Auto-generated message data\n";
1799 file << absl::StrFormat("; Source: %s region\n", range);
1800 file << absl::StrFormat("; Messages: %d\n\n", messages.size());
1801 file << absl::StrFormat("org $%06X\n\n", snes_addr);
1802
1803 // Write each message as db directives
1804 for (const auto& msg : messages) {
1805 file << absl::StrFormat("; Message $%02X: %s\n", msg.ID,
1806 msg.ContentsParsed.substr(0, 60));
1807 file << "db ";
1808 for (size_t i = 0; i < msg.Data.size(); ++i) {
1809 if (i > 0)
1810 file << ", ";
1811 file << absl::StrFormat("$%02X", msg.Data[i]);
1812 }
1813 file << ", $7F ; terminator\n\n";
1814 }
1815
1816 // End-of-region marker
1817 file << "db $FF ; end of message data\n";
1818 file.close();
1819
1820 formatter.BeginObject("ASM Export Result");
1821 formatter.AddField("output", output_path);
1822 formatter.AddField("range", range);
1823 formatter.AddField("messages_exported", static_cast<int>(messages.size()));
1824 formatter.AddField("status", "success");
1825 formatter.EndObject();
1826
1827 return absl::OkStatus();
1828}
1829
1831 Rom* rom, const resources::ArgumentParser& parser,
1832 resources::OutputFormatter& formatter) {
1833 const std::string project_path = parser.GetString("project").value();
1834 const std::string incoming_path = parser.GetString("file").value();
1835 const bool write = parser.HasFlag("write");
1836
1837 project::YazeProject source_project;
1838 auto& resource_labels = zelda3::GetResourceLabels();
1839 absl::Status open_status;
1840 {
1841 ScopedHackManifestBindingRestore restore_manifest(resource_labels);
1842 try {
1843 open_status = source_project.Open(project_path);
1844 } catch (const std::exception& error) {
1845 return absl::InvalidArgumentError(absl::StrFormat(
1846 "Cannot load project '%s': %s", project_path, error.what()));
1847 } catch (...) {
1848 return absl::InvalidArgumentError(absl::StrFormat(
1849 "Cannot load project '%s': unknown project parse error",
1850 project_path));
1851 }
1852 }
1853 if (!open_status.ok()) {
1854 return absl::Status(open_status.code(),
1855 absl::StrFormat("Cannot load project '%s': %s",
1856 project_path, open_status.message()));
1857 }
1858 if (absl::Status manifest_status =
1859 EnsureConfiguredHackManifestLoaded(&source_project, project_path);
1860 !manifest_status.ok()) {
1861 return manifest_status;
1862 }
1863
1865 .write = write,
1866 .expected_source_sha256 =
1867 parser.GetString("expected-source-sha256").value_or(""),
1868 };
1869 auto result_or =
1870 editor::SyncMessageSource(source_project, incoming_path, options);
1871 if (!result_or.ok()) {
1872 return result_or.status();
1873 }
1874 const auto& result = *result_or;
1875
1876 formatter.BeginObject("Message Source Sync");
1877 formatter.AddField("status", "success");
1878 formatter.AddField("mode", write ? "write" : "dry-run");
1879 formatter.AddField("changed", result.changed);
1880 formatter.AddField("wrote", result.wrote);
1881 formatter.AddField("canonical_bundle", result.canonical_bundle_path.string());
1882 formatter.AddField("generated_asm_include",
1883 result.generated_asm_include_path.string());
1884 formatter.AddHexField("first_expanded_id", result.first_expanded_id, 3);
1885 formatter.AddHexField("last_expanded_id", result.last_expanded_id, 3);
1886 formatter.AddField("expanded_count", result.expanded_count);
1887 formatter.AddField("incoming_updates", result.incoming_updates);
1888 formatter.AddField("encoded_size", static_cast<int>(result.encoded_size));
1889 formatter.AddField("capacity", static_cast<int>(result.capacity));
1890 formatter.AddField("source_sha256_before", result.source_sha256_before);
1891 formatter.AddField("proposed_source_sha256", result.proposed_source_sha256);
1892 formatter.EndObject();
1893 return absl::OkStatus();
1894}
1895
1896} // namespace handlers
1897} // namespace cli
1898} // 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:52
absl::Status LoadFromFile(const std::string &filename, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:632
auto filename() const
Definition rom.h:219
auto end()
Definition rom.h:216
auto mutable_data()
Definition rom.h:214
const auto & vector() const
Definition rom.h:217
absl::Status SaveToFile(const SaveSettings &settings)
Definition rom.cc:893
auto data() const
Definition rom.h:213
auto size() const
Definition rom.h:212
bool dirty() const
Definition rom.h:200
bool is_loaded() const
Definition rom.h:199
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.
const std::vector< VanillaMessageWrite > & writes() const
Unified interface for accessing resource labels with project overrides.
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
ABSL_DECLARE_FLAG(std::string, rom)
absl::Status EnsureConfiguredHackManifestLoaded(project::YazeProject *project, absl::string_view project_path)
absl::StatusOr< VanillaEditPlan > PlanVanillaEdits(const project::YazeProject &yaze_project, const Rom &rom, const std::vector< std::pair< int, std::vector< uint8_t > > > &edits, std::string *policy_warning)
absl::Status ValidateExpandedMessageBankSize(size_t message_count, const ExpandedMutationContext &context)
void AddVanillaApplyPreflight(const resources::ArgumentParser &parser, Rom *rom, const std::vector< std::pair< int, std::vector< uint8_t > > > &edits, resources::OutputFormatter &formatter)
absl::Status VerifyDiskSnapshotUnchanged(const std::filesystem::path &rom_path, const std::vector< uint8_t > &expected_bytes)
void AddVanillaPlanReport(resources::OutputFormatter &formatter, const VanillaEditPlan &edit)
absl::Status ValidateExpandedMessageId(int id, const ExpandedMutationContext &context)
absl::StatusOr< std::filesystem::path > CanonicalExistingPath(const std::string &path, absl::string_view label)
absl::Status WriteVanillaMessage(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter, int id, const std::string &text)
absl::StatusOr< ProjectMutationContext > LoadProjectMutationContext(const resources::ArgumentParser &parser, const Rom &rom, absl::string_view mutation_scope)
std::string FormatManifestConflict(const core::WriteConflict &conflict)
absl::Status WriteExpandedMessage(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter, int id, const std::string &text)
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 std::vector< std::pair< uint32_t, uint32_t > > &changed_ranges, 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::vector< uint8_t > CompressMessageWithDictionary(const std::vector< uint8_t > &data, const std::vector< std::vector< uint8_t > > &dictionary)
constexpr int kTextData
std::string MessageBankToString(MessageBank bank)
constexpr int kTextData2
absl::Status WriteExpandedTextData(Rom *rom, int start, int end, const std::vector< std::string > &messages)
constexpr int kPointersDictionaries
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)
constexpr int kTextData2End
constexpr uint8_t kBankSwitchCommand
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)
constexpr char kOracleExpandedRomWriteRefusal[]
std::vector< MessageData > ReadExpandedTextData(uint8_t *rom, int pos)
std::vector< uint8_t > ExpandMessageDictionary(const std::vector< uint8_t > &data, const std::vector< std::vector< uint8_t > > &dictionary)
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::vector< uint8_t > > ReadDictionaryEntryBytes(const uint8_t *rom, size_t rom_size)
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
constexpr size_t kCopierHeaderSize
Size of the SMC/SWC copier header that some ROM dumps start with.
Definition rom.h:29
uint32_t SnesToPc(uint32_t addr) noexcept
Definition snes.h:8
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
bool load_resource_labels
Definition rom.h:69
std::string filename
Definition rom.h:57
A conflict detected when yaze wants to write to an ASM-owned address.
AddressOwnership ownership
std::vector< uint8_t > Data
std::vector< uint8_t > DataParsed
std::vector< uint8_t > bytes
std::vector< std::string > errors
RomWritePolicy write_policy
Definition project.h:229
Modern project structure with comprehensive settings consolidation.
Definition project.h:290
core::HackManifest hack_manifest
Definition project.h:376
std::string hack_manifest_file
Definition project.h:332
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:2675
absl::Status Open(const std::string &project_path)
Definition project.cc:607