yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
command_context.cc
Go to the documentation of this file.
2
3#include <filesystem>
4#include <fstream>
5#include <iostream>
6
7#include "absl/flags/declare.h"
8#include "absl/flags/flag.h"
9#include "absl/strings/ascii.h"
10#include "absl/strings/match.h"
11#include "absl/strings/str_format.h"
12#include "absl/strings/str_join.h"
14#include "cli/util/hex_util.h"
15#include "core/project.h"
19
20using yaze::cli::util::ParseHexString;
21
22ABSL_DECLARE_FLAG(std::string, rom);
23ABSL_DECLARE_FLAG(bool, mock_rom);
24
25namespace yaze {
26namespace cli {
27namespace resources {
28
29namespace {
30
31constexpr int kTrackCustomObjectId = 0x31;
32
33bool ProjectUsesCustomObjects(const project::YazeProject& project) {
34 return !project.custom_objects_folder.empty() ||
35 !project.custom_object_files.empty();
36}
37
38bool SeedLegacyTrackObjectMapping(project::YazeProject* project,
39 std::string* warning) {
40 if (project == nullptr) {
41 return false;
42 }
43 if (project->custom_objects_folder.empty()) {
44 return false;
45 }
46 if (project->custom_object_files.find(kTrackCustomObjectId) !=
47 project->custom_object_files.end()) {
48 return false;
49 }
50
51 const auto& defaults =
53 kTrackCustomObjectId);
54 if (defaults.empty()) {
55 return false;
56 }
57
58 project->custom_object_files[kTrackCustomObjectId] = defaults;
59 if (warning != nullptr) {
60 *warning = absl::StrFormat(
61 "project context defines custom_objects_folder but is missing "
62 "custom_object_files[0x%02X]. Seeded default mapping (%d entries); "
63 "save project to persist.",
64 kTrackCustomObjectId, static_cast<int>(defaults.size()));
65 }
66 return true;
67}
68
69} // namespace
70
71// ============================================================================
72// CommandContext Implementation
73// ============================================================================
74
75CommandContext::CommandContext(const Config& config) : config_(config) {}
76
80
82 if (initialized_) {
83 return absl::OkStatus();
84 }
85
86 // Determine ROM source
87 std::string rom_path = config_.rom_path.has_value()
89 : absl::GetFlag(FLAGS_rom);
90
91 if (config_.external_rom_context != nullptr &&
94 } else if (config_.use_mock_rom) {
96 if (!status.ok()) {
97 return status;
98 }
100 } else if (!rom_path.empty()) {
101 auto status = rom_storage_.LoadFromFile(rom_path);
102 if (!status.ok()) {
103 return absl::FailedPreconditionError(absl::StrFormat(
104 "Failed to load ROM from '%s': %s", rom_path, status.message()));
105 }
107 }
108
109 if (active_rom_ == nullptr) {
110 return absl::FailedPreconditionError(
111 "No ROM loaded. Use --rom=<path> or --mock-rom for testing.");
112 }
113
114 auto project_status = ApplyProjectRuntimeContext();
115 if (!project_status.ok()) {
116 return project_status;
117 }
118
119 // Auto-load symbols if available
120 std::string symbols_path =
121 config_.symbols_path.has_value() ? *config_.symbols_path : "";
122 if (symbols_path.empty() && !rom_path.empty()) {
123 // Try ROM name with .mlb or .sym
124 std::string base = rom_path;
125 size_t last_dot = base.find_last_of('.');
126 if (last_dot != std::string::npos) {
127 base = base.substr(0, last_dot);
128 }
129
130 // Try common extensions
131 std::vector<std::string> exts = {".mlb", ".sfc.mlb", ".sym", ".cpu.sym",
132 "sourcemap.json"};
133 for (const auto& ext : exts) {
134 std::string path = base + ext;
135 if (std::ifstream(path).good()) {
136 auto sym_status = symbol_provider_.LoadSymbolFile(path);
137 if (sym_status.ok() && symbol_provider_.HasSymbols()) {
138 symbols_path = path;
139 break;
140 }
141 }
142 }
143 } else if (!symbols_path.empty()) {
144 auto sym_status = symbol_provider_.LoadSymbolFile(symbols_path);
145 if (!sym_status.ok() && config_.verbose) {
146 std::cerr << "Warning: Failed to load symbols from " << symbols_path
147 << ": " << sym_status.message() << std::endl;
148 }
149 }
150
151 initialized_ = true;
152 return absl::OkStatus();
153}
154
155absl::StatusOr<Rom*> CommandContext::GetRom() {
156 if (!initialized_) {
157 auto status = Initialize();
158 if (!status.ok()) {
159 return status;
160 }
161 }
162
163 if (active_rom_ == nullptr) {
164 return absl::FailedPreconditionError("ROM not loaded");
165 }
166
167 return active_rom_;
168}
169
176
178 if (!rom->resource_label()) {
179 return absl::FailedPreconditionError("ROM has no resource label manager");
180 }
181
182 if (!rom->resource_label()->labels_loaded_ ||
183 rom->resource_label()->labels_.empty()) {
184 project::YazeProject project;
185 project.use_embedded_labels = true;
186 auto labels_status = project.InitializeEmbeddedLabels(
188 if (labels_status.ok()) {
189 rom->resource_label()->labels_ = project.resource_labels;
190 rom->resource_label()->labels_loaded_ = true;
191 } else {
192 return labels_status;
193 }
194 }
195
196 return absl::OkStatus();
197}
198
201 return absl::OkStatus();
202 }
203 if (!config_.project_context_path.has_value()) {
204 return absl::OkStatus();
205 }
206 if (config_.project_context_path->empty()) {
207 return absl::InvalidArgumentError("--project-context cannot be empty");
208 }
209
210 project::YazeProject project;
211 auto open_status = project.Open(*config_.project_context_path);
212 if (!open_status.ok()) {
213 return absl::FailedPreconditionError(
214 absl::StrFormat("Failed to open project context '%s': %s",
215 *config_.project_context_path, open_status.message()));
216 }
217
218 // Keep editor parity by auto-enabling custom objects when project config
219 // defines custom object data but stale feature flags are still disabled.
220 if (ProjectUsesCustomObjects(project) &&
222 project.feature_flags.kEnableCustomObjects = true;
223 if (config_.verbose) {
224 std::cerr
225 << "Warning: project context has custom object data but "
226 "kEnableCustomObjects was disabled. Enabling for this command.\n";
227 }
228 }
229 std::string legacy_mapping_warning;
230 if (SeedLegacyTrackObjectMapping(&project, &legacy_mapping_warning) &&
232 std::cerr << "Warning: " << legacy_mapping_warning << '\n';
233 }
234
235 loaded_project_ = std::move(project);
239
240 auto& active_project = loaded_project_.value();
241 // Open() bound the label provider to the moved-from local above.
243 active_project.hack_manifest.loaded() ? &active_project.hack_manifest
244 : nullptr);
245 core::FeatureFlags::get() = active_project.feature_flags;
247
248 if (!active_project.custom_object_files.empty()) {
250 active_project.custom_object_files);
251 } else {
253 }
254
255 if (!active_project.custom_objects_folder.empty()) {
257 active_project.GetAbsolutePath(active_project.custom_objects_folder));
258 } else {
259 // Avoid inheriting a stale base path from a previous singleton state.
261 }
262
264 active_project.dungeon_tile_patterns.empty()
265 ? ""
266 : active_project.GetAbsolutePath(
267 active_project.dungeon_tile_patterns));
269 // Do not render a partial preview when an explicitly configured source is
270 // invalid. Restore immediately so a retry cannot skip the failed load.
271 const auto patterns = zelda3::CustomObjectManager::Get().GetTilePatterns();
272 if (!patterns.ok()) {
274 return patterns.status();
275 }
276 return absl::OkStatus();
277}
278
299
300// ============================================================================
301// ArgumentParser Implementation
302// ============================================================================
303
304ArgumentParser::ArgumentParser(const std::vector<std::string>& args)
305 : args_(args) {}
306
307std::optional<std::string> ArgumentParser::FindArgValue(
308 const std::string& name) const {
309 std::string flag = "--" + name;
310 std::string equals_form = flag + "=";
311
312 for (size_t i = 0; i < args_.size(); ++i) {
313 const std::string& arg = args_[i];
314
315 // Check for --name=value form
316 if (absl::StartsWith(arg, equals_form)) {
317 return arg.substr(equals_form.length());
318 }
319
320 // Check for --name value form
321 if (arg == flag && i + 1 < args_.size()) {
322 return args_[i + 1];
323 }
324 }
325
326 return std::nullopt;
327}
328
329std::optional<std::string> ArgumentParser::GetString(
330 const std::string& name) const {
331 return FindArgValue(name);
332}
333
334absl::StatusOr<int> ArgumentParser::GetInt(const std::string& name) const {
335 auto value = FindArgValue(name);
336 if (!value.has_value()) {
337 return absl::NotFoundError(
338 absl::StrFormat("Argument '--%s' not found", name));
339 }
340
341 // Try hex first (with 0x prefix)
342 if (absl::StartsWith(*value, "0x") || absl::StartsWith(*value, "0X")) {
343 int result;
344 if (ParseHexString(value->substr(2), &result)) {
345 return result;
346 }
347 return absl::InvalidArgumentError(
348 absl::StrFormat("Invalid hex integer for '--%s': %s", name, *value));
349 }
350
351 // Try decimal
352 int result;
353 if (absl::SimpleAtoi(*value, &result)) {
354 return result;
355 }
356
357 return absl::InvalidArgumentError(
358 absl::StrFormat("Invalid integer for '--%s': %s", name, *value));
359}
360
361absl::StatusOr<int> ArgumentParser::GetHex(const std::string& name) const {
362 auto value = FindArgValue(name);
363 if (!value.has_value()) {
364 return absl::NotFoundError(
365 absl::StrFormat("Argument '--%s' not found", name));
366 }
367
368 // Strip 0x prefix if present
369 std::string hex_str = *value;
370 if (absl::StartsWith(hex_str, "0x") || absl::StartsWith(hex_str, "0X")) {
371 hex_str = hex_str.substr(2);
372 }
373
374 int result;
375 if (ParseHexString(hex_str, &result)) {
376 return result;
377 }
378
379 return absl::InvalidArgumentError(
380 absl::StrFormat("Invalid hex value for '--%s': %s", name, *value));
381}
382
383bool ArgumentParser::HasFlag(const std::string& name) const {
384 std::string flag = "--" + name;
385 for (const auto& arg : args_) {
386 if (arg == flag) {
387 return true;
388 }
389 }
390 return false;
391}
392
393std::vector<std::string> ArgumentParser::GetPositional() const {
394 std::vector<std::string> positional;
395 for (size_t i = 0; i < args_.size(); ++i) {
396 const std::string& arg = args_[i];
397 if (!absl::StartsWith(arg, "--")) {
398 positional.push_back(arg);
399 } else if (arg.find('=') == std::string::npos && i + 1 < args_.size()) {
400 // Skip the next argument as it's the value for this flag
401 ++i;
402 }
403 }
404 return positional;
405}
406
408 const std::vector<std::string>& required) const {
409 std::vector<std::string> missing;
410 for (const auto& arg : required) {
411 if (!FindArgValue(arg).has_value()) {
412 missing.push_back("--" + arg);
413 }
414 }
415
416 if (!missing.empty()) {
417 return absl::InvalidArgumentError(absl::StrFormat(
418 "Missing required arguments: %s", absl::StrJoin(missing, ", ")));
419 }
420
421 return absl::OkStatus();
422}
423
424// ============================================================================
425// OutputFormatter Implementation
426// ============================================================================
427
428absl::StatusOr<OutputFormatter> OutputFormatter::FromString(
429 const std::string& format) {
430 std::string lower = absl::AsciiStrToLower(format);
431 if (lower == "json") {
433 } else if (lower == "text" || lower == "table") {
434 // "table" is text output; commands that support it print aligned rows.
436 } else {
437 return absl::InvalidArgumentError(absl::StrFormat(
438 "Unknown format: %s (expected 'json', 'text' or 'table')", format));
439 }
440}
441
442void OutputFormatter::BeginObject(const std::string& title) {
443 if (IsJson()) {
444 if (!json_stack_.empty()) {
445 if (json_stack_.back().type == JsonContext::Type::kArray) {
447 } else {
448 AddJsonFieldPrefix(title);
449 }
450 } else {
451 AddIndent();
452 }
453 buffer_ += "{\n";
456 } else if (IsText() && !title.empty()) {
457 buffer_ += absl::StrFormat("=== %s ===\n", title);
458 }
459}
460
462 if (IsJson()) {
463 if (json_stack_.empty()) {
464 return;
465 }
466 const bool empty = json_stack_.back().first;
467 json_stack_.pop_back();
469 if (empty) {
470 if (!buffer_.empty() && buffer_.back() == '\n') {
471 buffer_.pop_back();
472 }
473 buffer_ += "}";
474 return;
475 }
476 buffer_ += "\n";
477 AddIndent();
478 buffer_ += "}";
479 }
480}
481
482void OutputFormatter::AddField(const std::string& key,
483 const std::string& value) {
484 if (IsJson()) {
486 buffer_ += absl::StrFormat("\"%s\"", EscapeJson(value));
487 } else {
488 buffer_ += absl::StrFormat(" %-20s : %s\n", key, value);
489 }
490}
491
492void OutputFormatter::AddField(const std::string& key, const char* value) {
493 AddField(key, value != nullptr ? std::string(value) : std::string());
494}
495
496void OutputFormatter::AddField(const std::string& key, int value) {
497 if (IsJson()) {
499 buffer_ += absl::StrFormat("%d", value);
500 } else {
501 buffer_ += absl::StrFormat(" %-20s : %d\n", key, value);
502 }
503}
504
505void OutputFormatter::AddField(const std::string& key, uint64_t value) {
506 if (IsJson()) {
508 buffer_ += absl::StrFormat("%llu", value);
509 } else {
510 buffer_ += absl::StrFormat(" %-20s : %llu\n", key, value);
511 }
512}
513
514void OutputFormatter::AddField(const std::string& key, bool value) {
515 if (IsJson()) {
517 buffer_ += value ? "true" : "false";
518 } else {
519 buffer_ += absl::StrFormat(" %-20s : %s\n", key, value ? "yes" : "no");
520 }
521}
522
523void OutputFormatter::AddHexField(const std::string& key, uint64_t value,
524 int width) {
525 if (IsJson()) {
527 buffer_ += absl::StrFormat("\"0x%0*X\"", width, value);
528 } else {
529 buffer_ += absl::StrFormat(" %-20s : 0x%0*X\n", key, width, value);
530 }
531}
532
533void OutputFormatter::BeginArray(const std::string& key) {
534 if (IsJson()) {
535 if (!json_stack_.empty()) {
536 if (json_stack_.back().type == JsonContext::Type::kArray) {
538 } else {
540 }
541 } else {
542 AddIndent();
543 }
544 buffer_ += "[\n";
547 } else {
548 buffer_ += absl::StrFormat(" %s:\n", key);
549 }
550}
551
553 if (IsJson()) {
554 if (json_stack_.empty()) {
555 return;
556 }
557 const int item_count = json_stack_.back().item_count;
558 json_stack_.pop_back();
560 if (item_count == 0) {
561 if (!buffer_.empty() && buffer_.back() == '\n') {
562 buffer_.pop_back();
563 }
564 buffer_ += "]";
565 } else {
566 buffer_ += "\n";
567 AddIndent();
568 buffer_ += "]";
569 }
570 }
571}
572
573void OutputFormatter::AddArrayItem(const std::string& item) {
574 if (IsJson()) {
576 buffer_ += absl::StrFormat("\"%s\"", EscapeJson(item));
577 } else {
578 buffer_ += absl::StrFormat(" - %s\n", item);
579 }
580}
581
582void OutputFormatter::AddRawJsonField(const std::string& key,
583 const std::string& json) {
584 if (!IsJson()) {
585 return;
586 }
588 buffer_ += json;
589}
590
591void OutputFormatter::AddRawText(const std::string& text) {
592 if (IsText()) {
593 buffer_ += text;
594 }
595}
596
597std::string OutputFormatter::GetOutput() const {
598 return buffer_;
599}
600
602 std::cout << buffer_;
603 if (IsJson()) {
604 std::cout << "\n";
605 }
606}
607
609 for (int i = 0; i < indent_level_; ++i) {
610 buffer_ += " ";
611 }
612}
613
615 if (json_stack_.empty()) {
616 return;
617 }
618 auto& context = json_stack_.back();
619 if (context.type == JsonContext::Type::kArray) {
620 if (context.item_count > 0) {
621 buffer_ += ",\n";
622 }
623 AddIndent();
624 ++context.item_count;
625 }
626}
627
628void OutputFormatter::AddJsonFieldPrefix(const std::string& key) {
629 if (!json_stack_.empty()) {
630 auto& context = json_stack_.back();
631 if (context.type == JsonContext::Type::kObject) {
632 if (!context.first) {
633 buffer_ += ",\n";
634 }
635 context.first = false;
636 }
637 }
638 AddIndent();
639 buffer_ += absl::StrFormat("\"%s\": ", EscapeJson(key));
640}
641
642std::string OutputFormatter::EscapeJson(const std::string& str) const {
643 std::string result;
644 result.reserve(str.size() + 10);
645
646 for (char c : str) {
647 switch (c) {
648 case '"':
649 result += "\\\"";
650 break;
651 case '\\':
652 result += "\\\\";
653 break;
654 case '\b':
655 result += "\\b";
656 break;
657 case '\f':
658 result += "\\f";
659 break;
660 case '\n':
661 result += "\\n";
662 break;
663 case '\r':
664 result += "\\r";
665 break;
666 case '\t':
667 result += "\\t";
668 break;
669 default:
670 if (static_cast<unsigned char>(c) < 0x20) {
671 result += absl::StrFormat("\\u%04x", static_cast<int>(c));
672 } else {
673 result += c;
674 }
675 }
676 }
677
678 return result;
679}
680
681} // namespace resources
682} // namespace cli
683} // 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
project::ResourceLabelManager * resource_label()
Definition rom.h:225
absl::Status LoadFromFile(const std::string &filename, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:491
bool is_loaded() const
Definition rom.h:187
std::optional< std::string > FindArgValue(const std::string &name) const
std::vector< std::string > args_
ArgumentParser(const std::vector< std::string > &args)
std::vector< std::string > GetPositional() const
Get all remaining positional arguments.
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::Status RequireArgs(const std::vector< std::string > &required) const
Validate that required arguments are present.
absl::StatusOr< int > GetHex(const std::string &name) const
Parse a hex integer argument.
absl::StatusOr< int > GetInt(const std::string &name) const
Parse an integer argument (supports hex with 0x prefix)
std::optional< project::YazeProject > loaded_project_
absl::StatusOr< Rom * > GetRom()
Get the ROM instance (loads if not already loaded)
absl::Status EnsureLabelsLoaded(Rom *rom)
Ensure resource labels are loaded.
emu::debug::SymbolProvider * GetSymbolProvider()
Get the SymbolProvider instance.
std::optional< core::FeatureFlags::Flags > previous_feature_flags_
std::optional< zelda3::CustomObjectManager::State > previous_custom_object_state_
emu::debug::SymbolProvider symbol_provider_
absl::Status Initialize()
Initialize the context and load ROM if needed.
void AddJsonFieldPrefix(const std::string &key)
void BeginArray(const std::string &key)
Begin an array.
void AddRawText(const std::string &text)
Append preformatted text (for example an aligned table). Ignored in JSON mode.
void AddRawJsonField(const std::string &key, const std::string &json)
Add a field whose value is already-serialized JSON (numbers, nested arrays, null)....
std::string GetOutput() const
Get the formatted output.
static absl::StatusOr< OutputFormatter > FromString(const std::string &format)
Create formatter from string ("json" or "text")
void AddArrayItem(const std::string &item)
Add an item to current array.
std::vector< JsonContext > json_stack_
void BeginObject(const std::string &title="")
Start a JSON object or text section.
void EndObject()
End a JSON object or text section.
std::string EscapeJson(const std::string &str) const
void AddField(const std::string &key, const std::string &value)
Add a key-value pair.
bool IsJson() const
Check if using JSON format.
bool IsText() const
Check if using text format.
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.
static Flags & get()
Definition features.h:119
Provider for symbol (label) resolution in disassembly.
bool HasSymbols() const
Check if any symbols are loaded.
absl::Status LoadSymbolFile(const std::string &path, SymbolFormat format=SymbolFormat::kAuto)
Load symbols from a .sym file (various formats)
void RestoreState(const State &state)
static const std::vector< std::string > & DefaultSubtypeFilenamesForObject(int object_id)
void SetObjectFileMap(const std::unordered_map< int, std::vector< std::string > > &map)
static CustomObjectManager & Get()
void SetTilePatternsSource(const std::string &path)
void Initialize(const std::string &custom_objects_folder)
absl::StatusOr< std::shared_ptr< const DungeonTilePatterns > > GetTilePatterns()
static DrawRoutineRegistry & Get()
void SetHackManifest(const core::HackManifest *manifest)
Set the hack manifest reference for ASM-defined labels.
ABSL_DECLARE_FLAG(std::string, rom)
absl::Status InitializeMockRom(Rom &rom)
Initialize a mock ROM for testing without requiring an actual ROM file.
Definition mock_rom.cc:16
ResourceLabelProvider & GetResourceLabels()
Get the global ResourceLabelProvider instance.
Configuration for command context.
std::optional< std::string > project_context_path
std::optional< std::string > symbols_path
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > labels_
Definition project.h:589
Modern project structure with comprehensive settings consolidation.
Definition project.h:287
std::unordered_map< int, std::vector< std::string > > custom_object_files
Definition project.h:342
std::string custom_objects_folder
Definition project.h:327
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > resource_labels
Definition project.h:353
absl::Status InitializeEmbeddedLabels(const std::unordered_map< std::string, std::unordered_map< std::string, std::string > > &labels)
Definition project.cc:2955
absl::Status Open(const std::string &project_path)
Definition project.cc:537
core::FeatureFlags::Flags feature_flags
Definition project.h:345
static std::unordered_map< std::string, std::unordered_map< std::string, std::string > > ToResourceLabels()
Convert all labels to a structured map for project embedding.