yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
project_graph_tool.cc
Go to the documentation of this file.
2
3#include <filesystem>
4#include <fstream>
5#include <map>
6
7#include "absl/strings/str_cat.h"
8#include "absl/strings/str_format.h"
9#include "absl/strings/str_join.h"
11#include "util/json.h"
12#include "util/log.h"
13
14namespace yaze {
15namespace cli {
16namespace agent {
17namespace tools {
18
19namespace fs = std::filesystem;
20
21absl::StatusOr<uint32_t> ParseAddress(absl::string_view value) {
22 std::string token(value);
23 if (token.empty()) {
24 return absl::InvalidArgumentError("Address is empty");
25 }
26 if (token[0] == '$') {
27 token = token.substr(1);
28 } else if (token.size() > 2 && token[0] == '0' &&
29 (token[1] == 'x' || token[1] == 'X')) {
30 token = token.substr(2);
31 }
32 try {
33 return static_cast<uint32_t>(std::stoul(token, nullptr, 16));
34 } catch (...) {
35 return absl::InvalidArgumentError(absl::StrCat("Invalid address: ", value));
36 }
37}
38
39bool LoadJsonFile(const std::string& path, Json* out) {
40 if (path.empty()) {
41 return false;
42 }
43 std::ifstream file(path);
44 if (!file.is_open()) {
45 return false;
46 }
47 try {
48 file >> *out;
49 return true;
50 } catch (...) {
51 return false;
52 }
53}
54
55uint32_t NormalizeLoRomMirror(uint32_t address) {
56 if ((address & 0xFF0000u) >= 0x800000u) {
57 address &= 0x7FFFFFu;
58 }
59 return address;
60}
61
62int GetLoRomBankIndex(uint32_t address) {
63 return static_cast<int>((NormalizeLoRomMirror(address) >> 16) & 0xFFu);
64}
65
66absl::StatusOr<int> ParseBankIndex(absl::string_view value) {
67 if (value.empty()) {
68 return absl::InvalidArgumentError("Bank is empty");
69 }
70 try {
71 return std::stoi(std::string(value), nullptr, 16);
72 } catch (...) {
73 return absl::InvalidArgumentError(absl::StrCat("Invalid bank: ", value));
74 }
75}
76
77std::string GetProjectSymbolsPath(const project::YazeProject& project) {
78 if (!project.z3dk_settings.artifact_paths.symbols_mlb.empty()) {
80 }
81 if (!project.z3dk_settings.symbols_path.empty()) {
82 return project.z3dk_settings.symbols_path;
83 }
84 if (!project.symbols_filename.empty()) {
85 return project.symbols_filename;
86 }
87 return project.GetZ3dkArtifactPath("symbols.mlb");
88}
89
90std::map<std::string, core::AsarSymbol> LoadSymbolsFromMlb(
91 const std::string& path) {
92 std::map<std::string, core::AsarSymbol> symbols;
93 std::ifstream file(path);
94 if (!file.is_open()) {
95 return symbols;
96 }
97
98 std::string line;
99 while (std::getline(file, line)) {
100 const size_t first_colon = line.find(':');
101 const size_t second_colon = line.find(':', first_colon + 1);
102 if (first_colon == std::string::npos || second_colon == std::string::npos) {
103 continue;
104 }
105
106 auto addr_or = ParseAddress(
107 line.substr(first_colon + 1, second_colon - first_colon - 1));
108 if (!addr_or.ok()) {
109 continue;
110 }
111
112 core::AsarSymbol symbol;
113 symbol.name = line.substr(second_colon + 1);
114 symbol.address = *addr_or;
115 symbols[symbol.name] = std::move(symbol);
116 }
117 return symbols;
118}
119
120const Json* FindBestSourceEntry(const Json& sourcemap, uint32_t address) {
121 if (!sourcemap.contains("entries") || !sourcemap["entries"].is_array()) {
122 return nullptr;
123 }
124
125 const Json* best = nullptr;
126 uint32_t best_address = 0;
127 for (const auto& entry : sourcemap["entries"]) {
128 auto addr_or = ParseAddress(entry.value("address", "0x0"));
129 if (!addr_or.ok() || *addr_or > address) {
130 continue;
131 }
132 if (!best || *addr_or >= best_address) {
133 best = &entry;
134 best_address = *addr_or;
135 }
136 }
137 return best;
138}
139
140std::string LookupSourceFile(const Json& sourcemap, int file_id) {
141 if (!sourcemap.contains("files") || !sourcemap["files"].is_array()) {
142 return {};
143 }
144 for (const auto& file : sourcemap["files"]) {
145 if (file.value("id", -1) == file_id) {
146 return file.value("path", "");
147 }
148 }
149 return {};
150}
151
153 return LoadJsonFile(
155 ? project.GetZ3dkArtifactPath("sourcemap.json")
157 out);
158}
159
160bool LoadProjectHooks(const project::YazeProject& project, Json* out) {
162 ? project.GetZ3dkArtifactPath("hooks.json")
164 out);
165}
166
167std::string GetDisasmBankFile(const project::YazeProject& project, int bank) {
168 const auto dir =
169 std::filesystem::path(project.GetZ3dkArtifactPath("z3disasm"));
170 return (dir / absl::StrFormat("bank_%02X.asm", bank)).string();
171}
172
174 const resources::ArgumentParser& parser) {
175 return parser.RequireArgs({"query"});
176}
177
179 const resources::ArgumentParser& parser,
180 resources::OutputFormatter& formatter) {
181 (void)rom;
182 if (!project_) {
183 return absl::FailedPreconditionError("Project context not available.");
184 }
185
186 std::string query_type = parser.GetString("query").value();
187
188 if (query_type == "info") {
189 return GetProjectInfo(formatter);
190 } else if (query_type == "files") {
191 std::string path = parser.GetString("path").value_or(project_->code_folder);
192 return GetFileStructure(path, formatter);
193 } else if (query_type == "symbols") {
194 return GetSymbolTable(formatter);
195 } else if (query_type == "lookup") {
196 return LookupAddressOrSymbol(parser, formatter);
197 } else if (query_type == "writes") {
198 return GetWriteCoverage(formatter);
199 } else if (query_type == "bank") {
200 return GetBankContext(parser, formatter);
201 } else {
202 return absl::InvalidArgumentError(
203 absl::StrCat("Unknown query type: ", query_type));
204 }
205}
206
208 resources::OutputFormatter& formatter) const {
209 formatter.AddField("name", project_->name);
210 formatter.AddField("description", project_->metadata.description);
211 formatter.AddField("filepath", project_->filepath);
212 formatter.AddField("rom_filename", project_->rom_filename);
213 formatter.AddField("code_folder", project_->code_folder);
214 formatter.AddField("symbols_filename", project_->symbols_filename);
215 formatter.AddField("build_script", project_->build_script);
216 formatter.AddField("git_repository", project_->git_repository);
217 formatter.AddField("last_build_hash", project_->last_build_hash);
218 return absl::OkStatus();
219}
220
222 const std::string& path, resources::OutputFormatter& formatter) const {
223 fs::path abs_path = project_->GetAbsolutePath(path);
224 if (!fs::exists(abs_path)) {
225 return absl::NotFoundError(absl::StrCat("Path not found: ", path));
226 }
227
228 formatter.BeginArray("files");
229 for (const auto& entry : fs::directory_iterator(abs_path)) {
230 formatter.BeginObject();
231 formatter.AddField("name", entry.path().filename().string());
232 formatter.AddField("type", entry.is_directory() ? "directory" : "file");
233 formatter.AddField("path",
234 project_->GetRelativePath(entry.path().string()));
235 formatter.EndObject();
236 }
237 formatter.EndArray();
238 return absl::OkStatus();
239}
240
242 resources::OutputFormatter& formatter) const {
243 // Prefer the backend-agnostic pointer populated by the dispatcher.
244 // Fall back to the Asar wrapper so CLI-only builds (no editor) still
245 // get symbols from direct ApplyPatch calls.
246 // Note: AsarWrapper::GetSymbolTable() returns by value, so we must own
247 // a local copy in the fallback branch — we can't bind a reference to it.
248 std::map<std::string, core::AsarSymbol> fallback;
249 const bool had_live_source =
250 assembly_symbol_table_ != nullptr || asar_wrapper_ != nullptr;
251 bool loaded_from_artifact = false;
252 const std::map<std::string, core::AsarSymbol>* symbols_ptr =
254 if (!symbols_ptr && asar_wrapper_) {
255 fallback = asar_wrapper_->GetSymbolTable();
256 symbols_ptr = &fallback;
257 } else if (!symbols_ptr && project_) {
259 loaded_from_artifact = !fallback.empty();
260 symbols_ptr = &fallback;
261 }
262 if (!symbols_ptr) {
263 static const std::map<std::string, core::AsarSymbol> kEmpty;
264 symbols_ptr = &kEmpty;
265 }
266 const auto& symbols = *symbols_ptr;
267 if (symbols.empty()) {
268 if (!had_live_source && !loaded_from_artifact) {
269 return absl::FailedPreconditionError(
270 "No assembler backend or emitted symbol artifact is available.");
271 }
272 return absl::NotFoundError(
273 "No symbols loaded. Load symbols via the Assemble menu or ensure the "
274 "build script generates them.");
275 }
276
277 Json sourcemap;
278 const bool has_sourcemap =
279 project_ && LoadProjectSourceMap(*project_, &sourcemap);
280
281 formatter.BeginArray("symbols");
282 for (const auto& [name, symbol] : symbols) {
283 formatter.BeginObject();
284 formatter.AddField("name", symbol.name);
285 formatter.AddField("address", absl::StrFormat("$%06X", symbol.address));
286 formatter.AddField("bank", absl::StrFormat("$%02X", symbol.address >> 16));
287 if (has_sourcemap) {
288 if (const Json* entry = FindBestSourceEntry(sourcemap, symbol.address)) {
289 const int file_id = entry->value("file_id", -1);
290 const int line = entry->value("line", 0);
291 const std::string path = LookupSourceFile(sourcemap, file_id);
292 if (!path.empty()) {
293 formatter.AddField("file", path);
294 }
295 if (line > 0) {
296 formatter.AddField("line", line);
297 }
298 }
299 }
300 formatter.EndObject();
301 }
302 formatter.EndArray();
303 return absl::OkStatus();
304}
305
307 const resources::ArgumentParser& parser,
308 resources::OutputFormatter& formatter) const {
309 if (!project_) {
310 return absl::FailedPreconditionError("Project context not available.");
311 }
312
313 std::map<std::string, core::AsarSymbol> symbols;
315 symbols = *assembly_symbol_table_;
316 } else if (asar_wrapper_) {
317 symbols = asar_wrapper_->GetSymbolTable();
318 } else {
320 }
321
322 uint32_t address = 0;
323 if (auto symbol = parser.GetString("symbol"); symbol.has_value()) {
324 auto it = symbols.find(*symbol);
325 if (it == symbols.end()) {
326 return absl::NotFoundError(absl::StrCat("Unknown symbol: ", *symbol));
327 }
328 address = it->second.address;
329 formatter.AddField("symbol", it->second.name);
330 } else if (auto address_arg = parser.GetString("address");
331 address_arg.has_value()) {
332 auto address_or = ParseAddress(*address_arg);
333 if (!address_or.ok()) {
334 return address_or.status();
335 }
336 address = *address_or;
337 } else {
338 return absl::InvalidArgumentError(
339 "lookup query requires --symbol or --address");
340 }
341
342 formatter.AddField("address", absl::StrFormat("$%06X", address));
343 formatter.AddField("bank", absl::StrFormat("$%02X", address >> 16));
344
345 formatter.BeginArray("matching_symbols");
346 for (const auto& [name, symbol] : symbols) {
347 if (symbol.address != address) {
348 continue;
349 }
350 formatter.BeginObject();
351 formatter.AddField("name", name);
352 formatter.EndObject();
353 }
354 formatter.EndArray();
355
356 Json sourcemap;
357 if (LoadProjectSourceMap(*project_, &sourcemap)) {
358 if (const Json* entry = FindBestSourceEntry(sourcemap, address)) {
359 formatter.BeginObject("source");
360 formatter.AddField(
361 "file", LookupSourceFile(sourcemap, entry->value("file_id", -1)));
362 formatter.AddField("line", entry->value("line", 0));
363 formatter.AddField("entry_address", entry->value("address", "0x0"));
364 formatter.EndObject();
365 }
366 }
367
368 return absl::OkStatus();
369}
370
372 resources::OutputFormatter& formatter) const {
373 if (!project_) {
374 return absl::FailedPreconditionError("Project context not available.");
375 }
376
377 Json hooks;
378 if (!LoadProjectHooks(*project_, &hooks) || !hooks.contains("hooks") ||
379 !hooks["hooks"].is_array()) {
380 return absl::NotFoundError(
381 "No z3dk hook coverage artifact found. Assemble with z3dk first.");
382 }
383
384 std::map<int, int> bytes_by_bank;
385 formatter.BeginArray("writes");
386 for (const auto& hook : hooks["hooks"]) {
387 const std::string address_str = hook.value("address", "0x0");
388 auto address_or = ParseAddress(address_str);
389 if (!address_or.ok()) {
390 continue;
391 }
392 const uint32_t address = *address_or;
393 const int bank = GetLoRomBankIndex(address);
394 const int size = hook.value("size", 0);
395 bytes_by_bank[bank] += size;
396
397 formatter.BeginObject();
398 formatter.AddField("address", absl::StrFormat("$%06X", address));
399 formatter.AddField("bank", absl::StrFormat("$%02X", bank));
400 formatter.AddField("size", size);
401 formatter.AddField("kind", hook.value("kind", "patch"));
402 formatter.AddField("name", hook.value("name", ""));
403 if (hook.contains("source")) {
404 formatter.AddField("source", hook.value("source", ""));
405 }
406 formatter.EndObject();
407 }
408 formatter.EndArray();
409
410 formatter.BeginArray("banks");
411 for (const auto& [bank, bytes] : bytes_by_bank) {
412 formatter.BeginObject();
413 formatter.AddField("bank", absl::StrFormat("$%02X", bank));
414 formatter.AddField("bytes_written", bytes);
415 formatter.EndObject();
416 }
417 formatter.EndArray();
418 return absl::OkStatus();
419}
420
422 const resources::ArgumentParser& parser,
423 resources::OutputFormatter& formatter) const {
424 if (!project_) {
425 return absl::FailedPreconditionError("Project context not available.");
426 }
427
428 auto bank_arg = parser.GetString("bank");
429 if (!bank_arg.has_value()) {
430 return absl::InvalidArgumentError("bank query requires --bank=<hex>");
431 }
432 auto bank_or = ParseBankIndex(*bank_arg);
433 if (!bank_or.ok()) {
434 return bank_or.status();
435 }
436 const int bank = *bank_or;
437
438 formatter.AddField("bank", absl::StrFormat("$%02X", bank));
439 const std::string disasm_file = GetDisasmBankFile(*project_, bank);
440 formatter.AddField("disasm_file", disasm_file);
441 formatter.AddField("disasm_file_exists",
442 static_cast<bool>(std::filesystem::exists(disasm_file)));
443
444 Json sourcemap;
445 formatter.BeginArray("sources");
446 if (LoadProjectSourceMap(*project_, &sourcemap) &&
447 sourcemap.contains("entries") && sourcemap["entries"].is_array()) {
448 for (const auto& entry : sourcemap["entries"]) {
449 auto address_or = ParseAddress(entry.value("address", "0x0"));
450 if (!address_or.ok() || GetLoRomBankIndex(*address_or) != bank) {
451 continue;
452 }
453 formatter.BeginObject();
454 formatter.AddField("address", absl::StrFormat("$%06X", *address_or));
455 formatter.AddField(
456 "file", LookupSourceFile(sourcemap, entry.value("file_id", -1)));
457 formatter.AddField("line", entry.value("line", 0));
458 formatter.EndObject();
459 }
460 }
461 formatter.EndArray();
462
463 Json hooks;
464 formatter.BeginArray("hooks");
465 if (LoadProjectHooks(*project_, &hooks) && hooks.contains("hooks") &&
466 hooks["hooks"].is_array()) {
467 for (const auto& hook : hooks["hooks"]) {
468 auto address_or = ParseAddress(hook.value("address", "0x0"));
469 if (!address_or.ok() || GetLoRomBankIndex(*address_or) != bank) {
470 continue;
471 }
472 formatter.BeginObject();
473 formatter.AddField("address", absl::StrFormat("$%06X", *address_or));
474 formatter.AddField("kind", hook.value("kind", "patch"));
475 formatter.AddField("name", hook.value("name", ""));
476 formatter.AddField("size", hook.value("size", 0));
477 formatter.AddField("source", hook.value("source", ""));
478 formatter.EndObject();
479 }
480 }
481 formatter.EndArray();
482 return absl::OkStatus();
483}
484
485} // namespace tools
486} // namespace agent
487} // namespace cli
488} // namespace yaze
bool is_array() const
Definition json.h:58
bool contains(const std::string &) const
Definition json.h:53
T value(const std::string &, const T &def) const
Definition json.h:50
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
absl::Status GetProjectInfo(resources::OutputFormatter &formatter) const
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status GetSymbolTable(resources::OutputFormatter &formatter) const
absl::Status GetFileStructure(const std::string &path, resources::OutputFormatter &formatter) const
absl::Status GetWriteCoverage(resources::OutputFormatter &formatter) const
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status LookupAddressOrSymbol(const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) const
absl::Status GetBankContext(const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) const
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)
absl::Status RequireArgs(const std::vector< std::string > &required) const
Validate that required arguments are present.
const std::map< std::string, core::AsarSymbol > * assembly_symbol_table_
Utility for consistent output formatting across commands.
void BeginArray(const std::string &key)
Begin an 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.
std::map< std::string, AsarSymbol > GetSymbolTable() const
absl::StatusOr< uint32_t > ParseAddress(absl::string_view value)
bool LoadProjectSourceMap(const project::YazeProject &project, Json *out)
bool LoadProjectHooks(const project::YazeProject &project, Json *out)
std::map< std::string, core::AsarSymbol > LoadSymbolsFromMlb(const std::string &path)
std::string GetDisasmBankFile(const project::YazeProject &project, int bank)
uint32_t NormalizeLoRomMirror(uint32_t address)
int GetLoRomBankIndex(uint32_t address)
std::string GetProjectSymbolsPath(const project::YazeProject &project)
bool LoadJsonFile(const std::string &path, Json *out)
std::string LookupSourceFile(const Json &sourcemap, int file_id)
const Json * FindBestSourceEntry(const Json &sourcemap, uint32_t address)
absl::StatusOr< int > ParseBankIndex(absl::string_view value)
Symbol information extracted from Asar assembly.
Modern project structure with comprehensive settings consolidation.
Definition project.h:164
ProjectMetadata metadata
Definition project.h:166
std::string git_repository
Definition project.h:218
std::string GetZ3dkArtifactPath(absl::string_view artifact_name) const
Definition project.cc:1398
std::string GetRelativePath(const std::string &absolute_path) const
Definition project.cc:1299
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:1319
std::string last_build_hash
Definition project.h:220
std::string symbols_filename
Definition project.h:182
Z3dkSettings z3dk_settings
Definition project.h:207
Z3dkArtifactPaths artifact_paths
Definition project.h:152