diff --git a/src/app/net/net_library.cmake b/src/app/net/net_library.cmake index 23ceca44..cb927a42 100644 --- a/src/app/net/net_library.cmake +++ b/src/app/net/net_library.cmake @@ -101,7 +101,7 @@ if(YAZE_ENABLE_JSON) # CRITICAL FIX: Disable OpenSSL on Windows to avoid missing header errors # Windows CI doesn't have OpenSSL headers properly configured # WebSocket will work with plain HTTP (no SSL/TLS) on Windows - if(NOT WIN32) + if(NOT WIN32 AND NOT YAZE_PLATFORM_IOS) find_package(OpenSSL QUIET) if(OPENSSL_INCLUDE_DIR) target_include_directories(yaze_net PUBLIC ${OPENSSL_INCLUDE_DIR}) @@ -120,16 +120,16 @@ if(YAZE_ENABLE_JSON) message(STATUS " - WebSocket without SSL/TLS (OpenSSL not found)") endif() else() - message(STATUS " - Windows: WebSocket using plain HTTP (no SSL) - OpenSSL headers not available in CI") + message(STATUS " - OpenSSL disabled on Windows/iOS (plain HTTP only)") endif() else() # When gRPC is enabled, still enable OpenSSL features but use gRPC's OpenSSL # CRITICAL: Skip on Windows - gRPC's OpenSSL headers aren't accessible in Windows CI - if(NOT WIN32) + if(NOT WIN32 AND NOT YAZE_PLATFORM_IOS) target_compile_definitions(yaze_net PUBLIC CPPHTTPLIB_OPENSSL_SUPPORT) message(STATUS " - WebSocket with SSL/TLS support enabled via gRPC's OpenSSL") else() - message(STATUS " - Windows + gRPC: WebSocket using plain HTTP (no SSL) - OpenSSL headers not available") + message(STATUS " - Windows/iOS + gRPC: WebSocket using plain HTTP (no SSL)") endif() endif() diff --git a/src/app/net/websocket_client.cc b/src/app/net/websocket_client.cc index a6539df7..0c13396e 100644 --- a/src/app/net/websocket_client.cc +++ b/src/app/net/websocket_client.cc @@ -10,7 +10,7 @@ // Cross-platform WebSocket support using httplib // Skip httplib in WASM builds - use Emscripten WebSocket API instead #if defined(YAZE_WITH_JSON) && !defined(__EMSCRIPTEN__) -#ifndef _WIN32 +#if !defined(_WIN32) && !defined(YAZE_IOS) #define CPPHTTPLIB_OPENSSL_SUPPORT #endif #include "httplib.h" diff --git a/src/app/service/grpc_support.cmake b/src/app/service/grpc_support.cmake index edbfde33..777638a4 100644 --- a/src/app/service/grpc_support.cmake +++ b/src/app/service/grpc_support.cmake @@ -47,11 +47,6 @@ target_include_directories(yaze_grpc_support PUBLIC target_link_libraries(yaze_grpc_support PUBLIC yaze_util yaze_common - yaze_zelda3 - yaze_gfx - yaze_gui - yaze_emulator - yaze_net ${ABSL_TARGETS} ${YAZE_SDL2_TARGETS} ) @@ -114,8 +109,10 @@ set_target_properties(yaze_grpc_support PROPERTIES # Platform-specific compile definitions if(UNIX AND NOT APPLE) target_compile_definitions(yaze_grpc_support PRIVATE linux stricmp=strcasecmp) -elseif(APPLE) +elseif(YAZE_PLATFORM_MACOS) target_compile_definitions(yaze_grpc_support PRIVATE MACOS) +elseif(YAZE_PLATFORM_IOS) + target_compile_definitions(yaze_grpc_support PRIVATE YAZE_IOS) elseif(WIN32) target_compile_definitions(yaze_grpc_support PRIVATE WINDOWS) endif() diff --git a/src/app/service/rom_service_impl.cc b/src/app/service/rom_service_impl.cc index e91f740c..e8e0af5c 100644 --- a/src/app/service/rom_service_impl.cc +++ b/src/app/service/rom_service_impl.cc @@ -13,9 +13,9 @@ namespace yaze { namespace net { -RomServiceImpl::RomServiceImpl(Rom* rom, RomVersionManager* version_manager, +RomServiceImpl::RomServiceImpl(RomGetter rom_getter, RomVersionManager* version_manager, ProposalApprovalManager* approval_manager) - : rom_(rom), + : rom_getter_(rom_getter), version_mgr_(version_manager), approval_mgr_(approval_manager) {} @@ -26,7 +26,8 @@ void RomServiceImpl::SetConfig(const Config& config) { grpc::Status RomServiceImpl::ReadBytes(grpc::ServerContext* context, const rom_svc::ReadBytesRequest* request, rom_svc::ReadBytesResponse* response) { - if (!rom_ || !rom_->is_loaded()) { + Rom* rom = rom_getter_(); + if (!rom || !rom->is_loaded()) { return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION, "ROM not loaded"); } @@ -35,23 +36,24 @@ grpc::Status RomServiceImpl::ReadBytes(grpc::ServerContext* context, uint32_t length = request->length(); // Validate range - if (offset + length > rom_->size()) { + if (offset + length > rom->size()) { return grpc::Status(grpc::StatusCode::OUT_OF_RANGE, absl::StrFormat("Read beyond ROM: 0x%X+%d > %d", - offset, length, rom_->size())); + offset, length, rom->size())); } // Read data - const auto* data = rom_->data() + offset; + const auto* data = rom->data() + offset; response->set_data(data, length); return grpc::Status::OK; } -grpc::Status RomServiceImpl::WriteBytes( - grpc::ServerContext* context, const rom_svc::WriteBytesRequest* request, - rom_svc::WriteBytesResponse* response) { - if (!rom_ || !rom_->is_loaded()) { +grpc::Status RomServiceImpl::WriteBytes(grpc::ServerContext* context, + const rom_svc::WriteBytesRequest* request, + rom_svc::WriteBytesResponse* response) { + Rom* rom = rom_getter_(); + if (!rom || !rom->is_loaded()) { return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION, "ROM not loaded"); } @@ -60,122 +62,78 @@ grpc::Status RomServiceImpl::WriteBytes( const std::string& data = request->data(); // Validate range - if (offset + data.size() > rom_->size()) { + if (offset + data.size() > rom->size()) { return grpc::Status(grpc::StatusCode::OUT_OF_RANGE, absl::StrFormat("Write beyond ROM: 0x%X+%zu > %d", - offset, data.size(), rom_->size())); + offset, data.size(), rom->size())); } - // Check if approval required - if (config_.require_approval_for_writes && approval_mgr_) { - // Create a proposal for this write - std::string proposal_id = - absl::StrFormat("write_0x%X_%zu_bytes", offset, data.size()); - - // Check if proposal is approved - if (!approval_mgr_->IsProposalApproved(proposal_id)) { - response->set_success(false); - response->set_error("Write requires approval"); - response->set_proposal_id(proposal_id); - return grpc::Status::OK; // Not an error, just needs approval - } + if (config_.require_approval_for_writes) { + return grpc::Status(grpc::StatusCode::PERMISSION_DENIED, + "Direct ROM writes disabled; use proposal system"); } - // Create snapshot before write - if (version_mgr_) { - std::string snapshot_desc = absl::StrFormat( - "Before write to 0x%X (%zu bytes)", offset, data.size()); - // Creator is "system" for now, could be passed in context - version_mgr_->CreateSnapshot(snapshot_desc, "system"); + // Create auto-snapshot if enabled + auto status = MaybeCreateSnapshot(absl::StrFormat( + "Auto-snapshot before write at 0x%X (%zu bytes)", offset, data.size())); + if (!status.ok()) { + return grpc::Status(grpc::StatusCode::INTERNAL, + "Failed to create safety snapshot: " + + std::string(status.message())); } - // Perform write - std::memcpy(rom_->mutable_data() + offset, data.data(), data.size()); - + // Perform the write + std::memcpy(rom->mutable_data() + offset, data.data(), data.size()); response->set_success(true); return grpc::Status::OK; } -grpc::Status RomServiceImpl::GetRomInfo( - grpc::ServerContext* context, const rom_svc::GetRomInfoRequest* request, - rom_svc::GetRomInfoResponse* response) { - if (!rom_ || !rom_->is_loaded()) { - return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION, - "ROM not loaded"); +grpc::Status RomServiceImpl::GetRomInfo(grpc::ServerContext* context, + const rom_svc::GetRomInfoRequest* request, + rom_svc::GetRomInfoResponse* response) { + Rom* rom = rom_getter_(); + if (!rom || !rom->is_loaded()) { + response->set_title("ROM not loaded"); + return grpc::Status::OK; } - response->set_title(rom_->title()); - response->set_size(rom_->size()); - // response->set_is_loaded(rom_->is_loaded()); // Not in proto - // response->set_filename(rom_->filename()); // Not in proto - // Proto has: title, size, checksum, is_expanded, version + response->set_title(rom->title()); + response->set_size(rom->size()); + // Removed checksum, is_expanded, version as they don't exist in yaze::Rom return grpc::Status::OK; } -grpc::Status RomServiceImpl::ReadOverworldMap( - grpc::ServerContext* context, const rom_svc::ReadOverworldMapRequest* request, - rom_svc::ReadOverworldMapResponse* response) { - return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Not implemented"); +// ... Stubs for other methods to keep it building ... +grpc::Status RomServiceImpl::ReadOverworldMap(grpc::ServerContext* context, const rom_svc::ReadOverworldMapRequest* request, rom_svc::ReadOverworldMapResponse* response) { return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Not implemented"); } +grpc::Status RomServiceImpl::WriteOverworldTile(grpc::ServerContext* context, const rom_svc::WriteOverworldTileRequest* request, rom_svc::WriteOverworldTileResponse* response) { return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Not implemented"); } +grpc::Status RomServiceImpl::ReadDungeonRoom(grpc::ServerContext* context, const rom_svc::ReadDungeonRoomRequest* request, rom_svc::ReadDungeonRoomResponse* response) { return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Not implemented"); } +grpc::Status RomServiceImpl::WriteDungeonTile(grpc::ServerContext* context, const rom_svc::WriteDungeonTileRequest* request, rom_svc::WriteDungeonTileResponse* response) { return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Not implemented"); } +grpc::Status RomServiceImpl::ReadSprite(grpc::ServerContext* context, const rom_svc::ReadSpriteRequest* request, rom_svc::ReadSpriteResponse* response) { return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Not implemented"); } +grpc::Status RomServiceImpl::SubmitRomProposal(grpc::ServerContext* context, const rom_svc::SubmitRomProposalRequest* request, rom_svc::SubmitRomProposalResponse* response) { return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Not implemented"); } +grpc::Status RomServiceImpl::GetProposalStatus(grpc::ServerContext* context, const rom_svc::GetProposalStatusRequest* request, rom_svc::GetProposalStatusResponse* response) { return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Not implemented"); } +grpc::Status RomServiceImpl::CreateSnapshot(grpc::ServerContext* context, const rom_svc::CreateSnapshotRequest* request, rom_svc::CreateSnapshotResponse* response) { return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Not implemented"); } +grpc::Status RomServiceImpl::RestoreSnapshot(grpc::ServerContext* context, const rom_svc::RestoreSnapshotRequest* request, rom_svc::RestoreSnapshotResponse* response) { return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Not implemented"); } +grpc::Status RomServiceImpl::ListSnapshots(grpc::ServerContext* context, const rom_svc::ListSnapshotsRequest* request, rom_svc::ListSnapshotsResponse* response) { return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Not implemented"); } + +grpc::Status RomServiceImpl::ValidateRomLoaded() { + Rom* rom = rom_getter_(); + if (!rom || !rom->is_loaded()) { + return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION, "ROM not loaded"); + } + return grpc::Status::OK; } -grpc::Status RomServiceImpl::ReadDungeonRoom( - grpc::ServerContext* context, const rom_svc::ReadDungeonRoomRequest* request, - rom_svc::ReadDungeonRoomResponse* response) { - return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Not implemented"); -} - -grpc::Status RomServiceImpl::ReadSprite( - grpc::ServerContext* context, const rom_svc::ReadSpriteRequest* request, - rom_svc::ReadSpriteResponse* response) { - return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Not implemented"); -} - -grpc::Status RomServiceImpl::WriteOverworldTile( - grpc::ServerContext* context, const rom_svc::WriteOverworldTileRequest* request, - rom_svc::WriteOverworldTileResponse* response) { - return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Not implemented"); -} - -grpc::Status RomServiceImpl::WriteDungeonTile( - grpc::ServerContext* context, const rom_svc::WriteDungeonTileRequest* request, - rom_svc::WriteDungeonTileResponse* response) { - return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Not implemented"); -} - -grpc::Status RomServiceImpl::SubmitRomProposal( - grpc::ServerContext* context, const rom_svc::SubmitRomProposalRequest* request, - rom_svc::SubmitRomProposalResponse* response) { - return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Not implemented"); -} - -grpc::Status RomServiceImpl::GetProposalStatus( - grpc::ServerContext* context, const rom_svc::GetProposalStatusRequest* request, - rom_svc::GetProposalStatusResponse* response) { - return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Not implemented"); -} - -grpc::Status RomServiceImpl::CreateSnapshot( - grpc::ServerContext* context, const rom_svc::CreateSnapshotRequest* request, - rom_svc::CreateSnapshotResponse* response) { - return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Not implemented"); -} - -grpc::Status RomServiceImpl::RestoreSnapshot( - grpc::ServerContext* context, const rom_svc::RestoreSnapshotRequest* request, - rom_svc::RestoreSnapshotResponse* response) { - return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Not implemented"); -} - -grpc::Status RomServiceImpl::ListSnapshots( - grpc::ServerContext* context, const rom_svc::ListSnapshotsRequest* request, - rom_svc::ListSnapshotsResponse* response) { - return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, "Not implemented"); +absl::Status RomServiceImpl::MaybeCreateSnapshot(const std::string& description) { + if (!config_.enable_version_management || !version_mgr_) { + return absl::OkStatus(); + } + return version_mgr_->CreateSnapshot(description, "gRPC", false).status(); } } // namespace net } // namespace yaze -#endif // YAZE_WITH_GRPC \ No newline at end of file +#endif // YAZE_WITH_GRPC diff --git a/src/app/service/rom_service_impl.h b/src/app/service/rom_service_impl.h index a259469b..e951adec 100644 --- a/src/app/service/rom_service_impl.h +++ b/src/app/service/rom_service_impl.h @@ -24,6 +24,7 @@ // Note: Proto files will be generated to build directory #endif +#include #include "app/net/rom_version_manager.h" #include "rom/rom.h" @@ -35,34 +36,25 @@ namespace net { /** * @brief gRPC service implementation for remote ROM manipulation - * - * Enables remote clients (like z3ed CLI) to: - * - Read/write ROM data - * - Submit proposals for collaborative editing - * - Manage ROM versions and snapshots - * - Query ROM structures (overworld, dungeons, sprites) - * - * Thread-safe and designed for concurrent access. */ class RomServiceImpl final : public proto::RomService::Service { public: - /** - * @brief Configuration for the ROM service - */ + using RomGetter = std::function; + struct Config { - bool require_approval_for_writes = true; // Submit writes as proposals - bool enable_version_management = true; // Auto-snapshot before changes - int max_read_size_bytes = 1024 * 1024; // 1MB max per read - bool allow_raw_rom_access = true; // Allow direct byte access + bool require_approval_for_writes = true; + bool enable_version_management = true; + int max_read_size_bytes = 1024 * 1024; + bool allow_raw_rom_access = true; }; /** * @brief Construct ROM service - * @param rom Pointer to ROM instance (not owned) + * @param rom_getter Function to retrieve the active ROM instance * @param version_mgr Pointer to version manager (not owned, optional) * @param approval_mgr Pointer to approval manager (not owned, optional) */ - RomServiceImpl(Rom* rom, RomVersionManager* version_mgr = nullptr, + RomServiceImpl(RomGetter rom_getter, RomVersionManager* version_mgr = nullptr, ProposalApprovalManager* approval_mgr = nullptr); ~RomServiceImpl() override = default; @@ -155,7 +147,7 @@ class RomServiceImpl final : public proto::RomService::Service { private: Config config_; - Rom* rom_; // Not owned + RomGetter rom_getter_; RomVersionManager* version_mgr_; // Not owned, may be null ProposalApprovalManager* approval_mgr_; // Not owned, may be null diff --git a/src/app/service/unified_grpc_server.cc b/src/app/service/unified_grpc_server.cc index 19eabcda..3cd3beae 100644 --- a/src/app/service/unified_grpc_server.cc +++ b/src/app/service/unified_grpc_server.cc @@ -14,17 +14,19 @@ #include "app/service/imgui_test_harness_service.h" #include "protos/canvas_automation.grpc.pb.h" +#include "app/editor/editor_manager.h" + namespace yaze { YazeGRPCServer::YazeGRPCServer() : is_running_(false) {} -// Destructor defined here so CanvasAutomationServiceGrpc is a complete type YazeGRPCServer::~YazeGRPCServer() { Shutdown(); } absl::Status YazeGRPCServer::Initialize( - int port, test::TestManager* test_manager, Rom* rom, + int port, test::TestManager* test_manager, + RomGetter rom_getter, net::RomVersionManager* version_mgr, net::ProposalApprovalManager* approval_mgr, CanvasAutomationServiceImpl* canvas_service) { @@ -39,14 +41,12 @@ absl::Status YazeGRPCServer::Initialize( test_harness_service_ = std::make_unique(test_manager); std::cout << "✓ ImGuiTestHarness service initialized\n"; - } else if (config_.enable_test_harness) { - std::cout << "⚠ ImGuiTestHarness requested but no TestManager provided\n"; } - // Create ROM service if rom provided - if (config_.enable_rom_service && rom) { + // Create ROM service if rom_getter provided + if (config_.enable_rom_service && rom_getter) { rom_service_ = - std::make_unique(rom, version_mgr, approval_mgr); + std::make_unique(rom_getter, version_mgr, approval_mgr); // Configure ROM service net::RomServiceImpl::Config rom_config; @@ -115,6 +115,19 @@ absl::Status YazeGRPCServer::StartAsync() { return absl::OkStatus(); } +absl::Status YazeGRPCServer::AddService( + std::unique_ptr service) { + if (!service) { + return absl::InvalidArgumentError("Service is null"); + } + if (is_running_) { + return absl::FailedPreconditionError( + "Cannot add services after the server has started"); + } + extra_services_.push_back(std::move(service)); + return absl::OkStatus(); +} + void YazeGRPCServer::Shutdown() { if (server_ && is_running_) { std::cout << "⏹ Shutting down unified gRPC server...\n"; @@ -163,6 +176,10 @@ absl::Status YazeGRPCServer::BuildServer() { builder.RegisterService(canvas_grpc_service_.get()); } + for (auto& service : extra_services_) { + builder.RegisterService(service.get()); + } + // Build and start server_ = builder.BuildAndStart(); diff --git a/src/app/service/unified_grpc_server.h b/src/app/service/unified_grpc_server.h index a68223bc..4d7af743 100644 --- a/src/app/service/unified_grpc_server.h +++ b/src/app/service/unified_grpc_server.h @@ -4,6 +4,8 @@ #ifdef YAZE_WITH_GRPC #include +#include +#include #include "absl/status/status.h" #include "app/net/rom_version_manager.h" @@ -19,8 +21,12 @@ namespace yaze { // Forward declarations class CanvasAutomationServiceImpl; - class Rom; + +namespace editor { +class EditorManager; +} + namespace net { class ProposalApprovalManager; class RomServiceImpl; @@ -34,31 +40,11 @@ class ImGuiTestHarnessServiceImpl; /** * @class YazeGRPCServer * @brief YAZE's unified gRPC server for Zelda3 editor automation - * - * This server combines multiple automation services for the Zelda editor: - * 1. ImGuiTestHarness - GUI test automation (widget discovery, screenshots, - * etc.) - * 2. RomService - ROM manipulation (read/write, proposals, version management) - * 3. CanvasAutomation - Canvas operations (tiles, selection, zoom, pan) - * - * All services share the same gRPC server instance and port, allowing - * clients (CLI, AI agents, remote scripts) to interact with GUI, ROM data, - * and canvas operations simultaneously. - * - * Example usage: - * ```cpp - * YazeGRPCServer server; - * server.Initialize(50051, test_manager, rom, version_mgr, approval_mgr, - * canvas_service); server.Start(); - * // ... do work ... - * server.Shutdown(); - * ``` */ class YazeGRPCServer { public: - /** - * @brief Configuration for the unified server - */ + using RomGetter = std::function; + struct Config { int port = 50051; bool enable_test_harness = true; @@ -68,56 +54,31 @@ class YazeGRPCServer { }; YazeGRPCServer(); - // Destructor must be defined in .cc file to allow deletion of incomplete - // types ~YazeGRPCServer(); /** * @brief Initialize the server with all required services - * @param port Port to listen on (default 50051) - * @param test_manager TestManager for GUI automation (optional) - * @param rom ROM instance for ROM service (optional) - * @param version_mgr Version manager for ROM snapshots (optional) - * @param approval_mgr Approval manager for proposals (optional) - * @param canvas_service Canvas automation service implementation (optional) + * @param port Port to listen on + * @param test_manager TestManager for GUI automation + * @param rom_getter Function to retrieve active ROM + * @param version_mgr Version manager for ROM snapshots + * @param approval_mgr Approval manager for proposals + * @param canvas_service Canvas automation service implementation * @return OK status if initialized successfully */ absl::Status Initialize( - int port, test::TestManager* test_manager = nullptr, Rom* rom = nullptr, + int port, test::TestManager* test_manager = nullptr, + RomGetter rom_getter = nullptr, net::RomVersionManager* version_mgr = nullptr, net::ProposalApprovalManager* approval_mgr = nullptr, CanvasAutomationServiceImpl* canvas_service = nullptr); - /** - * @brief Start the gRPC server (blocking) - * Starts the server and blocks until Shutdown() is called - */ absl::Status Start(); - - /** - * @brief Start the server in a background thread (non-blocking) - * Returns immediately after starting the server - */ absl::Status StartAsync(); - - /** - * @brief Shutdown the server gracefully - */ + absl::Status AddService(std::unique_ptr service); void Shutdown(); - - /** - * @brief Check if server is currently running - */ bool IsRunning() const; - - /** - * @brief Get the port the server is listening on - */ int Port() const { return config_.port; } - - /** - * @brief Update configuration (must be called before Start) - */ void SetConfig(const Config& config) { config_ = config; } private: @@ -126,12 +87,11 @@ class YazeGRPCServer { std::unique_ptr test_harness_service_; std::unique_ptr rom_service_; CanvasAutomationServiceImpl* canvas_service_ = nullptr; - // Store as base grpc::Service* to avoid incomplete type issues std::unique_ptr canvas_grpc_service_; std::unique_ptr test_harness_grpc_wrapper_; + std::vector> extra_services_; bool is_running_; - // Build the gRPC server with all services absl::Status BuildServer(); }; @@ -139,10 +99,3 @@ class YazeGRPCServer { #endif // YAZE_WITH_GRPC #endif // YAZE_APP_CORE_SERVICE_UNIFIED_GRPC_SERVER_H_ - -// Backwards compatibility alias -#ifdef YAZE_WITH_GRPC -namespace yaze { -using UnifiedGRPCServer = YazeGRPCServer; -} -#endif diff --git a/src/cli/agent.cmake b/src/cli/agent.cmake index 7feec7f1..225b5bda 100644 --- a/src/cli/agent.cmake +++ b/src/cli/agent.cmake @@ -238,7 +238,7 @@ if(YAZE_ENABLE_REMOTE_AUTOMATION) list(APPEND YAZE_AGENT_SOURCES cli/service/agent/agent_control_server.cc cli/service/agent/emulator_service_impl.cc - cli/handlers/tools/emulator_commands.cc + cli/service/agent/rom_debug_agent.cc cli/service/gui/gui_automation_client.cc cli/service/gui/canvas_automation_client.cc ) diff --git a/src/cli/service/agent/agent_control_server.cc b/src/cli/service/agent/agent_control_server.cc index b9f42def..b0f69f87 100644 --- a/src/cli/service/agent/agent_control_server.cc +++ b/src/cli/service/agent/agent_control_server.cc @@ -31,7 +31,7 @@ void AgentControlServer::Stop() { void AgentControlServer::Run() { std::string server_address("0.0.0.0:50051"); - EmulatorServiceImpl service(emulator_); + yaze::net::EmulatorServiceImpl service(emulator_, nullptr); grpc::ServerBuilder builder; builder.AddListeningPort(server_address, grpc::InsecureServerCredentials()); diff --git a/src/cli/service/agent/emulator_service_impl.cc b/src/cli/service/agent/emulator_service_impl.cc index 9ec7808d..6ec72c26 100644 --- a/src/cli/service/agent/emulator_service_impl.cc +++ b/src/cli/service/agent/emulator_service_impl.cc @@ -1,192 +1,239 @@ #include "cli/service/agent/emulator_service_impl.h" -#include -#include +#include +#include #include +#include +#include #include -#include "absl/strings/escaping.h" -#include "absl/strings/str_format.h" -#include "app/emu/debug/breakpoint_manager.h" +#include "emu/emulator.h" +#include "rom/rom.h" #include "app/emu/debug/disassembler.h" -#include "app/emu/debug/disassembly_viewer.h" -#include "app/emu/debug/step_controller.h" -#include "app/emu/debug/watchpoint_manager.h" -#include "app/emu/emulator.h" -#include "app/emu/input/input_backend.h" // Required for SnesButton enum #include "app/service/screenshot_utils.h" -namespace yaze::agent { +namespace yaze::net { namespace { -// Helper to convert our gRPC Button enum to the emulator's SnesButton enum -emu::input::SnesButton ToSnesButton(Button button) { +emu::input::SnesButton ToSnesButton(agent::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 + case agent::A: return SnesButton::A; + case agent::B: return SnesButton::B; + case agent::X: return SnesButton::X; + case agent::Y: return SnesButton::Y; + case agent::L: return SnesButton::L; + case agent::R: return SnesButton::R; + case agent::SELECT: return SnesButton::SELECT; + case agent::START: return SnesButton::START; + case agent::UP: return SnesButton::UP; + case agent::DOWN: return SnesButton::DOWN; + case agent::LEFT: return SnesButton::LEFT; + case agent::RIGHT: return SnesButton::RIGHT; + default: return SnesButton::B; + } +} + +emu::BreakpointManager::Type ToBreakpointType(agent::BreakpointType proto_type) { + using emu::BreakpointManager; + switch (proto_type) { + case agent::EXECUTE: return BreakpointManager::Type::EXECUTE; + case agent::READ: return BreakpointManager::Type::READ; + case agent::WRITE: return BreakpointManager::Type::WRITE; + case agent::ACCESS: return BreakpointManager::Type::ACCESS; + case agent::CONDITIONAL: return BreakpointManager::Type::CONDITIONAL; + default: return BreakpointManager::Type::EXECUTE; + } +} + +emu::BreakpointManager::CpuType ToCpuType(agent::CpuType proto_cpu) { + using emu::BreakpointManager; + switch (proto_cpu) { + case agent::CPU_65816: return BreakpointManager::CpuType::CPU_65816; + case agent::SPC700: return BreakpointManager::CpuType::SPC700; + default: return BreakpointManager::CpuType::CPU_65816; + } +} + +agent::BreakpointType ToProtoBreakpointType(emu::BreakpointManager::Type type) { + using emu::BreakpointManager; + switch (type) { + case BreakpointManager::Type::EXECUTE: return agent::EXECUTE; + case BreakpointManager::Type::READ: return agent::READ; + case BreakpointManager::Type::WRITE: return agent::WRITE; + case BreakpointManager::Type::ACCESS: return agent::ACCESS; + case BreakpointManager::Type::CONDITIONAL: return agent::CONDITIONAL; + default: return agent::EXECUTE; + } +} + +agent::CpuType ToProtoCpuType(emu::BreakpointManager::CpuType cpu) { + using emu::BreakpointManager; + switch (cpu) { + case BreakpointManager::CpuType::CPU_65816: return agent::CPU_65816; + case BreakpointManager::CpuType::SPC700: return agent::SPC700; + default: return agent::CPU_65816; } } } // namespace -EmulatorServiceImpl::EmulatorServiceImpl(yaze::emu::Emulator* emulator) - : emulator_(emulator) {} +EmulatorServiceImpl::EmulatorServiceImpl(yaze::emu::Emulator* emulator, RomGetter rom_getter) + : emulator_(emulator), rom_getter_(rom_getter) {} -// --- Lifecycle --- +void EmulatorServiceImpl::CaptureCPUState(agent::CPUState* state) { + auto& cpu = emulator_->snes().cpu(); + state->set_a(cpu.A); + state->set_x(cpu.X); + state->set_y(cpu.Y); + state->set_pc(cpu.PC); + state->set_pb(cpu.PB); + state->set_db(cpu.DB); + state->set_sp(cpu.SP()); + state->set_d(cpu.D); + state->set_status(cpu.status); + state->set_flag_n(cpu.GetNegativeFlag()); + state->set_flag_v(cpu.GetOverflowFlag()); + state->set_flag_z(cpu.GetZeroFlag()); + state->set_flag_c(cpu.GetCarryFlag()); + state->set_cycles(emulator_->GetCurrentCycle()); +} -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); +// --- Core Lifecycle & Control --- + +grpc::Status EmulatorServiceImpl::ControlEmulator(grpc::ServerContext* context, + const agent::ControlRequest* request, + agent::CommandResponse* response) { + if (!emulator_) return grpc::Status(grpc::StatusCode::UNAVAILABLE, "Emulator not initialized."); + + std::string action = request->action(); + if (action == "start" || action == "resume") { + emulator_->set_running(true); + response->set_message("Emulator started/resumed."); + } else if (action == "stop" || action == "pause") { + emulator_->set_running(false); + response->set_message("Emulator stopped/paused."); + } else if (action == "reset") { + emulator_->snes().Reset(true); + response->set_message("Emulator reset."); + } else if (action == "init" || action == "initialize") { + Rom* rom = rom_getter_ ? rom_getter_() : nullptr; + if (rom && rom->is_loaded()) { + emulator_->EnsureInitialized(rom); + response->set_message("Emulator initialized with active ROM."); + } else { + return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION, "ROM not loaded in core."); + } + } else { + return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, "Unknown action: " + action); + } + 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); +grpc::Status EmulatorServiceImpl::StepEmulator(grpc::ServerContext* context, + const agent::StepControlRequest* request, + agent::StepResponse* response) { + if (!emulator_ || !emulator_->is_snes_initialized()) { + return grpc::Status(grpc::StatusCode::UNAVAILABLE, "SNES is not initialized."); + } + + std::string mode = request->mode(); + if (mode == "instruction") { + emulator_->StepSingleInstruction(); + response->set_message("Stepped 1 instruction."); + } else if (mode == "over") { + InitializeStepController(); + auto result = step_controller_.StepOver(); + response->set_message(result.message); + } else if (mode == "out") { + InitializeStepController(); + auto result = step_controller_.StepOut(); + response->set_message(result.message); + } else { + return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, "Unknown step mode: " + mode); + } + + CaptureCPUState(response->mutable_cpu_state()); 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."); +grpc::Status EmulatorServiceImpl::RunToBreakpoint(grpc::ServerContext* context, + const agent::Empty* request, + agent::BreakpointHitResponse* response) { + if (!emulator_ || !emulator_->is_snes_initialized()) { + return grpc::Status(grpc::StatusCode::UNAVAILABLE, "SNES is not initialized."); + } + + const int kMaxInstructions = 1000000; + int instruction_count = 0; + auto& bp_manager = emulator_->breakpoint_manager(); + auto& cpu = emulator_->snes().cpu(); + + while (instruction_count++ < kMaxInstructions) { + uint32_t pc = (cpu.PB << 16) | cpu.PC; + if (bp_manager.ShouldBreakOnExecute(pc, emu::BreakpointManager::CpuType::CPU_65816)) { + response->set_hit(true); + auto* last_hit = bp_manager.GetLastHit(); + if (last_hit) { + auto* bp_info = response->mutable_breakpoint(); + bp_info->set_id(last_hit->id); + bp_info->set_address(last_hit->address); + bp_info->set_type(ToProtoBreakpointType(last_hit->type)); + bp_info->set_cpu(ToProtoCpuType(last_hit->cpu)); + bp_info->set_enabled(last_hit->enabled); + } + CaptureCPUState(response->mutable_cpu_state()); + return grpc::Status::OK; + } + emulator_->StepSingleInstruction(); + } + response->set_hit(false); 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 --- +// --- Input & State --- grpc::Status EmulatorServiceImpl::PressButtons(grpc::ServerContext* context, - const ButtonRequest* request, - CommandResponse* response) { - if (!emulator_) - return grpc::Status(grpc::StatusCode::UNAVAILABLE, - "Emulator not initialized."); + const agent::ButtonRequest* request, + agent::CommandResponse* response) { + if (!emulator_) return grpc::Status(grpc::StatusCode::UNAVAILABLE, "Emulator not initialized."); auto& input_manager = emulator_->input_manager(); for (int i = 0; i < request->buttons_size(); i++) { - input_manager.PressButton( - ToSnesButton(static_cast