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"},
55 {0x02,
"Flying Tile"},
57 {0x04,
"Pull Switch"},
59 {0x06,
"Wall Master"},
60 {0x07,
"Moldorm (tail)"},
61 {0x08,
"Octorock (4)"},
63 {0x0A,
"Octorok (Stone)"},
65 {0x0C,
"Snap Dragon"},
66 {0x0D,
"Octoballoon"},
67 {0x0E,
"Octoballoon Hatchling"},
70 {0x11,
"Mini Helmasaur"},
71 {0x12,
"Gargoyle's Domain (Fireball)"},
73 {0x14,
"Sahasrahla/Elder"},
74 {0x15,
"Bush Hoarder"},
98 return "Frame Counter";
101 return "Link X Position";
104 return "Link Y Position";
108 return "Link Direction";
110 return "Overworld Area";
112 return "Dungeon Room";
114 return "Player Health";
116 return "Player Max Health";
118 return "Player Rupees";
122 int offset = address - 0x7E0D00;
123 int sprite_index = offset % 16;
124 return absl::StrFormat(
"Sprite Table (Sprite %d)", sprite_index);
127 return "Save Data / Inventory";
142 return "sprite_table";
153 return "player_state";
169 {
"link_direction",
"Link's facing direction",
171 {
"sprite_y_low",
"Sprite Y positions (low byte)",
173 {
"sprite_x_low",
"Sprite X positions (low byte)",
181 {
"overworld_area",
"Current overworld area",
187 {
"player_max_health",
"Player max health",
198 int bytes_per_line)
const {
199 std::ostringstream oss;
200 for (
size_t i = 0; i < data.size(); ++i) {
201 if (i > 0 && i % bytes_per_line == 0)
205 oss << absl::StrFormat(
"%02X", data[i]);
211 const std::vector<uint8_t>& data)
const {
213 result.reserve(data.size());
214 for (uint8_t
byte : data) {
215 result += (std::isprint(
byte) ?
static_cast<char>(byte) :
'.');
221 const std::string& addr_str)
const {
222 std::string s = addr_str;
225 if (!s.empty() && s[0] ==
'$') {
227 }
else if (s.size() > 2 && s[0] ==
'0' && (s[1] ==
'x' || s[1] ==
'X')) {
233 return static_cast<uint32_t
>(std::stoul(s,
nullptr, 16));
234 }
catch (
const std::exception& e) {
235 return absl::InvalidArgumentError(
236 absl::StrCat(
"Invalid address: ", addr_str));
252 auto addr_str = parser.
GetString(
"address");
253 if (!addr_str.has_value()) {
254 return absl::InvalidArgumentError(
"Missing required argument: address");
257 if (!addr_result.ok())
258 return addr_result.status();
259 uint32_t address = *addr_result;
261 int length = std::stoi(parser.
GetString(
"length").value_or(
"16"));
262 if (length <= 0 || length > 0x10000) {
263 return absl::InvalidArgumentError(
"Length must be between 1 and 65536");
268 formatter.
AddField(
"address", absl::StrFormat(
"$%06X", address));
269 formatter.
AddField(
"length", length);
273 "Connect to emulator via gRPC to read actual memory data");
279 "Sprite table: Check sprite_state ($DD0), sprite_type ($E20), "
280 "sprite_health ($E50) for each sprite (0-15)");
285 "Player state: Position at $20-$23, state at $5D, direction at $2F");
289 "Game mode: 0x07=Dungeon, 0x09=Overworld, 0x19=Inventory, "
294 return absl::OkStatus();
298 int sprite_index,
const std::vector<uint8_t>& wram)
const {
299 std::map<std::string, std::string> result;
301 if (wram.size() < 0x1000)
304 uint8_t state = wram[0x0DD0 + sprite_index];
305 uint8_t type = wram[0x0E20 + sprite_index];
306 uint8_t health = wram[0x0E50 + sprite_index];
307 uint8_t y_low = wram[0x0D00 + sprite_index];
308 uint8_t x_low = wram[0x0D10 + sprite_index];
309 uint8_t y_high = wram[0x0D20 + sprite_index];
310 uint8_t x_high = wram[0x0D30 + sprite_index];
312 uint16_t x = (x_high << 8) | x_low;
313 uint16_t y = (y_high << 8) | y_low;
315 result[
"sprite_index"] = std::to_string(sprite_index);
316 result[
"state"] = absl::StrFormat(
"$%02X", state);
317 result[
"active"] = (state != 0x00) ?
"yes" :
"no";
318 result[
"type"] = absl::StrFormat(
"$%02X", type);
320 auto type_it = kSpriteTypes.find(type);
321 if (type_it != kSpriteTypes.end()) {
322 result[
"type_name"] = type_it->second;
325 result[
"health"] = std::to_string(health);
326 result[
"position"] = absl::StrFormat(
"(%d, %d)", x, y);
332 const std::vector<uint8_t>& wram)
const {
333 std::map<std::string, std::string> result;
335 if (wram.size() < 0x100)
338 uint16_t x = (wram[0x23] << 8) | wram[0x22];
339 uint16_t y = (wram[0x21] << 8) | wram[0x20];
340 uint8_t state = wram[0x5D];
341 uint8_t direction = wram[0x2F];
343 result[
"position"] = absl::StrFormat(
"(%d, %d)", x, y);
344 result[
"state"] = absl::StrFormat(
"$%02X", state);
346 auto dir_it = kLinkDirections.find(direction);
347 if (dir_it != kLinkDirections.end()) {
348 result[
"direction"] = dir_it->second;
350 result[
"direction"] = absl::StrFormat(
"$%02X", direction);
357 const std::vector<uint8_t>& wram)
const {
358 std::map<std::string, std::string> result;
360 if (wram.size() < 0x20)
363 uint8_t mode = wram[0x10];
364 uint8_t submodule = wram[0x11];
366 result[
"mode"] = absl::StrFormat(
"$%02X", mode);
367 result[
"submodule"] = absl::StrFormat(
"$%02X", submodule);
369 auto mode_it = kGameModes.find(mode);
370 if (mode_it != kGameModes.end()) {
371 result[
"mode_name"] = mode_it->second;
389 auto pattern_opt = parser.
GetString(
"pattern");
390 if (!pattern_opt.has_value()) {
391 return absl::InvalidArgumentError(
"Missing required argument: pattern");
393 std::string pattern_str = *pattern_opt;
394 int max_results = std::stoi(parser.
GetString(
"max-results").value_or(
"10"));
397 if (!pattern_result.ok())
398 return pattern_result.status();
400 auto [pattern, mask] = *pattern_result;
403 formatter.
AddField(
"pattern", pattern_str);
404 formatter.
AddField(
"pattern_length",
static_cast<int>(pattern.size()));
405 formatter.
AddField(
"max_results", max_results);
407 "Connect to emulator via gRPC to search actual memory");
410 std::ostringstream parsed;
411 for (
size_t i = 0; i < pattern.size(); ++i) {
415 parsed << absl::StrFormat(
"%02X", pattern[i]);
420 formatter.
AddField(
"parsed_pattern", parsed.str());
423 return absl::OkStatus();
426absl::StatusOr<std::pair<std::vector<uint8_t>, std::vector<bool>>>
428 std::vector<uint8_t> pattern;
429 std::vector<bool> mask;
433 for (
char c : pattern_str) {
434 if (!std::isspace(c))
438 if (clean.length() % 2 != 0) {
439 return absl::InvalidArgumentError(
440 "Pattern must have even number of hex characters");
443 for (
size_t i = 0; i < clean.length(); i += 2) {
444 std::string byte_str = clean.substr(i, 2);
446 if (byte_str ==
"??" || byte_str ==
"**") {
447 pattern.push_back(0x00);
448 mask.push_back(
false);
451 uint8_t
byte =
static_cast<uint8_t
>(std::stoul(byte_str,
nullptr, 16));
452 pattern.push_back(
byte);
453 mask.push_back(
true);
454 }
catch (
const std::exception&) {
455 return absl::InvalidArgumentError(
456 absl::StrCat(
"Invalid hex byte: ", byte_str));
461 return std::make_pair(pattern, mask);
465 const std::vector<uint8_t>& memory, uint32_t base_address,
466 const std::vector<uint8_t>& pattern,
const std::vector<bool>& mask,
467 int max_results)
const {
468 std::vector<PatternMatch> matches;
470 if (pattern.empty() || memory.size() < pattern.size())
473 for (
size_t i = 0; i <= memory.size() - pattern.size(); ++i) {
475 for (
size_t j = 0; j < pattern.size() && match; ++j) {
476 if (mask[j] && memory[i + j] != pattern[j]) {
483 m.
address = base_address +
static_cast<uint32_t
>(i);
485 memory.begin() + i, memory.begin() + i + pattern.size());
487 matches.push_back(m);
489 if (
static_cast<int>(matches.size()) >= max_results)
510 if (!addr_result.ok())
511 return addr_result.status();
512 uint32_t address = *addr_result;
514 std::string expected_str = parser.
GetString(
"expected").value_or(
"");
517 formatter.
AddField(
"address", absl::StrFormat(
"$%06X", address));
520 if (!expected_str.empty()) {
521 formatter.
AddField(
"expected", expected_str);
523 "Connect to emulator via gRPC to compare actual memory");
526 "note",
"Provide --expected <hex> to compare against expected values");
530 return absl::OkStatus();
539 return absl::OkStatus();
545 std::string region = parser.
GetString(
"region").value_or(
"all");
548 std::vector<std::string> checks;
549 if (region ==
"all" || region ==
"sprites") {
551 "Sprite table: Check for invalid states, out-of-bounds positions");
553 if (region ==
"all" || region ==
"player") {
555 "Player state: Check for invalid positions, corrupted state");
557 if (region ==
"all" || region ==
"game") {
559 "Game mode: Check for invalid mode/submodule combinations");
562 std::ostringstream checks_str;
563 for (
const auto& check : checks) {
564 checks_str <<
"- " << check <<
"\n";
568 formatter.
AddField(
"region", region);
570 "Connect to emulator via gRPC to check actual memory");
571 formatter.
AddField(
"available_checks", checks_str.str());
573 return absl::OkStatus();
577 const std::vector<uint8_t>& wram)
const {
578 std::vector<MemoryAnomaly> anomalies;
580 if (wram.size() < 0x1000)
584 uint8_t state = wram[0x0DD0 + i];
585 uint8_t type = wram[0x0E20 + i];
588 if (state > 0 && state > 0x10) {
589 anomalies.push_back({
592 absl::StrFormat(
"Sprite %d has unusual state $%02X", i, state),
598 if (state != 0 && type == 0) {
599 anomalies.push_back({
602 absl::StrFormat(
"Sprite %d is active but has type 0", i),
612 const std::vector<uint8_t>& wram)
const {
613 std::vector<MemoryAnomaly> anomalies;
615 if (wram.size() < 0x100)
618 uint16_t x = (wram[0x23] << 8) | wram[0x22];
619 uint16_t y = (wram[0x21] << 8) | wram[0x20];
622 if (x > 0x2000 || y > 0x2000) {
623 anomalies.push_back({
626 absl::StrFormat(
"Link position (%d, %d) seems out of bounds", x, y),
635 const std::vector<uint8_t>& wram)
const {
636 std::vector<MemoryAnomaly> anomalies;
638 if (wram.size() < 0x20)
641 uint8_t mode = wram[0x10];
645 anomalies.push_back({
648 absl::StrFormat(
"Invalid game mode $%02X", mode),
662 return absl::OkStatus();
668 std::string filter = parser.
GetString(
"filter").value_or(
"");
673 if (!filter.empty()) {
674 std::vector<MemoryRegionInfo> filtered;
675 for (
const auto& region : regions) {
676 if (region.name.find(filter) != std::string::npos ||
677 region.description.find(filter) != std::string::npos) {
678 filtered.push_back(region);
687 for (
const auto& region : regions) {
689 formatter.
AddField(
"name", region.name);
690 formatter.
AddField(
"start", absl::StrFormat(
"$%06X", region.start_address));
691 formatter.
AddField(
"end", absl::StrFormat(
"$%06X", region.end_address));
692 formatter.
AddField(
"type", region.data_type);
693 formatter.
AddField(
"description", region.description);
698 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.