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"
18
20
21ABSL_DECLARE_FLAG(std::string, rom);
22ABSL_DECLARE_FLAG(bool, mock_rom);
23
24namespace yaze {
25namespace cli {
26namespace resources {
27
28namespace {
29
30constexpr int kTrackCustomObjectId = 0x31;
31
32bool ProjectUsesCustomObjects(const project::YazeProject& project) {
33 return !project.custom_objects_folder.empty() ||
34 !project.custom_object_files.empty();
35}
36
37bool SeedLegacyTrackObjectMapping(project::YazeProject* project,
38 std::string* warning) {
39 if (project == nullptr) {
40 return false;
41 }
42 if (project->custom_objects_folder.empty()) {
43 return false;
44 }
45 if (project->custom_object_files.find(kTrackCustomObjectId) !=
46 project->custom_object_files.end()) {
47 return false;
48 }
49
50 const auto& defaults =
52 kTrackCustomObjectId);
53 if (defaults.empty()) {
54 return false;
55 }
56
57 project->custom_object_files[kTrackCustomObjectId] = defaults;
58 if (warning != nullptr) {
59 *warning = absl::StrFormat(
60 "project context defines custom_objects_folder but is missing "
61 "custom_object_files[0x%02X]. Seeded default mapping (%d entries); "
62 "save project to persist.",
63 kTrackCustomObjectId, static_cast<int>(defaults.size()));
64 }
65 return true;
66}
67
68} // namespace
69
70// ============================================================================
71// CommandContext Implementation
72// ============================================================================
73
74CommandContext::CommandContext(const Config& config) : config_(config) {}
75
79
81 if (initialized_) {
82 return absl::OkStatus();
83 }
84
85 // Determine ROM source
86 std::string rom_path = config_.rom_path.has_value()
88 : absl::GetFlag(FLAGS_rom);
89
90 if (config_.external_rom_context != nullptr &&
93 } else if (config_.use_mock_rom) {
95 if (!status.ok()) {
96 return status;
97 }
99 } else if (!rom_path.empty()) {
100 auto status = rom_storage_.LoadFromFile(rom_path);
101 if (!status.ok()) {
102 return absl::FailedPreconditionError(absl::StrFormat(
103 "Failed to load ROM from '%s': %s", rom_path, status.message()));
104 }
106 }
107
108 if (active_rom_ == nullptr) {
109 return absl::FailedPreconditionError(
110 "No ROM loaded. Use --rom=<path> or --mock-rom for testing.");
111 }
112
113 auto project_status = ApplyProjectRuntimeContext();
114 if (!project_status.ok()) {
115 return project_status;
116 }
117
118 // Auto-load symbols if available
119 std::string symbols_path =
120 config_.symbols_path.has_value() ? *config_.symbols_path : "";
121 if (symbols_path.empty() && !rom_path.empty()) {
122 // Try ROM name with .mlb or .sym
123 std::string base = rom_path;
124 size_t last_dot = base.find_last_of('.');
125 if (last_dot != std::string::npos) {
126 base = base.substr(0, last_dot);
127 }
128
129 // Try common extensions
130 std::vector<std::string> exts = {".mlb", ".sfc.mlb", ".sym", ".cpu.sym",
131 "sourcemap.json"};
132 for (const auto& ext : exts) {
133 std::string path = base + ext;
134 if (std::ifstream(path).good()) {
135 auto sym_status = symbol_provider_.LoadSymbolFile(path);
136 if (sym_status.ok() && symbol_provider_.HasSymbols()) {
137 symbols_path = path;
138 break;
139 }
140 }
141 }
142 } else if (!symbols_path.empty()) {
143 auto sym_status = symbol_provider_.LoadSymbolFile(symbols_path);
144 if (!sym_status.ok() && config_.verbose) {
145 std::cerr << "Warning: Failed to load symbols from " << symbols_path
146 << ": " << sym_status.message() << std::endl;
147 }
148 }
149
150 initialized_ = true;
151 return absl::OkStatus();
152}
153
154absl::StatusOr<Rom*> CommandContext::GetRom() {
155 if (!initialized_) {
156 auto status = Initialize();
157 if (!status.ok()) {
158 return status;
159 }
160 }
161
162 if (active_rom_ == nullptr) {
163 return absl::FailedPreconditionError("ROM not loaded");
164 }
165
166 return active_rom_;
167}
168
175
177 if (!rom->resource_label()) {
178 return absl::FailedPreconditionError("ROM has no resource label manager");
179 }
180
181 if (!rom->resource_label()->labels_loaded_ ||
182 rom->resource_label()->labels_.empty()) {
183 project::YazeProject project;
184 project.use_embedded_labels = true;
185 auto labels_status = project.InitializeEmbeddedLabels(
187 if (labels_status.ok()) {
188 rom->resource_label()->labels_ = project.resource_labels;
189 rom->resource_label()->labels_loaded_ = true;
190 } else {
191 return labels_status;
192 }
193 }
194
195 return absl::OkStatus();
196}
197
200 return absl::OkStatus();
201 }
202 if (!config_.project_context_path.has_value()) {
203 return absl::OkStatus();
204 }
205 if (config_.project_context_path->empty()) {
206 return absl::InvalidArgumentError("--project-context cannot be empty");
207 }
208
209 project::YazeProject project;
210 auto open_status = project.Open(*config_.project_context_path);
211 if (!open_status.ok()) {
212 return absl::FailedPreconditionError(
213 absl::StrFormat("Failed to open project context '%s': %s",
214 *config_.project_context_path, open_status.message()));
215 }
216
217 // Keep editor parity by auto-enabling custom objects when project config
218 // defines custom object data but stale feature flags are still disabled.
219 if (ProjectUsesCustomObjects(project) &&
221 project.feature_flags.kEnableCustomObjects = true;
222 if (config_.verbose) {
223 std::cerr
224 << "Warning: project context has custom object data but "
225 "kEnableCustomObjects was disabled. Enabling for this command.\n";
226 }
227 }
228 std::string legacy_mapping_warning;
229 if (SeedLegacyTrackObjectMapping(&project, &legacy_mapping_warning) &&
231 std::cerr << "Warning: " << legacy_mapping_warning << '\n';
232 }
233
234 loaded_project_ = std::move(project);
238
239 auto& active_project = loaded_project_.value();
240 core::FeatureFlags::get() = active_project.feature_flags;
242
243 if (!active_project.custom_object_files.empty()) {
245 active_project.custom_object_files);
246 } else {
248 }
249
250 if (!active_project.custom_objects_folder.empty()) {
252 active_project.GetAbsolutePath(active_project.custom_objects_folder));
253 } else {
254 // Avoid inheriting a stale base path from a previous singleton state.
256 }
257
259 return absl::OkStatus();
260}
261
282
283// ============================================================================
284// ArgumentParser Implementation
285// ============================================================================
286
287ArgumentParser::ArgumentParser(const std::vector<std::string>& args)
288 : args_(args) {}
289
290std::optional<std::string> ArgumentParser::FindArgValue(
291 const std::string& name) const {
292 std::string flag = "--" + name;
293 std::string equals_form = flag + "=";
294
295 for (size_t i = 0; i < args_.size(); ++i) {
296 const std::string& arg = args_[i];
297
298 // Check for --name=value form
299 if (absl::StartsWith(arg, equals_form)) {
300 return arg.substr(equals_form.length());
301 }
302
303 // Check for --name value form
304 if (arg == flag && i + 1 < args_.size()) {
305 return args_[i + 1];
306 }
307 }
308
309 return std::nullopt;
310}
311
312std::optional<std::string> ArgumentParser::GetString(
313 const std::string& name) const {
314 return FindArgValue(name);
315}
316
317absl::StatusOr<int> ArgumentParser::GetInt(const std::string& name) const {
318 auto value = FindArgValue(name);
319 if (!value.has_value()) {
320 return absl::NotFoundError(
321 absl::StrFormat("Argument '--%s' not found", name));
322 }
323
324 // Try hex first (with 0x prefix)
325 if (absl::StartsWith(*value, "0x") || absl::StartsWith(*value, "0X")) {
326 int result;
327 if (ParseHexString(value->substr(2), &result)) {
328 return result;
329 }
330 return absl::InvalidArgumentError(
331 absl::StrFormat("Invalid hex integer for '--%s': %s", name, *value));
332 }
333
334 // Try decimal
335 int result;
336 if (absl::SimpleAtoi(*value, &result)) {
337 return result;
338 }
339
340 return absl::InvalidArgumentError(
341 absl::StrFormat("Invalid integer for '--%s': %s", name, *value));
342}
343
344absl::StatusOr<int> ArgumentParser::GetHex(const std::string& name) const {
345 auto value = FindArgValue(name);
346 if (!value.has_value()) {
347 return absl::NotFoundError(
348 absl::StrFormat("Argument '--%s' not found", name));
349 }
350
351 // Strip 0x prefix if present
352 std::string hex_str = *value;
353 if (absl::StartsWith(hex_str, "0x") || absl::StartsWith(hex_str, "0X")) {
354 hex_str = hex_str.substr(2);
355 }
356
357 int result;
358 if (ParseHexString(hex_str, &result)) {
359 return result;
360 }
361
362 return absl::InvalidArgumentError(
363 absl::StrFormat("Invalid hex value for '--%s': %s", name, *value));
364}
365
366bool ArgumentParser::HasFlag(const std::string& name) const {
367 std::string flag = "--" + name;
368 for (const auto& arg : args_) {
369 if (arg == flag) {
370 return true;
371 }
372 }
373 return false;
374}
375
376std::vector<std::string> ArgumentParser::GetPositional() const {
377 std::vector<std::string> positional;
378 for (size_t i = 0; i < args_.size(); ++i) {
379 const std::string& arg = args_[i];
380 if (!absl::StartsWith(arg, "--")) {
381 positional.push_back(arg);
382 } else if (arg.find('=') == std::string::npos && i + 1 < args_.size()) {
383 // Skip the next argument as it's the value for this flag
384 ++i;
385 }
386 }
387 return positional;
388}
389
391 const std::vector<std::string>& required) const {
392 std::vector<std::string> missing;
393 for (const auto& arg : required) {
394 if (!FindArgValue(arg).has_value()) {
395 missing.push_back("--" + arg);
396 }
397 }
398
399 if (!missing.empty()) {
400 return absl::InvalidArgumentError(absl::StrFormat(
401 "Missing required arguments: %s", absl::StrJoin(missing, ", ")));
402 }
403
404 return absl::OkStatus();
405}
406
407// ============================================================================
408// OutputFormatter Implementation
409// ============================================================================
410
411absl::StatusOr<OutputFormatter> OutputFormatter::FromString(
412 const std::string& format) {
413 std::string lower = absl::AsciiStrToLower(format);
414 if (lower == "json") {
416 } else if (lower == "text") {
418 } else {
419 return absl::InvalidArgumentError(absl::StrFormat(
420 "Unknown format: %s (expected 'json' or 'text')", format));
421 }
422}
423
424void OutputFormatter::BeginObject(const std::string& title) {
425 if (IsJson()) {
426 if (!first_field_) {
427 buffer_ += ",\n";
428 }
429 AddIndent();
430 // Only output a key if we are inside another object (indent > 0) and not in an array
431 if (!title.empty() && !in_array_ && indent_level_ > 0) {
432 buffer_ += absl::StrFormat("\"%s\": {\n", EscapeJson(title));
433 } else {
434 buffer_ += "{\n";
435 }
437 first_field_ = true;
438 } else if (IsText() && !title.empty()) {
439 buffer_ += absl::StrFormat("=== %s ===\n", title);
440 }
441}
442
444 if (IsJson()) {
446 if (first_field_) {
447 if (!buffer_.empty() && buffer_.back() == '\n') {
448 buffer_.pop_back();
449 }
450 buffer_ += "}";
451 return;
452 }
453 buffer_ += "\n";
454 AddIndent();
455 buffer_ += "}";
456 }
457}
458
459void OutputFormatter::AddField(const std::string& key,
460 const std::string& value) {
461 if (IsJson()) {
462 if (!first_field_) {
463 buffer_ += ",\n";
464 }
465 first_field_ = false;
466 AddIndent();
467 buffer_ +=
468 absl::StrFormat("\"%s\": \"%s\"", EscapeJson(key), EscapeJson(value));
469 } else {
470 buffer_ += absl::StrFormat(" %-20s : %s\n", key, value);
471 }
472}
473
474void OutputFormatter::AddField(const std::string& key, const char* value) {
475 AddField(key, value != nullptr ? std::string(value) : std::string());
476}
477
478void OutputFormatter::AddField(const std::string& key, int value) {
479 if (IsJson()) {
480 if (!first_field_) {
481 buffer_ += ",\n";
482 }
483 first_field_ = false;
484 AddIndent();
485 buffer_ += absl::StrFormat("\"%s\": %d", EscapeJson(key), value);
486 } else {
487 buffer_ += absl::StrFormat(" %-20s : %d\n", key, value);
488 }
489}
490
491void OutputFormatter::AddField(const std::string& key, uint64_t value) {
492 if (IsJson()) {
493 if (!first_field_) {
494 buffer_ += ",\n";
495 }
496 first_field_ = false;
497 AddIndent();
498 buffer_ += absl::StrFormat("\"%s\": %llu", EscapeJson(key), value);
499 } else {
500 buffer_ += absl::StrFormat(" %-20s : %llu\n", key, value);
501 }
502}
503
504void OutputFormatter::AddField(const std::string& key, bool value) {
505 if (IsJson()) {
506 if (!first_field_) {
507 buffer_ += ",\n";
508 }
509 first_field_ = false;
510 AddIndent();
511 buffer_ += absl::StrFormat("\"%s\": %s", EscapeJson(key),
512 value ? "true" : "false");
513 } else {
514 buffer_ += absl::StrFormat(" %-20s : %s\n", key, value ? "yes" : "no");
515 }
516}
517
518void OutputFormatter::AddHexField(const std::string& key, uint64_t value,
519 int width) {
520 if (IsJson()) {
521 if (!first_field_) {
522 buffer_ += ",\n";
523 }
524 first_field_ = false;
525 AddIndent();
526 buffer_ +=
527 absl::StrFormat("\"%s\": \"0x%0*X\"", EscapeJson(key), 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 in_array_ = true;
536
537 if (IsJson()) {
538 if (!first_field_) {
539 buffer_ += ",\n";
540 }
541 first_field_ = false;
542 AddIndent();
543 buffer_ += absl::StrFormat("\"%s\": [\n", EscapeJson(key));
545 first_field_ = true; // Reset for array elements
546 } else {
547 buffer_ += absl::StrFormat(" %s:\n", key);
548 }
549}
550
552 in_array_ = false;
553
554 if (IsJson()) {
556 if (array_item_count_ == 0) {
557 if (!buffer_.empty() && buffer_.back() == '\n') {
558 buffer_.pop_back();
559 }
560 buffer_ += "]";
561 } else {
562 buffer_ += "\n";
563 AddIndent();
564 buffer_ += "]";
565 }
567 false; // Array itself was a field, so next field needs a comma
568 }
569}
570
571void OutputFormatter::AddArrayItem(const std::string& item) {
572 if (IsJson()) {
573 if (array_item_count_ > 0) {
574 buffer_ += ",\n";
575 }
576 AddIndent();
577 buffer_ += absl::StrFormat("\"%s\"", EscapeJson(item));
578 } else {
579 buffer_ += absl::StrFormat(" - %s\n", item);
580 }
582}
583
584std::string OutputFormatter::GetOutput() const {
585 return buffer_;
586}
587
589 std::cout << buffer_;
590 if (IsJson()) {
591 std::cout << "\n";
592 }
593}
594
596 for (int i = 0; i < indent_level_; ++i) {
597 buffer_ += " ";
598 }
599}
600
601std::string OutputFormatter::EscapeJson(const std::string& str) const {
602 std::string result;
603 result.reserve(str.size() + 10);
604
605 for (char c : str) {
606 switch (c) {
607 case '"':
608 result += "\\\"";
609 break;
610 case '\\':
611 result += "\\\\";
612 break;
613 case '\b':
614 result += "\\b";
615 break;
616 case '\f':
617 result += "\\f";
618 break;
619 case '\n':
620 result += "\\n";
621 break;
622 case '\r':
623 result += "\\r";
624 break;
625 case '\t':
626 result += "\\t";
627 break;
628 default:
629 if (c < 0x20) {
630 result += absl::StrFormat("\\u%04x", static_cast<int>(c));
631 } else {
632 result += c;
633 }
634 }
635 }
636
637 return result;
638}
639
640} // namespace resources
641} // namespace cli
642} // 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:28
project::ResourceLabelManager * resource_label()
Definition rom.h:150
absl::Status LoadFromFile(const std::string &filename, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:155
bool is_loaded() const
Definition rom.h:132
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 BeginArray(const std::string &key)
Begin an array.
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.
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:118
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 Initialize(const std::string &custom_objects_folder)
static DrawRoutineRegistry & Get()
ABSL_DECLARE_FLAG(std::string, rom)
bool ParseHexString(absl::string_view str, int *out)
Definition hex_util.h:17
absl::Status InitializeMockRom(Rom &rom)
Initialize a mock ROM for testing without requiring an actual ROM file.
Definition mock_rom.cc:16
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:416
Modern project structure with comprehensive settings consolidation.
Definition project.h:164
std::unordered_map< int, std::vector< std::string > > custom_object_files
Definition project.h:189
std::string custom_objects_folder
Definition project.h:184
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > resource_labels
Definition project.h:197
absl::Status InitializeEmbeddedLabels(const std::unordered_map< std::string, std::unordered_map< std::string, std::string > > &labels)
Definition project.cc:2208
absl::Status Open(const std::string &project_path)
Definition project.cc:323
core::FeatureFlags::Flags feature_flags
Definition project.h:192
static std::unordered_map< std::string, std::unordered_map< std::string, std::string > > ToResourceLabels()
Convert all labels to a structured map for project embedding.