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