yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
mesen_handlers.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cctype>
5#include <charconv>
6#include <chrono>
7#include <cstdlib>
8#include <filesystem>
9#include <fstream>
10#include <sstream>
11#include <thread>
12#include <unordered_map>
13
14#include "absl/flags/declare.h"
15#include "absl/flags/flag.h"
16#include "absl/strings/numbers.h"
17#include "absl/strings/str_cat.h"
18#include "absl/strings/str_format.h"
19#include "absl/strings/str_join.h"
20#include "absl/time/clock.h"
21#include "absl/time/time.h"
23#include "nlohmann/json.hpp"
24#include "util/rom_hash.h"
25
26ABSL_DECLARE_FLAG(std::string, mesen_socket);
27
28namespace yaze {
29namespace cli {
30namespace handlers {
31
32namespace {
33
35 bool connected = false;
36 bool running = false;
37 bool paused = false;
38 uint64_t frame = 0;
39 uint32_t pc = 0;
40 int last_breakpoint_id = -1;
41 std::string last_action = "init";
42 std::unordered_map<int, uint32_t> breakpoints_by_id;
43};
44
46 static EmulatorAgentSessionState state;
47 return state;
48}
49
53
55 const std::shared_ptr<emu::mesen::MesenSocketClient>& client) {
56 auto& session = SessionState();
57 session.connected = client && client->IsConnected();
58 if (!session.connected) {
59 return;
60 }
61 if (auto state_or = client->GetState(); state_or.ok()) {
62 session.running = state_or->running;
63 session.paused = state_or->paused;
64 session.frame = state_or->frame;
65 }
66 if (auto cpu_or = client->GetCpuState(); cpu_or.ok()) {
67 session.pc =
68 (static_cast<uint32_t>(cpu_or->K) << 16) | (cpu_or->PC & 0xFFFF);
69 }
70}
71
72int ClampTimeoutMs(int timeout_ms) {
73 return std::max(1, timeout_ms);
74}
75
76int ClampPollMs(int poll_ms) {
77 return std::clamp(poll_ms, 1, 1000);
78}
79
80std::string DefaultMetaPathForState(const std::string& state_path) {
81 return state_path + ".meta.json";
82}
83
84::absl::Status EnsureFileExists(const std::string& path,
85 const char* label_for_errors) {
86 std::error_code ec;
87 if (!std::filesystem::exists(path, ec) || ec) {
88 return ::absl::NotFoundError(
89 ::absl::StrFormat("%s not found: %s", label_for_errors, path));
90 }
91 if (std::filesystem::is_directory(path, ec) || ec) {
92 return ::absl::FailedPreconditionError(
93 ::absl::StrFormat("%s is a directory: %s", label_for_errors, path));
94 }
95 return ::absl::OkStatus();
96}
97
98::absl::StatusOr<nlohmann::json> LoadJsonFile(const std::string& path) {
99 std::ifstream in(path);
100 if (!in.is_open()) {
101 return ::absl::NotFoundError(::absl::StrFormat("File not found: %s", path));
102 }
103 nlohmann::json j;
104 try {
105 in >> j;
106 } catch (const std::exception& e) {
107 return ::absl::InvalidArgumentError(
108 ::absl::StrFormat("Invalid JSON at %s: %s", path, e.what()));
109 }
110 return j;
111}
112
113::absl::Status WriteJsonFile(const std::string& path, const nlohmann::json& j) {
114 std::ofstream out(path);
115 if (!out.is_open()) {
116 return ::absl::PermissionDeniedError(
117 ::absl::StrFormat("Failed to open output file: %s", path));
118 }
119 out << j.dump(2);
120 if (!out.good()) {
121 return ::absl::InternalError(
122 ::absl::StrFormat("Failed to write output file: %s", path));
123 }
124 return ::absl::OkStatus();
125}
126
127std::string OptionalScenario(const resources::ArgumentParser& parser) {
128 return parser.GetString("scenario").value_or("");
129}
130
132 bool fresh = false;
133 std::string state_path;
134 std::string rom_path;
135 std::string meta_path;
136 std::string expected_scenario;
137 std::string recorded_scenario;
138 std::string current_rom_sha1;
140 std::string recorded_rom_sha1;
142 std::vector<std::string> stale_reasons;
143};
144
145::absl::StatusOr<SavestateFreshnessResult> ComputeSavestateFreshness(
146 const std::string& state_path, const std::string& rom_path,
147 const std::string& meta_path, const std::string& expected_scenario) {
148 auto state_status = EnsureFileExists(state_path, "state file");
149 if (!state_status.ok()) {
150 return state_status;
151 }
152 auto rom_status = EnsureFileExists(rom_path, "ROM file");
153 if (!rom_status.ok()) {
154 return rom_status;
155 }
156 auto meta_status = EnsureFileExists(meta_path, "metadata file");
157 if (!meta_status.ok()) {
158 return meta_status;
159 }
160
161 auto meta_or = LoadJsonFile(meta_path);
162 if (!meta_or.ok()) {
163 return meta_or.status();
164 }
165 const auto& meta = *meta_or;
166
168 result.state_path = state_path;
169 result.rom_path = rom_path;
170 result.meta_path = meta_path;
171 result.expected_scenario = expected_scenario;
173 result.current_state_sha1 = util::ComputeFileSha1Hex(state_path);
174 if (result.current_rom_sha1.empty()) {
175 return ::absl::InternalError("Failed to hash ROM file");
176 }
177 if (result.current_state_sha1.empty()) {
178 return ::absl::InternalError("Failed to hash state file");
179 }
180
181 result.recorded_rom_sha1 = meta.value("rom_sha1", "");
182 result.recorded_state_sha1 = meta.value("state_sha1", "");
183 result.recorded_scenario = meta.value("scenario", "");
184
185 const bool rom_match = !result.recorded_rom_sha1.empty() &&
186 result.recorded_rom_sha1 == result.current_rom_sha1;
187 const bool state_match =
188 !result.recorded_state_sha1.empty() &&
190 const bool scenario_match =
191 result.expected_scenario.empty() ||
192 result.expected_scenario == result.recorded_scenario;
193 if (!rom_match) {
194 result.stale_reasons.push_back("rom_sha1_mismatch");
195 }
196 if (!state_match) {
197 result.stale_reasons.push_back("state_sha1_mismatch");
198 }
199 if (!scenario_match) {
200 result.stale_reasons.push_back("scenario_mismatch");
201 }
202 result.fresh = result.stale_reasons.empty();
203 return result;
204}
205
207 const SavestateFreshnessResult& result) {
208 formatter.AddField("status", result.fresh ? "pass" : "fail");
209 formatter.AddField("ok", result.fresh);
210 formatter.AddField("state", result.state_path);
211 formatter.AddField("rom", result.rom_path);
212 formatter.AddField("meta", result.meta_path);
213 formatter.AddField("fresh", result.fresh);
214 formatter.AddField("recorded_rom_sha1", result.recorded_rom_sha1);
215 formatter.AddField("current_rom_sha1", result.current_rom_sha1);
216 formatter.AddField("recorded_state_sha1", result.recorded_state_sha1);
217 formatter.AddField("current_state_sha1", result.current_state_sha1);
218 if (!result.recorded_scenario.empty()) {
219 formatter.AddField("recorded_scenario", result.recorded_scenario);
220 }
221 if (!result.expected_scenario.empty()) {
222 formatter.AddField("expected_scenario", result.expected_scenario);
223 }
224 if (!result.stale_reasons.empty()) {
225 formatter.AddField("reasons", absl::StrJoin(result.stale_reasons, ","));
226 }
227}
228
229::absl::StatusOr<nlohmann::json> BuildSavestateMetadata(
230 const std::string& state_path, const std::string& rom_path,
231 const std::string& scenario, const std::string& generator_name) {
232 auto state_status = EnsureFileExists(state_path, "state file");
233 if (!state_status.ok()) {
234 return state_status;
235 }
236 auto rom_status = EnsureFileExists(rom_path, "ROM file");
237 if (!rom_status.ok()) {
238 return rom_status;
239 }
240
241 const std::string rom_sha1 = util::ComputeFileSha1Hex(rom_path);
242 const std::string state_sha1 = util::ComputeFileSha1Hex(state_path);
243 if (rom_sha1.empty()) {
244 return ::absl::InternalError("Failed to hash ROM file");
245 }
246 if (state_sha1.empty()) {
247 return ::absl::InternalError("Failed to hash state file");
248 }
249
250 nlohmann::json meta;
251 meta["schema"] = "mesen_state_meta/v1";
252 meta["state_path"] = state_path;
253 meta["state_sha1"] = state_sha1;
254 meta["rom_path"] = rom_path;
255 meta["rom_sha1"] = rom_sha1;
256 meta["generated_at"] = absl::FormatTime(absl::Now(), absl::UTCTimeZone());
257 meta["generator"] = generator_name;
258 if (!scenario.empty()) {
259 meta["scenario"] = scenario;
260 }
261
262 // Best-effort runtime capture when Mesen2 is connected.
264 if (client && client->IsConnected()) {
265 nlohmann::json runtime;
266 if (auto state_or = client->GetState(); state_or.ok()) {
267 runtime["frame"] = state_or->frame;
268 runtime["running"] = state_or->running;
269 runtime["paused"] = state_or->paused;
270 }
271 if (auto cpu_or = client->GetCpuState(); cpu_or.ok()) {
272 const uint32_t pc =
273 (static_cast<uint32_t>(cpu_or->K) << 16) | (cpu_or->PC & 0xFFFF);
274 runtime["pc"] = absl::StrFormat("0x%06X", pc);
275 }
276 if (auto game_or = client->GetGameState(); game_or.ok()) {
277 runtime["indoors"] = game_or->game.indoors;
278 runtime["room_id"] = absl::StrFormat("0x%04X", game_or->game.room_id);
279 runtime["link_x"] = game_or->link.x;
280 runtime["link_y"] = game_or->link.y;
281 }
282 if (!runtime.empty()) {
283 meta["runtime"] = runtime;
284 }
285 }
286 return meta;
287}
288
289::absl::StatusOr<std::filesystem::path> FindLatestStateFileInDir(
290 const std::filesystem::path& states_dir) {
291 std::error_code ec;
292 if (!std::filesystem::exists(states_dir, ec) || ec ||
293 !std::filesystem::is_directory(states_dir, ec) || ec) {
294 return ::absl::NotFoundError(::absl::StrFormat(
295 "states directory not found: %s", states_dir.string()));
296 }
297
298 bool found = false;
299 std::filesystem::path latest_path;
300 std::filesystem::file_time_type latest_time;
301 for (const auto& entry :
302 std::filesystem::directory_iterator(states_dir, ec)) {
303 if (ec) {
304 break;
305 }
306 if (!entry.is_regular_file()) {
307 continue;
308 }
309 const auto ext = entry.path().extension().string();
310 if (ext != ".state" && ext != ".mss") {
311 continue;
312 }
313 const auto file_time = entry.last_write_time(ec);
314 if (ec) {
315 continue;
316 }
317 if (!found || file_time > latest_time) {
318 found = true;
319 latest_time = file_time;
320 latest_path = entry.path();
321 }
322 }
323 if (!found) {
324 return ::absl::NotFoundError(::absl::StrFormat(
325 "no state files (.state/.mss) found in %s", states_dir.string()));
326 }
327 return latest_path;
328}
329
330::absl::Status ExportSessionStateToFile(const std::string& file_path) {
331 nlohmann::json j;
332 const auto& s = SessionState();
333 j["connected"] = s.connected;
334 j["running"] = s.running;
335 j["paused"] = s.paused;
336 j["frame"] = s.frame;
337 j["pc"] = s.pc;
338 j["last_breakpoint_id"] = s.last_breakpoint_id;
339 j["last_action"] = s.last_action;
340 j["breakpoints"] = nlohmann::json::array();
341 for (const auto& [id, addr] : s.breakpoints_by_id) {
342 j["breakpoints"].push_back({{"id", id}, {"address", addr}});
343 }
344
345 std::ofstream out(file_path);
346 if (!out.is_open()) {
347 return ::absl::PermissionDeniedError("Failed to open session export path");
348 }
349 out << j.dump(2);
350 if (!out.good()) {
351 return ::absl::InternalError("Failed to write session export file");
352 }
353 return ::absl::OkStatus();
354}
355
356::absl::Status ImportSessionStateFromFile(const std::string& file_path) {
357 std::ifstream in(file_path);
358 if (!in.is_open()) {
359 return ::absl::NotFoundError("Session import file not found");
360 }
361 nlohmann::json j;
362 try {
363 in >> j;
364 } catch (const std::exception& e) {
365 return ::absl::InvalidArgumentError(
366 ::absl::StrFormat("Invalid session JSON: %s", e.what()));
367 }
368
370 loaded.connected = j.value("connected", false);
371 loaded.running = j.value("running", false);
372 loaded.paused = j.value("paused", false);
373 loaded.frame = j.value("frame", 0ULL);
374 loaded.pc = j.value("pc", 0U);
375 loaded.last_breakpoint_id = j.value("last_breakpoint_id", -1);
376 loaded.last_action = j.value("last_action", "import");
377 if (j.contains("breakpoints") && j["breakpoints"].is_array()) {
378 for (const auto& bp : j["breakpoints"]) {
379 const int id = bp.value("id", -1);
380 const uint32_t addr = bp.value("address", 0U);
381 if (id >= 0) {
382 loaded.breakpoints_by_id[id] = addr;
383 }
384 }
385 }
386
387 SessionState() = std::move(loaded);
388 return ::absl::OkStatus();
389}
390
391::absl::Status EnsureConnected() {
393 if (!client->IsConnected()) {
394 std::string socket_path = ::absl::GetFlag(FLAGS_mesen_socket);
395 if (socket_path.empty()) {
396 const char* env_path = std::getenv("MESEN2_SOCKET_PATH");
397 if (env_path && env_path[0] != '\0') {
398 socket_path = env_path;
399 }
400 }
401 auto status =
402 socket_path.empty() ? client->Connect() : client->Connect(socket_path);
403 if (!status.ok()) {
404 return ::absl::Status(
405 status.code(),
406 ::absl::StrCat("Not connected to Mesen2. Is Mesen2-OoS running? ",
407 status.message()));
408 }
409 auto& session = SessionState();
410 session.connected = true;
411 session.last_action = "connect";
412 }
414 return ::absl::OkStatus();
415}
416
417::absl::StatusOr<int> ParseOptionalInt(const resources::ArgumentParser& parser,
418 const std::string& name,
419 int default_value) {
420 if (!parser.GetString(name).has_value()) {
421 return default_value;
422 }
423 return parser.GetInt(name);
424}
425
426std::vector<uint8_t> ParseHexBytes(const std::string& data_str) {
427 std::string hex;
428 hex.reserve(data_str.size());
429 for (char c : data_str) {
430 if (std::isxdigit(static_cast<unsigned char>(c))) {
431 hex.push_back(c);
432 }
433 }
434
435 std::vector<uint8_t> data;
436 for (size_t i = 0; i + 1 < hex.size(); i += 2) {
437 int byte = 0;
438 auto res = std::from_chars(hex.data() + i, hex.data() + i + 2, byte, 16);
439 if (res.ec == std::errc()) {
440 data.push_back(static_cast<uint8_t>(byte));
441 }
442 }
443 return data;
444}
445
446::absl::StatusOr<emu::mesen::CpuState> PollCpuStateWithDeadline(
447 const std::shared_ptr<emu::mesen::MesenSocketClient>& client,
448 ::absl::Time deadline) {
449 while (::absl::Now() < deadline) {
450 auto cpu_or = client->GetCpuState();
451 if (cpu_or.ok()) {
452 return cpu_or;
453 }
454 std::this_thread::sleep_for(std::chrono::milliseconds(5));
455 }
456 return ::absl::DeadlineExceededError(
457 "Timed out while polling Mesen2 CPU state");
458}
459
460} // namespace
461
462// MesenGamestateCommandHandler
464 const resources::ArgumentParser& parser) {
465 (void)parser;
466 return ::absl::OkStatus();
467}
468
470 Rom* rom, const resources::ArgumentParser& parser,
471 resources::OutputFormatter& formatter) {
472 (void)rom;
473 (void)parser;
474 auto status = EnsureConnected();
475 if (!status.ok())
476 return status;
477
479 auto result = client->GetGameState();
480 if (!result.ok())
481 return result.status();
482
483 const auto& state = *result;
484 formatter.AddField("link_x", state.link.x);
485 formatter.AddField("link_y", state.link.y);
486 formatter.AddField("link_layer", state.link.layer);
487 formatter.AddField("link_direction", static_cast<int>(state.link.direction));
488 formatter.AddField("health", static_cast<int>(state.items.current_health));
489 formatter.AddField("max_health", static_cast<int>(state.items.max_health));
490 formatter.AddField("magic", static_cast<int>(state.items.magic));
491 formatter.AddField("rupees", static_cast<int>(state.items.rupees));
492 formatter.AddField("bombs", static_cast<int>(state.items.bombs));
493 formatter.AddField("arrows", static_cast<int>(state.items.arrows));
494 formatter.AddField("mode", static_cast<int>(state.game.mode));
495 formatter.AddField("submode", static_cast<int>(state.game.submode));
496 formatter.AddField("indoors", state.game.indoors);
497 if (state.game.indoors) {
498 formatter.AddHexField("room_id", state.game.room_id, 4);
499 } else {
500 formatter.AddHexField("overworld_area", state.game.overworld_area, 2);
501 }
502
503 return ::absl::OkStatus();
504}
505
506// MesenSpritesCommandHandler
508 const resources::ArgumentParser& parser) {
509 (void)parser;
510 return ::absl::OkStatus();
511}
512
514 Rom* rom, const resources::ArgumentParser& parser,
515 resources::OutputFormatter& formatter) {
516 (void)rom;
517 auto status = EnsureConnected();
518 if (!status.ok())
519 return status;
520
521 bool show_all = parser.HasFlag("all");
523 auto result = client->GetSprites(show_all);
524 if (!result.ok())
525 return result.status();
526
527 formatter.AddField("count", static_cast<int>(result->size()));
528 formatter.BeginArray("sprites");
529 for (const auto& sprite : *result) {
530 formatter.AddArrayItem(::absl::StrFormat(
531 "[#%d] type=0x%02X state=%d @(%d,%d) hp=%d", sprite.slot, sprite.type,
532 sprite.state, sprite.x, sprite.y, sprite.health));
533 }
534 formatter.EndArray();
535
536 return ::absl::OkStatus();
537}
538
539// MesenCpuCommandHandler
541 const resources::ArgumentParser& parser) {
542 (void)parser;
543 return ::absl::OkStatus();
544}
545
547 Rom* rom, const resources::ArgumentParser& parser,
548 resources::OutputFormatter& formatter) {
549 (void)rom;
550 (void)parser;
551 auto status = EnsureConnected();
552 if (!status.ok())
553 return status;
554
556 auto result = client->GetCpuState();
557 if (!result.ok())
558 return result.status();
559
560 const auto& cpu = *result;
561 uint32_t pc = (static_cast<uint32_t>(cpu.K) << 16) | (cpu.PC & 0xFFFF);
562 formatter.AddHexField("pc", pc, 6);
563 formatter.AddHexField("a", cpu.A, 4);
564 formatter.AddHexField("x", cpu.X, 4);
565 formatter.AddHexField("y", cpu.Y, 4);
566 formatter.AddHexField("sp", cpu.SP, 4);
567 formatter.AddHexField("d", cpu.D, 4);
568 formatter.AddHexField("dbr", cpu.DBR, 2);
569 formatter.AddHexField("p", cpu.P, 2);
570 formatter.AddField("emulation_mode", cpu.emulation_mode);
571
572 return ::absl::OkStatus();
573}
574
575// MesenMemoryReadCommandHandler
577 const resources::ArgumentParser& parser) {
578 return parser.RequireArgs({"address"});
579}
580
582 Rom* rom, const resources::ArgumentParser& parser,
583 resources::OutputFormatter& formatter) {
584 (void)rom;
585 auto status = EnsureConnected();
586 if (!status.ok())
587 return status;
588
589 auto addr_or = parser.GetHex("address");
590 if (!addr_or.ok())
591 return addr_or.status();
592 uint32_t addr = static_cast<uint32_t>(*addr_or);
593
594 auto length_or = ParseOptionalInt(parser, "length", 16);
595 if (!length_or.ok())
596 return length_or.status();
597 int length = *length_or;
598
600 auto result = client->ReadBlock(addr, length);
601 if (!result.ok())
602 return result.status();
603
604 formatter.AddHexField("address", addr, 6);
605 formatter.AddField("length", static_cast<int>(result->size()));
606 formatter.BeginArray("bytes");
607 for (uint8_t byte : *result) {
608 formatter.AddArrayItem(::absl::StrFormat("%02X", byte));
609 }
610 formatter.EndArray();
611
612 return ::absl::OkStatus();
613}
614
615// MesenMemoryWriteCommandHandler
617 const resources::ArgumentParser& parser) {
618 return parser.RequireArgs({"address", "data"});
619}
620
622 Rom* rom, const resources::ArgumentParser& parser,
623 resources::OutputFormatter& formatter) {
624 (void)rom;
625 auto status = EnsureConnected();
626 if (!status.ok())
627 return status;
628
629 auto addr_or = parser.GetHex("address");
630 if (!addr_or.ok())
631 return addr_or.status();
632 uint32_t addr = static_cast<uint32_t>(*addr_or);
633
634 auto data_str = parser.GetString("data");
635 if (!data_str.has_value()) {
636 return ::absl::InvalidArgumentError("--data is required");
637 }
638
639 auto data = ParseHexBytes(*data_str);
640 if (data.empty()) {
641 return ::absl::InvalidArgumentError("Invalid --data hex string");
642 }
643
645 auto write_status = client->WriteBlock(addr, data);
646 if (!write_status.ok())
647 return write_status;
648
649 formatter.AddHexField("address", addr, 6);
650 formatter.AddField("bytes_written", static_cast<int>(data.size()));
651 formatter.AddField("status", "ok");
652 return ::absl::OkStatus();
653}
654
655// MesenDisasmCommandHandler
657 const resources::ArgumentParser& parser) {
658 return parser.RequireArgs({"address"});
659}
660
662 Rom* rom, const resources::ArgumentParser& parser,
663 resources::OutputFormatter& formatter) {
664 (void)rom;
665 auto status = EnsureConnected();
666 if (!status.ok())
667 return status;
668
669 auto addr_or = parser.GetHex("address");
670 if (!addr_or.ok())
671 return addr_or.status();
672 uint32_t addr = static_cast<uint32_t>(*addr_or);
673
674 auto count_or = ParseOptionalInt(parser, "count", 10);
675 if (!count_or.ok())
676 return count_or.status();
677 int count = *count_or;
678
680 auto result = client->Disassemble(addr, count);
681 if (!result.ok())
682 return result.status();
683
684 formatter.AddHexField("address", addr, 6);
685 formatter.AddField("disassembly", *result);
686 return ::absl::OkStatus();
687}
688
689// MesenTraceCommandHandler
691 const resources::ArgumentParser& parser) {
692 (void)parser;
693 return ::absl::OkStatus();
694}
695
697 Rom* rom, const resources::ArgumentParser& parser,
698 resources::OutputFormatter& formatter) {
699 (void)rom;
700 auto status = EnsureConnected();
701 if (!status.ok())
702 return status;
703
704 auto count_or = ParseOptionalInt(parser, "count", 20);
705 if (!count_or.ok())
706 return count_or.status();
707 int count = *count_or;
708
710 auto result = client->GetTrace(count);
711 if (!result.ok())
712 return result.status();
713
714 formatter.AddField("count", count);
715 formatter.AddField("trace", *result);
716 return ::absl::OkStatus();
717}
718
719// MesenBreakpointCommandHandler
721 const resources::ArgumentParser& parser) {
722 return parser.RequireArgs({"action"});
723}
724
726 Rom* rom, const resources::ArgumentParser& parser,
727 resources::OutputFormatter& formatter) {
728 (void)rom;
729 auto status = EnsureConnected();
730 if (!status.ok())
731 return status;
732
733 auto action = parser.GetString("action");
734 if (!action.has_value()) {
735 return ::absl::InvalidArgumentError("--action is required");
736 }
737
738 if (*action == "add") {
739 auto addr_or = parser.GetHex("address");
740 if (!addr_or.ok())
741 return addr_or.status();
742 uint32_t addr = static_cast<uint32_t>(*addr_or);
743
744 std::string type_str = parser.GetString("type").value_or("exec");
746 if (type_str == "read")
748 else if (type_str == "write")
750 else if (type_str == "rw")
752
754 auto result = client->AddBreakpoint(addr, type);
755 if (!result.ok())
756 return result.status();
757 formatter.AddField("breakpoint_id", *result);
758 formatter.AddHexField("address", addr, 6);
759 formatter.AddField("status", "added");
760 } else if (*action == "remove") {
761 auto id_or = parser.GetInt("id");
762 if (!id_or.ok())
763 return id_or.status();
764 int id = *id_or;
765
767 auto remove_status = client->RemoveBreakpoint(id);
768 if (!remove_status.ok())
769 return remove_status;
770 formatter.AddField("breakpoint_id", id);
771 formatter.AddField("status", "removed");
772 } else if (*action == "clear") {
774 auto clear_status = client->ClearBreakpoints();
775 if (!clear_status.ok())
776 return clear_status;
777 formatter.AddField("status", "cleared");
778 } else if (*action == "list") {
779 formatter.AddField("status", "not_implemented");
780 } else {
781 return ::absl::InvalidArgumentError("Unknown action: " + *action);
782 }
783
784 return ::absl::OkStatus();
785}
786
787// MesenControlCommandHandler
789 const resources::ArgumentParser& parser) {
790 return parser.RequireArgs({"action"});
791}
792
794 Rom* rom, const resources::ArgumentParser& parser,
795 resources::OutputFormatter& formatter) {
796 (void)rom;
797 auto status = EnsureConnected();
798 if (!status.ok())
799 return status;
800
801 auto action = parser.GetString("action");
802 if (!action.has_value()) {
803 return ::absl::InvalidArgumentError("--action required");
804 }
805
807 if (*action == "pause") {
808 auto result = client->Pause();
809 if (!result.ok())
810 return result;
811 } else if (*action == "resume") {
812 auto result = client->Resume();
813 if (!result.ok())
814 return result;
815 } else if (*action == "step") {
816 auto result = client->Step(1);
817 if (!result.ok())
818 return result;
819 } else if (*action == "frame") {
820 auto result = client->Frame();
821 if (!result.ok())
822 return result;
823 } else if (*action == "reset") {
824 auto result = client->Reset();
825 if (!result.ok())
826 return result;
827 } else {
828 return ::absl::InvalidArgumentError("Unknown action: " + *action);
829 }
830
831 auto& session = SessionState();
832 session.last_action = *action;
833 UpdateSessionFromRuntime(client);
834 formatter.AddField("status", "ok");
835 formatter.AddField("action", *action);
836 return ::absl::OkStatus();
837}
838
839// MesenSessionCommandHandler
841 const resources::ArgumentParser& parser) {
842 if (!parser.GetString("action").has_value()) {
843 return ::absl::OkStatus();
844 }
845 const auto action = parser.GetString("action").value_or("show");
846 if (action == "show" || action == "reset") {
847 return ::absl::OkStatus();
848 }
849 if (action == "export" || action == "import") {
850 return parser.RequireArgs({"file"});
851 }
852 return ::absl::InvalidArgumentError(
853 "--action must be show|reset|export|import");
854}
855
857 const auto& session = SessionState();
858 formatter.AddField("connected", session.connected);
859 formatter.AddField("running", session.running);
860 formatter.AddField("paused", session.paused);
861 formatter.AddField("frame", static_cast<uint64_t>(session.frame));
862 formatter.AddHexField("pc", session.pc, 6);
863 formatter.AddField("breakpoint_count",
864 static_cast<int>(session.breakpoints_by_id.size()));
865 formatter.AddField("last_breakpoint_id", session.last_breakpoint_id);
866 formatter.AddField("last_action", session.last_action);
867 return ::absl::OkStatus();
868}
869
871 Rom* rom, const resources::ArgumentParser& parser,
872 resources::OutputFormatter& formatter) {
873 (void)rom;
874 const auto action = parser.GetString("action").value_or("show");
875 if (action == "reset") {
876 ResetSessionState();
877 auto& session = SessionState();
878 session.last_action = "reset";
879 formatter.AddField("status", "ok");
880 formatter.AddField("action", "reset");
881 return AddSessionFields(formatter);
882 }
883
884 if (action == "export") {
885 auto file = parser.GetString("file");
886 if (!file.has_value()) {
887 return ::absl::InvalidArgumentError("--file required for export");
888 }
889 auto export_status = ExportSessionStateToFile(*file);
890 if (!export_status.ok()) {
891 return export_status;
892 }
893 auto& session = SessionState();
894 session.last_action = "export";
895 formatter.AddField("status", "ok");
896 formatter.AddField("action", "export");
897 formatter.AddField("file", *file);
898 return AddSessionFields(formatter);
899 }
900
901 if (action == "import") {
902 auto file = parser.GetString("file");
903 if (!file.has_value()) {
904 return ::absl::InvalidArgumentError("--file required for import");
905 }
906 auto import_status = ImportSessionStateFromFile(*file);
907 if (!import_status.ok()) {
908 return import_status;
909 }
910 auto& session = SessionState();
911 session.last_action = "import";
912 formatter.AddField("status", "ok");
913 formatter.AddField("action", "import");
914 formatter.AddField("file", *file);
915 return AddSessionFields(formatter);
916 }
917
918 auto status = EnsureConnected();
919 if (!status.ok()) {
920 return status;
921 }
923 UpdateSessionFromRuntime(client);
924 return AddSessionFields(formatter);
925}
926
927// MesenAwaitCommandHandler
929 const resources::ArgumentParser& parser) {
930 auto status = parser.RequireArgs({"type"});
931 if (!status.ok()) {
932 return status;
933 }
934 const auto type = parser.GetString("type").value_or("");
935 if (type == "frame") {
936 return parser.RequireArgs({"count"});
937 }
938 if (type == "pc") {
939 return parser.RequireArgs({"address"});
940 }
941 if (type == "breakpoint") {
942 return parser.RequireArgs({"id"});
943 }
944 return ::absl::InvalidArgumentError(
945 "--type must be one of frame|pc|breakpoint");
946}
947
949 Rom* rom, const resources::ArgumentParser& parser,
950 resources::OutputFormatter& formatter) {
951 (void)rom;
952 auto status = EnsureConnected();
953 if (!status.ok()) {
954 return status;
955 }
957 auto& session = SessionState();
958
959 auto timeout_or = ParseOptionalInt(parser, "timeout-ms", 2000);
960 if (!timeout_or.ok()) {
961 return timeout_or.status();
962 }
963 auto poll_or = ParseOptionalInt(parser, "poll-ms", 25);
964 if (!poll_or.ok()) {
965 return poll_or.status();
966 }
967 const int timeout_ms = ClampTimeoutMs(*timeout_or);
968 const int poll_ms = ClampPollMs(*poll_or);
969 const auto start = ::absl::Now();
970 const auto deadline = start + ::absl::Milliseconds(timeout_ms);
971
972 const auto type = parser.GetString("type").value_or("");
973 if (type == "frame") {
974 auto count_or = parser.GetInt("count");
975 if (!count_or.ok()) {
976 return count_or.status();
977 }
978 UpdateSessionFromRuntime(client);
979 const uint64_t target =
980 session.frame + static_cast<uint64_t>(std::max(0, *count_or));
981 while (::absl::Now() < deadline) {
982 if (auto state_or = client->GetState(); state_or.ok()) {
983 session.frame = state_or->frame;
984 session.running = state_or->running;
985 session.paused = state_or->paused;
986 if (session.frame >= target) {
987 session.last_action = "await-frame";
988 formatter.AddField("status", "ok");
989 formatter.AddField("type", "frame");
990 formatter.AddField("target_frame", static_cast<uint64_t>(target));
991 formatter.AddField("frame", static_cast<uint64_t>(session.frame));
992 formatter.AddField("elapsed_ms",
993 static_cast<uint64_t>(::absl::ToInt64Milliseconds(
994 ::absl::Now() - start)));
995 return ::absl::OkStatus();
996 }
997 }
998 std::this_thread::sleep_for(std::chrono::milliseconds(poll_ms));
999 }
1000 return ::absl::DeadlineExceededError("Timed out waiting for target frame");
1001 }
1002
1003 if (type == "pc") {
1004 auto addr_or = parser.GetHex("address");
1005 if (!addr_or.ok()) {
1006 return addr_or.status();
1007 }
1008 const uint32_t target_pc = static_cast<uint32_t>(*addr_or);
1009 while (::absl::Now() < deadline) {
1010 auto cpu_or = PollCpuStateWithDeadline(client, deadline);
1011 if (!cpu_or.ok()) {
1012 return cpu_or.status();
1013 }
1014 const uint32_t pc =
1015 (static_cast<uint32_t>(cpu_or->K) << 16) | (cpu_or->PC & 0xFFFF);
1016 session.pc = pc;
1017 if (pc == target_pc) {
1018 session.last_action = "await-pc";
1019 formatter.AddField("status", "ok");
1020 formatter.AddField("type", "pc");
1021 formatter.AddHexField("target_pc", target_pc, 6);
1022 formatter.AddHexField("pc", pc, 6);
1023 formatter.AddField("elapsed_ms",
1024 static_cast<uint64_t>(::absl::ToInt64Milliseconds(
1025 ::absl::Now() - start)));
1026 return ::absl::OkStatus();
1027 }
1028 std::this_thread::sleep_for(std::chrono::milliseconds(poll_ms));
1029 }
1030 return ::absl::DeadlineExceededError("Timed out waiting for target pc");
1031 }
1032
1033 auto id_or = parser.GetInt("id");
1034 if (!id_or.ok()) {
1035 return id_or.status();
1036 }
1037 const int id = *id_or;
1038 const auto it = session.breakpoints_by_id.find(id);
1039 if (it == session.breakpoints_by_id.end()) {
1040 return ::absl::NotFoundError("Unknown breakpoint id in session state");
1041 }
1042 const uint32_t target_pc = it->second;
1043 while (::absl::Now() < deadline) {
1044 auto state_or = client->GetState();
1045 if (!state_or.ok()) {
1046 return state_or.status();
1047 }
1048 session.running = state_or->running;
1049 session.paused = state_or->paused;
1050 session.frame = state_or->frame;
1051 auto cpu_or = client->GetCpuState();
1052 if (!cpu_or.ok()) {
1053 return cpu_or.status();
1054 }
1055 const uint32_t pc =
1056 (static_cast<uint32_t>(cpu_or->K) << 16) | (cpu_or->PC & 0xFFFF);
1057 session.pc = pc;
1058 if (session.paused && pc == target_pc) {
1059 session.last_action = "await-breakpoint";
1060 formatter.AddField("status", "ok");
1061 formatter.AddField("type", "breakpoint");
1062 formatter.AddField("breakpoint_id", id);
1063 formatter.AddHexField("pc", pc, 6);
1064 formatter.AddField("frame", static_cast<uint64_t>(session.frame));
1065 formatter.AddField("elapsed_ms",
1066 static_cast<uint64_t>(::absl::ToInt64Milliseconds(
1067 ::absl::Now() - start)));
1068 return ::absl::OkStatus();
1069 }
1070 std::this_thread::sleep_for(std::chrono::milliseconds(poll_ms));
1071 }
1072 return ::absl::DeadlineExceededError("Timed out waiting for breakpoint hit");
1073}
1074
1075// MesenGoalCommandHandler
1077 const resources::ArgumentParser& parser) {
1078 auto status = parser.RequireArgs({"goal"});
1079 if (!status.ok()) {
1080 return status;
1081 }
1082 const auto goal = parser.GetString("goal").value_or("");
1083 if (goal == "break-at" || goal == "capture-state-at-pc") {
1084 return parser.RequireArgs({"address"});
1085 }
1086 if (goal == "run-frames") {
1087 return parser.RequireArgs({"count"});
1088 }
1089 return ::absl::InvalidArgumentError(
1090 "Unknown --goal. Supported: break-at|run-frames|capture-state-at-pc");
1091}
1092
1094 Rom* rom, const resources::ArgumentParser& parser,
1095 resources::OutputFormatter& formatter) {
1096 (void)rom;
1097 auto status = EnsureConnected();
1098 if (!status.ok()) {
1099 return status;
1100 }
1102 auto& session = SessionState();
1103 const auto goal = parser.GetString("goal").value_or("");
1104 if (goal == "run-frames") {
1105 auto count_or = parser.GetInt("count");
1106 if (!count_or.ok()) {
1107 return count_or.status();
1108 }
1109 const int frame_count = std::max(1, *count_or);
1110 auto timeout_or = ParseOptionalInt(parser, "timeout-ms", 2000);
1111 if (!timeout_or.ok()) {
1112 return timeout_or.status();
1113 }
1114 auto poll_or = ParseOptionalInt(parser, "poll-ms", 25);
1115 if (!poll_or.ok()) {
1116 return poll_or.status();
1117 }
1118 const int timeout_ms = ClampTimeoutMs(*timeout_or);
1119 const int poll_ms = ClampPollMs(*poll_or);
1120 const auto start = ::absl::Now();
1121 const auto deadline = start + ::absl::Milliseconds(timeout_ms);
1122
1123 if (auto pause_status = client->Pause(); !pause_status.ok()) {
1124 return pause_status;
1125 }
1126 UpdateSessionFromRuntime(client);
1127 const uint64_t target_frame =
1128 session.frame + static_cast<uint64_t>(frame_count);
1129 if (auto resume_status = client->Resume(); !resume_status.ok()) {
1130 return resume_status;
1131 }
1132 bool reached = false;
1133 while (::absl::Now() < deadline) {
1134 auto state_or = client->GetState();
1135 if (!state_or.ok()) {
1136 break;
1137 }
1138 session.running = state_or->running;
1139 session.paused = state_or->paused;
1140 session.frame = state_or->frame;
1141 if (session.frame >= target_frame) {
1142 reached = true;
1143 break;
1144 }
1145 std::this_thread::sleep_for(std::chrono::milliseconds(poll_ms));
1146 }
1147 (void)client->Pause();
1148 UpdateSessionFromRuntime(client);
1149 if (!reached) {
1150 return ::absl::DeadlineExceededError("Goal run-frames timed out");
1151 }
1152 session.last_action = "goal-run-frames";
1153 formatter.AddField("status", "ok");
1154 formatter.AddField("goal", "run-frames");
1155 formatter.AddField("requested_frames", frame_count);
1156 formatter.AddField("frame", static_cast<uint64_t>(session.frame));
1157 formatter.AddField("elapsed_ms",
1158 static_cast<uint64_t>(
1159 ::absl::ToInt64Milliseconds(::absl::Now() - start)));
1160 return ::absl::OkStatus();
1161 }
1162
1163 if (goal != "break-at" && goal != "capture-state-at-pc") {
1164 return ::absl::InvalidArgumentError("Unsupported goal");
1165 }
1166 auto addr_or = parser.GetHex("address");
1167 if (!addr_or.ok()) {
1168 return addr_or.status();
1169 }
1170 const uint32_t target_pc = static_cast<uint32_t>(*addr_or);
1171 auto timeout_or = ParseOptionalInt(parser, "timeout-ms", 2000);
1172 if (!timeout_or.ok()) {
1173 return timeout_or.status();
1174 }
1175 auto poll_or = ParseOptionalInt(parser, "poll-ms", 25);
1176 if (!poll_or.ok()) {
1177 return poll_or.status();
1178 }
1179 const int timeout_ms = ClampTimeoutMs(*timeout_or);
1180 const int poll_ms = ClampPollMs(*poll_or);
1181 const auto start = ::absl::Now();
1182 const auto deadline = start + ::absl::Milliseconds(timeout_ms);
1183
1184 if (auto pause_status = client->Pause(); !pause_status.ok()) {
1185 return pause_status;
1186 }
1187 auto bp_or =
1188 client->AddBreakpoint(target_pc, emu::mesen::BreakpointType::kExecute);
1189 if (!bp_or.ok()) {
1190 return bp_or.status();
1191 }
1192 const int bp_id = *bp_or;
1193 session.breakpoints_by_id[bp_id] = target_pc;
1194 session.last_breakpoint_id = bp_id;
1195
1196 if (auto resume_status = client->Resume(); !resume_status.ok()) {
1197 (void)client->RemoveBreakpoint(bp_id);
1198 session.breakpoints_by_id.erase(bp_id);
1199 return resume_status;
1200 }
1201
1202 bool hit = false;
1203 while (::absl::Now() < deadline) {
1204 auto state_or = client->GetState();
1205 if (!state_or.ok()) {
1206 break;
1207 }
1208 session.running = state_or->running;
1209 session.paused = state_or->paused;
1210 session.frame = state_or->frame;
1211 auto cpu_or = client->GetCpuState();
1212 if (!cpu_or.ok()) {
1213 break;
1214 }
1215 const uint32_t pc =
1216 (static_cast<uint32_t>(cpu_or->K) << 16) | (cpu_or->PC & 0xFFFF);
1217 session.pc = pc;
1218 if (session.paused && pc == target_pc) {
1219 hit = true;
1220 break;
1221 }
1222 std::this_thread::sleep_for(std::chrono::milliseconds(poll_ms));
1223 }
1224
1225 (void)client->Pause();
1226 (void)client->RemoveBreakpoint(bp_id);
1227 session.breakpoints_by_id.erase(bp_id);
1228 UpdateSessionFromRuntime(client);
1229 if (!hit) {
1230 return ::absl::DeadlineExceededError("Goal break-at timed out");
1231 }
1232
1233 session.last_action = (goal == "capture-state-at-pc")
1234 ? "goal-capture-state-at-pc"
1235 : "goal-break-at";
1236 if (goal == "capture-state-at-pc") {
1237 auto game_state_or = client->GetGameState();
1238 if (game_state_or.ok()) {
1239 formatter.AddHexField("room_id", game_state_or->game.room_id, 4);
1240 formatter.AddField("indoors", game_state_or->game.indoors);
1241 formatter.AddField("link_x", game_state_or->link.x);
1242 formatter.AddField("link_y", game_state_or->link.y);
1243 formatter.AddField("health", game_state_or->items.current_health);
1244 }
1245 }
1246 formatter.AddField("status", "ok");
1247 formatter.AddField("goal", goal);
1248 formatter.AddField("breakpoint_id", bp_id);
1249 formatter.AddHexField("target_pc", target_pc, 6);
1250 formatter.AddHexField("pc", session.pc, 6);
1251 formatter.AddField("frame", static_cast<uint64_t>(session.frame));
1252 formatter.AddField("elapsed_ms",
1253 static_cast<uint64_t>(
1254 ::absl::ToInt64Milliseconds(::absl::Now() - start)));
1255 return ::absl::OkStatus();
1256}
1257
1258// MesenStateVerifyCommandHandler
1260 const resources::ArgumentParser& parser) {
1261 return parser.RequireArgs({"state", "rom-file"});
1262}
1263
1265 Rom* rom, const resources::ArgumentParser& parser,
1266 resources::OutputFormatter& formatter) {
1267 (void)rom;
1268 const std::string state_path = *parser.GetString("state");
1269 const std::string rom_path = *parser.GetString("rom-file");
1270 const std::string meta_path =
1271 parser.GetString("meta").value_or(DefaultMetaPathForState(state_path));
1272 const std::string expected_scenario = OptionalScenario(parser);
1273
1274 auto result_or = ComputeSavestateFreshness(state_path, rom_path, meta_path,
1275 expected_scenario);
1276 if (!result_or.ok()) {
1277 return result_or.status();
1278 }
1279 AddSavestateFreshnessFields(formatter, *result_or);
1280 if (!result_or->fresh) {
1281 return ::absl::FailedPreconditionError(
1282 "Savestate freshness verification failed");
1283 }
1284 return ::absl::OkStatus();
1285}
1286
1287// MesenStateRegenCommandHandler
1289 const resources::ArgumentParser& parser) {
1290 return parser.RequireArgs({"state", "rom-file"});
1291}
1292
1294 Rom* rom, const resources::ArgumentParser& parser,
1295 resources::OutputFormatter& formatter) {
1296 (void)rom;
1297 const std::string state_path = *parser.GetString("state");
1298 const std::string rom_path = *parser.GetString("rom-file");
1299 const std::string meta_path =
1300 parser.GetString("meta").value_or(DefaultMetaPathForState(state_path));
1301 const std::string scenario = OptionalScenario(parser);
1302
1303 auto meta_or = BuildSavestateMetadata(state_path, rom_path, scenario,
1304 "z3ed mesen-state-regen");
1305 if (!meta_or.ok()) {
1306 return meta_or.status();
1307 }
1308
1309 auto write_status = WriteJsonFile(meta_path, *meta_or);
1310 if (!write_status.ok())
1311 return write_status;
1312
1313 formatter.AddField("status", "ok");
1314 formatter.AddField("state", state_path);
1315 formatter.AddField("rom", rom_path);
1316 formatter.AddField("meta", meta_path);
1317 formatter.AddField("rom_sha1", meta_or->value("rom_sha1", ""));
1318 formatter.AddField("state_sha1", meta_or->value("state_sha1", ""));
1319 formatter.AddField("scenario", scenario);
1320 return ::absl::OkStatus();
1321}
1322
1323// MesenStateCaptureCommandHandler
1325 const resources::ArgumentParser& parser) {
1326 return parser.RequireArgs({"state", "rom-file"});
1327}
1328
1330 Rom* rom, const resources::ArgumentParser& parser,
1331 resources::OutputFormatter& formatter) {
1332 (void)rom;
1333 const std::string requested_state_path = *parser.GetString("state");
1334 const std::string rom_path = *parser.GetString("rom-file");
1335 const std::string scenario = OptionalScenario(parser);
1336 const std::string meta_path = parser.GetString("meta").value_or(
1337 DefaultMetaPathForState(requested_state_path));
1338 const int wait_ms = parser.GetInt("wait-ms").value_or(400);
1339 const int slot = parser.GetInt("slot").value_or(-1);
1340 const std::string states_dir = parser.GetString("states-dir").value_or("");
1341
1342 auto rom_status = EnsureFileExists(rom_path, "ROM file");
1343 if (!rom_status.ok()) {
1344 return rom_status;
1345 }
1346
1347 std::string resolved_state_path = requested_state_path;
1348 bool captured_from_mesen_slot = false;
1349 if (slot >= 0) {
1350 auto status = EnsureConnected();
1351 if (!status.ok()) {
1352 return status;
1353 }
1355 auto save_status = client->SaveState(slot);
1356 if (!save_status.ok()) {
1357 return save_status;
1358 }
1359 captured_from_mesen_slot = true;
1360 std::this_thread::sleep_for(
1361 std::chrono::milliseconds(std::max(1, wait_ms)));
1362 }
1363
1364 std::error_code ec;
1365 if (!std::filesystem::exists(resolved_state_path, ec) || ec) {
1366 if (states_dir.empty()) {
1367 return ::absl::NotFoundError(
1368 ::absl::StrFormat("state file not found after capture: %s (use "
1369 "--states-dir to auto-locate latest state file)",
1370 resolved_state_path));
1371 }
1372 auto latest_or = FindLatestStateFileInDir(states_dir);
1373 if (!latest_or.ok()) {
1374 return latest_or.status();
1375 }
1376 const auto source_path = *latest_or;
1377 auto parent = std::filesystem::path(resolved_state_path).parent_path();
1378 if (!parent.empty()) {
1379 std::filesystem::create_directories(parent, ec);
1380 if (ec) {
1381 return ::absl::PermissionDeniedError(::absl::StrFormat(
1382 "failed to create directory: %s", parent.string()));
1383 }
1384 }
1385 std::filesystem::copy_file(
1386 source_path, resolved_state_path,
1387 std::filesystem::copy_options::overwrite_existing, ec);
1388 if (ec) {
1389 return ::absl::InternalError(::absl::StrFormat(
1390 "failed to copy state from %s to %s: %s", source_path.string(),
1391 resolved_state_path, ec.message()));
1392 }
1393 }
1394
1395 auto meta_or = BuildSavestateMetadata(resolved_state_path, rom_path, scenario,
1396 "z3ed mesen-state-capture");
1397 if (!meta_or.ok()) {
1398 return meta_or.status();
1399 }
1400 auto write_status = WriteJsonFile(meta_path, *meta_or);
1401 if (!write_status.ok()) {
1402 return write_status;
1403 }
1404
1405 auto verify_or = ComputeSavestateFreshness(resolved_state_path, rom_path,
1406 meta_path, scenario);
1407 if (!verify_or.ok()) {
1408 return verify_or.status();
1409 }
1410 formatter.AddField("status", "ok");
1411 formatter.AddField("state", resolved_state_path);
1412 formatter.AddField("rom", rom_path);
1413 formatter.AddField("meta", meta_path);
1414 formatter.AddField("scenario", scenario);
1415 formatter.AddField("captured_from_mesen_slot", captured_from_mesen_slot);
1416 formatter.AddField("slot", slot);
1417 formatter.AddField("fresh", verify_or->fresh);
1418 formatter.AddField("rom_sha1", meta_or->value("rom_sha1", ""));
1419 formatter.AddField("state_sha1", meta_or->value("state_sha1", ""));
1420 return ::absl::OkStatus();
1421}
1422
1423// MesenStateHookCommandHandler
1425 const resources::ArgumentParser& parser) {
1426 return parser.RequireArgs({"state", "rom-file"});
1427}
1428
1430 Rom* rom, const resources::ArgumentParser& parser,
1431 resources::OutputFormatter& formatter) {
1432 (void)rom;
1433 const std::string state_path = *parser.GetString("state");
1434 const std::string rom_path = *parser.GetString("rom-file");
1435 const std::string meta_path =
1436 parser.GetString("meta").value_or(DefaultMetaPathForState(state_path));
1437 const std::string expected_scenario = OptionalScenario(parser);
1438
1439 auto result_or = ComputeSavestateFreshness(state_path, rom_path, meta_path,
1440 expected_scenario);
1441 if (!result_or.ok()) {
1442 return result_or.status();
1443 }
1444
1445 AddSavestateFreshnessFields(formatter, *result_or);
1446 formatter.AddField(
1447 "hook_summary",
1448 absl::StrFormat(
1449 "fresh=%s scenario=%s rom_sha1=%s state_sha1=%s",
1450 result_or->fresh ? "true" : "false",
1451 result_or->recorded_scenario.empty() ? "(none)"
1452 : result_or->recorded_scenario,
1453 result_or->current_rom_sha1, result_or->current_state_sha1));
1454 if (!result_or->fresh) {
1455 return ::absl::FailedPreconditionError("mesen preflight hook failed");
1456 }
1457 return ::absl::OkStatus();
1458}
1459
1460// Factory function
1461std::vector<std::unique_ptr<resources::CommandHandler>>
1463 std::vector<std::unique_ptr<resources::CommandHandler>> handlers;
1464 handlers.push_back(std::make_unique<MesenGamestateCommandHandler>());
1465 handlers.push_back(std::make_unique<MesenSpritesCommandHandler>());
1466 handlers.push_back(std::make_unique<MesenCpuCommandHandler>());
1467 handlers.push_back(std::make_unique<MesenMemoryReadCommandHandler>());
1468 handlers.push_back(std::make_unique<MesenMemoryWriteCommandHandler>());
1469 handlers.push_back(std::make_unique<MesenDisasmCommandHandler>());
1470 handlers.push_back(std::make_unique<MesenTraceCommandHandler>());
1471 handlers.push_back(std::make_unique<MesenBreakpointCommandHandler>());
1472 handlers.push_back(std::make_unique<MesenControlCommandHandler>());
1473 handlers.push_back(std::make_unique<MesenSessionCommandHandler>());
1474 handlers.push_back(std::make_unique<MesenAwaitCommandHandler>());
1475 handlers.push_back(std::make_unique<MesenGoalCommandHandler>());
1476 handlers.push_back(std::make_unique<MesenStateVerifyCommandHandler>());
1477 handlers.push_back(std::make_unique<MesenStateRegenCommandHandler>());
1478 handlers.push_back(std::make_unique<MesenStateCaptureCommandHandler>());
1479 handlers.push_back(std::make_unique<MesenStateHookCommandHandler>());
1480 return handlers;
1481}
1482
1483} // namespace handlers
1484} // namespace cli
1485} // 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
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
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)
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)
Utility for consistent output formatting across commands.
void BeginArray(const std::string &key)
Begin an array.
void AddArrayItem(const std::string &item)
Add an item to current array.
void AddField(const std::string &key, const std::string &value)
Add a key-value pair.
void AddHexField(const std::string &key, uint64_t value, int width=2)
Add a hex-formatted field.
static std::shared_ptr< MesenSocketClient > GetOrCreate()
ABSL_DECLARE_FLAG(std::string, mesen_socket)
::absl::StatusOr< nlohmann::json > BuildSavestateMetadata(const std::string &state_path, const std::string &rom_path, const std::string &scenario, const std::string &generator_name)
::absl::Status WriteJsonFile(const std::string &path, const nlohmann::json &j)
::absl::StatusOr< int > ParseOptionalInt(const resources::ArgumentParser &parser, const std::string &name, int default_value)
void AddSavestateFreshnessFields(resources::OutputFormatter &formatter, const SavestateFreshnessResult &result)
::absl::StatusOr< std::filesystem::path > FindLatestStateFileInDir(const std::filesystem::path &states_dir)
::absl::Status EnsureFileExists(const std::string &path, const char *label_for_errors)
std::vector< uint8_t > ParseHexBytes(const std::string &data_str)
::absl::Status ExportSessionStateToFile(const std::string &file_path)
::absl::StatusOr< SavestateFreshnessResult > ComputeSavestateFreshness(const std::string &state_path, const std::string &rom_path, const std::string &meta_path, const std::string &expected_scenario)
::absl::StatusOr< emu::mesen::CpuState > PollCpuStateWithDeadline(const std::shared_ptr< emu::mesen::MesenSocketClient > &client, ::absl::Time deadline)
::absl::Status ImportSessionStateFromFile(const std::string &file_path)
std::string DefaultMetaPathForState(const std::string &state_path)
void UpdateSessionFromRuntime(const std::shared_ptr< emu::mesen::MesenSocketClient > &client)
std::string OptionalScenario(const resources::ArgumentParser &parser)
std::vector< std::unique_ptr< resources::CommandHandler > > CreateMesenCommandHandlers()
Factory function to create Mesen2 command handlers.
::absl::Status AddSessionFields(resources::OutputFormatter &formatter)
BreakpointType
Breakpoint types.
std::string ComputeFileSha1Hex(const std::string &path)
Definition rom_hash.cc:213