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