diff --git a/docs/z3ed/NETWORKING.md b/docs/z3ed/NETWORKING.md index 44d13c5b..6f8f251b 100644 --- a/docs/z3ed/NETWORKING.md +++ b/docs/z3ed/NETWORKING.md @@ -1,22 +1,20 @@ -# Z3ED Networking & Collaboration +# z3ed Networking, Collaboration, and Remote Access -## Overview +**Version**: 0.2.0-alpha +**Last Updated**: October 5, 2025 -Z3ED provides comprehensive networking capabilities across all three components: -- **yaze app**: GUI application with real-time collaboration -- **z3ed CLI**: Command-line interface for remote operations -- **yaze-server**: WebSocket server for coordination +## 1. Overview -## Architecture +This document provides a comprehensive overview of the networking, collaboration, and remote access features within the z3ed ecosystem. These systems are designed to enable everything from real-time collaborative ROM hacking to powerful AI-driven remote automation. -### Cross-Platform Design +The architecture is composed of three main communication layers: +1. **WebSocket Protocol**: A real-time messaging layer for multi-user collaboration, managed by the `yaze-server`. +2. **gRPC Service**: A high-performance RPC layer for programmatic remote ROM manipulation, primarily used by the `z3ed` CLI and automated testing harnesses. +3. **Collaboration Service**: A high-level C++ API within the YAZE application that integrates version management with the networking protocols. -All networking code is designed to work on: -- ✅ **Windows** - Using native Win32 sockets (ws2_32) -- ✅ **macOS** - Using native BSD sockets -- ✅ **Linux** - Using native BSD sockets +## 2. Architecture -### Components +### 2.1. System Diagram ``` ┌─────────────┐ WebSocket ┌──────────────┐ @@ -24,8 +22,7 @@ All networking code is designed to work on: │ (GUI) │ │ (Node.js) │ └─────────────┘ └──────────────┘ ▲ ▲ - │ │ - │ WebSocket │ + │ gRPC (for GUI testing) │ WebSocket │ │ └─────────────┐ │ │ │ @@ -33,194 +30,113 @@ All networking code is designed to work on: │ z3ed CLI │◄──────────────────────┘ │ │ └─────────────┘ - - gRPC (for GUI testing) - │ - │ - ┌──────▼──────┐ - │ yaze app │ - │ (ImGui) │ - └─────────────┘ ``` -## WebSocket Protocol +### 2.2. Core Components -### Connection +The collaboration system is built on two key C++ components: -```cpp -#include "app/net/websocket_client.h" +1. **ROM Version Manager** (`app/net/rom_version_manager.h`) + - **Purpose**: Protects the ROM from corruption and unwanted changes. + - **Features**: + - Automatic periodic snapshots and manual checkpoints. + - "Safe points" to mark host-verified versions. + - Corruption detection and automatic recovery to the last safe point. + - Ability to roll back to any previous version. -net::WebSocketClient client; +2. **Proposal Approval Manager** (`app/net/rom_version_manager.h`) + - **Purpose**: Manages a collaborative voting system for applying changes. + - **Features**: + - Multiple approval modes: `Host-Only` (default), `Majority`, and `Unanimous`. + - Automatically creates snapshots before and after a proposal is applied. + - Handles the submission, voting, and application/rejection of proposals. -// Connect to server -auto status = client.Connect("localhost", 8765); +3. **Collaboration Panel** (`app/gui/widgets/collaboration_panel.h`) + - **Purpose**: Provides a dedicated UI within the YAZE editor for managing collaboration. + - **Features**: + - Version history timeline with one-click restore. + - ROM synchronization tracking. + - Visual snapshot gallery. + - A voting and approval interface for pending proposals. -// Set up callbacks -client.OnMessage("rom_sync", [](const nlohmann::json& payload) { - // Handle ROM sync -}); +## 3. Protocols -client.OnStateChange([](net::ConnectionState state) { - // Handle state changes -}); +### 3.1. WebSocket Protocol (yaze-server v2.0) + +Used for real-time, multi-user collaboration. + +**Connection**: +```javascript +const ws = new WebSocket('ws://localhost:8765'); ``` -### Message Types +**Message Types**: -#### 1. Session Management +| Type | Sender | Payload Description | +| :--- | :--- | :--- | +| `host_session` | Client | Initiates a new session with a name, username, and optional ROM hash. | +| `join_session` | Client | Joins an existing session using a 6-character code. | +| `rom_sync` | Client | Broadcasts a base64-encoded ROM diff to all participants. | +| `proposal_share` | Client | Shares a new proposal (e.g., from an AI agent) with the group. | +| `proposal_vote` | Client | Submits a vote (approve/reject) for a specific proposal. | +| `snapshot_share` | Client | Shares a snapshot (e.g., a screenshot) with the group. | +| `proposal_update` | Server | Broadcasts the new status of a proposal (`approved`, `rejected`). | +| `proposal_vote_received`| Server | Confirms a vote was received and shows the current vote tally. | +| `session_hosted` | Server | Confirms a session was created and returns its code. | +| `session_joined` | Server | Confirms a user joined and provides session state (participants, history). | -**Host Session**: -```json -{ - "type": "host_session", - "payload": { - "session_name": "My ROM Hack", - "username": "host", - "rom_hash": "abc123", - "ai_enabled": true - } +### 3.2. gRPC Service (Remote ROM Manipulation) + +Provides a high-performance API for programmatic access to the ROM, used by the `z3ed` CLI and test harnesses. + +**Status**: ✅ Designed and Implemented. Pending final build system integration. + +**Protocol Buffer (`protos/rom_service.proto`)**: + +```proto +service RomService { + // Core + rpc ReadBytes(ReadBytesRequest) returns (ReadBytesResponse); + rpc WriteBytes(WriteBytesRequest) returns (WriteBytesResponse); + rpc GetRomInfo(GetRomInfoRequest) returns (GetRomInfoResponse); + + // Overworld + rpc ReadOverworldMap(ReadOverworldMapRequest) returns (ReadOverworldMapResponse); + rpc WriteOverworldTile(WriteOverworldTileRequest) returns (WriteOverworldTileResponse); + + // Dungeon + rpc ReadDungeonRoom(ReadDungeonRoomRequest) returns (ReadDungeonRoomResponse); + rpc WriteDungeonTile(WriteDungeonTileRequest) returns (WriteDungeonTileResponse); + + // Proposals + rpc SubmitRomProposal(SubmitRomProposalRequest) returns (SubmitRomProposalResponse); + rpc GetProposalStatus(GetProposalStatusRequest) returns (GetProposalStatusResponse); + + // Version Management + rpc CreateSnapshot(CreateSnapshotRequest) returns (CreateSnapshotResponse); + rpc RestoreSnapshot(RestoreSnapshotRequest) returns (RestoreSnapshotResponse); + rpc ListSnapshots(ListSnapshotsRequest) returns (ListSnapshotsResponse); } ``` -**Join Session**: -```json -{ - "type": "join_session", - "payload": { - "session_code": "ABC123", - "username": "participant" - } -} +**Use Case: Write with Approval via `z3ed`** +The `z3ed` CLI can submit a change as a proposal via gRPC and wait for a host to approve it in the YAZE GUI. + +```bash +# 1. CLI connects and submits a proposal via gRPC +z3ed net connect --host server.example.com --port 50051 +z3ed agent run --prompt "Make dungeon 5 harder" --submit-grpc-proposal + +# 2. The YAZE GUI receives the proposal and the host approves it. + +# 3. The z3ed CLI polls for the status and receives the approval. ``` -#### 2. Proposal System (NEW) +## 4. Client Integration -**Share Proposal**: -```json -{ - "type": "proposal_share", - "payload": { - "sender": "username", - "proposal_data": { - "description": "Place tile 0x42 at (5,7)", - "type": "tile_edit", - "data": {...} - } - } -} -``` +### 4.1. YAZE App Integration -**Vote on Proposal** (NEW): -```json -{ - "type": "proposal_vote", - "payload": { - "proposal_id": "prop_123", - "approved": true, - "username": "voter" - } -} -``` - -Response: -```json -{ - "type": "proposal_vote_received", - "payload": { - "proposal_id": "prop_123", - "username": "voter", - "approved": true, - "votes": { - "host": true, - "user1": true, - "user2": false - }, - "timestamp": 1234567890 - } -} -``` - -**Update Proposal Status**: -```json -{ - "type": "proposal_update", - "payload": { - "proposal_id": "prop_123", - "status": "approved" // or "rejected", "applied" - } -} -``` - -#### 3. ROM Synchronization - -**Send ROM Sync**: -```json -{ - "type": "rom_sync", - "payload": { - "sender": "username", - "diff_data": "base64_encoded_diff", - "rom_hash": "new_hash" - } -} -``` - -#### 4. Snapshots - -**Share Snapshot**: -```json -{ - "type": "snapshot_share", - "payload": { - "sender": "username", - "snapshot_data": "base64_encoded_image", - "snapshot_type": "screenshot" - } -} -``` - -## YAZE App Integration - -### Using WebSocketClient - -```cpp -#include "app/net/websocket_client.h" - -// Create client -auto client = std::make_unique(); - -// Connect -if (auto status = client->Connect("localhost", 8765); !status.ok()) { - // Handle error -} - -// Host a session -auto session_info = client->HostSession( - "My Hack", - "username", - rom->GetHash(), - true // AI enabled -); - -// Set up proposal callback -client->OnMessage("proposal_shared", [this](const nlohmann::json& payload) { - std::string proposal_id = payload["proposal_id"]; - nlohmann::json proposal_data = payload["proposal_data"]; - - // Add to approval manager - approval_mgr->SubmitProposal( - proposal_id, - payload["sender"], - proposal_data["description"], - proposal_data - ); -}); - -// Vote on proposal -client->VoteOnProposal(proposal_id, true, "my_username"); -``` - -### Using CollaborationService +The `CollaborationService` provides a high-level API that integrates the version manager, approval manager, and WebSocket client. ```cpp #include "app/net/collaboration_service.h" @@ -228,297 +144,67 @@ client->VoteOnProposal(proposal_id, true, "my_username"); // High-level service that integrates everything auto collab_service = std::make_unique(rom); -// Initialize with version manager and approval manager +// Initialize with managers collab_service->Initialize(config, version_mgr, approval_mgr); // Connect and host collab_service->Connect("localhost", 8765); collab_service->HostSession("My Hack", "username"); -// Submit local changes as proposal -collab_service->SubmitChangesAsProposal( - "Modified dungeon room 5", - "username" -); - -// Auto-sync is handled automatically +// Submit local changes as a proposal to the group +collab_service->SubmitChangesAsProposal("Modified dungeon room 5", "username"); ``` -## Z3ED CLI Integration +### 4.2. z3ed CLI Integration -### Connection Commands +The CLI provides `net` commands for interacting with the collaboration server. ```bash -# Connect to collaboration server +# Connect to a collaboration server z3ed net connect --host localhost --port 8765 -# Join session +# Join an existing session z3ed net join --code ABC123 --username myname -# Leave session -z3ed net leave -``` +# Submit an AI-generated change as a proposal and wait for it to be approved +z3ed agent run --prompt "Make boss room more challenging" --submit-proposal --wait-approval -### Proposal Commands - -```bash -# Submit proposal from z3ed -z3ed agent run --prompt "Place tile 42 at (5,7)" --submit-proposal - -# Check proposal status +# Manually check a proposal's status z3ed net proposal status --id prop_123 - -# Wait for approval (blocking) -z3ed net proposal wait --id prop_123 --timeout 60 ``` -### Example Workflow - -```bash -# 1. Connect to server -z3ed net connect --host localhost - -# 2. Join session -z3ed net join --code XYZ789 --username alice - -# 3. Submit AI-generated proposal -z3ed agent run --prompt "Make boss room more challenging" \ - --submit-proposal --wait-approval - -# 4. If approved, changes are applied -# If rejected, original ROM is preserved -``` - -## Windows-Specific Notes - -### Building on Windows - -The networking library automatically links Windows socket support: - -```cmake -if(WIN32) - target_link_libraries(yaze_net PUBLIC ws2_32) -endif() -``` - -### vcpkg Dependencies - -For Windows with vcpkg: - -```powershell -# Install dependencies -vcpkg install openssl:x64-windows - -# CMake will automatically detect and use them -``` - -### Windows Firewall - -You may need to allow connections: - -```powershell -# Allow yaze-server -netsh advfirewall firewall add rule name="YAZE Server" dir=in action=allow protocol=TCP localport=8765 - -# Or through UI: Windows Defender Firewall → Allow an app -``` - -## Security Considerations - -### Transport Security - -1. **Use WSS (WebSocket Secure)** in production: - ```cpp - client->Connect("wss://server.example.com", 443); - ``` - -2. **Server configuration** with SSL: - ```javascript - const https = require('https'); - const fs = require('fs'); - - const server = https.createServer({ - cert: fs.readFileSync('cert.pem'), - key: fs.readFileSync('key.pem') - }); - ``` - -### Approval Security - -1. **Host-only mode** (safest): - ```cpp - approval_mgr->SetApprovalMode(ApprovalMode::kHostOnly); - ``` - -2. **Verify identities**: Use authentication tokens - -3. **Rate limiting**: Server limits messages to 100/minute - -### ROM Protection - -1. **Always create snapshots** before applying proposals: - ```cpp - config.create_snapshot_before_sync = true; - ``` - -2. **Mark safe points** after verification: - ```cpp - version_mgr->MarkAsSafePoint(snapshot_id); - ``` - -3. **Auto-rollback** on errors: - ```cpp - if (error) { - version_mgr->RestoreSnapshot(snapshot_before); - } - ``` - -## Platform-Specific Implementation - -### httplib WebSocket Support - -The implementation uses `cpp-httplib` for cross-platform support: - -- **Windows**: Uses Winsock2 (ws2_32.dll) -- **macOS/Linux**: Uses BSD sockets -- **SSL/TLS**: Optional OpenSSL support - -### Threading - -All platforms use C++11 threads: - -```cpp -#include -#include - -std::thread receive_thread([this]() { - // Platform-independent receive loop -}); -``` - -## Error Handling - -### Connection Errors - -```cpp -auto status = client->Connect(host, port); -if (!status.ok()) { - if (absl::IsUnavailable(status)) { - // Server not reachable - } else if (absl::IsDeadlineExceeded(status)) { - // Connection timeout - } -} -``` - -### Network State - -```cpp -client->OnStateChange([](ConnectionState state) { - switch (state) { - case ConnectionState::kConnected: - // Ready to use - break; - case ConnectionState::kDisconnected: - // Clean shutdown - break; - case ConnectionState::kReconnecting: - // Attempting reconnect - break; - case ConnectionState::kError: - // Fatal error - break; - } -}); -``` - -## Performance - -### Compression - -Large messages are compressed: - -```cpp -// ROM diffs are compressed before sending -std::string compressed = CompressDiff(diff_data); -client->SendRomSync(compressed, hash, sender); -``` - -### Batching - -Small changes are batched: - -```cpp -config.sync_interval_ms = 5000; // Batch changes over 5 seconds -``` - -### Rate Limiting - -Server enforces: -- 100 messages per minute per client -- 5MB max ROM diff size -- 10MB max snapshot size - -## Testing - -### Unit Tests - -```cpp -TEST(WebSocketClientTest, ConnectAndDisconnect) { - net::WebSocketClient client; - ASSERT_TRUE(client.Connect("localhost", 8765).ok()); - EXPECT_TRUE(client.IsConnected()); - client.Disconnect(); - EXPECT_FALSE(client.IsConnected()); -} -``` - -### Integration Tests - -```bash -# Start server -cd yaze-server -npm start - -# Run tests -cd yaze -cmake --build build --target yaze_net_tests -./build/bin/yaze_net_tests -``` - -## Troubleshooting - -### "Failed to connect" -- Check server is running: `ps aux | grep node` -- Check port is available: `netstat -an | grep 8765` -- Check firewall settings - -### "Connection timeout" -- Increase timeout: `client->SetTimeout(10);` -- Check network connectivity -- Verify server address - -### "SSL handshake failed" -- Verify OpenSSL is installed -- Check certificate validity -- Use WSS URL: `wss://` not `ws://` - -### Windows-specific: "ws2_32.dll not found" -- Reinstall Windows SDK -- Check PATH environment variable -- Use vcpkg for dependencies - -## Future Enhancements - -- [ ] WebRTC for peer-to-peer connections -- [ ] Binary protocol for faster ROM syncs -- [ ] Automatic reconnection with exponential backoff -- [ ] Connection pooling for multiple sessions -- [ ] NAT traversal for home networks -- [ ] End-to-end encryption for proposals - -## See Also - -- [Collaboration Guide](COLLABORATION.md) - Version management and approval -- [Z3ED README](README.md) - Main documentation -- [yaze-server README](../../../yaze-server/README.md) - Server setup +## 5. Best Practices & Troubleshooting + +### Best Practices +- **For Hosts**: Enable auto-backups, mark safe points after playtesting, and use `Host-Only` approval mode for maximum control. +- **For Participants**: Submit all changes as proposals, wait for approval, and use descriptive names. +- **For Everyone**: Test changes in an emulator before submitting, make small atomic changes, and communicate with the team. + +### Troubleshooting +- **"Failed to connect"**: Check that the `yaze-server` is running and the port is not blocked by a firewall. +- **"Corruption detected"**: Use `version_mgr->AutoRecover()` or manually restore the last known safe point from the Collaboration Panel. +- **"Snapshot not found"**: Verify the snapshot ID is correct and wasn't deleted due to storage limits. +- **"SSL handshake failed"**: Ensure you are using a `wss://` URL and that a valid SSL certificate is configured on the server. + +## 6. Security Considerations +- **Transport Security**: Production environments should use WebSocket Secure (`wss://`) with a valid SSL/TLS certificate. +- **Approval Security**: `Host-Only` mode is the safest. For more open collaboration, consider a token-based authentication system. +- **ROM Protection**: The `RomVersionManager` is the primary defense. Always create snapshots before applying proposals and mark safe points often. + - **Rate Limiting**: The `yaze-server` enforces a rate limit (default: 100 messages/minute) to prevent abuse. + +## 7. Future Enhancements + +- **Encrypted WebSocket (WSS) support** +- **Diff visualization in UI** +- **Merge conflict resolution** +- **Branch/fork support for experimental changes** +- **AI-assisted proposal review** +- **Cloud snapshot backup** +- **Multi-host sessions** +- **Access control lists (ACLs)** +- **WebRTC for peer-to-peer connections** +- **Binary protocol for faster ROM syncs** +- **Automatic reconnection with exponential backoff** +- **Connection pooling for multiple sessions** +- **NAT traversal for home networks** +- **End-to-end encryption for proposals** \ No newline at end of file diff --git a/docs/z3ed/README.md b/docs/z3ed/README.md index 0e4dd9f5..3ec1d94e 100644 --- a/docs/z3ed/README.md +++ b/docs/z3ed/README.md @@ -135,6 +135,14 @@ The `z3ed` CLI is the foundation for an AI-driven Model-Code-Program (MCP) loop, - `agent simple-chat`: A lightweight, non-TUI chat mode for scripting and automation. - `agent test ...`: Commands for running and managing automated GUI tests. - `agent learn ...`: **NEW**: Manage learned knowledge (preferences, ROM patterns, project context, conversation memory). +- `agent todo create "Description" [--category=] [--priority=]`: Create a new TODO item. +- `agent todo list [--status=] [--category=]`: List TODOs, with optional filters. +- `agent todo update --status=`: Update the status of a TODO item. +- `agent todo show `: Display full details of a TODO item. +- `agent todo delete `: Delete a TODO item. +- `agent todo clear-completed`: Remove all completed TODOs. +- `agent todo next`: Get the next actionable TODO based on dependencies and priority. +- `agent todo plan`: Generate a topologically-sorted execution plan for all TODOs. ### Resource Commands @@ -239,7 +247,23 @@ All learned data is stored in `~/.yaze/agent/`: - `projects.json`: Project contexts - `memories.json`: Conversation summaries -## 9. CLI Output & Help System +## 9. TODO Management System + +The TODO Management System enables the z3ed AI agent to create, track, and execute complex multi-step tasks with dependency management and prioritization. + +### Core Capabilities +- ✅ Create and manage TODO items with priorities +- ✅ Track task status (pending, in_progress, completed, blocked, cancelled) +- ✅ Dependency tracking between tasks +- ✅ Automatic execution plan generation +- ✅ Persistent storage in JSON format +- ✅ Category-based organization +- ✅ Tools/functions tracking per task + +### Storage Location +TODOs are persisted to: `~/.yaze/agent/todos.json` (macOS/Linux) or `%APPDATA%/yaze/agent/todos.json` (Windows) + +## 10. CLI Output & Help System The `z3ed` CLI features a modernized output system designed to be clean for users and informative for developers. @@ -895,6 +919,7 @@ The AI response appears in your chat history and can reference specific details - **Native Gemini Function Calling**: Upgraded from manual curl to native function calling API with automatic tool schema generation - **Multimodal Vision Testing**: Comprehensive test suite for Gemini vision capabilities with screenshot integration - **AI-Controlled GUI Automation**: Natural language parsing (`AIActionParser`) and test script generation (`GuiActionGenerator`) for automated tile placement +- **TODO Management System**: Full `TodoManager` class with CRUD operations, CLI commands, dependency tracking, execution planning, and JSON persistence. #### Version Management & Protection - **ROM Version Management System**: `RomVersionManager` with automatic snapshots, safe points, corruption detection, and rollback capabilities @@ -908,11 +933,16 @@ The AI response appears in your chat history and can reference specific details - **Collaboration UI Panel**: `CollaborationPanel` widget with version history, ROM sync tracking, snapshot gallery, and approval workflow - **gRPC ROM Service**: Complete protocol buffer and implementation for remote ROM manipulation (pending build integration) +#### UI/UX Enhancements +- **Welcome Screen Enhancement**: Dynamic theme integration, Zelda-themed animations, and project cards. +- **Component Refactoring**: `PaletteWidget` renamed and moved, UI organization improved (`app/editor/ui/` for welcome_screen, editor_selection_dialog, background_renderer). + #### Build System & Infrastructure - **gRPC Windows Build Optimization**: vcpkg integration for 10-20x faster Windows builds, removed abseil-cpp submodule - **Cross-Platform Networking**: Native socket support (ws2_32 on Windows, BSD sockets on Unix) - **Namespace Refactoring**: Created `app/net` namespace for networking components - **Improved Documentation**: Consolidated architecture, enhancement plans, networking guide, and build instructions with JSON-first approach +- **Build System Improvements**: `mac-ai` preset, proto fixes, and updated GEMINI.md with AI build policies. ## 12. Troubleshooting diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index af1dbc46..697ec0d7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -457,9 +457,10 @@ else() set(YAZE_RES_FILE "${CMAKE_CURRENT_BINARY_DIR}/yaze.res") # Add a custom command to generate the .res file from the .rc and .ico + # /I adds include directory so RC compiler can find yaze.ico add_custom_command( OUTPUT ${YAZE_RES_FILE} - COMMAND ${CMAKE_RC_COMPILER} /fo ${YAZE_RES_FILE} ${YAZE_RC_FILE} + COMMAND ${CMAKE_RC_COMPILER} /fo ${YAZE_RES_FILE} /I "${CMAKE_SOURCE_DIR}/assets" ${YAZE_RC_FILE} DEPENDS ${YAZE_RC_FILE} ${YAZE_ICO_FILE} COMMENT "Generating yaze.res from yaze.rc and yaze.ico" VERBATIM diff --git a/src/cli/agent.cmake b/src/cli/agent.cmake index 3fbffc4a..d6617014 100644 --- a/src/cli/agent.cmake +++ b/src/cli/agent.cmake @@ -68,6 +68,8 @@ set(YAZE_AGENT_SOURCES cli/service/agent/proposal_executor.cc cli/handlers/agent/tool_commands.cc cli/handlers/agent/todo_commands.cc + cli/handlers/agent/hex_commands.cc + cli/handlers/agent/palette_commands.cc cli/service/agent/conversational_agent_service.cc cli/service/agent/simple_chat_session.cc cli/service/agent/tool_dispatcher.cc diff --git a/src/cli/handlers/agent.cc b/src/cli/handlers/agent.cc index 1bde73c2..c6348b58 100644 --- a/src/cli/handlers/agent.cc +++ b/src/cli/handlers/agent.cc @@ -151,6 +151,28 @@ absl::Status Agent::Run(const std::vector& arg_vec) { if (subcommand == "simple-chat") { return agent::HandleSimpleChatCommand(subcommand_args, &rom_, absl::GetFlag(FLAGS_quiet)); } + + // Hex manipulation commands + if (subcommand == "hex-read") { + return agent::HandleHexRead(subcommand_args, &rom_); + } + if (subcommand == "hex-write") { + return agent::HandleHexWrite(subcommand_args, &rom_); + } + if (subcommand == "hex-search") { + return agent::HandleHexSearch(subcommand_args, &rom_); + } + + // Palette manipulation commands + if (subcommand == "palette-get-colors") { + return agent::HandlePaletteGetColors(subcommand_args, &rom_); + } + if (subcommand == "palette-set-color") { + return agent::HandlePaletteSetColor(subcommand_args, &rom_); + } + if (subcommand == "palette-analyze") { + return agent::HandlePaletteAnalyze(subcommand_args, &rom_); + } return absl::InvalidArgumentError(std::string(agent::kUsage)); } diff --git a/src/cli/handlers/agent/commands.h b/src/cli/handlers/agent/commands.h index 7397ff63..dd6e5826 100644 --- a/src/cli/handlers/agent/commands.h +++ b/src/cli/handlers/agent/commands.h @@ -69,6 +69,22 @@ absl::Status HandleSimpleChatCommand(const std::vector&, Rom* rom, absl::Status HandleTestConversationCommand( const std::vector& arg_vec); +// Hex manipulation commands +absl::Status HandleHexRead(const std::vector& arg_vec, + Rom* rom_context = nullptr); +absl::Status HandleHexWrite(const std::vector& arg_vec, + Rom* rom_context = nullptr); +absl::Status HandleHexSearch(const std::vector& arg_vec, + Rom* rom_context = nullptr); + +// Palette manipulation commands +absl::Status HandlePaletteGetColors(const std::vector& arg_vec, + Rom* rom_context = nullptr); +absl::Status HandlePaletteSetColor(const std::vector& arg_vec, + Rom* rom_context = nullptr); +absl::Status HandlePaletteAnalyze(const std::vector& arg_vec, + Rom* rom_context = nullptr); + } // namespace agent } // namespace cli } // namespace yaze diff --git a/src/cli/handlers/agent/hex_commands.cc b/src/cli/handlers/agent/hex_commands.cc new file mode 100644 index 00000000..d9afe3ee --- /dev/null +++ b/src/cli/handlers/agent/hex_commands.cc @@ -0,0 +1,287 @@ +#include "cli/handlers/agent/hex_commands.h" + +#include +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/strings/str_format.h" +#include "absl/strings/str_split.h" +#include "app/rom.h" + +namespace yaze { +namespace cli { +namespace agent { + +namespace { + +// Parse hex address from string (supports 0x prefix) +absl::StatusOr ParseHexAddress(const std::string& str) { + try { + size_t pos; + uint32_t addr = std::stoul(str, &pos, 16); + if (pos != str.size()) { + return absl::InvalidArgumentError( + absl::StrFormat("Invalid hex address: %s", str)); + } + return addr; + } catch (const std::exception& e) { + return absl::InvalidArgumentError( + absl::StrFormat("Failed to parse address '%s': %s", str, e.what())); + } +} + +// Parse hex byte pattern (supports wildcards ??) +std::vector> ParseHexPattern(const std::string& pattern) { + std::vector> result; + std::vector bytes = absl::StrSplit(pattern, ' '); + + for (const auto& byte_str : bytes) { + if (byte_str.empty()) continue; + + if (byte_str == "??" || byte_str == "?") { + result.push_back({0x00, false}); // Wildcard + } else { + try { + uint8_t value = static_cast(std::stoul(byte_str, nullptr, 16)); + result.push_back({value, true}); // Exact match + } catch (const std::exception&) { + std::cerr << "Warning: Invalid byte in pattern: " << byte_str << std::endl; + } + } + } + + return result; +} + +// Format bytes as hex string +std::string FormatHexBytes(const uint8_t* data, size_t length, + const std::string& format) { + std::ostringstream oss; + + if (format == "hex" || format == "both") { + for (size_t i = 0; i < length; ++i) { + oss << std::hex << std::setw(2) << std::setfill('0') + << static_cast(data[i]); + if (i < length - 1) oss << " "; + } + } + + if (format == "both") { + oss << " | "; + } + + if (format == "ascii" || format == "both") { + for (size_t i = 0; i < length; ++i) { + char c = static_cast(data[i]); + oss << (std::isprint(c) ? c : '.'); + } + } + + return oss.str(); +} + +} // namespace + +absl::Status HandleHexRead(const std::vector& args, + Rom* rom_context) { + if (!rom_context || !rom_context->is_loaded()) { + return absl::FailedPreconditionError("ROM not loaded"); + } + + // Parse arguments + std::string address_str; + int length = 16; + std::string format = "both"; + + for (const auto& arg : args) { + if (arg.rfind("--address=", 0) == 0) { + address_str = arg.substr(10); + } else if (arg.rfind("--length=", 0) == 0) { + length = std::stoi(arg.substr(9)); + } else if (arg.rfind("--format=", 0) == 0) { + format = arg.substr(9); + } + } + + if (address_str.empty()) { + return absl::InvalidArgumentError("--address required"); + } + + // Parse address + auto addr_or = ParseHexAddress(address_str); + if (!addr_or.ok()) { + return addr_or.status(); + } + uint32_t address = addr_or.value(); + + // Validate range + if (address + length > rom_context->size()) { + return absl::OutOfRangeError( + absl::StrFormat("Read beyond ROM: 0x%X+%d > %zu", + address, length, rom_context->size())); + } + + // Read and format data + const uint8_t* data = rom_context->data() + address; + std::string formatted = FormatHexBytes(data, length, format); + + // Output + std::cout << absl::StrFormat("Address 0x%06X [%d bytes]:\n", address, length); + std::cout << formatted << std::endl; + + return absl::OkStatus(); +} + +absl::Status HandleHexWrite(const std::vector& args, + Rom* rom_context) { + if (!rom_context || !rom_context->is_loaded()) { + return absl::FailedPreconditionError("ROM not loaded"); + } + + // Parse arguments + std::string address_str; + std::string data_str; + + for (const auto& arg : args) { + if (arg.rfind("--address=", 0) == 0) { + address_str = arg.substr(10); + } else if (arg.rfind("--data=", 0) == 0) { + data_str = arg.substr(7); + } + } + + if (address_str.empty() || data_str.empty()) { + return absl::InvalidArgumentError("--address and --data required"); + } + + // Parse address + auto addr_or = ParseHexAddress(address_str); + if (!addr_or.ok()) { + return addr_or.status(); + } + uint32_t address = addr_or.value(); + + // Parse data bytes + std::vector byte_strs = absl::StrSplit(data_str, ' '); + std::vector bytes; + + for (const auto& byte_str : byte_strs) { + if (byte_str.empty()) continue; + try { + uint8_t value = static_cast(std::stoul(byte_str, nullptr, 16)); + bytes.push_back(value); + } catch (const std::exception& e) { + return absl::InvalidArgumentError( + absl::StrFormat("Invalid byte '%s': %s", byte_str, e.what())); + } + } + + if (bytes.empty()) { + return absl::InvalidArgumentError("No valid bytes to write"); + } + + // Validate range + if (address + bytes.size() > rom_context->size()) { + return absl::OutOfRangeError( + absl::StrFormat("Write beyond ROM: 0x%X+%zu > %zu", + address, bytes.size(), rom_context->size())); + } + + // Write data + for (size_t i = 0; i < bytes.size(); ++i) { + rom_context->WriteByte(address + i, bytes[i]); + } + + // Output confirmation + std::cout << absl::StrFormat("✓ Wrote %zu bytes to address 0x%06X\n", + bytes.size(), address); + std::cout << " Data: " << FormatHexBytes(bytes.data(), bytes.size(), "hex") + << std::endl; + + // Note: In a full implementation, this would create a proposal + std::cout << "Note: Changes written directly to ROM (proposal system TBD)\n"; + + return absl::OkStatus(); +} + +absl::Status HandleHexSearch(const std::vector& args, + Rom* rom_context) { + if (!rom_context || !rom_context->is_loaded()) { + return absl::FailedPreconditionError("ROM not loaded"); + } + + // Parse arguments + std::string pattern_str; + uint32_t start_address = 0; + uint32_t end_address = rom_context->size(); + + for (const auto& arg : args) { + if (arg.rfind("--pattern=", 0) == 0) { + pattern_str = arg.substr(10); + } else if (arg.rfind("--start=", 0) == 0) { + auto addr_or = ParseHexAddress(arg.substr(8)); + if (addr_or.ok()) { + start_address = addr_or.value(); + } + } else if (arg.rfind("--end=", 0) == 0) { + auto addr_or = ParseHexAddress(arg.substr(6)); + if (addr_or.ok()) { + end_address = addr_or.value(); + } + } + } + + if (pattern_str.empty()) { + return absl::InvalidArgumentError("--pattern required"); + } + + // Parse pattern + auto pattern = ParseHexPattern(pattern_str); + if (pattern.empty()) { + return absl::InvalidArgumentError("Empty or invalid pattern"); + } + + // Search for pattern + std::vector matches; + const uint8_t* rom_data = rom_context->data(); + + for (uint32_t i = start_address; i <= end_address - pattern.size(); ++i) { + bool match = true; + for (size_t j = 0; j < pattern.size(); ++j) { + if (pattern[j].second && // If not wildcard + rom_data[i + j] != pattern[j].first) { + match = false; + break; + } + } + if (match) { + matches.push_back(i); + } + } + + // Output results + std::cout << absl::StrFormat("Pattern: %s\n", pattern_str); + std::cout << absl::StrFormat("Search range: 0x%06X - 0x%06X\n", + start_address, end_address); + std::cout << absl::StrFormat("Found %zu match(es):\n", matches.size()); + + for (size_t i = 0; i < matches.size() && i < 100; ++i) { // Limit output + uint32_t addr = matches[i]; + std::string context = FormatHexBytes(rom_data + addr, pattern.size(), "hex"); + std::cout << absl::StrFormat(" 0x%06X: %s\n", addr, context); + } + + if (matches.size() > 100) { + std::cout << absl::StrFormat(" ... and %zu more matches\n", + matches.size() - 100); + } + + return absl::OkStatus(); +} + +} // namespace agent +} // namespace cli +} // namespace yaze diff --git a/src/cli/handlers/agent/hex_commands.h b/src/cli/handlers/agent/hex_commands.h new file mode 100644 index 00000000..6fddb5e1 --- /dev/null +++ b/src/cli/handlers/agent/hex_commands.h @@ -0,0 +1,55 @@ +#ifndef YAZE_CLI_HANDLERS_AGENT_HEX_COMMANDS_H_ +#define YAZE_CLI_HANDLERS_AGENT_HEX_COMMANDS_H_ + +#include +#include + +#include "absl/status/status.h" + +namespace yaze { +class Rom; + +namespace cli { +namespace agent { + +/** + * @brief Read bytes from ROM at a specific address + * + * @param args Command arguments: [address, length, format] + * @param rom_context ROM instance to read from + * @return absl::Status Result of the operation + * + * Example: hex-read --address=0x1C800 --length=16 --format=both + */ +absl::Status HandleHexRead(const std::vector& args, + Rom* rom_context = nullptr); + +/** + * @brief Write bytes to ROM at a specific address (creates proposal) + * + * @param args Command arguments: [address, data] + * @param rom_context ROM instance to write to + * @return absl::Status Result of the operation + * + * Example: hex-write --address=0x1C800 --data="FF 00 12 34" + */ +absl::Status HandleHexWrite(const std::vector& args, + Rom* rom_context = nullptr); + +/** + * @brief Search for a byte pattern in ROM + * + * @param args Command arguments: [pattern, start_address, end_address] + * @param rom_context ROM instance to search in + * @return absl::Status Result of the operation + * + * Example: hex-search --pattern="FF 00 ?? 12" --start=0x00000 + */ +absl::Status HandleHexSearch(const std::vector& args, + Rom* rom_context = nullptr); + +} // namespace agent +} // namespace cli +} // namespace yaze + +#endif // YAZE_CLI_HANDLERS_AGENT_HEX_COMMANDS_H_ diff --git a/src/cli/handlers/agent/palette_commands.cc b/src/cli/handlers/agent/palette_commands.cc new file mode 100644 index 00000000..96f92029 --- /dev/null +++ b/src/cli/handlers/agent/palette_commands.cc @@ -0,0 +1,344 @@ +#include "cli/handlers/agent/palette_commands.h" + +#include +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/strings/str_format.h" +#include "app/gfx/snes_palette.h" +#include "app/rom.h" + +namespace yaze { +namespace cli { +namespace agent { + +namespace { + +// Convert SNES color to RGB +struct RGB { + uint8_t r, g, b; +}; + +RGB SnesColorToRGB(uint16_t snes_color) { + // SNES color format: 0bbbbbgggggrrrrr (5 bits per channel) + uint8_t r = (snes_color & 0x1F) << 3; + uint8_t g = ((snes_color >> 5) & 0x1F) << 3; + uint8_t b = ((snes_color >> 10) & 0x1F) << 3; + return {r, g, b}; +} + +// Convert RGB to SNES color +uint16_t RGBToSnesColor(uint8_t r, uint8_t g, uint8_t b) { + return ((r >> 3) & 0x1F) | (((g >> 3) & 0x1F) << 5) | (((b >> 3) & 0x1F) << 10); +} + +// Parse hex color (supports #RRGGBB or RRGGBB) +absl::StatusOr ParseHexColor(const std::string& str) { + std::string clean = str; + if (!clean.empty() && clean[0] == '#') { + clean = clean.substr(1); + } + + if (clean.length() != 6) { + return absl::InvalidArgumentError( + absl::StrFormat("Invalid hex color format: %s (expected 6 hex digits)", str)); + } + + try { + unsigned long value = std::stoul(clean, nullptr, 16); + RGB rgb; + rgb.r = (value >> 16) & 0xFF; + rgb.g = (value >> 8) & 0xFF; + rgb.b = value & 0xFF; + return rgb; + } catch (const std::exception& e) { + return absl::InvalidArgumentError( + absl::StrFormat("Failed to parse hex color '%s': %s", str, e.what())); + } +} + +// Format color based on requested format +std::string FormatColor(uint16_t snes_color, const std::string& format) { + if (format == "snes") { + return absl::StrFormat("$%04X", snes_color); + } + + RGB rgb = SnesColorToRGB(snes_color); + + if (format == "rgb") { + return absl::StrFormat("rgb(%d, %d, %d)", rgb.r, rgb.g, rgb.b); + } + + // Default to hex + return absl::StrFormat("#%02X%02X%02X", rgb.r, rgb.g, rgb.b); +} + +} // namespace + +absl::Status HandlePaletteGetColors(const std::vector& args, + Rom* rom_context) { + if (!rom_context || !rom_context->is_loaded()) { + return absl::FailedPreconditionError("ROM not loaded"); + } + + // Parse arguments + int group = -1; + int palette = -1; + std::string format = "hex"; + + for (const auto& arg : args) { + if (arg.rfind("--group=", 0) == 0) { + group = std::stoi(arg.substr(8)); + } else if (arg.rfind("--palette=", 0) == 0) { + palette = std::stoi(arg.substr(10)); + } else if (arg.rfind("--format=", 0) == 0) { + format = arg.substr(9); + } + } + + if (group < 0 || palette < 0) { + return absl::InvalidArgumentError("--group and --palette required"); + } + + // Validate indices + if (palette > 7) { + return absl::OutOfRangeError( + absl::StrFormat("Palette index %d out of range (0-7)", palette)); + } + + // Calculate palette address in ROM + // ALTTP palettes are stored at different locations depending on type + // For now, use a simplified overworld palette calculation + constexpr uint32_t kPaletteBase = 0xDE6C8; // Overworld palettes start + constexpr uint32_t kColorsPerPalette = 16; + constexpr uint32_t kBytesPerColor = 2; + + uint32_t palette_addr = kPaletteBase + + (group * 8 * kColorsPerPalette * kBytesPerColor) + + (palette * kColorsPerPalette * kBytesPerColor); + + if (palette_addr + (kColorsPerPalette * kBytesPerColor) > rom_context->size()) { + return absl::OutOfRangeError( + absl::StrFormat("Palette address 0x%X beyond ROM", palette_addr)); + } + + // Read palette colors + std::cout << absl::StrFormat("Palette Group %d, Palette %d:\n", group, palette); + std::cout << absl::StrFormat("ROM Address: 0x%06X\n\n", palette_addr); + + for (int i = 0; i < kColorsPerPalette; ++i) { + uint32_t color_addr = palette_addr + (i * kBytesPerColor); + auto snes_color_or = rom_context->ReadWord(color_addr); + if (!snes_color_or.ok()) { + return snes_color_or.status(); + } + uint16_t snes_color = snes_color_or.value(); + + std::string formatted = FormatColor(snes_color, format); + std::cout << absl::StrFormat(" Color %2d: %s", i, formatted); + + // Show all formats for first color as example + if (i == 0) { + std::cout << absl::StrFormat(" (SNES: $%04X, RGB: %s, HEX: %s)", + snes_color, + FormatColor(snes_color, "rgb"), + FormatColor(snes_color, "hex")); + } + std::cout << "\n"; + } + + return absl::OkStatus(); +} + +absl::Status HandlePaletteSetColor(const std::vector& args, + Rom* rom_context) { + if (!rom_context || !rom_context->is_loaded()) { + return absl::FailedPreconditionError("ROM not loaded"); + } + + // Parse arguments + int group = -1; + int palette = -1; + int color_index = -1; + std::string color_str; + + for (const auto& arg : args) { + if (arg.rfind("--group=", 0) == 0) { + group = std::stoi(arg.substr(8)); + } else if (arg.rfind("--palette=", 0) == 0) { + palette = std::stoi(arg.substr(10)); + } else if (arg.rfind("--index=", 0) == 0) { + color_index = std::stoi(arg.substr(8)); + } else if (arg.rfind("--color=", 0) == 0) { + color_str = arg.substr(8); + } + } + + if (group < 0 || palette < 0 || color_index < 0 || color_str.empty()) { + return absl::InvalidArgumentError( + "--group, --palette, --index, and --color required"); + } + + // Validate indices + if (palette > 7) { + return absl::OutOfRangeError( + absl::StrFormat("Palette index %d out of range (0-7)", palette)); + } + if (color_index >= 16) { + return absl::OutOfRangeError( + absl::StrFormat("Color index %d out of range (0-15)", color_index)); + } + + // Parse color + auto rgb_or = ParseHexColor(color_str); + if (!rgb_or.ok()) { + return rgb_or.status(); + } + RGB rgb = rgb_or.value(); + uint16_t snes_color = RGBToSnesColor(rgb.r, rgb.g, rgb.b); + + // Calculate address + constexpr uint32_t kPaletteBase = 0xDE6C8; + constexpr uint32_t kColorsPerPalette = 16; + constexpr uint32_t kBytesPerColor = 2; + + uint32_t color_addr = kPaletteBase + + (group * 8 * kColorsPerPalette * kBytesPerColor) + + (palette * kColorsPerPalette * kBytesPerColor) + + (color_index * kBytesPerColor); + + if (color_addr + kBytesPerColor > rom_context->size()) { + return absl::OutOfRangeError( + absl::StrFormat("Color address 0x%X beyond ROM", color_addr)); + } + + // Read old value + auto old_color_or = rom_context->ReadWord(color_addr); + if (!old_color_or.ok()) { + return old_color_or.status(); + } + uint16_t old_color = old_color_or.value(); + + // Write new value + auto write_status = rom_context->WriteWord(color_addr, snes_color); + if (!write_status.ok()) { + return write_status; + } + + // Output confirmation + std::cout << absl::StrFormat("✓ Set color in Palette %d/%d, Index %d\n", + group, palette, color_index); + std::cout << absl::StrFormat(" Address: 0x%06X\n", color_addr); + std::cout << absl::StrFormat(" Old: %s\n", FormatColor(old_color, "hex")); + std::cout << absl::StrFormat(" New: %s (SNES: $%04X)\n", + FormatColor(snes_color, "hex"), snes_color); + std::cout << "Note: Changes written directly to ROM (proposal system TBD)\n"; + + return absl::OkStatus(); +} + +absl::Status HandlePaletteAnalyze(const std::vector& args, + Rom* rom_context) { + if (!rom_context || !rom_context->is_loaded()) { + return absl::FailedPreconditionError("ROM not loaded"); + } + + // Parse arguments + std::string target_type; + std::string target_id; + + for (const auto& arg : args) { + if (arg.rfind("--type=", 0) == 0) { + target_type = arg.substr(7); + } else if (arg.rfind("--id=", 0) == 0) { + target_id = arg.substr(5); + } + } + + if (target_type.empty() || target_id.empty()) { + return absl::InvalidArgumentError("--type and --id required"); + } + + if (target_type == "palette") { + // Parse palette ID (assume format "group/palette") + size_t slash_pos = target_id.find('/'); + if (slash_pos == std::string::npos) { + return absl::InvalidArgumentError( + "Palette ID format should be 'group/palette' (e.g., '0/0')"); + } + + int group = std::stoi(target_id.substr(0, slash_pos)); + int palette = std::stoi(target_id.substr(slash_pos + 1)); + + // Read palette + constexpr uint32_t kPaletteBase = 0xDE6C8; + constexpr uint32_t kColorsPerPalette = 16; + constexpr uint32_t kBytesPerColor = 2; + + uint32_t palette_addr = kPaletteBase + + (group * 8 * kColorsPerPalette * kBytesPerColor) + + (palette * kColorsPerPalette * kBytesPerColor); + + // Analyze colors + std::map color_usage; + int transparent_count = 0; + int darkest = 0xFFFF, brightest = 0; + + for (int i = 0; i < kColorsPerPalette; ++i) { + uint32_t color_addr = palette_addr + (i * kBytesPerColor); + auto snes_color_or = rom_context->ReadWord(color_addr); + if (!snes_color_or.ok()) { + return snes_color_or.status(); + } + uint16_t snes_color = snes_color_or.value(); + + color_usage[snes_color]++; + + if (snes_color == 0) { + transparent_count++; + } + + // Calculate brightness (simple sum of RGB components) + RGB rgb = SnesColorToRGB(snes_color); + int brightness = rgb.r + rgb.g + rgb.b; + if (brightness < (((darkest & 0x1F) + ((darkest >> 5) & 0x1F) + ((darkest >> 10) & 0x1F)) * 8)) { + darkest = snes_color; + } + if (brightness > (((brightest & 0x1F) + ((brightest >> 5) & 0x1F) + ((brightest >> 10) & 0x1F)) * 8)) { + brightest = snes_color; + } + } + + // Output analysis + std::cout << absl::StrFormat("Palette Analysis: Group %d, Palette %d\n", group, palette); + std::cout << absl::StrFormat("Address: 0x%06X\n\n", palette_addr); + std::cout << absl::StrFormat("Total colors: %d\n", kColorsPerPalette); + std::cout << absl::StrFormat("Unique colors: %zu\n", color_usage.size()); + std::cout << absl::StrFormat("Transparent/black (0): %d\n", transparent_count); + std::cout << absl::StrFormat("Darkest color: %s\n", FormatColor(darkest, "hex")); + std::cout << absl::StrFormat("Brightest color: %s\n", FormatColor(brightest, "hex")); + + if (color_usage.size() < kColorsPerPalette) { + std::cout << "\nDuplicate colors found:\n"; + for (const auto& [color, count] : color_usage) { + if (count > 1) { + std::cout << absl::StrFormat(" %s appears %d times\n", + FormatColor(color, "hex"), count); + } + } + } + + } else { + return absl::UnimplementedError( + absl::StrFormat("Analysis for type '%s' not yet implemented", target_type)); + } + + return absl::OkStatus(); +} + +} // namespace agent +} // namespace cli +} // namespace yaze diff --git a/src/cli/handlers/agent/palette_commands.h b/src/cli/handlers/agent/palette_commands.h new file mode 100644 index 00000000..edd32077 --- /dev/null +++ b/src/cli/handlers/agent/palette_commands.h @@ -0,0 +1,55 @@ +#ifndef YAZE_CLI_HANDLERS_AGENT_PALETTE_COMMANDS_H_ +#define YAZE_CLI_HANDLERS_AGENT_PALETTE_COMMANDS_H_ + +#include +#include + +#include "absl/status/status.h" + +namespace yaze { +class Rom; + +namespace cli { +namespace agent { + +/** + * @brief Get all colors from a specific palette + * + * @param args Command arguments: [group, palette, format] + * @param rom_context ROM instance to read from + * @return absl::Status Result of the operation + * + * Example: palette-get-colors --group=0 --palette=0 --format=hex + */ +absl::Status HandlePaletteGetColors(const std::vector& args, + Rom* rom_context = nullptr); + +/** + * @brief Set a specific color in a palette (creates proposal) + * + * @param args Command arguments: [group, palette, color_index, color] + * @param rom_context ROM instance to modify + * @return absl::Status Result of the operation + * + * Example: palette-set-color --group=0 --palette=0 --index=5 --color=FF0000 + */ +absl::Status HandlePaletteSetColor(const std::vector& args, + Rom* rom_context = nullptr); + +/** + * @brief Analyze color usage and statistics for a palette or bitmap + * + * @param args Command arguments: [target_type, target_id] + * @param rom_context ROM instance to analyze + * @return absl::Status Result of the operation + * + * Example: palette-analyze --type=palette --id=0 + */ +absl::Status HandlePaletteAnalyze(const std::vector& args, + Rom* rom_context = nullptr); + +} // namespace agent +} // namespace cli +} // namespace yaze + +#endif // YAZE_CLI_HANDLERS_AGENT_PALETTE_COMMANDS_H_ diff --git a/src/cli/service/agent/vim_mode.cc b/src/cli/service/agent/vim_mode.cc new file mode 100644 index 00000000..c01b4b8d --- /dev/null +++ b/src/cli/service/agent/vim_mode.cc @@ -0,0 +1,452 @@ +#include "cli/service/agent/vim_mode.h" + +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +namespace yaze { +namespace cli { +namespace agent { + +namespace { + +// Key codes for special keys +constexpr int KEY_ESC = 27; +constexpr int KEY_ENTER = 10; +constexpr int KEY_BACKSPACE = 127; +constexpr int KEY_CTRL_P = 16; +constexpr int KEY_CTRL_N = 14; +constexpr int KEY_TAB = 9; + +// Terminal control sequences +const char* CLEAR_LINE = "\033[2K\r"; +const char* MOVE_CURSOR_HOME = "\r"; +const char* SAVE_CURSOR = "\033[s"; +const char* RESTORE_CURSOR = "\033[u"; + +#ifndef _WIN32 +// Set terminal to raw mode (character-by-character input) +void SetRawMode(bool enable) { + static struct termios orig_termios; + static bool has_orig = false; + + if (enable) { + if (!has_orig) { + tcgetattr(STDIN_FILENO, &orig_termios); + has_orig = true; + } + + struct termios raw = orig_termios; + raw.c_lflag &= ~(ECHO | ICANON); // Disable echo and canonical mode + raw.c_cc[VMIN] = 1; // Read at least 1 character + raw.c_cc[VTIME] = 0; // No timeout + tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw); + } else { + if (has_orig) { + tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios); + } + } +} +#endif + +} // namespace + +VimMode::VimMode() { +#ifndef _WIN32 + SetRawMode(true); +#endif +} + +std::string VimMode::GetModeString() const { + switch (mode_) { + case VimModeType::NORMAL: + return "NORMAL"; + case VimModeType::INSERT: + return "INSERT"; + case VimModeType::VISUAL: + return "VISUAL"; + case VimModeType::COMMAND_LINE: + return "COMMAND"; + } + return "UNKNOWN"; +} + +void VimMode::Reset() { + current_line_.clear(); + cursor_pos_ = 0; + line_complete_ = false; + redo_stack_.clear(); +} + +void VimMode::AddToHistory(const std::string& line) { + if (!line.empty()) { + history_.push_back(line); + history_index_ = -1; // Reset to no history selection + } +} + +bool VimMode::ProcessKey(int ch) { + line_complete_ = false; + + switch (mode_) { + case VimModeType::NORMAL: + HandleNormalMode(ch); + break; + case VimModeType::INSERT: + HandleInsertMode(ch); + break; + case VimModeType::VISUAL: + // Visual mode not yet implemented + SwitchMode(VimModeType::NORMAL); + break; + case VimModeType::COMMAND_LINE: + // Command line mode not yet implemented + if (ch == KEY_ESC) { + SwitchMode(VimModeType::NORMAL); + } + break; + } + + return line_complete_; +} + +void VimMode::SwitchMode(VimModeType new_mode) { + if (new_mode != mode_) { + mode_ = new_mode; + Render(); // Redraw with new mode indicator + } +} + +void VimMode::HandleNormalMode(int ch) { + switch (ch) { + // Enter insert mode + case 'i': + SwitchMode(VimModeType::INSERT); + break; + case 'a': + MoveRight(); + SwitchMode(VimModeType::INSERT); + break; + case 'o': + // Insert new line below (just append and go to insert mode) + MoveToLineEnd(); + SwitchMode(VimModeType::INSERT); + break; + case 'O': + // Insert new line above (go to beginning and insert mode) + MoveToLineStart(); + SwitchMode(VimModeType::INSERT); + break; + + // Movement + case 'h': + MoveLeft(); + break; + case 'l': + MoveRight(); + break; + case 'w': + MoveWordForward(); + break; + case 'b': + MoveWordBackward(); + break; + case '0': + MoveToLineStart(); + break; + case '$': + MoveToLineEnd(); + break; + + // Editing + case 'x': + DeleteChar(); + break; + case 'd': + // Simple implementation: dd deletes line + { + int next_ch; +#ifdef _WIN32 + next_ch = _getch(); +#else + read(STDIN_FILENO, &next_ch, 1); +#endif + if (next_ch == 'd') { + DeleteLine(); + } + } + break; + case 'y': + // yy yanks line + { + int next_ch; +#ifdef _WIN32 + next_ch = _getch(); +#else + read(STDIN_FILENO, &next_ch, 1); +#endif + if (next_ch == 'y') { + YankLine(); + } + } + break; + case 'p': + PasteAfter(); + break; + case 'P': + PasteBefore(); + break; + case 'u': + Undo(); + break; + case KEY_CTRL_P: + case 'k': + HistoryPrev(); + break; + case KEY_CTRL_N: + case 'j': + HistoryNext(); + break; + + // Accept line (Enter in normal mode) + case KEY_ENTER: + line_complete_ = true; + break; + + // Command mode + case ':': + SwitchMode(VimModeType::COMMAND_LINE); + break; + } + + Render(); +} + +void VimMode::HandleInsertMode(int ch) { + switch (ch) { + case KEY_ESC: + SwitchMode(VimModeType::NORMAL); + if (cursor_pos_ > 0) { + cursor_pos_--; // Vim moves cursor left on ESC + } + break; + case KEY_ENTER: + line_complete_ = true; + break; + case KEY_BACKSPACE: + case 8: // Ctrl+H + Backspace(); + break; + case KEY_TAB: + Complete(); + break; + case KEY_CTRL_P: + HistoryPrev(); + break; + case KEY_CTRL_N: + HistoryNext(); + break; + default: + if (ch >= 32 && ch < 127) { // Printable ASCII + InsertChar(static_cast(ch)); + } + break; + } + + if (!line_complete_) { + Render(); + } +} + +// Movement implementations +void VimMode::MoveLeft() { + if (cursor_pos_ > 0) { + cursor_pos_--; + } +} + +void VimMode::MoveRight() { + if (cursor_pos_ < static_cast(current_line_.length())) { + cursor_pos_++; + } +} + +void VimMode::MoveWordForward() { + while (cursor_pos_ < static_cast(current_line_.length()) && + !std::isspace(current_line_[cursor_pos_])) { + cursor_pos_++; + } + while (cursor_pos_ < static_cast(current_line_.length()) && + std::isspace(current_line_[cursor_pos_])) { + cursor_pos_++; + } +} + +void VimMode::MoveWordBackward() { + if (cursor_pos_ > 0) cursor_pos_--; + while (cursor_pos_ > 0 && std::isspace(current_line_[cursor_pos_])) { + cursor_pos_--; + } + while (cursor_pos_ > 0 && !std::isspace(current_line_[cursor_pos_ - 1])) { + cursor_pos_--; + } +} + +void VimMode::MoveToLineStart() { + cursor_pos_ = 0; +} + +void VimMode::MoveToLineEnd() { + cursor_pos_ = current_line_.length(); +} + +// Editing implementations +void VimMode::DeleteChar() { + if (cursor_pos_ < static_cast(current_line_.length())) { + undo_stack_.push_back(current_line_); + current_line_.erase(cursor_pos_, 1); + } +} + +void VimMode::DeleteLine() { + undo_stack_.push_back(current_line_); + yank_buffer_ = current_line_; + current_line_.clear(); + cursor_pos_ = 0; +} + +void VimMode::YankLine() { + yank_buffer_ = current_line_; +} + +void VimMode::PasteBefore() { + if (!yank_buffer_.empty()) { + undo_stack_.push_back(current_line_); + current_line_.insert(cursor_pos_, yank_buffer_); + } +} + +void VimMode::PasteAfter() { + if (!yank_buffer_.empty()) { + undo_stack_.push_back(current_line_); + if (cursor_pos_ < static_cast(current_line_.length())) { + cursor_pos_++; + } + current_line_.insert(cursor_pos_, yank_buffer_); + cursor_pos_ += yank_buffer_.length() - 1; + } +} + +void VimMode::Undo() { + if (!undo_stack_.empty()) { + redo_stack_.push_back(current_line_); + current_line_ = undo_stack_.back(); + undo_stack_.pop_back(); + cursor_pos_ = std::min(cursor_pos_, static_cast(current_line_.length())); + } +} + +void VimMode::Redo() { + if (!redo_stack_.empty()) { + undo_stack_.push_back(current_line_); + current_line_ = redo_stack_.back(); + redo_stack_.pop_back(); + cursor_pos_ = std::min(cursor_pos_, static_cast(current_line_.length())); + } +} + +void VimMode::InsertChar(char c) { + undo_stack_.push_back(current_line_); + current_line_.insert(cursor_pos_, 1, c); + cursor_pos_++; +} + +void VimMode::Backspace() { + if (cursor_pos_ > 0) { + undo_stack_.push_back(current_line_); + current_line_.erase(cursor_pos_ - 1, 1); + cursor_pos_--; + } +} + +void VimMode::Delete() { + DeleteChar(); +} + +void VimMode::Complete() { + if (autocomplete_callback_) { + autocomplete_options_ = autocomplete_callback_(current_line_); + if (!autocomplete_options_.empty()) { + // Simple implementation: insert first suggestion + std::string completion = autocomplete_options_[0]; + undo_stack_.push_back(current_line_); + current_line_ = completion; + cursor_pos_ = completion.length(); + } + } +} + +// History navigation +void VimMode::HistoryPrev() { + if (history_.empty()) return; + + if (history_index_ == -1) { + history_index_ = history_.size() - 1; + } else if (history_index_ > 0) { + history_index_--; + } + + current_line_ = history_[history_index_]; + cursor_pos_ = current_line_.length(); +} + +void VimMode::HistoryNext() { + if (history_.empty() || history_index_ == -1) return; + + if (history_index_ < static_cast(history_.size()) - 1) { + history_index_++; + current_line_ = history_[history_index_]; + } else { + history_index_ = -1; + current_line_.clear(); + } + + cursor_pos_ = current_line_.length(); +} + +void VimMode::Render() const { + // Clear line and redraw + std::cout << CLEAR_LINE; + + // Show mode indicator + if (mode_ == VimModeType::INSERT) { + std::cout << "-- INSERT -- "; + } else if (mode_ == VimModeType::NORMAL) { + std::cout << "-- NORMAL -- "; + } else if (mode_ == VimModeType::COMMAND_LINE) { + std::cout << ":"; + } + + // Show prompt and line + std::cout << current_line_; + + // Move cursor to correct position + int display_offset = (mode_ == VimModeType::INSERT ? 13 : 13); // Length of "-- INSERT -- " + std::cout << "\r"; + for (int i = 0; i < display_offset + cursor_pos_; ++i) { + std::cout << "\033[C"; // Move cursor right + } + + std::cout.flush(); +} + +} // namespace agent +} // namespace cli +} // namespace yaze