From 6982d63173c7426d8c1c0732b4bdf7467fec7bfa Mon Sep 17 00:00:00 2001 From: scawful Date: Sun, 5 Oct 2025 23:59:38 -0400 Subject: [PATCH] refactor: Implement gRPC Wrapper for Canvas Automation Service - Introduced a gRPC service wrapper for CanvasAutomationServiceImpl, enabling seamless integration of canvas operations with gRPC. - Added a helper function to convert absl::Status to grpc::Status, ensuring consistent error handling across service calls. - Updated UnifiedGRPCServer to initialize and register the Canvas Automation service, enhancing the server's capabilities for handling canvas-related requests. - Refactored service initialization to accommodate the new canvas service, improving modularity and maintainability of the server code. --- .../core/service/canvas_automation_service.cc | 160 +++++++++++++++++- .../core/service/canvas_automation_service.h | 14 ++ src/app/core/service/unified_grpc_server.cc | 45 +++-- src/app/core/service/unified_grpc_server.h | 45 +++-- 4 files changed, 240 insertions(+), 24 deletions(-) diff --git a/src/app/core/service/canvas_automation_service.cc b/src/app/core/service/canvas_automation_service.cc index 5a3fd318..02868bda 100644 --- a/src/app/core/service/canvas_automation_service.cc +++ b/src/app/core/service/canvas_automation_service.cc @@ -2,12 +2,55 @@ #ifdef YAZE_WITH_GRPC -#include "src/protos/canvas_automation.pb.h" +#include +#include "protos/canvas_automation.pb.h" +#include "protos/canvas_automation.grpc.pb.h" #include "app/editor/overworld/overworld_editor.h" #include "app/gui/canvas/canvas_automation_api.h" namespace yaze { +namespace { + +// Helper to convert absl::Status to grpc::Status +grpc::Status ConvertStatus(const absl::Status& status) { + if (status.ok()) { + return grpc::Status::OK; + } + + grpc::StatusCode code; + switch (status.code()) { + case absl::StatusCode::kNotFound: + code = grpc::StatusCode::NOT_FOUND; + break; + case absl::StatusCode::kInvalidArgument: + code = grpc::StatusCode::INVALID_ARGUMENT; + break; + case absl::StatusCode::kFailedPrecondition: + code = grpc::StatusCode::FAILED_PRECONDITION; + break; + case absl::StatusCode::kOutOfRange: + code = grpc::StatusCode::OUT_OF_RANGE; + break; + case absl::StatusCode::kUnimplemented: + code = grpc::StatusCode::UNIMPLEMENTED; + break; + case absl::StatusCode::kInternal: + code = grpc::StatusCode::INTERNAL; + break; + case absl::StatusCode::kUnavailable: + code = grpc::StatusCode::UNAVAILABLE; + break; + default: + code = grpc::StatusCode::UNKNOWN; + break; + } + + return grpc::Status(code, std::string(status.message())); +} + +} // namespace + void CanvasAutomationServiceImpl::RegisterCanvas(const std::string& canvas_id, gui::Canvas* canvas) { canvases_[canvas_id] = canvas; @@ -332,6 +375,121 @@ absl::Status CanvasAutomationServiceImpl::IsTileVisible( return absl::OkStatus(); } +// ============================================================================ +// gRPC Service Wrapper +// ============================================================================ + +/** + * @brief gRPC service wrapper that forwards to CanvasAutomationServiceImpl + * + * This adapter implements the proto-generated Service interface and + * forwards all calls to our implementation, converting between gRPC + * and absl::Status types. + */ +class CanvasAutomationServiceGrpc final : public proto::CanvasAutomation::Service { + public: + explicit CanvasAutomationServiceGrpc(CanvasAutomationServiceImpl* impl) + : impl_(impl) {} + + // Tile Operations + grpc::Status SetTile(grpc::ServerContext* context, + const proto::SetTileRequest* request, + proto::SetTileResponse* response) override { + return ConvertStatus(impl_->SetTile(request, response)); + } + + grpc::Status GetTile(grpc::ServerContext* context, + const proto::GetTileRequest* request, + proto::GetTileResponse* response) override { + return ConvertStatus(impl_->GetTile(request, response)); + } + + grpc::Status SetTiles(grpc::ServerContext* context, + const proto::SetTilesRequest* request, + proto::SetTilesResponse* response) override { + return ConvertStatus(impl_->SetTiles(request, response)); + } + + // Selection Operations + grpc::Status SelectTile(grpc::ServerContext* context, + const proto::SelectTileRequest* request, + proto::SelectTileResponse* response) override { + return ConvertStatus(impl_->SelectTile(request, response)); + } + + grpc::Status SelectTileRect(grpc::ServerContext* context, + const proto::SelectTileRectRequest* request, + proto::SelectTileRectResponse* response) override { + return ConvertStatus(impl_->SelectTileRect(request, response)); + } + + grpc::Status GetSelection(grpc::ServerContext* context, + const proto::GetSelectionRequest* request, + proto::GetSelectionResponse* response) override { + return ConvertStatus(impl_->GetSelection(request, response)); + } + + grpc::Status ClearSelection(grpc::ServerContext* context, + const proto::ClearSelectionRequest* request, + proto::ClearSelectionResponse* response) override { + return ConvertStatus(impl_->ClearSelection(request, response)); + } + + // View Operations + grpc::Status ScrollToTile(grpc::ServerContext* context, + const proto::ScrollToTileRequest* request, + proto::ScrollToTileResponse* response) override { + return ConvertStatus(impl_->ScrollToTile(request, response)); + } + + grpc::Status CenterOn(grpc::ServerContext* context, + const proto::CenterOnRequest* request, + proto::CenterOnResponse* response) override { + return ConvertStatus(impl_->CenterOn(request, response)); + } + + grpc::Status SetZoom(grpc::ServerContext* context, + const proto::SetZoomRequest* request, + proto::SetZoomResponse* response) override { + return ConvertStatus(impl_->SetZoom(request, response)); + } + + grpc::Status GetZoom(grpc::ServerContext* context, + const proto::GetZoomRequest* request, + proto::GetZoomResponse* response) override { + return ConvertStatus(impl_->GetZoom(request, response)); + } + + // Query Operations + grpc::Status GetDimensions(grpc::ServerContext* context, + const proto::GetDimensionsRequest* request, + proto::GetDimensionsResponse* response) override { + return ConvertStatus(impl_->GetDimensions(request, response)); + } + + grpc::Status GetVisibleRegion(grpc::ServerContext* context, + const proto::GetVisibleRegionRequest* request, + proto::GetVisibleRegionResponse* response) override { + return ConvertStatus(impl_->GetVisibleRegion(request, response)); + } + + grpc::Status IsTileVisible(grpc::ServerContext* context, + const proto::IsTileVisibleRequest* request, + proto::IsTileVisibleResponse* response) override { + return ConvertStatus(impl_->IsTileVisible(request, response)); + } + + private: + CanvasAutomationServiceImpl* impl_; +}; + +// Factory function to create the gRPC wrapper +// Returns as base grpc::Service* to avoid incomplete type issues in headers +std::unique_ptr CreateCanvasAutomationServiceGrpc( + CanvasAutomationServiceImpl* impl) { + return std::make_unique(impl); +} + } // namespace yaze #endif // YAZE_WITH_GRPC diff --git a/src/app/core/service/canvas_automation_service.h b/src/app/core/service/canvas_automation_service.h index ebf30ec4..540ea568 100644 --- a/src/app/core/service/canvas_automation_service.h +++ b/src/app/core/service/canvas_automation_service.h @@ -1,6 +1,7 @@ #ifndef YAZE_APP_CORE_SERVICE_CANVAS_AUTOMATION_SERVICE_H_ #define YAZE_APP_CORE_SERVICE_CANVAS_AUTOMATION_SERVICE_H_ +#include "grpcpp/impl/service_type.h" #ifdef YAZE_WITH_GRPC #include @@ -126,6 +127,19 @@ class CanvasAutomationServiceImpl { std::unordered_map overworld_editors_; }; +/** + * @brief Factory function to create gRPC service wrapper + * + * Creates the gRPC service wrapper for CanvasAutomationServiceImpl. + * The wrapper handles the conversion between gRPC and absl::Status. + * Returns as base grpc::Service to avoid incomplete type issues. + * + * @param impl Pointer to implementation (not owned) + * @return Unique pointer to gRPC service + */ +std::unique_ptr CreateCanvasAutomationServiceGrpc( + CanvasAutomationServiceImpl* impl); + } // namespace yaze #endif // YAZE_WITH_GRPC diff --git a/src/app/core/service/unified_grpc_server.cc b/src/app/core/service/unified_grpc_server.cc index 97c156dd..47ecd258 100644 --- a/src/app/core/service/unified_grpc_server.cc +++ b/src/app/core/service/unified_grpc_server.cc @@ -7,27 +7,31 @@ #include "absl/strings/str_format.h" #include "app/core/service/imgui_test_harness_service.h" +#include "app/core/service/canvas_automation_service.h" #include "app/net/rom_service_impl.h" #include "app/rom.h" #include +#include "protos/canvas_automation.grpc.pb.h" namespace yaze { -UnifiedGRPCServer::UnifiedGRPCServer() +YazeGRPCServer::YazeGRPCServer() : is_running_(false) { } -UnifiedGRPCServer::~UnifiedGRPCServer() { +// Destructor defined here so CanvasAutomationServiceGrpc is a complete type +YazeGRPCServer::~YazeGRPCServer() { Shutdown(); } -absl::Status UnifiedGRPCServer::Initialize( +absl::Status YazeGRPCServer::Initialize( int port, test::TestManager* test_manager, app::Rom* rom, app::net::RomVersionManager* version_mgr, - app::net::ProposalApprovalManager* approval_mgr) { + app::net::ProposalApprovalManager* approval_mgr, + CanvasAutomationServiceImpl* canvas_service) { if (is_running_) { return absl::FailedPreconditionError("Server is already running"); @@ -59,7 +63,16 @@ absl::Status UnifiedGRPCServer::Initialize( std::cout << "⚠ ROM service requested but no ROM provided\n"; } - if (!test_harness_service_ && !rom_service_) { + // Create Canvas Automation service if canvas_service provided + if (config_.enable_canvas_automation && canvas_service) { + // Store the provided service (not owned by us) + canvas_service_ = std::unique_ptr(canvas_service); + std::cout << "✓ Canvas Automation service initialized\n"; + } else if (config_.enable_canvas_automation) { + std::cout << "⚠ Canvas Automation requested but no service provided\n"; + } + + if (!test_harness_service_ && !rom_service_ && !canvas_service_) { return absl::InvalidArgumentError( "At least one service must be enabled and initialized"); } @@ -67,13 +80,13 @@ absl::Status UnifiedGRPCServer::Initialize( return absl::OkStatus(); } -absl::Status UnifiedGRPCServer::Start() { +absl::Status YazeGRPCServer::Start() { auto status = BuildServer(); if (!status.ok()) { return status; } - std::cout << "✓ Unified gRPC server listening on 0.0.0.0:" << config_.port << "\n"; + std::cout << "✓ YAZE gRPC automation server listening on 0.0.0.0:" << config_.port << "\n"; if (test_harness_service_) { std::cout << " ✓ ImGuiTestHarness available\n"; @@ -81,6 +94,9 @@ absl::Status UnifiedGRPCServer::Start() { if (rom_service_) { std::cout << " ✓ ROM service available\n"; } + if (canvas_service_) { + std::cout << " ✓ Canvas Automation available\n"; + } std::cout << "\nServer is ready to accept requests...\n"; @@ -90,7 +106,7 @@ absl::Status UnifiedGRPCServer::Start() { return absl::OkStatus(); } -absl::Status UnifiedGRPCServer::StartAsync() { +absl::Status YazeGRPCServer::StartAsync() { auto status = BuildServer(); if (!status.ok()) { return status; @@ -102,7 +118,7 @@ absl::Status UnifiedGRPCServer::StartAsync() { return absl::OkStatus(); } -void UnifiedGRPCServer::Shutdown() { +void YazeGRPCServer::Shutdown() { if (server_ && is_running_) { std::cout << "⏹ Shutting down unified gRPC server...\n"; server_->Shutdown(); @@ -112,11 +128,11 @@ void UnifiedGRPCServer::Shutdown() { } } -bool UnifiedGRPCServer::IsRunning() const { +bool YazeGRPCServer::IsRunning() const { return is_running_; } -absl::Status UnifiedGRPCServer::BuildServer() { +absl::Status YazeGRPCServer::BuildServer() { if (is_running_) { return absl::FailedPreconditionError("Server already running"); } @@ -142,6 +158,13 @@ absl::Status UnifiedGRPCServer::BuildServer() { builder.RegisterService(rom_service_.get()); } + if (canvas_service_) { + std::cout << " Registering Canvas Automation service...\n"; + // Create gRPC wrapper using factory function + canvas_grpc_service_ = CreateCanvasAutomationServiceGrpc(canvas_service_.get()); + builder.RegisterService(canvas_grpc_service_.get()); + } + // Build and start server_ = builder.BuildAndStart(); diff --git a/src/app/core/service/unified_grpc_server.h b/src/app/core/service/unified_grpc_server.h index a2c2992e..bc7bb245 100644 --- a/src/app/core/service/unified_grpc_server.h +++ b/src/app/core/service/unified_grpc_server.h @@ -8,6 +8,9 @@ #include "absl/status/status.h" #include "app/net/rom_version_manager.h" +// Include grpcpp for grpc::Service forward declaration +#include + namespace grpc { class Server; } @@ -15,6 +18,8 @@ class Server; namespace yaze { // Forward declarations +class CanvasAutomationServiceImpl; + namespace app { class Rom; namespace net { @@ -29,26 +34,28 @@ class ImGuiTestHarnessServiceImpl; } /** - * @class UnifiedGRPCServer - * @brief Unified gRPC server hosting both ImGuiTestHarness and RomService + * @class YazeGRPCServer + * @brief YAZE's unified gRPC server for Zelda3 editor automation * - * This server combines: + * 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) * - * Both services share the same gRPC server instance and port, allowing - * clients to interact with both the GUI and ROM data simultaneously. + * 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 - * UnifiedGRPCServer server; - * server.Initialize(50051, test_manager, rom, version_mgr, approval_mgr); + * YazeGRPCServer server; + * server.Initialize(50051, test_manager, rom, version_mgr, approval_mgr, canvas_service); * server.Start(); * // ... do work ... * server.Shutdown(); * ``` */ -class UnifiedGRPCServer { +class YazeGRPCServer { public: /** * @brief Configuration for the unified server @@ -57,11 +64,13 @@ class UnifiedGRPCServer { int port = 50051; bool enable_test_harness = true; bool enable_rom_service = true; + bool enable_canvas_automation = true; bool require_approval_for_rom_writes = true; }; - UnifiedGRPCServer(); - ~UnifiedGRPCServer(); + YazeGRPCServer(); + // Destructor must be defined in .cc file to allow deletion of incomplete types + ~YazeGRPCServer(); /** * @brief Initialize the server with all required services @@ -70,6 +79,7 @@ class UnifiedGRPCServer { * @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) * @return OK status if initialized successfully */ absl::Status Initialize( @@ -77,7 +87,8 @@ class UnifiedGRPCServer { test::TestManager* test_manager = nullptr, app::Rom* rom = nullptr, app::net::RomVersionManager* version_mgr = nullptr, - app::net::ProposalApprovalManager* approval_mgr = nullptr); + app::net::ProposalApprovalManager* approval_mgr = nullptr, + CanvasAutomationServiceImpl* canvas_service = nullptr); /** * @brief Start the gRPC server (blocking) @@ -116,9 +127,12 @@ class UnifiedGRPCServer { std::unique_ptr server_; std::unique_ptr test_harness_service_; std::unique_ptr rom_service_; + std::unique_ptr canvas_service_; + // Store as base grpc::Service* to avoid incomplete type issues + std::unique_ptr canvas_grpc_service_; bool is_running_; - // Build the gRPC server with both services + // Build the gRPC server with all services absl::Status BuildServer(); }; @@ -126,3 +140,10 @@ class UnifiedGRPCServer { #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