8#include "absl/strings/str_cat.h"
9#include "absl/strings/str_format.h"
10#include "absl/strings/str_split.h"
22 {0x01,
"Title Screen"},
23 {0x02,
"File Select"},
26 {0x05,
"Loading Area"},
27 {0x06,
"Pre-Dungeon"},
29 {0x08,
"Pre-Overworld"},
31 {0x0A,
"Pre-Overworld (Special)"},
32 {0x0B,
"Overworld (Special)"},
33 {0x0C,
"Blank Screen"},
34 {0x0D,
"Text (Dialogue)"},
35 {0x0E,
"Closing Spotlight"},
36 {0x0F,
"Opening Spotlight"},
37 {0x10,
"Spotlight (Other)"},
38 {0x11,
"Dungeon Spotlight"},
39 {0x12,
"Dungeon Endgame"},
40 {0x13,
"Ganon Emerging"},
41 {0x14,
"Ganon Phase 1"},
42 {0x15,
"Ganon Phase 2"},
43 {0x16,
"Triforce Room"},
44 {0x17,
"Ending Sequence"},
48 {0x1B,
"Attract Mode"},
53 {0x00,
"Raven"}, {0x01,
"Vulture"},
54 {0x02,
"Flying Tile"}, {0x03,
"Empty"},
55 {0x04,
"Pull Switch"}, {0x05,
"Octorock"},
56 {0x06,
"Wall Master"}, {0x07,
"Moldorm (tail)"},
57 {0x08,
"Octorock (4)"}, {0x09,
"Chicken"},
58 {0x0A,
"Octorok (Stone)"}, {0x0B,
"Buzzblob"},
59 {0x0C,
"Snap Dragon"}, {0x0D,
"Octoballoon"},
60 {0x0E,
"Octoballoon Hatchling"}, {0x0F,
"Hinox"},
61 {0x10,
"Moblin"}, {0x11,
"Mini Helmasaur"},
62 {0x12,
"Gargoyle's Domain (Fireball)"}, {0x13,
"Antifairy"},
63 {0x14,
"Sahasrahla/Elder"}, {0x15,
"Bush Hoarder"},
87 return "Frame Counter";
90 return "Link X Position";
93 return "Link Y Position";
97 return "Link Direction";
99 return "Overworld Area";
101 return "Dungeon Room";
103 return "Player Health";
105 return "Player Max Health";
107 return "Player Rupees";
111 int offset = address - 0x7E0D00;
112 int sprite_index = offset % 16;
113 return absl::StrFormat(
"Sprite Table (Sprite %d)", sprite_index);
116 return "Save Data / Inventory";
131 return "sprite_table";
142 return "player_state";
158 {
"link_direction",
"Link's facing direction",
160 {
"sprite_y_low",
"Sprite Y positions (low byte)",
162 {
"sprite_x_low",
"Sprite X positions (low byte)",
176 {
"player_max_health",
"Player max health",
187 int bytes_per_line)
const {
188 std::ostringstream oss;
189 for (
size_t i = 0; i < data.size(); ++i) {
190 if (i > 0 && i % bytes_per_line == 0)
194 oss << absl::StrFormat(
"%02X", data[i]);
200 const std::vector<uint8_t>& data)
const {
202 result.reserve(data.size());
203 for (uint8_t
byte : data) {
204 result += (std::isprint(
byte) ?
static_cast<char>(byte) :
'.');
210 const std::string& addr_str)
const {
211 std::string s = addr_str;
214 if (!s.empty() && s[0] ==
'$') {
216 }
else if (s.size() > 2 && s[0] ==
'0' && (s[1] ==
'x' || s[1] ==
'X')) {
222 return static_cast<uint32_t
>(std::stoul(s,
nullptr, 16));
223 }
catch (
const std::exception& e) {
224 return absl::InvalidArgumentError(
225 absl::StrCat(
"Invalid address: ", addr_str));
241 auto addr_str = parser.
GetString(
"address");
242 if (!addr_str.has_value()) {
243 return absl::InvalidArgumentError(
"Missing required argument: address");
246 if (!addr_result.ok())
247 return addr_result.status();
248 uint32_t address = *addr_result;
250 int length = std::stoi(parser.
GetString(
"length").value_or(
"16"));
251 if (length <= 0 || length > 0x10000) {
252 return absl::InvalidArgumentError(
"Length must be between 1 and 65536");
257 formatter.
AddField(
"address", absl::StrFormat(
"$%06X", address));
258 formatter.
AddField(
"length", length);
261 formatter.
AddField(
"note",
"Connect to emulator via gRPC to read actual memory data");
266 "Sprite table: Check sprite_state ($DD0), sprite_type ($E20), "
267 "sprite_health ($E50) for each sprite (0-15)");
271 "Player state: Position at $20-$23, state at $5D, direction at $2F");
274 "Game mode: 0x07=Dungeon, 0x09=Overworld, 0x19=Inventory, "
279 return absl::OkStatus();
283 int sprite_index,
const std::vector<uint8_t>& wram)
const {
284 std::map<std::string, std::string> result;
286 if (wram.size() < 0x1000)
289 uint8_t state = wram[0x0DD0 + sprite_index];
290 uint8_t type = wram[0x0E20 + sprite_index];
291 uint8_t health = wram[0x0E50 + sprite_index];
292 uint8_t y_low = wram[0x0D00 + sprite_index];
293 uint8_t x_low = wram[0x0D10 + sprite_index];
294 uint8_t y_high = wram[0x0D20 + sprite_index];
295 uint8_t x_high = wram[0x0D30 + sprite_index];
297 uint16_t x = (x_high << 8) | x_low;
298 uint16_t y = (y_high << 8) | y_low;
300 result[
"sprite_index"] = std::to_string(sprite_index);
301 result[
"state"] = absl::StrFormat(
"$%02X", state);
302 result[
"active"] = (state != 0x00) ?
"yes" :
"no";
303 result[
"type"] = absl::StrFormat(
"$%02X", type);
305 auto type_it = kSpriteTypes.find(type);
306 if (type_it != kSpriteTypes.end()) {
307 result[
"type_name"] = type_it->second;
310 result[
"health"] = std::to_string(health);
311 result[
"position"] = absl::StrFormat(
"(%d, %d)", x, y);
317 const std::vector<uint8_t>& wram)
const {
318 std::map<std::string, std::string> result;
320 if (wram.size() < 0x100)
323 uint16_t x = (wram[0x23] << 8) | wram[0x22];
324 uint16_t y = (wram[0x21] << 8) | wram[0x20];
325 uint8_t state = wram[0x5D];
326 uint8_t direction = wram[0x2F];
328 result[
"position"] = absl::StrFormat(
"(%d, %d)", x, y);
329 result[
"state"] = absl::StrFormat(
"$%02X", state);
331 auto dir_it = kLinkDirections.find(direction);
332 if (dir_it != kLinkDirections.end()) {
333 result[
"direction"] = dir_it->second;
335 result[
"direction"] = absl::StrFormat(
"$%02X", direction);
342 const std::vector<uint8_t>& wram)
const {
343 std::map<std::string, std::string> result;
345 if (wram.size() < 0x20)
348 uint8_t mode = wram[0x10];
349 uint8_t submodule = wram[0x11];
351 result[
"mode"] = absl::StrFormat(
"$%02X", mode);
352 result[
"submodule"] = absl::StrFormat(
"$%02X", submodule);
354 auto mode_it = kGameModes.find(mode);
355 if (mode_it != kGameModes.end()) {
356 result[
"mode_name"] = mode_it->second;
374 auto pattern_opt = parser.
GetString(
"pattern");
375 if (!pattern_opt.has_value()) {
376 return absl::InvalidArgumentError(
"Missing required argument: pattern");
378 std::string pattern_str = *pattern_opt;
379 int max_results = std::stoi(parser.
GetString(
"max-results").value_or(
"10"));
382 if (!pattern_result.ok())
383 return pattern_result.status();
385 auto [pattern, mask] = *pattern_result;
388 formatter.
AddField(
"pattern", pattern_str);
389 formatter.
AddField(
"pattern_length",
static_cast<int>(pattern.size()));
390 formatter.
AddField(
"max_results", max_results);
391 formatter.
AddField(
"note",
"Connect to emulator via gRPC to search actual memory");
394 std::ostringstream parsed;
395 for (
size_t i = 0; i < pattern.size(); ++i) {
399 parsed << absl::StrFormat(
"%02X", pattern[i]);
404 formatter.
AddField(
"parsed_pattern", parsed.str());
407 return absl::OkStatus();
410absl::StatusOr<std::pair<std::vector<uint8_t>, std::vector<bool>>>
412 std::vector<uint8_t> pattern;
413 std::vector<bool> mask;
417 for (
char c : pattern_str) {
418 if (!std::isspace(c))
422 if (clean.length() % 2 != 0) {
423 return absl::InvalidArgumentError(
424 "Pattern must have even number of hex characters");
427 for (
size_t i = 0; i < clean.length(); i += 2) {
428 std::string byte_str = clean.substr(i, 2);
430 if (byte_str ==
"??" || byte_str ==
"**") {
431 pattern.push_back(0x00);
432 mask.push_back(
false);
435 uint8_t
byte =
static_cast<uint8_t
>(std::stoul(byte_str,
nullptr, 16));
436 pattern.push_back(
byte);
437 mask.push_back(
true);
438 }
catch (
const std::exception&) {
439 return absl::InvalidArgumentError(
440 absl::StrCat(
"Invalid hex byte: ", byte_str));
445 return std::make_pair(pattern, mask);
449 const std::vector<uint8_t>& memory, uint32_t base_address,
450 const std::vector<uint8_t>& pattern,
const std::vector<bool>& mask,
451 int max_results)
const {
452 std::vector<PatternMatch> matches;
454 if (pattern.empty() || memory.size() < pattern.size())
457 for (
size_t i = 0; i <= memory.size() - pattern.size(); ++i) {
459 for (
size_t j = 0; j < pattern.size() && match; ++j) {
460 if (mask[j] && memory[i + j] != pattern[j]) {
467 m.
address = base_address +
static_cast<uint32_t
>(i);
469 std::vector<uint8_t>(memory.begin() + i,
470 memory.begin() + i + pattern.size());
472 matches.push_back(m);
474 if (
static_cast<int>(matches.size()) >= max_results)
495 if (!addr_result.ok())
496 return addr_result.status();
497 uint32_t address = *addr_result;
499 std::string expected_str = parser.
GetString(
"expected").value_or(
"");
502 formatter.
AddField(
"address", absl::StrFormat(
"$%06X", address));
505 if (!expected_str.empty()) {
506 formatter.
AddField(
"expected", expected_str);
507 formatter.
AddField(
"note",
"Connect to emulator via gRPC to compare actual memory");
509 formatter.
AddField(
"note",
"Provide --expected <hex> to compare against expected values");
513 return absl::OkStatus();
522 return absl::OkStatus();
528 std::string region = parser.
GetString(
"region").value_or(
"all");
531 std::vector<std::string> checks;
532 if (region ==
"all" || region ==
"sprites") {
533 checks.push_back(
"Sprite table: Check for invalid states, out-of-bounds positions");
535 if (region ==
"all" || region ==
"player") {
536 checks.push_back(
"Player state: Check for invalid positions, corrupted state");
538 if (region ==
"all" || region ==
"game") {
539 checks.push_back(
"Game mode: Check for invalid mode/submodule combinations");
542 std::ostringstream checks_str;
543 for (
const auto& check : checks) {
544 checks_str <<
"- " << check <<
"\n";
548 formatter.
AddField(
"region", region);
549 formatter.
AddField(
"note",
"Connect to emulator via gRPC to check actual memory");
550 formatter.
AddField(
"available_checks", checks_str.str());
552 return absl::OkStatus();
556 const std::vector<uint8_t>& wram)
const {
557 std::vector<MemoryAnomaly> anomalies;
559 if (wram.size() < 0x1000)
563 uint8_t state = wram[0x0DD0 + i];
564 uint8_t type = wram[0x0E20 + i];
567 if (state > 0 && state > 0x10) {
568 anomalies.push_back({
571 absl::StrFormat(
"Sprite %d has unusual state $%02X", i, state),
577 if (state != 0 && type == 0) {
578 anomalies.push_back({
581 absl::StrFormat(
"Sprite %d is active but has type 0", i),
591 const std::vector<uint8_t>& wram)
const {
592 std::vector<MemoryAnomaly> anomalies;
594 if (wram.size() < 0x100)
597 uint16_t x = (wram[0x23] << 8) | wram[0x22];
598 uint16_t y = (wram[0x21] << 8) | wram[0x20];
601 if (x > 0x2000 || y > 0x2000) {
602 anomalies.push_back({
605 absl::StrFormat(
"Link position (%d, %d) seems out of bounds", x, y),
614 const std::vector<uint8_t>& wram)
const {
615 std::vector<MemoryAnomaly> anomalies;
617 if (wram.size() < 0x20)
620 uint8_t mode = wram[0x10];
624 anomalies.push_back({
627 absl::StrFormat(
"Invalid game mode $%02X", mode),
641 return absl::OkStatus();
647 std::string filter = parser.
GetString(
"filter").value_or(
"");
652 if (!filter.empty()) {
653 std::vector<MemoryRegionInfo> filtered;
654 for (
const auto& region : regions) {
655 if (region.name.find(filter) != std::string::npos ||
656 region.description.find(filter) != std::string::npos) {
657 filtered.push_back(region);
666 for (
const auto& region : regions) {
668 formatter.
AddField(
"name", region.name);
669 formatter.
AddField(
"start", absl::StrFormat(
"$%06X", region.start_address));
670 formatter.
AddField(
"end", absl::StrFormat(
"$%06X", region.end_address));
671 formatter.
AddField(
"type", region.data_type);
672 formatter.
AddField(
"description", region.description);
677 return absl::OkStatus();
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
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.