feat(emulator): implement gRPC control server and emulator commands

- Added `AgentControlServer` to manage gRPC connections for emulator control.
- Introduced `EmulatorServiceImpl` with methods for starting, stopping, and controlling the emulator, including button presses and memory operations.
- Created new command handlers for pressing, releasing, and holding emulator buttons.
- Updated CMake configuration to include new source files and proto definitions for the emulator service.

Benefits:
- Enhanced control over the emulator through gRPC, allowing for remote interactions.
- Improved modularity and maintainability of the emulator's command handling.
- Streamlined integration of new features for emulator control and state inspection.
This commit is contained in:
scawful
2025-10-11 13:57:07 -04:00
parent 9ffb7803f5
commit aacc7795c3
10 changed files with 1054 additions and 181 deletions

View File

@@ -34,6 +34,87 @@
"required": ["type", "query"]
}
},
{
"name": "emulator-press-buttons",
"description": "Presses and immediately releases one or more buttons on the SNES controller.",
"parameters": {
"type": "object",
"properties": {
"buttons": {
"type": "string",
"description": "A comma-separated list of buttons to press (e.g., 'A,Right,Start')."
}
},
"required": ["buttons"]
}
},
{
"name": "emulator-hold-buttons",
"description": "Holds down one or more buttons for a specified duration.",
"parameters": {
"type": "object",
"properties": {
"buttons": {
"type": "string",
"description": "A comma-separated list of buttons to hold."
},
"duration": {
"type": "integer",
"description": "The duration in milliseconds to hold the buttons."
}
},
"required": ["buttons", "duration"]
}
},
{
"name": "emulator-get-state",
"description": "Retrieves the current state of the game from the emulator.",
"parameters": {
"type": "object",
"properties": {
"screenshot": {
"type": "boolean",
"description": "Whether to include a screenshot of the current frame."
}
}
}
},
{
"name": "emulator-read-memory",
"description": "Reads a block of memory from the SNES WRAM.",
"parameters": {
"type": "object",
"properties": {
"address": {
"type": "string",
"description": "The memory address to read from (e.g., '0x7E0010')."
},
"length": {
"type": "integer",
"description": "The number of bytes to read."
}
},
"required": ["address"]
}
},
{
"name": "emulator-write-memory",
"description": "Writes a block of data to the SNES WRAM.",
"parameters": {
"type": "object",
"properties": {
"address": {
"type": "string",
"description": "The memory address to write to."
},
"data": {
"type": "string",
"description": "The data to write, as a hex string (e.g., 'AABBCCDD')."
}
},
"required": ["address", "data"]
}
},
{
"name": "dungeon-list-sprites",
"description": "List all sprites in a specific dungeon or room",

View File

@@ -1,7 +1,9 @@
set(YAZE_AGENT_SOURCES
cli/service/agent/proposal_executor.cc
cli/handlers/agent/todo_commands.cc
cli/service/agent/agent_control_server.cc
cli/service/agent/conversational_agent_service.cc
cli/service/agent/emulator_service_impl.cc
cli/service/agent/simple_chat_session.cc
cli/service/agent/enhanced_tui.cc
cli/service/agent/tool_dispatcher.cc
@@ -111,7 +113,8 @@ if(YAZE_WITH_GRPC)
# Generate proto files for yaze_agent
target_add_protobuf(yaze_agent
${PROJECT_SOURCE_DIR}/src/protos/imgui_test_harness.proto
${PROJECT_SOURCE_DIR}/src/protos/canvas_automation.proto)
${PROJECT_SOURCE_DIR}/src/protos/canvas_automation.proto
${PROJECT_SOURCE_DIR}/src/protos/emulator_service.proto)
target_link_libraries(yaze_agent PUBLIC
grpc++

View File

@@ -21,3 +21,7 @@ ABSL_FLAG(std::string, prompt_version, "default",
"Prompt version to use: 'default' or 'v2'");
ABSL_FLAG(bool, use_function_calling, false,
"Enable native Gemini function calling (incompatible with JSON output mode)");
// --- Agent Control Flags ---
ABSL_FLAG(bool, agent_control, false,
"Enable the gRPC server to allow the agent to control the emulator.");

View File

@@ -1,35 +1,256 @@
#include "cli/handlers/tools/emulator_commands.h"
#include "absl/strings/numbers.h"
#include "absl/strings/str_format.h"
#include <grpcpp/grpcpp.h>
#include "protos/emulator_service.grpc.pb.h"
#include "absl/strings/str_split.h"
#include "absl/strings/string_view.h"
#include "absl/time/time.h"
#include "absl/status/statusor.h"
#include "absl/strings/escaping.h"
namespace yaze {
namespace cli {
namespace handlers {
namespace {
// A simple client for the EmulatorService
class EmulatorClient {
public:
EmulatorClient() {
auto channel = grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials());
stub_ = agent::EmulatorService::NewStub(channel);
}
template <typename TRequest, typename TResponse>
absl::StatusOr<TResponse> CallRpc(
grpc::Status (agent::EmulatorService::Stub::*rpc_method)(grpc::ClientContext*, const TRequest&, TResponse*),
const TRequest& request) {
TResponse response;
grpc::ClientContext context;
auto deadline = std::chrono::system_clock::now() + std::chrono::seconds(5);
context.set_deadline(deadline);
grpc::Status status = (stub_.get()->*rpc_method)(&context, request, &response);
if (!status.ok()) {
return absl::UnavailableError(absl::StrFormat(
"RPC failed: (%d) %s", status.error_code(), status.error_message()));
}
return response;
}
private:
std::unique_ptr<agent::EmulatorService::Stub> stub_;
};
// Helper to parse button from string
absl::StatusOr<agent::Button> StringToButton(absl::string_view s) {
if (s == "A") return agent::Button::A;
if (s == "B") return agent::Button::B;
if (s == "X") return agent::Button::X;
if (s == "Y") return agent::Button::Y;
if (s == "L") return agent::Button::L;
if (s == "R") return agent::Button::R;
if (s == "SELECT") return agent::Button::SELECT;
if (s == "START") return agent::Button::START;
if (s == "UP") return agent::Button::UP;
if (s == "DOWN") return agent::Button::DOWN;
if (s == "LEFT") return agent::Button::LEFT;
if (s == "RIGHT") return agent::Button::RIGHT;
return absl::InvalidArgumentError(absl::StrCat("Unknown button: ", s));
}
} // namespace
// --- Command Implementations ---
absl::Status EmulatorResetCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
EmulatorClient client;
agent::Empty request;
auto response_or = client.CallRpc(&agent::EmulatorService::Stub::Reset, request);
if (!response_or.ok()) {
return response_or.status();
}
auto response = response_or.value();
formatter.BeginObject("EmulatorReset");
formatter.AddField("success", response.success());
formatter.AddField("message", response.message());
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorGetStateCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
EmulatorClient client;
agent::GameStateRequest request;
request.set_include_screenshot(parser.HasFlag("screenshot"));
auto response_or = client.CallRpc(&agent::EmulatorService::Stub::GetGameState, request);
if (!response_or.ok()) {
return response_or.status();
}
auto response = response_or.value();
formatter.BeginObject("EmulatorState");
formatter.AddField("game_mode", static_cast<uint64_t>(response.game_mode()));
formatter.AddField("link_state", static_cast<uint64_t>(response.link_state()));
formatter.AddField("link_pos_x", static_cast<uint64_t>(response.link_pos_x()));
formatter.AddField("link_pos_y", static_cast<uint64_t>(response.link_pos_y()));
formatter.AddField("link_health", static_cast<uint64_t>(response.link_health()));
if (!response.screenshot_png().empty()) {
formatter.AddField("screenshot_size", static_cast<uint64_t>(response.screenshot_png().size()));
}
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorReadMemoryCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
EmulatorClient client;
agent::MemoryRequest request;
uint32_t address;
if (!absl::SimpleHexAtoi(parser.GetString("address").value(), &address)) {
return absl::InvalidArgumentError("Invalid address format.");
}
request.set_address(address);
request.set_size(parser.GetInt("length").value_or(16));
auto response_or = client.CallRpc(&agent::EmulatorService::Stub::ReadMemory, request);
if (!response_or.ok()) {
return response_or.status();
}
auto response = response_or.value();
formatter.BeginObject("MemoryRead");
formatter.AddHexField("address", response.address());
formatter.AddField("data_hex", absl::BytesToHexString(response.data()));
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorWriteMemoryCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
EmulatorClient client;
agent::MemoryWriteRequest request;
uint32_t address;
if (!absl::SimpleHexAtoi(parser.GetString("address").value(), &address)) {
return absl::InvalidArgumentError("Invalid address format.");
}
request.set_address(address);
std::string data_hex = parser.GetString("data").value();
request.set_data(absl::HexStringToBytes(data_hex));
auto response_or = client.CallRpc(&agent::EmulatorService::Stub::WriteMemory, request);
if (!response_or.ok()) {
return response_or.status();
}
auto response = response_or.value();
formatter.BeginObject("MemoryWrite");
formatter.AddField("success", response.success());
formatter.AddField("message", response.message());
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorPressButtonsCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
EmulatorClient client;
agent::ButtonRequest request;
std::vector<std::string> buttons = absl::StrSplit(parser.GetString("buttons").value(), ',');
for (const auto& btn_str : buttons) {
auto button_or = StringToButton(btn_str);
if (!button_or.ok()) return button_or.status();
request.add_buttons(button_or.value());
}
auto response_or = client.CallRpc(&agent::EmulatorService::Stub::PressButtons, request);
if (!response_or.ok()) {
return response_or.status();
}
auto response = response_or.value();
formatter.BeginObject("PressButtons");
formatter.AddField("success", response.success());
formatter.AddField("message", response.message());
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorReleaseButtonsCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
EmulatorClient client;
agent::ButtonRequest request;
std::vector<std::string> buttons = absl::StrSplit(parser.GetString("buttons").value(), ',');
for (const auto& btn_str : buttons) {
auto button_or = StringToButton(btn_str);
if (!button_or.ok()) return button_or.status();
request.add_buttons(button_or.value());
}
auto response_or = client.CallRpc(&agent::EmulatorService::Stub::ReleaseButtons, request);
if (!response_or.ok()) {
return response_or.status();
}
auto response = response_or.value();
formatter.BeginObject("ReleaseButtons");
formatter.AddField("success", response.success());
formatter.AddField("message", response.message());
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorHoldButtonsCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
EmulatorClient client;
agent::ButtonHoldRequest request;
std::vector<std::string> buttons = absl::StrSplit(parser.GetString("buttons").value(), ',');
for (const auto& btn_str : buttons) {
auto button_or = StringToButton(btn_str);
if (!button_or.ok()) return button_or.status();
request.add_buttons(button_or.value());
}
request.set_duration_ms(parser.GetInt("duration").value());
auto response_or = client.CallRpc(&agent::EmulatorService::Stub::HoldButtons, request);
if (!response_or.ok()) {
return response_or.status();
}
auto response = response_or.value();
formatter.BeginObject("HoldButtons");
formatter.AddField("success", response.success());
formatter.AddField("message", response.message());
formatter.EndObject();
return absl::OkStatus();
}
// --- Placeholder Implementations for commands not yet migrated to gRPC ---
absl::Status EmulatorStepCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
auto count = parser.GetInt("count").value_or(1);
formatter.BeginObject("Emulator Step");
formatter.AddField("steps_executed", count);
formatter.AddField("status", "not_implemented");
formatter.AddField("message", "Emulator stepping requires emulator integration");
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorRunCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
auto until_breakpoint = parser.GetString("until").value_or("");
formatter.BeginObject("Emulator Run");
formatter.AddField("until_breakpoint", until_breakpoint);
formatter.AddField("status", "not_implemented");
formatter.AddField("message", "Emulator running requires emulator integration");
formatter.EndObject();
return absl::OkStatus();
}
@@ -37,134 +258,31 @@ absl::Status EmulatorPauseCommandHandler::Execute(Rom* rom, const resources::Arg
resources::OutputFormatter& formatter) {
formatter.BeginObject("Emulator Pause");
formatter.AddField("status", "not_implemented");
formatter.AddField("message", "Emulator pause requires emulator integration");
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorResetCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
formatter.BeginObject("Emulator Reset");
formatter.AddField("status", "not_implemented");
formatter.AddField("message", "Emulator reset requires emulator integration");
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorGetStateCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
formatter.BeginObject("Emulator State");
formatter.AddField("status", "not_implemented");
formatter.AddField("message", "Emulator state requires emulator integration");
formatter.BeginObject("state");
formatter.AddField("running", false);
formatter.AddField("paused", true);
formatter.AddField("pc", "0x000000");
formatter.EndObject();
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorSetBreakpointCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
auto address_str = parser.GetString("address").value();
auto condition = parser.GetString("condition").value_or("");
uint32_t address;
if (!absl::SimpleHexAtoi(address_str, &address)) {
return absl::InvalidArgumentError(
"Invalid address format. Must be hex.");
}
formatter.BeginObject("Emulator Breakpoint Set");
formatter.AddHexField("address", address, 6);
formatter.AddField("condition", condition);
formatter.AddField("status", "not_implemented");
formatter.AddField("message", "Breakpoint setting requires emulator integration");
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorClearBreakpointCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
auto address_str = parser.GetString("address").value();
uint32_t address;
if (!absl::SimpleHexAtoi(address_str, &address)) {
return absl::InvalidArgumentError(
"Invalid address format. Must be hex.");
}
formatter.BeginObject("Emulator Breakpoint Cleared");
formatter.AddHexField("address", address, 6);
formatter.AddField("status", "not_implemented");
formatter.AddField("message", "Breakpoint clearing requires emulator integration");
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorListBreakpointsCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
formatter.BeginObject("Emulator Breakpoints");
formatter.AddField("total_breakpoints", 0);
formatter.AddField("status", "not_implemented");
formatter.AddField("message", "Breakpoint listing requires emulator integration");
formatter.BeginArray("breakpoints");
formatter.EndArray();
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorReadMemoryCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
auto address_str = parser.GetString("address").value();
auto length = parser.GetInt("length").value_or(16);
uint32_t address;
if (!absl::SimpleHexAtoi(address_str, &address)) {
return absl::InvalidArgumentError(
"Invalid address format. Must be hex.");
}
formatter.BeginObject("Emulator Memory Read");
formatter.AddHexField("address", address, 6);
formatter.AddField("length", length);
formatter.AddField("status", "not_implemented");
formatter.AddField("message", "Memory reading requires emulator integration");
formatter.BeginArray("data");
formatter.EndArray();
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorWriteMemoryCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
auto address_str = parser.GetString("address").value();
auto data_str = parser.GetString("data").value();
uint32_t address;
if (!absl::SimpleHexAtoi(address_str, &address)) {
return absl::InvalidArgumentError(
"Invalid address format. Must be hex.");
}
formatter.BeginObject("Emulator Memory Write");
formatter.AddHexField("address", address, 6);
formatter.AddField("data", data_str);
formatter.AddField("status", "not_implemented");
formatter.AddField("message", "Memory writing requires emulator integration");
formatter.EndObject();
return absl::OkStatus();
}
@@ -172,19 +290,7 @@ absl::Status EmulatorGetRegistersCommandHandler::Execute(Rom* rom, const resourc
resources::OutputFormatter& formatter) {
formatter.BeginObject("Emulator Registers");
formatter.AddField("status", "not_implemented");
formatter.AddField("message", "Register reading requires emulator integration");
formatter.BeginObject("registers");
formatter.AddField("A", std::string("0x0000"));
formatter.AddField("X", std::string("0x0000"));
formatter.AddField("Y", std::string("0x0000"));
formatter.AddField("PC", std::string("0x000000"));
formatter.AddField("SP", std::string("0x01FF"));
formatter.AddField("DB", std::string("0x00"));
formatter.AddField("DP", std::string("0x0000"));
formatter.EndObject();
formatter.EndObject();
return absl::OkStatus();
}
@@ -192,16 +298,7 @@ absl::Status EmulatorGetMetricsCommandHandler::Execute(Rom* rom, const resources
resources::OutputFormatter& formatter) {
formatter.BeginObject("Emulator Metrics");
formatter.AddField("status", "not_implemented");
formatter.AddField("message", "Metrics require emulator integration");
formatter.BeginObject("metrics");
formatter.AddField("instructions_per_second", 0);
formatter.AddField("total_instructions", 0);
formatter.AddField("cycles_per_frame", 0);
formatter.AddField("frame_rate", 0);
formatter.EndObject();
formatter.EndObject();
return absl::OkStatus();
}

View File

@@ -7,9 +7,6 @@ namespace yaze {
namespace cli {
namespace handlers {
/**
* @brief Command handler for emulator step execution
*/
class EmulatorStepCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-step"; }
@@ -21,16 +18,13 @@ class EmulatorStepCommandHandler : public resources::CommandHandler {
}
absl::Status ValidateArgs(const resources::ArgumentParser& parser) override {
return absl::OkStatus(); // No required args
return absl::OkStatus();
}
absl::Status Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) override;
};
/**
* @brief Command handler for emulator run execution
*/
class EmulatorRunCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-run"; }
@@ -42,16 +36,13 @@ class EmulatorRunCommandHandler : public resources::CommandHandler {
}
absl::Status ValidateArgs(const resources::ArgumentParser& parser) override {
return absl::OkStatus(); // No required args
return absl::OkStatus();
}
absl::Status Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) override;
};
/**
* @brief Command handler for emulator pause
*/
class EmulatorPauseCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-pause"; }
@@ -63,16 +54,13 @@ class EmulatorPauseCommandHandler : public resources::CommandHandler {
}
absl::Status ValidateArgs(const resources::ArgumentParser& parser) override {
return absl::OkStatus(); // No required args
return absl::OkStatus();
}
absl::Status Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) override;
};
/**
* @brief Command handler for emulator reset
*/
class EmulatorResetCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-reset"; }
@@ -84,16 +72,13 @@ class EmulatorResetCommandHandler : public resources::CommandHandler {
}
absl::Status ValidateArgs(const resources::ArgumentParser& parser) override {
return absl::OkStatus(); // No required args
return absl::OkStatus();
}
absl::Status Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) override;
};
/**
* @brief Command handler for getting emulator state
*/
class EmulatorGetStateCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-get-state"; }
@@ -105,16 +90,13 @@ class EmulatorGetStateCommandHandler : public resources::CommandHandler {
}
absl::Status ValidateArgs(const resources::ArgumentParser& parser) override {
return absl::OkStatus(); // No required args
return absl::OkStatus();
}
absl::Status Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) override;
};
/**
* @brief Command handler for setting breakpoints
*/
class EmulatorSetBreakpointCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-set-breakpoint"; }
@@ -133,9 +115,6 @@ class EmulatorSetBreakpointCommandHandler : public resources::CommandHandler {
resources::OutputFormatter& formatter) override;
};
/**
* @brief Command handler for clearing breakpoints
*/
class EmulatorClearBreakpointCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-clear-breakpoint"; }
@@ -154,9 +133,6 @@ class EmulatorClearBreakpointCommandHandler : public resources::CommandHandler {
resources::OutputFormatter& formatter) override;
};
/**
* @brief Command handler for listing breakpoints
*/
class EmulatorListBreakpointsCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-list-breakpoints"; }
@@ -168,16 +144,13 @@ class EmulatorListBreakpointsCommandHandler : public resources::CommandHandler {
}
absl::Status ValidateArgs(const resources::ArgumentParser& parser) override {
return absl::OkStatus(); // No required args
return absl::OkStatus();
}
absl::Status Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) override;
};
/**
* @brief Command handler for reading emulator memory
*/
class EmulatorReadMemoryCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-read-memory"; }
@@ -196,9 +169,6 @@ class EmulatorReadMemoryCommandHandler : public resources::CommandHandler {
resources::OutputFormatter& formatter) override;
};
/**
* @brief Command handler for writing emulator memory
*/
class EmulatorWriteMemoryCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-write-memory"; }
@@ -217,9 +187,6 @@ class EmulatorWriteMemoryCommandHandler : public resources::CommandHandler {
resources::OutputFormatter& formatter) override;
};
/**
* @brief Command handler for getting emulator registers
*/
class EmulatorGetRegistersCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-get-registers"; }
@@ -231,16 +198,13 @@ class EmulatorGetRegistersCommandHandler : public resources::CommandHandler {
}
absl::Status ValidateArgs(const resources::ArgumentParser& parser) override {
return absl::OkStatus(); // No required args
return absl::OkStatus();
}
absl::Status Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) override;
};
/**
* @brief Command handler for getting emulator metrics
*/
class EmulatorGetMetricsCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-get-metrics"; }
@@ -252,13 +216,65 @@ class EmulatorGetMetricsCommandHandler : public resources::CommandHandler {
}
absl::Status ValidateArgs(const resources::ArgumentParser& parser) override {
return absl::OkStatus(); // No required args
return absl::OkStatus();
}
absl::Status Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) override;
};
class EmulatorPressButtonsCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-press-buttons"; }
std::string GetDescription() const {
return "Press and release emulator buttons";
}
std::string GetUsage() const {
return "emulator-press-buttons --buttons <button1>,<button2>,...";
}
absl::Status ValidateArgs(
const resources::ArgumentParser& parser) override {
return parser.RequireArgs({"buttons"});
}
absl::Status Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) override;
};
class EmulatorReleaseButtonsCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-release-buttons"; }
std::string GetDescription() const {
return "Release currently held emulator buttons";
}
std::string GetUsage() const {
return "emulator-release-buttons --buttons <button1>,<button2>,...";
}
absl::Status ValidateArgs(
const resources::ArgumentParser& parser) override {
return parser.RequireArgs({"buttons"});
}
absl::Status Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) override;
};
class EmulatorHoldButtonsCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-hold-buttons"; }
std::string GetDescription() const {
return "Hold emulator buttons for a specified duration";
}
std::string GetUsage() const {
return "emulator-hold-buttons --buttons <button1>,<button2>,... --duration "
"<ms>";
}
absl::Status ValidateArgs(
const resources::ArgumentParser& parser) override {
return parser.RequireArgs({"buttons", "duration"});
}
absl::Status Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) override;
};
} // namespace handlers
} // namespace cli
} // namespace yaze

