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 // Open() points the global label provider at this local project, so save the
211 // caller's wiring first and put it back if the open fails.
212 const auto label_state = zelda3::GetResourceLabels().SnapshotState();
213 project::YazeProject project;
214 auto open_status = project.Open(*config_.project_context_path);
215 if (!open_status.ok()) {
217 return absl::FailedPreconditionError(
218 absl::StrFormat("Failed to open project context '%s': %s",
219 *config_.project_context_path, open_status.message()));
220 }
221
222 // Keep editor parity by auto-enabling custom objects when project config
223 // defines custom object data but stale feature flags are still disabled.
224 if (ProjectUsesCustomObjects(project) &&
226 project.feature_flags.kEnableCustomObjects = true;
227 if (config_.verbose) {
228 std::cerr
229 << "Warning: project context has custom object data but "
230 "kEnableCustomObjects was disabled. Enabling for this command.\n";
231 }
232 }
233 std::string legacy_mapping_warning;
234 if (SeedLegacyTrackObjectMapping(&project, &legacy_mapping_warning) &&
236 std::cerr << "Warning: " << legacy_mapping_warning << '\n';
237 }
238
239 loaded_project_ = std::move(project);
243
244 auto& active_project = loaded_project_.value();
245 // Project names (e.g. Oracle sprite slots) replace vanilla names in output.
246 // Re-point the provider at the stored copy; the local project moved above.
247 previous_label_state_ = label_state;
248 active_project.InitializeResourceLabelProvider();
249 core::FeatureFlags::get() = active_project.feature_flags;
251
252 if (!active_project.custom_object_files.empty()) {
254 active_project.custom_object_files);
255 } else {
257 }
258
259 if (!active_project.custom_objects_folder.empty()) {
261 active_project.GetAbsolutePath(active_project.custom_objects_folder));
262 } else {
263 // Avoid inheriting a stale base path from a previous singleton state.
265 }
266
268 active_project.dungeon_tile_patterns.empty()
269 ? ""
270 : active_project.GetAbsolutePath(
271 active_project.dungeon_tile_patterns));
273 // Do not render a partial preview when an explicitly configured source is
274 // invalid. Restore immediately so a retry cannot skip the failed load.
275 const auto patterns = zelda3::CustomObjectManager::Get().GetTilePatterns();
276 if (!patterns.ok()) {
278 return patterns.status();
279 }
280 return absl::OkStatus();
281}
282
308
309// ============================================================================
310// ArgumentParser Implementation
311// ============================================================================
312
313ArgumentParser::ArgumentParser(const std::vector<std::string>& args)
314 : args_(args) {}
315
316std::optional<std::string> ArgumentParser::FindArgValue(
317 const std::string& name) const {
318 std::string flag = "--" + name;
319 std::string equals_form = flag + "=";
320
321 for (size_t i = 0; i < args_.size(); ++i) {
322 const std::string& arg = args_[i];
323
324 // Check for --name=value form
325 if (absl::StartsWith(arg, equals_form)) {
326 return arg.substr(equals_form.length());
327 }
328
329 // Check for --name value form
330 if (arg == flag && i + 1 < args_.size()) {
331 return args_[i + 1];
332 }
333 }
334
335 return std::nullopt;
336}
337
338std::optional<std::string> ArgumentParser::GetString(
339 const std::string& name) const {
340 return FindArgValue(name);
341}
342
343absl::StatusOr<int> ArgumentParser::GetInt(const std::string& name) const {
344 auto value = FindArgValue(name);
345 if (!value.has_value()) {
346 return absl::NotFoundError(
347 absl::StrFormat("Argument '--%s' not found", name));
348 }
349
350 // Try hex first (with 0x prefix)
351 if (absl::StartsWith(*value, "0x") || absl::StartsWith(*value, "0X")) {
352 int result;
353 if (ParseHexString(value->substr(2), &result)) {
354 return result;
355 }
356 return absl::InvalidArgumentError(
357 absl::StrFormat("Invalid hex integer for '--%s': %s", name, *value));
358 }
359
360 // Try decimal
361 int result;
362 if (absl::SimpleAtoi(*value, &result)) {
363 return result;
364 }
365
366 return absl::InvalidArgumentError(
367 absl::StrFormat("Invalid integer for '--%s': %s", name, *value));
368}
369
370absl::StatusOr<int> ArgumentParser::GetHex(const std::string& name) const {
371 auto value = FindArgValue(name);
372 if (!value.has_value()) {
373 return absl::NotFoundError(
374 absl::StrFormat("Argument '--%s' not found", name));
375 }
376
377 // Strip 0x prefix if present
378 std::string hex_str = *value;
379 if (absl::StartsWith(hex_str, "0x") || absl::StartsWith(hex_str, "0X")) {
380 hex_str = hex_str.substr(2);
381 }
382
383 int result;
384 if (ParseHexString(hex_str, &result)) {
385 return result;
386 }
387
388 return absl::InvalidArgumentError(
389 absl::StrFormat("Invalid hex value for '--%s': %s", name, *value));
390}
391
392bool ArgumentParser::HasFlag(const std::string& name) const {
393 std::string flag = "--" + name;
394 for (const auto& arg : args_) {
395 if (arg == flag) {
396 return true;
397 }
398 }
399 return false;
400}
401
402std::vector<std::string> ArgumentParser::GetPositional() const {
403 std::vector<std::string> positional;
404 for (size_t i = 0; i < args_.size(); ++i) {
405 const std::string& arg = args_[i];
406 if (!absl::StartsWith(arg, "--")) {
407 positional.push_back(arg);
408 } else if (arg.find('=') == std::string::npos && i + 1 < args_.size()) {
409 // Skip the next argument as it's the value for this flag
410 ++i;
411 }
412 }
413 return positional;
414}
415
417 const std::vector<std::string>& required) const {
418 std::vector<std::string> missing;
419 for (const auto& arg : required) {
420 if (!FindArgValue(arg).has_value()) {
421 missing.push_back("--" + arg);
422 }
423 }
424
425 if (!missing.empty()) {
426 return absl::InvalidArgumentError(absl::StrFormat(
427 "Missing required arguments: %s", absl::StrJoin(missing, ", ")));
428 }
429
430 return absl::OkStatus();
431}
432
433// ============================================================================
434// OutputFormatter Implementation
435// ============================================================================
436
437absl::StatusOr<OutputFormatter> OutputFormatter::FromString(
438 const std::string& format) {
439 std::string lower = absl::AsciiStrToLower(format);
440 if (lower == "json") {
442 } else if (lower == "text" || lower == "table") {
443 // "table" is text output; commands that support it print aligned rows.
445 } else {
446 return absl::InvalidArgumentError(absl::StrFormat(
447 "Unknown format: %s (expected 'json', 'text' or 'table')", format));
448 }
449}
450
451void OutputFormatter::BeginObject(const std::string& title) {
452 if (IsJson()) {
453 if (!json_stack_.empty()) {
454 if (json_stack_.back().type == JsonContext::Type::kArray) {
456 } else {
457 AddJsonFieldPrefix(title);
458 }
459 } else {
460 AddIndent();
461 }
462 buffer_ += "{\n";
465 } else if (IsText() && !title.empty()) {
466 buffer_ += absl::StrFormat("=== %s ===\n", title);
467 }
468}
469
471 if (IsJson()) {
472 if (json_stack_.empty()) {
473 return;
474 }
475 const bool empty = json_stack_.back().first;
476 json_stack_.pop_back();
478 if (empty) {
479 if (!buffer_.empty() && buffer_.back() == '\n') {
480 buffer_.pop_back();
481 }
482 buffer_ += "}";
483 return;
484 }
485 buffer_ += "\n";
486 AddIndent();
487 buffer_ += "}";
488 }
489}
490
491void OutputFormatter::AddField(const std::string& key,
492 const std::string& value) {
493 if (IsJson()) {
495 buffer_ += absl::StrFormat("\"%s\"", EscapeJson(value));
496 } else {
497 buffer_ += absl::StrFormat(" %-20s : %s\n", key, value);
498 }
499}
500
501void OutputFormatter::AddField(const std::string& key, const char* value) {
502 AddField(key, value != nullptr ? std::string(value) : std::string());
503}
504
505void OutputFormatter::AddField(const std::string& key, int value) {
506 if (IsJson()) {
508 buffer_ += absl::StrFormat("%d", value);
509 } else {
510 buffer_ += absl::StrFormat(" %-20s : %d\n", key, value);
511 }
512}
513
514void OutputFormatter::AddField(const std::string& key, uint64_t value) {
515 if (IsJson()) {
517 buffer_ += absl::StrFormat("%llu", value);
518 } else {
519 buffer_ += absl::StrFormat(" %-20s : %llu\n", key, value);
520 }
521}
522
523void OutputFormatter::AddField(const std::string& key, bool value) {
524 if (IsJson()) {
526 buffer_ += value ? "true" : "false";
527 } else {
528 buffer_ += absl::StrFormat(" %-20s : %s\n", key, value ? "yes" : "no");
529 }
530}
531
532void OutputFormatter::AddHexField(const std::string& key, uint64_t value,
533 int width) {
534 if (IsJson()) {
536 buffer_ += absl::StrFormat("\"0x%0*X\"", width, value);
537 } else {
538 buffer_ += absl::StrFormat(" %-20s : 0x%0*X\n", key, width, value);
539 }
540}
541
542void OutputFormatter::BeginArray(const std::string& key) {
543 if (IsJson()) {
544 if (!json_stack_.empty()) {
545 if (json_stack_.back().type == JsonContext::Type::kArray) {
547 } else {
549 }
550 } else {
551 AddIndent();
552 }
553 buffer_ += "[\n";
556 } else {
557 buffer_ += absl::StrFormat(" %s:\n", key);
558 }
559}
560
562 if (IsJson()) {
563 if (json_stack_.empty()) {
564 return;
565 }
566 const int item_count = json_stack_.back().item_count;
567 json_stack_.pop_back();
569 if (item_count == 0) {
570 if (!buffer_.empty() && buffer_.back() == '\n') {
571 buffer_.pop_back();
572 }
573 buffer_ += "]";
574 } else {
575 buffer_ += "\n";
576 AddIndent();
577 buffer_ += "]";
578 }
579 }
580}
581
582void OutputFormatter::AddArrayItem(const std::string& item) {
583 if (IsJson()) {
585 buffer_ += absl::StrFormat("\"%s\"", EscapeJson(item));
586 } else {
587 buffer_ += absl::StrFormat(" - %s\n", item);
588 }
589}
590
591void OutputFormatter::AddRawJsonField(const std::string& key,
592 const std::string& json) {
593 if (!IsJson()) {
594 return;
595 }
597 buffer_ += json;
598}
599
600void OutputFormatter::AddRawText(const std::string& text) {
601 if (IsText()) {
602 buffer_ += text;
603 }
604}
605
606std::string OutputFormatter::GetOutput() const {
607 return buffer_;
608}
609
611 std::cout << buffer_;
612 if (IsJson()) {
613 std::cout << "\n";
614 }
615}
616
618 for (int i = 0; i < indent_level_; ++i) {
619 buffer_ += " ";
620 }
621}
622
624 if (json_stack_.empty()) {
625 return;
626 }
627 auto& context = json_stack_.back();
628 if (context.type == JsonContext::Type::kArray) {
629 if (context.item_count > 0) {
630 buffer_ += ",\n";
631 }
632 AddIndent();
633 ++context.item_count;
634 }
635}
636
637void OutputFormatter::AddJsonFieldPrefix(const std::string& key) {
638 if (!json_stack_.empty()) {
639 auto& context = json_stack_.back();
640 if (context.type == JsonContext::Type::kObject) {
641 if (!context.first) {
642 buffer_ += ",\n";
643 }
644 context.first = false;
645 }
646 }
647 AddIndent();
648 buffer_ += absl::StrFormat("\"%s\": ", EscapeJson(key));
649}
650
651std::string OutputFormatter::EscapeJson(const std::string& str) const {
652 std::string result;
653 result.reserve(str.size() + 10);
654
655 for (char c : str) {
656 switch (c) {
657 case '"':
658 result += "\\\"";
659 break;
660 case '\\':
661 result += "\\\\";
662 break;
663 case '\b':
664 result += "\\b";
665 break;
666 case '\f':
667 result += "\\f";
668 break;
669 case '\n':
670 result += "\\n";
671 break;
672 case '\r':
673 result += "\\r";
674 break;
675 case '\t':
676 result += "\\t";
677 break;
678 default:
679 if (static_cast<unsigned char>(c) < 0x20) {
680 result += absl::StrFormat("\\u%04x", static_cast<int>(c));
681 } else {
682 result += c;
683 }
684 }
685 }
686
687 return result;
688}
689
690} // namespace resources
691} // namespace cli
692} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:52
project::ResourceLabelManager * resource_label()
Definition rom.h:242
absl::Status LoadFromFile(const std::string &filename, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:632
bool is_loaded() const
Definition rom.h:199
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.
std::optional< zelda3::ResourceLabelProvider::State > previous_label_state_
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:122
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 RestoreState(const State &state)
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:623
Modern project structure with comprehensive settings consolidation.
Definition project.h:290
std::unordered_map< int, std::vector< std::string > > custom_object_files
Definition project.h:358
std::string custom_objects_folder
Definition project.h:330
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > resource_labels
Definition project.h:369
absl::Status InitializeEmbeddedLabels(const std::unordered_map< std::string, std::unordered_map< std::string, std::string > > &labels)
Definition project.cc:3619
absl::Status Open(const std::string &project_path)
Definition project.cc:607
core::FeatureFlags::Flags feature_flags
Definition project.h:361
static std::unordered_map< std::string, std::unordered_map< std::string, std::string > > ToResourceLabels()
Convert all labels to a structured map for project embedding.