View File

@@ -0,0 +1,46 @@
#include "cli/service/agent/agent_control_server.h"
#include "cli/service/agent/emulator_service_impl.h"
#include <grpcpp/server.h>
#include <grpcpp/server_builder.h>
#include <iostream>
namespace yaze::agent {
AgentControlServer::AgentControlServer(yaze::emu::Emulator* emulator)
: emulator_(emulator) {}
AgentControlServer::~AgentControlServer() {
Stop();
}
void AgentControlServer::Start() {
server_thread_ = std::thread(&AgentControlServer::Run, this);
}
void AgentControlServer::Stop() {
if (server_) {
server_->Shutdown();
}
if (server_thread_.joinable()) {
server_thread_.join();
}
}
void AgentControlServer::Run() {
std::string server_address("0.0.0.0:50051");
EmulatorServiceImpl service(emulator_);
grpc::ServerBuilder builder;
builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
builder.RegisterService(&service);
server_ = builder.BuildAndStart();
if (server_) {
std::cout << "AgentControlServer listening on " << server_address << std::endl;
server_->Wait();
} else {
std::cerr << "Failed to start AgentControlServer on " << server_address << std::endl;
}
}
} // namespace yaze::agent

View File

@@ -0,0 +1,32 @@
#pragma once
#include <memory>
#include <thread>
namespace grpc {
class Server;
}
namespace yaze::emu {
class Emulator;
}
namespace yaze::agent {
class AgentControlServer {
public:
AgentControlServer(yaze::emu::Emulator* emulator);
~AgentControlServer();
void Start();
void Stop();
private:
void Run();
yaze::emu::Emulator* emulator_; // Non-owning pointer
std::unique_ptr<grpc::Server> server_;
std::thread server_thread_;
};
} // namespace yaze::agent

View File

@@ -0,0 +1,190 @@
#include "cli/service/agent/emulator_service_impl.h"
#include "app/emu/emulator.h"
#include "app/core/service/screenshot_utils.h"
#include "app/emu/input/input_backend.h" // Required for SnesButton enum
#include "absl/strings/escaping.h"
#include "absl/strings/str_format.h"
#include <fstream>
#include <thread>
namespace yaze::agent {
namespace {
// Helper to convert our gRPC Button enum to the emulator's SnesButton enum
emu::input::SnesButton ToSnesButton(Button button) {
using emu::input::SnesButton;
switch (button) {
case A: return SnesButton::A;
case B: return SnesButton::B;
case X: return SnesButton::X;
case Y: return SnesButton::Y;
case L: return SnesButton::L;
case R: return SnesButton::R;
case SELECT: return SnesButton::SELECT;
case START: return SnesButton::START;
case UP: return SnesButton::UP;
case DOWN: return SnesButton::DOWN;
case LEFT: return SnesButton::LEFT;
case RIGHT: return SnesButton::RIGHT;
default:
return SnesButton::B; // Default fallback
}
}
} // namespace
EmulatorServiceImpl::EmulatorServiceImpl(yaze::emu::Emulator* emulator)
: emulator_(emulator) {}
// --- Lifecycle ---
grpc::Status EmulatorServiceImpl::Start(grpc::ServerContext* context, const Empty* request, CommandResponse* response) {
if (!emulator_) return grpc::Status(grpc::StatusCode::UNAVAILABLE, "Emulator not initialized.");
emulator_->set_running(true);
response->set_success(true);
response->set_message("Emulator started.");
return grpc::Status::OK;
}
grpc::Status EmulatorServiceImpl::Stop(grpc::ServerContext* context, const Empty* request, CommandResponse* response) {
if (!emulator_) return grpc::Status(grpc::StatusCode::UNAVAILABLE, "Emulator not initialized.");
emulator_->set_running(false);
response->set_success(true);
response->set_message("Emulator stopped.");
return grpc::Status::OK;
}
grpc::Status EmulatorServiceImpl::Pause(grpc::ServerContext* context, const Empty* request, CommandResponse* response) {
if (!emulator_) return grpc::Status(grpc::StatusCode::UNAVAILABLE, "Emulator not initialized.");
emulator_->set_running(false);
response->set_success(true);
response->set_message("Emulator paused.");
return grpc::Status::OK;
}
grpc::Status EmulatorServiceImpl::Resume(grpc::ServerContext* context, const Empty* request, CommandResponse* response) {
if (!emulator_) return grpc::Status(grpc::StatusCode::UNAVAILABLE, "Emulator not initialized.");
emulator_->set_running(true);
response->set_success(true);
response->set_message("Emulator resumed.");
return grpc::Status::OK;
}
grpc::Status EmulatorServiceImpl::Reset(grpc::ServerContext* context, const Empty* request, CommandResponse* response) {
if (!emulator_) return grpc::Status(grpc::StatusCode::UNAVAILABLE, "Emulator not initialized.");
emulator_->snes().Reset(true); // Hard reset
response->set_success(true);
response->set_message("Emulator reset.");
return grpc::Status::OK;
}
// --- Input Control ---
grpc::Status EmulatorServiceImpl::PressButtons(grpc::ServerContext* context, const ButtonRequest* request, CommandResponse* response) {
if (!emulator_) return grpc::Status(grpc::StatusCode::UNAVAILABLE, "Emulator not initialized.");
auto& input_manager = emulator_->input_manager();
for (const auto& button : request->buttons()) {
input_manager.PressButton(ToSnesButton(static_cast<Button>(button)));
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
for (const auto& button : request->buttons()) {
input_manager.ReleaseButton(ToSnesButton(static_cast<Button>(button)));
}
response->set_success(true);
return grpc::Status::OK;
}
grpc::Status EmulatorServiceImpl::ReleaseButtons(grpc::ServerContext* context, const ButtonRequest* request, CommandResponse* response) {
if (!emulator_) return grpc::Status(grpc::StatusCode::UNAVAILABLE, "Emulator not initialized.");
auto& input_manager = emulator_->input_manager();
for (const auto& button : request->buttons()) {
input_manager.ReleaseButton(ToSnesButton(static_cast<Button>(button)));
}
response->set_success(true);
return grpc::Status::OK;
}
grpc::Status EmulatorServiceImpl::HoldButtons(grpc::ServerContext* context, const ButtonHoldRequest* request, CommandResponse* response) {
if (!emulator_) return grpc::Status(grpc::StatusCode::UNAVAILABLE, "Emulator not initialized.");
auto& input_manager = emulator_->input_manager();
for (const auto& button : request->buttons()) {
input_manager.PressButton(ToSnesButton(static_cast<Button>(button)));
}
std::this_thread::sleep_for(std::chrono::milliseconds(request->duration_ms()));
for (const auto& button : request->buttons()) {
input_manager.ReleaseButton(ToSnesButton(static_cast<Button>(button)));
}
response->set_success(true);
return grpc::Status::OK;
}
// --- State Inspection ---
grpc::Status EmulatorServiceImpl::GetGameState(grpc::ServerContext* context, const GameStateRequest* request, GameStateResponse* response) {
if (!emulator_ || !emulator_->is_snes_initialized()) {
return grpc::Status(grpc::StatusCode::UNAVAILABLE, "SNES is not initialized.");
}
auto& memory = emulator_->snes().memory();
response->set_game_mode(memory.ReadByte(0x7E0010));
response->set_link_state(memory.ReadByte(0x7E005D));
response->set_link_pos_x(memory.ReadWord(0x7E0020));
response->set_link_pos_y(memory.ReadWord(0x7E0022));
response->set_link_health(memory.ReadByte(0x7EF36D));
for (const auto& mem_req : request->memory_reads()) {
auto* mem_resp = response->add_memory_responses();
mem_resp->set_address(mem_req.address());
std::vector<uint8_t> data(mem_req.size());
for (uint32_t i = 0; i < mem_req.size(); ++i) {
data[i] = memory.ReadByte(mem_req.address() + i);
}
mem_resp->set_data(data.data(), data.size());
}
#ifdef YAZE_WITH_GRPC
if (request->include_screenshot()) {
auto screenshot = yaze::test::CaptureHarnessScreenshot();
if (screenshot.ok()) {
// Read the screenshot file and convert to PNG data
std::ifstream file(screenshot->file_path, std::ios::binary);
if (file.good()) {
std::string png_data((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
response->set_screenshot_png(png_data);
}
}
}
#endif
return grpc::Status::OK;
}
grpc::Status EmulatorServiceImpl::ReadMemory(grpc::ServerContext* context, const MemoryRequest* request, MemoryResponse* response) {
if (!emulator_ || !emulator_->is_snes_initialized()) {
return grpc::Status(grpc::StatusCode::UNAVAILABLE, "SNES is not initialized.");
}
auto& memory = emulator_->snes().memory();
response->set_address(request->address());
std::vector<uint8_t> data(request->size());
for (uint32_t i = 0; i < request->size(); ++i) {
data[i] = memory.ReadByte(request->address() + i);
}
response->set_data(data.data(), data.size());
return grpc::Status::OK;
}
grpc::Status EmulatorServiceImpl::WriteMemory(grpc::ServerContext* context, const MemoryWriteRequest* request, CommandResponse* response) {
if (!emulator_ || !emulator_->is_snes_initialized()) {
return grpc::Status(grpc::StatusCode::UNAVAILABLE, "SNES is not initialized.");
}
auto& memory = emulator_->snes().memory();
const std::string& data = request->data();
for (uint32_t i = 0; i < data.size(); ++i) {
memory.WriteByte(request->address() + i, static_cast<uint8_t>(data[i]));
}
response->set_success(true);
response->set_message(absl::StrFormat("Wrote %d bytes to 0x%X.", data.size(), request->address()));
return grpc::Status::OK;
}
} // namespace yaze::agent

View File

@@ -0,0 +1,38 @@
#pragma once
#include <grpcpp/grpcpp.h>
#include "protos/emulator_service.grpc.pb.h"
// Forward declaration to avoid circular dependencies
namespace yaze::emu {
class Emulator;
}
namespace yaze::agent {
class EmulatorServiceImpl final : public EmulatorService::Service {
public:
explicit EmulatorServiceImpl(yaze::emu::Emulator* emulator);
// --- Lifecycle ---
grpc::Status Start(grpc::ServerContext* context, const Empty* request, CommandResponse* response) override;
grpc::Status Stop(grpc::ServerContext* context, const Empty* request, CommandResponse* response) override;
grpc::Status Pause(grpc::ServerContext* context, const Empty* request, CommandResponse* response) override;
grpc::Status Resume(grpc::ServerContext* context, const Empty* request, CommandResponse* response) override;
grpc::Status Reset(grpc::ServerContext* context, const Empty* request, CommandResponse* response) override;
// --- Input Control ---
grpc::Status PressButtons(grpc::ServerContext* context, const ButtonRequest* request, CommandResponse* response) override;
grpc::Status ReleaseButtons(grpc::ServerContext* context, const ButtonRequest* request, CommandResponse* response) override;
grpc::Status HoldButtons(grpc::ServerContext* context, const ButtonHoldRequest* request, CommandResponse* response) override;
// --- State Inspection ---
grpc::Status GetGameState(grpc::ServerContext* context, const GameStateRequest* request, GameStateResponse* response) override;
grpc::Status ReadMemory(grpc::ServerContext* context, const MemoryRequest* request, MemoryResponse* response) override;
grpc::Status WriteMemory(grpc::ServerContext* context, const MemoryWriteRequest* request, CommandResponse* response) override;
private:
yaze::emu::Emulator* emulator_; // Non-owning pointer to the emulator instance
};
} // namespace yaze::agent

View File

@@ -0,0 +1,366 @@
syntax = "proto3";
package yaze.agent;
// The main service for controlling the yaze emulator
service EmulatorService {
// --- Lifecycle ---
rpc Start(Empty) returns (CommandResponse);
rpc Stop(Empty) returns (CommandResponse);
rpc Pause(Empty) returns (CommandResponse);
rpc Resume(Empty) returns (CommandResponse);
rpc Reset(Empty) returns (CommandResponse);
// --- Input Control ---
rpc PressButtons(ButtonRequest) returns (CommandResponse);
rpc ReleaseButtons(ButtonRequest) returns (CommandResponse);
rpc HoldButtons(ButtonHoldRequest) returns (CommandResponse);
// --- State Inspection (The Feedback Loop) ---
rpc GetGameState(GameStateRequest) returns (GameStateResponse);
rpc ReadMemory(MemoryRequest) returns (MemoryResponse);
rpc WriteMemory(MemoryWriteRequest) returns (CommandResponse);
// --- Advanced Debugging (NEW) ---
// Breakpoints
rpc AddBreakpoint(BreakpointRequest) returns (BreakpointResponse);
rpc RemoveBreakpoint(BreakpointIdRequest) returns (CommandResponse);
rpc ListBreakpoints(Empty) returns (BreakpointListResponse);
rpc SetBreakpointEnabled(BreakpointStateRequest) returns (CommandResponse);
// Watchpoints (memory access tracking)
rpc AddWatchpoint(WatchpointRequest) returns (WatchpointResponse);
rpc RemoveWatchpoint(WatchpointIdRequest) returns (CommandResponse);
rpc ListWatchpoints(Empty) returns (WatchpointListResponse);
rpc GetWatchpointHistory(WatchpointHistoryRequest) returns (WatchpointHistoryResponse);
// Execution Control
rpc StepInstruction(Empty) returns (StepResponse);
rpc RunToBreakpoint(Empty) returns (BreakpointHitResponse);
rpc StepOver(Empty) returns (StepResponse); // Step over subroutines
rpc StepOut(Empty) returns (StepResponse); // Step out of subroutine
// Disassembly & Code Analysis
rpc GetDisassembly(DisassemblyRequest) returns (DisassemblyResponse);
rpc GetExecutionTrace(TraceRequest) returns (TraceResponse);
// Symbol Management (for Oracle of Secrets labels)
rpc LoadSymbols(SymbolFileRequest) returns (CommandResponse);
rpc ResolveSymbol(SymbolLookupRequest) returns (SymbolLookupResponse);
rpc GetSymbolAt(AddressRequest) returns (SymbolLookupResponse);
// Debugging Session
rpc CreateDebugSession(DebugSessionRequest) returns (DebugSessionResponse);
rpc GetDebugStatus(Empty) returns (DebugStatusResponse);
}
// --- Message Definitions ---
message Empty {}
message CommandResponse {
bool success = 1;
string message = 2;
}
enum Button {
BUTTON_UNSPECIFIED = 0;
A = 1;
B = 2;
X = 3;
Y = 4;
L = 5;
R = 6;
SELECT = 7;
START = 8;
UP = 9;
DOWN = 10;
LEFT = 11;
RIGHT = 12;
}
message ButtonRequest {
repeated Button buttons = 1;
}
message ButtonHoldRequest {
repeated Button buttons = 1;
uint32 duration_ms = 2; // How long to hold the buttons
}
message GameStateRequest {
bool include_screenshot = 1;
repeated MemoryRequest memory_reads = 2;
}
message GameStateResponse {
// Key player and game variables
uint32 game_mode = 1;
uint32 link_state = 2;
uint32 link_pos_x = 3;
uint32 link_pos_y = 4;
uint32 link_health = 5;
// Screenshot of the current frame
bytes screenshot_png = 6; // PNG encoded image data
// Results of any requested memory reads
repeated MemoryResponse memory_responses = 7;
}
message MemoryRequest {
uint32 address = 1;
uint32 size = 2;
}
message MemoryResponse {
uint32 address = 1;
bytes data = 2;
}
message MemoryWriteRequest {
uint32 address = 1;
bytes data = 2;
}
// --- Advanced Debugging Messages (NEW) ---
// Breakpoint types (matches BreakpointManager::Type)
enum BreakpointType {
BREAKPOINT_TYPE_UNSPECIFIED = 0;
EXECUTE = 1; // Break when PC reaches address
READ = 2; // Break when memory is read
WRITE = 3; // Break when memory is written
ACCESS = 4; // Break on read OR write
CONDITIONAL = 5; // Break when condition is true
}
// CPU type for breakpoints
enum CpuType {
CPU_TYPE_UNSPECIFIED = 0;
CPU_65816 = 1;
SPC700 = 2;
}
// Breakpoint request
message BreakpointRequest {
uint32 address = 1;
BreakpointType type = 2;
CpuType cpu = 3;
string condition = 4; // Optional condition (e.g., "A > 0x10")
string description = 5; // User-friendly label
}
message BreakpointResponse {
bool success = 1;
uint32 breakpoint_id = 2;
string message = 3;
}
message BreakpointIdRequest {
uint32 breakpoint_id = 1;
}
message BreakpointStateRequest {
uint32 breakpoint_id = 1;
bool enabled = 2;
}
message BreakpointInfo {
uint32 id = 1;
uint32 address = 2;
BreakpointType type = 3;
CpuType cpu = 4;
bool enabled = 5;
string condition = 6;
string description = 7;
uint32 hit_count = 8;
}
message BreakpointListResponse {
repeated BreakpointInfo breakpoints = 1;
}
message BreakpointHitResponse {
bool hit = 1;
BreakpointInfo breakpoint = 2;
CPUState cpu_state = 3;
string message = 4;
}
// Watchpoint messages
message WatchpointRequest {
uint32 start_address = 1;
uint32 end_address = 2; // For range watchpoints
bool track_reads = 3;
bool track_writes = 4;
bool break_on_access = 5;
string description = 6;
}
message WatchpointResponse {
bool success = 1;
uint32 watchpoint_id = 2;
string message = 3;
}
message WatchpointIdRequest {
uint32 watchpoint_id = 1;
}
message WatchpointInfo {
uint32 id = 1;
uint32 start_address = 2;
uint32 end_address = 3;
bool track_reads = 4;
bool track_writes = 5;
bool break_on_access = 6;
bool enabled = 7;
string description = 8;
}
message WatchpointListResponse {
repeated WatchpointInfo watchpoints = 1;
}
message WatchpointHistoryRequest {
uint32 watchpoint_id = 1;
uint32 max_entries = 2; // Max history entries to return
}
message AccessLogEntry {
uint32 pc = 1;
uint32 address = 2;
uint32 old_value = 3;
uint32 new_value = 4;
bool is_write = 5;
uint64 cycle_count = 6;
string description = 7;
}
message WatchpointHistoryResponse {
repeated AccessLogEntry history = 1;
}
// CPU State (for stepping/debugging)
message CPUState {
uint32 a = 1;
uint32 x = 2;
uint32 y = 3;
uint32 sp = 4;
uint32 pc = 5;
uint32 db = 6; // Data bank
uint32 pb = 7; // Program bank
uint32 d = 8; // Direct page
uint32 status = 9; // Processor status
bool flag_n = 10; // Negative
bool flag_v = 11; // Overflow
bool flag_d = 12; // Decimal
bool flag_i = 13; // Interrupt disable
bool flag_z = 14; // Zero
bool flag_c = 15; // Carry
uint64 cycles = 16;
}
message StepResponse {
bool success = 1;
CPUState cpu_state = 2;
DisassemblyLine instruction = 3;
string message = 4;
}
// Disassembly messages
message DisassemblyRequest {
uint32 start_address = 1;
uint32 count = 2; // Number of instructions
bool include_execution_count = 3;
}
message DisassemblyLine {
uint32 address = 1;
uint32 opcode = 2;
repeated uint32 operands = 3;
string mnemonic = 4;
string operand_str = 5;
uint32 size = 6;
uint64 execution_count = 7;
bool is_breakpoint = 8;
}
message DisassemblyResponse {
repeated DisassemblyLine lines = 1;
}
// Execution trace
message TraceRequest {
uint32 max_entries = 1; // Max trace entries to return
uint32 start_address = 2; // Optional: filter by address range
uint32 end_address = 3;
}
message TraceEntry {
uint32 address = 1;
string instruction = 2;
CPUState cpu_state_before = 3;
uint64 cycle_count = 4;
}
message TraceResponse {
repeated TraceEntry entries = 1;
uint32 total_count = 2;
}
// Symbol management
message SymbolFileRequest {
string filepath = 1; // Path to symbol file (e.g., .sym, .map)
SymbolFormat format = 2;
}
enum SymbolFormat {
SYMBOL_FORMAT_UNSPECIFIED = 0;
ASAR = 1; // Asar assembler format
WLA_DX = 2; // WLA-DX assembler format
CA65 = 3; // ca65 assembler format
MESEN = 4; // Mesen debug symbol format
CUSTOM_JSON = 5; // Custom JSON format
}
message SymbolLookupRequest {
string symbol_name = 1;
}
message AddressRequest {
uint32 address = 1;
}
message SymbolLookupResponse {
bool found = 1;
string symbol_name = 2;
uint32 address = 3;
string type = 4; // "function", "data", "label", etc.
string description = 5;
}
// Debug session
message DebugSessionRequest {
string session_name = 1;
string rom_hash = 2; // For verification
bool enable_all_features = 3;
}
message DebugSessionResponse {
bool success = 1;
string session_id = 2;
string message = 3;
}
message DebugStatusResponse {
bool is_running = 1;
bool is_paused = 2;
CPUState cpu_state = 3;
uint32 active_breakpoints = 4;
uint32 active_watchpoints = 5;
BreakpointInfo last_breakpoint_hit = 6;
double fps = 7;
uint64 cycles = 8;
}