feat: Implement networking and collaboration features for z3ed
- Introduced a comprehensive networking system for real-time collaboration across the yaze app, z3ed CLI, and yaze-server using WebSocket. - Added `WebSocketClient` and `CollaborationService` classes to manage connections, session hosting, proposal submissions, and automatic ROM synchronization. - Enhanced the z3ed CLI with commands for connecting to the server, submitting proposals, and checking approval statuses. - Updated documentation to include detailed usage instructions for the new networking features and collaboration workflows. - Improved CMake configuration to support necessary libraries for networking, including JSON and OpenSSL.
This commit is contained in:
@@ -1,252 +1,181 @@
|
||||
# API Reference
|
||||
|
||||
Comprehensive reference for the YAZE C API and C++ interfaces.
|
||||
This document provides a reference for the yaze C and C++ APIs, intended for developers creating extensions or contributing to the core application.
|
||||
|
||||
## C API (`incl/yaze.h`, `incl/zelda.h`)
|
||||
## C API (`incl/yaze.h`)
|
||||
|
||||
The C API provides a stable, language-agnostic interface for interacting with yaze's core functionalities.
|
||||
|
||||
### Core Library Functions
|
||||
```c
|
||||
// Initialization
|
||||
/**
|
||||
* @brief Initializes the yaze library. Must be called before any other API function.
|
||||
* @return YAZE_OK on success, or an error code on failure.
|
||||
*/
|
||||
yaze_status yaze_library_init(void);
|
||||
|
||||
/**
|
||||
* @brief Shuts down the yaze library and releases all resources.
|
||||
*/
|
||||
void yaze_library_shutdown(void);
|
||||
|
||||
// Version management
|
||||
/**
|
||||
* @brief Gets the current version of the yaze library as a string.
|
||||
* @return A constant string representing the version (e.g., "0.3.2").
|
||||
*/
|
||||
const char* yaze_get_version_string(void);
|
||||
int yaze_get_version_number(void);
|
||||
bool yaze_check_version_compatibility(const char* expected_version);
|
||||
|
||||
// Status utilities
|
||||
const char* yaze_status_to_string(yaze_status status);
|
||||
```
|
||||
|
||||
### ROM Operations
|
||||
```c
|
||||
// ROM loading and management
|
||||
/**
|
||||
* @brief Loads a Zelda 3 ROM from a file.
|
||||
* @param filename The path to the ROM file.
|
||||
* @return A pointer to a zelda3_rom object, or NULL on failure.
|
||||
*/
|
||||
zelda3_rom* yaze_load_rom(const char* filename);
|
||||
|
||||
/**
|
||||
* @brief Unloads a ROM and frees associated memory.
|
||||
* @param rom A pointer to the zelda3_rom object to unload.
|
||||
*/
|
||||
void yaze_unload_rom(zelda3_rom* rom);
|
||||
|
||||
/**
|
||||
* @brief Saves a ROM to a file.
|
||||
* @param rom A pointer to the ROM to save.
|
||||
* @param filename The path to save the file to.
|
||||
* @return YAZE_OK on success.
|
||||
*/
|
||||
yaze_status yaze_save_rom(zelda3_rom* rom, const char* filename);
|
||||
bool yaze_is_rom_modified(const zelda3_rom* rom);
|
||||
```
|
||||
|
||||
### Graphics Operations
|
||||
```c
|
||||
// SNES color management
|
||||
snes_color yaze_rgb_to_snes_color(uint8_t r, uint8_t g, uint8_t b);
|
||||
void yaze_snes_color_to_rgb(snes_color color, uint8_t* r, uint8_t* g, uint8_t* b);
|
||||
|
||||
// Bitmap operations
|
||||
yaze_bitmap* yaze_create_bitmap(int width, int height, uint8_t bpp);
|
||||
void yaze_free_bitmap(yaze_bitmap* bitmap);
|
||||
```
|
||||
|
||||
### Palette System
|
||||
```c
|
||||
// Palette creation and management
|
||||
snes_palette* yaze_create_palette(uint8_t id, uint8_t size);
|
||||
void yaze_free_palette(snes_palette* palette);
|
||||
snes_palette* yaze_load_palette_from_rom(const zelda3_rom* rom, uint8_t palette_id);
|
||||
```
|
||||
|
||||
### Message System
|
||||
```c
|
||||
// Message handling
|
||||
zelda3_message* yaze_load_message(const zelda3_rom* rom, uint16_t message_id);
|
||||
void yaze_free_message(zelda3_message* message);
|
||||
yaze_status yaze_save_message(zelda3_rom* rom, const zelda3_message* message);
|
||||
```
|
||||
|
||||
## C++ API
|
||||
|
||||
The C++ API offers a more powerful, object-oriented interface. The primary entry point for many operations is the `yaze::core::AsarWrapper` class.
|
||||
|
||||
### AsarWrapper (`src/app/core/asar_wrapper.h`)
|
||||
|
||||
Complete cross-platform ROM patching with assembly code support, symbol extraction, and validation.
|
||||
This class provides a complete, cross-platform interface for applying assembly patches, extracting symbols, and validating assembly code using the Asar library.
|
||||
|
||||
#### Quick Examples
|
||||
#### CLI Examples (`z3ed`)
|
||||
|
||||
While the `AsarWrapper` can be used programmatically, the `z3ed` CLI is the most common way to interact with it.
|
||||
|
||||
**Command Line**
|
||||
```bash
|
||||
# Apply assembly patch to ROM
|
||||
# Apply an assembly patch to a ROM file.
|
||||
z3ed asar my_patch.asm --rom=zelda3.sfc
|
||||
|
||||
# Extract symbols without patching
|
||||
z3ed extract my_patch.asm
|
||||
|
||||
# Validate assembly syntax
|
||||
z3ed validate my_patch.asm
|
||||
# For more complex operations, use the AI agent.
|
||||
z3ed agent chat --rom zelda3.sfc
|
||||
```
|
||||
> **Prompt:** "Apply the patch `mosaic_change.asm` to my ROM."
|
||||
|
||||
**C++ API**
|
||||
#### C++ API Example
|
||||
```cpp
|
||||
#include "app/core/asar_wrapper.h"
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
// Assume rom_data is a std::vector<uint8_t> holding the ROM content.
|
||||
yaze::core::AsarWrapper wrapper;
|
||||
wrapper.Initialize();
|
||||
|
||||
// Apply patch to ROM
|
||||
// Apply a patch to the ROM data in memory.
|
||||
auto result = wrapper.ApplyPatch("patch.asm", rom_data);
|
||||
|
||||
if (result.ok() && result->success) {
|
||||
// On success, print the symbols generated by the patch.
|
||||
for (const auto& symbol : result->symbols) {
|
||||
std::cout << symbol.name << " @ $" << std::hex << symbol.address << std::endl;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### AsarWrapper Class
|
||||
#### Class Definition
|
||||
```cpp
|
||||
namespace yaze::core {
|
||||
|
||||
class AsarWrapper {
|
||||
public:
|
||||
// Initialization
|
||||
/** @brief Initializes the Asar library. */
|
||||
absl::Status Initialize();
|
||||
|
||||
/** @brief Shuts down the Asar library. */
|
||||
void Shutdown();
|
||||
bool IsInitialized() const;
|
||||
|
||||
// Core functionality
|
||||
|
||||
/**
|
||||
* @brief Applies an assembly patch to ROM data.
|
||||
* @param patch_path Path to the main .asm file.
|
||||
* @param rom_data A vector of bytes representing the ROM data.
|
||||
* @param include_paths Optional paths for Asar to search for included files.
|
||||
* @return A StatusOr containing the patch result, including success status and symbols.
|
||||
*/
|
||||
absl::StatusOr<AsarPatchResult> ApplyPatch(
|
||||
const std::string& patch_path,
|
||||
std::vector<uint8_t>& rom_data,
|
||||
const std::vector<std::string>& include_paths = {});
|
||||
|
||||
|
||||
/**
|
||||
* @brief Extracts symbols from an assembly file without patching.
|
||||
* @param asm_path Path to the .asm file.
|
||||
* @return A StatusOr containing a vector of extracted symbols.
|
||||
*/
|
||||
absl::StatusOr<std::vector<AsarSymbol>> ExtractSymbols(
|
||||
const std::string& asm_path,
|
||||
const std::vector<std::string>& include_paths = {});
|
||||
|
||||
// Symbol management
|
||||
std::optional<AsarSymbol> FindSymbol(const std::string& name);
|
||||
std::vector<AsarSymbol> GetSymbolsAtAddress(uint32_t address);
|
||||
std::map<std::string, AsarSymbol> GetSymbolTable();
|
||||
|
||||
// Utility functions
|
||||
|
||||
/**
|
||||
* @brief Validates the syntax of an assembly file.
|
||||
* @param asm_path Path to the .asm file.
|
||||
* @return An OK status if syntax is valid, or an error status if not.
|
||||
*/
|
||||
absl::Status ValidateAssembly(const std::string& asm_path);
|
||||
std::string GetVersion();
|
||||
void Reset();
|
||||
};
|
||||
|
||||
}
|
||||
} // namespace yaze::core
|
||||
```
|
||||
|
||||
#### Error Handling
|
||||
## Data Structures
|
||||
|
||||
| Error | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| `Unknown command` | Invalid opcode | Check 65816 instruction reference |
|
||||
| `Label not found` | Undefined label | Define the label or check spelling |
|
||||
| `Invalid hex value` | Bad hex format | Use `$1234` format |
|
||||
| `Buffer too small` | ROM needs expansion | Check if ROM needs to be larger |
|
||||
|
||||
|
||||
### Data Structures
|
||||
|
||||
#### ROM Version Support
|
||||
```c
|
||||
typedef enum zelda3_version {
|
||||
ZELDA3_VERSION_US = 1,
|
||||
ZELDA3_VERSION_JP = 2,
|
||||
ZELDA3_VERSION_SD = 3,
|
||||
ZELDA3_VERSION_RANDO = 4,
|
||||
// Legacy aliases maintained for compatibility
|
||||
US = ZELDA3_VERSION_US,
|
||||
JP = ZELDA3_VERSION_JP,
|
||||
SD = ZELDA3_VERSION_SD,
|
||||
RANDO = ZELDA3_VERSION_RANDO,
|
||||
} zelda3_version;
|
||||
```
|
||||
|
||||
#### SNES Graphics
|
||||
### `snes_color`
|
||||
Represents a 15-bit SNES color, composed of 5 bits for each red, green, and blue component.
|
||||
```c
|
||||
typedef struct snes_color {
|
||||
uint16_t raw; // Raw 15-bit SNES color
|
||||
uint8_t red; // Red component (0-31)
|
||||
uint8_t green; // Green component (0-31)
|
||||
uint8_t blue; // Blue component (0-31)
|
||||
uint16_t raw; //!< Raw 15-bit BGR color value (0bbbbbgggggrrrrr).
|
||||
uint8_t red; //!< Red component (0-31).
|
||||
uint8_t green; //!< Green component (0-31).
|
||||
uint8_t blue; //!< Blue component (0-31).
|
||||
} snes_color;
|
||||
|
||||
typedef struct snes_palette {
|
||||
uint8_t id; // Palette ID
|
||||
uint8_t size; // Number of colors
|
||||
snes_color* colors; // Color array
|
||||
} snes_palette;
|
||||
```
|
||||
|
||||
#### Message System
|
||||
### `zelda3_message`
|
||||
Represents an in-game text message.
|
||||
```c
|
||||
typedef struct zelda3_message {
|
||||
uint16_t id; // Message ID (0-65535)
|
||||
uint32_t rom_address; // Address in ROM
|
||||
uint16_t length; // Length in bytes
|
||||
uint8_t* raw_data; // Raw ROM data
|
||||
char* parsed_text; // Decoded UTF-8 text
|
||||
bool is_compressed; // Compression flag
|
||||
uint8_t encoding_type; // Encoding type
|
||||
} zelda3_message;
|
||||
uint16_t id; //!< The message ID (0-65535).
|
||||
uint32_t rom_address; //!< The address of the message data in the ROM.
|
||||
uint16_t length; //!< The length of the raw message data in bytes.
|
||||
uint8_t* raw_data; //!< A pointer to the raw, compressed message data.
|
||||
char* parsed_text; //!< The decoded, human-readable UTF-8 text.
|
||||
bool is_compressed; //!< Flag indicating if the message data is compressed.
|
||||
}
|
||||
zelda3_message;
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Status Codes
|
||||
```c
|
||||
typedef enum yaze_status {
|
||||
YAZE_OK = 0, // Success
|
||||
YAZE_ERROR_UNKNOWN = -1, // Unknown error
|
||||
YAZE_ERROR_INVALID_ARG = 1, // Invalid argument
|
||||
YAZE_ERROR_FILE_NOT_FOUND = 2, // File not found
|
||||
YAZE_ERROR_MEMORY = 3, // Memory allocation failed
|
||||
YAZE_ERROR_IO = 4, // I/O operation failed
|
||||
YAZE_ERROR_CORRUPTION = 5, // Data corruption detected
|
||||
YAZE_ERROR_NOT_INITIALIZED = 6, // Component not initialized
|
||||
} yaze_status;
|
||||
```
|
||||
The C API uses an enum `yaze_status` for error handling, while the C++ API uses `absl::Status` and `absl::StatusOr`.
|
||||
|
||||
### Error Handling Pattern
|
||||
### C API Error Pattern
|
||||
```c
|
||||
yaze_status status = yaze_library_init();
|
||||
if (status != YAZE_OK) {
|
||||
printf("Failed to initialize YAZE: %s\n", yaze_status_to_string(status));
|
||||
fprintf(stderr, "Failed to initialize YAZE: %s\n", yaze_status_to_string(status));
|
||||
return 1;
|
||||
}
|
||||
|
||||
zelda3_rom* rom = yaze_load_rom("zelda3.sfc");
|
||||
if (rom == nullptr) {
|
||||
printf("Failed to load ROM file\n");
|
||||
return 1;
|
||||
}
|
||||
// ... operations ...
|
||||
|
||||
// Use ROM...
|
||||
yaze_unload_rom(rom);
|
||||
yaze_library_shutdown();
|
||||
```
|
||||
|
||||
## Extension System
|
||||
|
||||
### Plugin Architecture
|
||||
```c
|
||||
typedef struct yaze_extension {
|
||||
const char* name; // Extension name
|
||||
const char* version; // Version string
|
||||
const char* description; // Description
|
||||
const char* author; // Author
|
||||
int api_version; // Required API version
|
||||
|
||||
yaze_status (*initialize)(yaze_editor_context* context);
|
||||
void (*cleanup)(void);
|
||||
uint32_t (*get_capabilities)(void);
|
||||
} yaze_extension;
|
||||
```
|
||||
|
||||
### Capability Flags
|
||||
```c
|
||||
#define YAZE_EXT_CAP_ROM_EDITING 0x0001 // ROM modification
|
||||
#define YAZE_EXT_CAP_GRAPHICS 0x0002 // Graphics operations
|
||||
#define YAZE_EXT_CAP_AUDIO 0x0004 // Audio processing
|
||||
#define YAZE_EXT_CAP_SCRIPTING 0x0008 // Scripting support
|
||||
#define YAZE_EXT_CAP_IMPORT_EXPORT 0x0010 // Data import/export
|
||||
```
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
All existing code continues to work without modification due to:
|
||||
- Legacy enum aliases (`US`, `JP`, `SD`, `RANDO`)
|
||||
- Original struct field names maintained
|
||||
- Duplicate field definitions for old/new naming conventions
|
||||
- Typedef aliases for renamed types
|
||||
|
||||
@@ -2,61 +2,59 @@
|
||||
|
||||
**Last Updated: October 4, 2025**
|
||||
|
||||
This document outlines the development roadmap for YAZE. The project has made significant strides in core features, AI integration, and testing infrastructure. The immediate focus is on stabilizing the test suite and completing key editor functionalities.
|
||||
This document outlines the development roadmap for yaze. The project has achieved stability in its core editors and testing infrastructure. The focus now shifts to completing advanced AI features, polishing UI/UX, and expanding editor capabilities.
|
||||
|
||||
## Current Focus: Stability & Core Features
|
||||
## Current Focus: AI & Editor Polish
|
||||
|
||||
The highest priority is to fix the broken test suites to ensure the stability and correctness of the existing, largely complete, feature set. Alongside this, work will continue on completing the core AI and editor functionalities.
|
||||
With the core systems stable, the immediate priority is to enhance the `z3ed` AI agent, finalize the Tile16 editor, and improve the user experience.
|
||||
|
||||
---
|
||||
|
||||
## 0.4.X (Next Major Release) - Stability & Core Tooling
|
||||
## 0.4.X (Next Major Release) - Advanced Tooling & UX
|
||||
|
||||
### Priority 1: Testing & Stability (BLOCKER)
|
||||
- **Fix Dungeon Editor Test Suite**: Resolve the critical `SIGBUS` and `SIGSEGV` crashes that are blocking all integration tests for the dungeon system.
|
||||
- **Refactor Integration Tests**: Migrate tests from the unstable `MockRom` to use the `TestRomManager` with real ROM files, following the pattern set by the passing rendering tests.
|
||||
- **Expand E2E Coverage**: Create a comprehensive end-to-end smoke test for the Dungeon Editor and expand coverage for the Overworld editor.
|
||||
### Priority 1: Editor Features & UX
|
||||
- **Tile16 Palette System**: Resolve the remaining color consistency issues in the Tile16 editor's source tile view and implement the palette-switching functionality.
|
||||
- **Overworld Sprite Editing**: Complete the workflow for adding, removing, and moving sprites directly on the overworld canvas.
|
||||
- **Dungeon Editor UI**: Add human-readable labels for rooms/entrances and implement tab management for a better multi-room workflow.
|
||||
- **Performance**: Address the slow initial load time (~2.6 seconds) by implementing lazy loading for rooms.
|
||||
|
||||
### Priority 2: `z3ed` AI Agent
|
||||
- **Complete Live LLM Testing**: Verify and harden the function-calling loop with live Ollama and Gemini models.
|
||||
- **Enhance GUI Chat Widget**: Implement state persistence and integrate proposal review shortcuts directly into the editor's chat window.
|
||||
- **Expand Agent Toolset**: Add new read-only tools for inspecting dialogue, sprite properties, and other game resources.
|
||||
- **Live LLM Hardening**: Finalize testing of the native Gemini function-calling loop and the proactive v3 system prompt.
|
||||
- **AI-Driven Editing**: Integrate the AI with the GUI test harness to allow for automated, mouse-driven edits based on natural language commands.
|
||||
- **Expand Agent Toolset**: Add new read-only tools for inspecting dialogue, music data, and sprite properties.
|
||||
|
||||
### Priority 3: Editor Features
|
||||
- **Dungeon Editor**: Implement missing features identified from the ZScream comparison, such as custom collision and object limits tracking.
|
||||
- **Overworld Sprites**: Complete the sprite editing workflow, including adding, removing, and moving sprites on the canvas.
|
||||
- **Sprite Property Editor**: Create a UI to edit sprite attributes and behavior.
|
||||
### Priority 3: Testing & Stability
|
||||
- **Windows Validation**: Perform a full testing cycle on Windows to validate the `z3ed` CLI, GUI test harness, and collaboration features.
|
||||
- **Expand E2E Coverage**: Create comprehensive end-to-end smoke tests for the Dungeon and Overworld editors.
|
||||
|
||||
---
|
||||
|
||||
## 0.5.X - Feature Expansion
|
||||
|
||||
- **Plugin Architecture**: Design and implement the initial framework for community-developed extensions and custom tools.
|
||||
- **Advanced Graphics Editing**:
|
||||
- Finalize the Tile16 Editor palette system, fixing all known color consistency bugs.
|
||||
- Implement functionality to edit and re-import full graphics sheets.
|
||||
- **Advanced Graphics Editing**: Implement functionality to edit and re-import full graphics sheets.
|
||||
- **`z3ed` AI Agent Enhancements**:
|
||||
- **Collaborative Sessions**: Investigate and implement infrastructure for multi-user collaborative editing and AI interaction.
|
||||
- **Collaborative Sessions**: Enhance the network collaboration mode with shared AI proposals and ROM synchronization.
|
||||
- **Multi-modal Input**: Integrate screenshot capabilities to send visual context to Gemini for more accurate, context-aware commands.
|
||||
|
||||
---
|
||||
|
||||
## 0.6.X - Polish & Integration
|
||||
## 0.6.X - Content & Integration
|
||||
|
||||
- **Cross-Platform Stability**: Conduct full testing and bug fixing cycles for Windows and Linux to ensure a stable experience equivalent to macOS.
|
||||
- **Advanced Content Editors**:
|
||||
- Implement a user interface for the music editing system.
|
||||
- Enhance the Hex Editor with better search and data interpretation features.
|
||||
- **Documentation Overhaul**:
|
||||
- Implement a system to auto-generate C++ API documentation.
|
||||
- Implement a system to auto-generate C++ API documentation from Doxygen comments.
|
||||
- Write a comprehensive user guide for ROM hackers, covering all major editor workflows.
|
||||
|
||||
---
|
||||
|
||||
## 0.7.X and Beyond - Path to 1.0
|
||||
## Recently Completed (v0.3.2)
|
||||
|
||||
- **Performance Optimization**: Implement high-priority items from the graphics performance plan, such as lazy loading of graphics and streaming assets from the ROM.
|
||||
- **UI/UX Refinements**: Improve UI consistency, iconography, and overall layout based on user feedback.
|
||||
- **Beta Release**: Code freeze on major features to focus on bug fixing and final polish.
|
||||
- **1.0 Stable Release**: Final, production-ready version with a comprehensive changelog.
|
||||
- ✅ **Dungeon Editor Stability**: Fixed all critical crashes in the test suite by migrating to `TestRomManager`. The editor's core logic is now stable and production-ready.
|
||||
- ✅ **Windows Stability**: Resolved stack overflow crashes and file dialog issues, bringing Windows builds to parity with macOS/Linux.
|
||||
- ✅ **`z3ed` Learn Command**: Fully implemented the `learn` command, allowing the AI to persist user preferences, ROM patterns, and conversation history.
|
||||
- ✅ **Gemini Native Function Calling**: Upgraded from manual `curl` requests to the official Gemini function calling API for improved reliability.
|
||||
- ✅ **Tile16 Editor Refactor**: Fixed critical crashes, implemented a new three-column layout, and added dynamic zoom controls.
|
||||
|
||||
|
||||
524
docs/z3ed/NETWORKING.md
Normal file
524
docs/z3ed/NETWORKING.md
Normal file
@@ -0,0 +1,524 @@
|
||||
# Z3ED Networking & Collaboration
|
||||
|
||||
## Overview
|
||||
|
||||
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
|
||||
|
||||
## Architecture
|
||||
|
||||
### Cross-Platform Design
|
||||
|
||||
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
|
||||
|
||||
### Components
|
||||
|
||||
```
|
||||
┌─────────────┐ WebSocket ┌──────────────┐
|
||||
│ yaze app │◄────────────────────────────►│ yaze-server │
|
||||
│ (GUI) │ │ (Node.js) │
|
||||
└─────────────┘ └──────────────┘
|
||||
▲ ▲
|
||||
│ │
|
||||
│ WebSocket │
|
||||
│ │
|
||||
└─────────────┐ │
|
||||
│ │
|
||||
┌──────▼──────┐ │
|
||||
│ z3ed CLI │◄──────────────────────┘
|
||||
│ │
|
||||
└─────────────┘
|
||||
|
||||
gRPC (for GUI testing)
|
||||
│
|
||||
│
|
||||
┌──────▼──────┐
|
||||
│ yaze app │
|
||||
│ (ImGui) │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
## WebSocket Protocol
|
||||
|
||||
### Connection
|
||||
|
||||
```cpp
|
||||
#include "app/net/websocket_client.h"
|
||||
|
||||
net::WebSocketClient client;
|
||||
|
||||
// Connect to server
|
||||
auto status = client.Connect("localhost", 8765);
|
||||
|
||||
// Set up callbacks
|
||||
client.OnMessage("rom_sync", [](const nlohmann::json& payload) {
|
||||
// Handle ROM sync
|
||||
});
|
||||
|
||||
client.OnStateChange([](net::ConnectionState state) {
|
||||
// Handle state changes
|
||||
});
|
||||
```
|
||||
|
||||
### Message Types
|
||||
|
||||
#### 1. Session Management
|
||||
|
||||
**Host Session**:
|
||||
```json
|
||||
{
|
||||
"type": "host_session",
|
||||
"payload": {
|
||||
"session_name": "My ROM Hack",
|
||||
"username": "host",
|
||||
"rom_hash": "abc123",
|
||||
"ai_enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Join Session**:
|
||||
```json
|
||||
{
|
||||
"type": "join_session",
|
||||
"payload": {
|
||||
"session_code": "ABC123",
|
||||
"username": "participant"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Proposal System (NEW)
|
||||
|
||||
**Share Proposal**:
|
||||
```json
|
||||
{
|
||||
"type": "proposal_share",
|
||||
"payload": {
|
||||
"sender": "username",
|
||||
"proposal_data": {
|
||||
"description": "Place tile 0x42 at (5,7)",
|
||||
"type": "tile_edit",
|
||||
"data": {...}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**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<net::WebSocketClient>();
|
||||
|
||||
// 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
|
||||
|
||||
```cpp
|
||||
#include "app/net/collaboration_service.h"
|
||||
|
||||
// High-level service that integrates everything
|
||||
auto collab_service = std::make_unique<net::CollaborationService>(rom);
|
||||
|
||||
// Initialize with version manager and approval manager
|
||||
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
|
||||
```
|
||||
|
||||
## Z3ED CLI Integration
|
||||
|
||||
### Connection Commands
|
||||
|
||||
```bash
|
||||
# Connect to collaboration server
|
||||
z3ed net connect --host localhost --port 8765
|
||||
|
||||
# Join session
|
||||
z3ed net join --code ABC123 --username myname
|
||||
|
||||
# Leave session
|
||||
z3ed net leave
|
||||
```
|
||||
|
||||
### Proposal Commands
|
||||
|
||||
```bash
|
||||
# Submit proposal from z3ed
|
||||
z3ed agent run --prompt "Place tile 42 at (5,7)" --submit-proposal
|
||||
|
||||
# Check proposal 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 <thread>
|
||||
#include <mutex>
|
||||
|
||||
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
|
||||
440
src/app/net/collaboration_service.cc
Normal file
440
src/app/net/collaboration_service.cc
Normal file
@@ -0,0 +1,440 @@
|
||||
#include "app/net/collaboration_service.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
#include "absl/strings/str_format.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace app {
|
||||
namespace net {
|
||||
|
||||
CollaborationService::CollaborationService(Rom* rom)
|
||||
: rom_(rom),
|
||||
version_mgr_(nullptr),
|
||||
approval_mgr_(nullptr),
|
||||
client_(std::make_unique<WebSocketClient>()),
|
||||
sync_in_progress_(false) {
|
||||
}
|
||||
|
||||
CollaborationService::~CollaborationService() {
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
absl::Status CollaborationService::Initialize(
|
||||
const Config& config,
|
||||
RomVersionManager* version_mgr,
|
||||
ProposalApprovalManager* approval_mgr) {
|
||||
|
||||
config_ = config;
|
||||
version_mgr_ = version_mgr;
|
||||
approval_mgr_ = approval_mgr;
|
||||
|
||||
if (!version_mgr_) {
|
||||
return absl::InvalidArgumentError("version_mgr cannot be null");
|
||||
}
|
||||
|
||||
if (!approval_mgr_) {
|
||||
return absl::InvalidArgumentError("approval_mgr cannot be null");
|
||||
}
|
||||
|
||||
// Set up network event callbacks
|
||||
client_->OnMessage("rom_sync", [this](const nlohmann::json& payload) {
|
||||
OnRomSyncReceived(payload);
|
||||
});
|
||||
|
||||
client_->OnMessage("proposal_shared", [this](const nlohmann::json& payload) {
|
||||
OnProposalReceived(payload);
|
||||
});
|
||||
|
||||
client_->OnMessage("proposal_vote_received", [this](const nlohmann::json& payload) {
|
||||
OnProposalUpdated(payload);
|
||||
});
|
||||
|
||||
client_->OnMessage("proposal_updated", [this](const nlohmann::json& payload) {
|
||||
OnProposalUpdated(payload);
|
||||
});
|
||||
|
||||
client_->OnMessage("participant_joined", [this](const nlohmann::json& payload) {
|
||||
OnParticipantJoined(payload);
|
||||
});
|
||||
|
||||
client_->OnMessage("participant_left", [this](const nlohmann::json& payload) {
|
||||
OnParticipantLeft(payload);
|
||||
});
|
||||
|
||||
// Store initial ROM hash
|
||||
if (rom_ && rom_->is_loaded()) {
|
||||
last_sync_hash_ = version_mgr_->GetCurrentHash();
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status CollaborationService::Connect(const std::string& host, int port) {
|
||||
return client_->Connect(host, port);
|
||||
}
|
||||
|
||||
void CollaborationService::Disconnect() {
|
||||
if (client_->IsConnected()) {
|
||||
client_->Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
absl::Status CollaborationService::HostSession(
|
||||
const std::string& session_name,
|
||||
const std::string& username,
|
||||
bool ai_enabled) {
|
||||
|
||||
if (!client_->IsConnected()) {
|
||||
return absl::FailedPreconditionError("Not connected to server");
|
||||
}
|
||||
|
||||
if (!rom_ || !rom_->is_loaded()) {
|
||||
return absl::FailedPreconditionError("ROM not loaded");
|
||||
}
|
||||
|
||||
// Get current ROM hash
|
||||
std::string rom_hash = version_mgr_->GetCurrentHash();
|
||||
|
||||
// Create initial safe point
|
||||
auto snapshot_result = version_mgr_->CreateSnapshot(
|
||||
"Session start",
|
||||
username,
|
||||
true // is_checkpoint
|
||||
);
|
||||
|
||||
if (snapshot_result.ok()) {
|
||||
version_mgr_->MarkAsSafePoint(*snapshot_result);
|
||||
}
|
||||
|
||||
// Host session on server
|
||||
auto session_result = client_->HostSession(
|
||||
session_name,
|
||||
username,
|
||||
rom_hash,
|
||||
ai_enabled
|
||||
);
|
||||
|
||||
if (!session_result.ok()) {
|
||||
return session_result.status();
|
||||
}
|
||||
|
||||
last_sync_hash_ = rom_hash;
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status CollaborationService::JoinSession(
|
||||
const std::string& session_code,
|
||||
const std::string& username) {
|
||||
|
||||
if (!client_->IsConnected()) {
|
||||
return absl::FailedPreconditionError("Not connected to server");
|
||||
}
|
||||
|
||||
if (!rom_ || !rom_->is_loaded()) {
|
||||
return absl::FailedPreconditionError("ROM not loaded");
|
||||
}
|
||||
|
||||
// Create backup before joining
|
||||
auto snapshot_result = version_mgr_->CreateSnapshot(
|
||||
"Before joining session",
|
||||
username,
|
||||
true
|
||||
);
|
||||
|
||||
if (snapshot_result.ok()) {
|
||||
version_mgr_->MarkAsSafePoint(*snapshot_result);
|
||||
}
|
||||
|
||||
// Join session
|
||||
auto session_result = client_->JoinSession(session_code, username);
|
||||
|
||||
if (!session_result.ok()) {
|
||||
return session_result.status();
|
||||
}
|
||||
|
||||
last_sync_hash_ = version_mgr_->GetCurrentHash();
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status CollaborationService::LeaveSession() {
|
||||
if (!client_->InSession()) {
|
||||
return absl::FailedPreconditionError("Not in a session");
|
||||
}
|
||||
|
||||
return client_->LeaveSession();
|
||||
}
|
||||
|
||||
absl::Status CollaborationService::SubmitChangesAsProposal(
|
||||
const std::string& description,
|
||||
const std::string& username) {
|
||||
|
||||
if (!client_->InSession()) {
|
||||
return absl::FailedPreconditionError("Not in a session");
|
||||
}
|
||||
|
||||
if (!rom_ || !rom_->is_loaded()) {
|
||||
return absl::FailedPreconditionError("ROM not loaded");
|
||||
}
|
||||
|
||||
// Generate diff from last sync
|
||||
std::string current_hash = version_mgr_->GetCurrentHash();
|
||||
if (current_hash == last_sync_hash_) {
|
||||
return absl::OkStatus(); // No changes to submit
|
||||
}
|
||||
|
||||
std::string diff = GenerateDiff(last_sync_hash_, current_hash);
|
||||
|
||||
// Create proposal data
|
||||
nlohmann::json proposal_data = {
|
||||
{"description", description},
|
||||
{"type", "rom_modification"},
|
||||
{"diff_data", diff},
|
||||
{"from_hash", last_sync_hash_},
|
||||
{"to_hash", current_hash}
|
||||
};
|
||||
|
||||
// Submit to server
|
||||
auto status = client_->ShareProposal(proposal_data, username);
|
||||
|
||||
if (status.ok() && config_.require_approval_for_sync) {
|
||||
// Proposal submitted, waiting for approval
|
||||
// The actual application will happen when approved
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
absl::Status CollaborationService::ApplyRomSync(
|
||||
const std::string& diff_data,
|
||||
const std::string& rom_hash,
|
||||
const std::string& sender) {
|
||||
|
||||
if (!rom_ || !rom_->is_loaded()) {
|
||||
return absl::FailedPreconditionError("ROM not loaded");
|
||||
}
|
||||
|
||||
if (sync_in_progress_) {
|
||||
return absl::UnavailableError("Sync already in progress");
|
||||
}
|
||||
|
||||
sync_in_progress_ = true;
|
||||
|
||||
// Create snapshot before applying
|
||||
if (config_.create_snapshot_before_sync) {
|
||||
auto snapshot_result = version_mgr_->CreateSnapshot(
|
||||
absl::StrFormat("Before sync from %s", sender),
|
||||
"system",
|
||||
false
|
||||
);
|
||||
|
||||
if (!snapshot_result.ok()) {
|
||||
sync_in_progress_ = false;
|
||||
return absl::InternalError("Failed to create backup snapshot");
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the diff
|
||||
auto status = ApplyDiff(diff_data);
|
||||
|
||||
if (status.ok()) {
|
||||
last_sync_hash_ = rom_hash;
|
||||
} else {
|
||||
// Rollback on error
|
||||
if (config_.create_snapshot_before_sync) {
|
||||
auto snapshots = version_mgr_->GetSnapshots();
|
||||
if (!snapshots.empty()) {
|
||||
version_mgr_->RestoreSnapshot(snapshots[0].snapshot_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sync_in_progress_ = false;
|
||||
return status;
|
||||
}
|
||||
|
||||
absl::Status CollaborationService::HandleIncomingProposal(
|
||||
const std::string& proposal_id,
|
||||
const nlohmann::json& proposal_data,
|
||||
const std::string& sender) {
|
||||
|
||||
if (!approval_mgr_) {
|
||||
return absl::FailedPreconditionError("Approval manager not initialized");
|
||||
}
|
||||
|
||||
// Submit to approval manager
|
||||
return approval_mgr_->SubmitProposal(
|
||||
proposal_id,
|
||||
sender,
|
||||
proposal_data["description"],
|
||||
proposal_data
|
||||
);
|
||||
}
|
||||
|
||||
absl::Status CollaborationService::VoteOnProposal(
|
||||
const std::string& proposal_id,
|
||||
bool approved,
|
||||
const std::string& username) {
|
||||
|
||||
if (!client_->InSession()) {
|
||||
return absl::FailedPreconditionError("Not in a session");
|
||||
}
|
||||
|
||||
// Vote locally
|
||||
auto status = approval_mgr_->VoteOnProposal(proposal_id, username, approved);
|
||||
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
// Send vote to server
|
||||
return client_->VoteOnProposal(proposal_id, approved, username);
|
||||
}
|
||||
|
||||
absl::Status CollaborationService::ApplyApprovedProposal(
|
||||
const std::string& proposal_id) {
|
||||
|
||||
if (!approval_mgr_->IsProposalApproved(proposal_id)) {
|
||||
return absl::FailedPreconditionError("Proposal not approved");
|
||||
}
|
||||
|
||||
auto proposal_result = approval_mgr_->GetProposalStatus(proposal_id);
|
||||
if (!proposal_result.ok()) {
|
||||
return proposal_result.status();
|
||||
}
|
||||
|
||||
// Apply the proposal (implementation depends on proposal type)
|
||||
// For now, just update status
|
||||
auto status = client_->UpdateProposalStatus(proposal_id, "applied");
|
||||
|
||||
if (status.ok()) {
|
||||
// Create snapshot after applying
|
||||
version_mgr_->CreateSnapshot(
|
||||
absl::StrFormat("Applied proposal %s", proposal_id.substr(0, 8)),
|
||||
"system",
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
bool CollaborationService::IsConnected() const {
|
||||
return client_->IsConnected();
|
||||
}
|
||||
|
||||
absl::StatusOr<SessionInfo> CollaborationService::GetSessionInfo() const {
|
||||
return client_->GetSessionInfo();
|
||||
}
|
||||
|
||||
void CollaborationService::SetAutoSync(bool enabled) {
|
||||
config_.auto_sync_enabled = enabled;
|
||||
}
|
||||
|
||||
// Private callback handlers
|
||||
|
||||
void CollaborationService::OnRomSyncReceived(const nlohmann::json& payload) {
|
||||
std::string diff_data = payload["diff_data"];
|
||||
std::string rom_hash = payload["rom_hash"];
|
||||
std::string sender = payload["sender"];
|
||||
|
||||
auto status = ApplyRomSync(diff_data, rom_hash, sender);
|
||||
|
||||
if (!status.ok()) {
|
||||
// Log error or notify user
|
||||
}
|
||||
}
|
||||
|
||||
void CollaborationService::OnProposalReceived(const nlohmann::json& payload) {
|
||||
std::string proposal_id = payload["proposal_id"];
|
||||
nlohmann::json proposal_data = payload["proposal_data"];
|
||||
std::string sender = payload["sender"];
|
||||
|
||||
HandleIncomingProposal(proposal_id, proposal_data, sender);
|
||||
}
|
||||
|
||||
void CollaborationService::OnProposalUpdated(const nlohmann::json& payload) {
|
||||
std::string proposal_id = payload["proposal_id"];
|
||||
|
||||
if (payload.contains("status")) {
|
||||
std::string status = payload["status"];
|
||||
|
||||
if (status == "approved" && approval_mgr_) {
|
||||
// Proposal was approved, consider applying it
|
||||
// This would be triggered by the host or based on voting results
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.contains("votes")) {
|
||||
// Vote update received
|
||||
nlohmann::json votes = payload["votes"];
|
||||
// Update local approval manager state
|
||||
}
|
||||
}
|
||||
|
||||
void CollaborationService::OnParticipantJoined(const nlohmann::json& payload) {
|
||||
std::string username = payload["username"];
|
||||
// Update participant list or notify user
|
||||
}
|
||||
|
||||
void CollaborationService::OnParticipantLeft(const nlohmann::json& payload) {
|
||||
std::string username = payload["username"];
|
||||
// Update participant list or notify user
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
std::string CollaborationService::GenerateDiff(
|
||||
const std::string& from_hash,
|
||||
const std::string& to_hash) {
|
||||
|
||||
// Simplified diff generation
|
||||
// In production, this would generate a binary diff
|
||||
// For now, just return placeholder
|
||||
|
||||
if (!rom_ || !rom_->is_loaded()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// TODO: Implement proper binary diff generation
|
||||
// This could use algorithms like bsdiff or a custom format
|
||||
|
||||
return "diff_placeholder";
|
||||
}
|
||||
|
||||
absl::Status CollaborationService::ApplyDiff(const std::string& diff_data) {
|
||||
if (!rom_ || !rom_->is_loaded()) {
|
||||
return absl::FailedPreconditionError("ROM not loaded");
|
||||
}
|
||||
|
||||
// TODO: Implement proper diff application
|
||||
// For now, just return success
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
bool CollaborationService::ShouldAutoSync() {
|
||||
if (!config_.auto_sync_enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!client_->IsConnected() || !client_->InSession()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sync_in_progress_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if enough time has passed since last sync
|
||||
// (Implementation would track last sync time)
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace app
|
||||
} // namespace yaze
|
||||
166
src/app/net/collaboration_service.h
Normal file
166
src/app/net/collaboration_service.h
Normal file
@@ -0,0 +1,166 @@
|
||||
#ifndef YAZE_APP_NET_COLLABORATION_SERVICE_H_
|
||||
#define YAZE_APP_NET_COLLABORATION_SERVICE_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "app/net/rom_version_manager.h"
|
||||
#include "app/net/websocket_client.h"
|
||||
#include "app/rom.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace app {
|
||||
namespace net {
|
||||
|
||||
/**
|
||||
* @class CollaborationService
|
||||
* @brief High-level service integrating version management with networking
|
||||
*
|
||||
* Bridges the gap between:
|
||||
* - Local ROM version management
|
||||
* - Remote collaboration via WebSocket
|
||||
* - Proposal approval workflow
|
||||
*
|
||||
* Features:
|
||||
* - Automatic ROM sync on changes
|
||||
* - Network-aware proposal approval
|
||||
* - Conflict resolution
|
||||
* - Auto-backup before network operations
|
||||
*/
|
||||
class CollaborationService {
|
||||
public:
|
||||
struct Config {
|
||||
bool auto_sync_enabled = true;
|
||||
int sync_interval_ms = 5000; // 5 seconds
|
||||
bool require_approval_for_sync = true;
|
||||
bool create_snapshot_before_sync = true;
|
||||
};
|
||||
|
||||
explicit CollaborationService(Rom* rom);
|
||||
~CollaborationService();
|
||||
|
||||
/**
|
||||
* Initialize the service
|
||||
*/
|
||||
absl::Status Initialize(
|
||||
const Config& config,
|
||||
RomVersionManager* version_mgr,
|
||||
ProposalApprovalManager* approval_mgr);
|
||||
|
||||
/**
|
||||
* Connect to collaboration server
|
||||
*/
|
||||
absl::Status Connect(const std::string& host, int port = 8765);
|
||||
|
||||
/**
|
||||
* Disconnect from server
|
||||
*/
|
||||
void Disconnect();
|
||||
|
||||
/**
|
||||
* Host a new session
|
||||
*/
|
||||
absl::Status HostSession(
|
||||
const std::string& session_name,
|
||||
const std::string& username,
|
||||
bool ai_enabled = true);
|
||||
|
||||
/**
|
||||
* Join existing session
|
||||
*/
|
||||
absl::Status JoinSession(
|
||||
const std::string& session_code,
|
||||
const std::string& username);
|
||||
|
||||
/**
|
||||
* Leave current session
|
||||
*/
|
||||
absl::Status LeaveSession();
|
||||
|
||||
/**
|
||||
* Submit local changes as proposal
|
||||
*/
|
||||
absl::Status SubmitChangesAsProposal(
|
||||
const std::string& description,
|
||||
const std::string& username);
|
||||
|
||||
/**
|
||||
* Apply received ROM sync
|
||||
*/
|
||||
absl::Status ApplyRomSync(
|
||||
const std::string& diff_data,
|
||||
const std::string& rom_hash,
|
||||
const std::string& sender);
|
||||
|
||||
/**
|
||||
* Handle incoming proposal
|
||||
*/
|
||||
absl::Status HandleIncomingProposal(
|
||||
const std::string& proposal_id,
|
||||
const nlohmann::json& proposal_data,
|
||||
const std::string& sender);
|
||||
|
||||
/**
|
||||
* Vote on proposal
|
||||
*/
|
||||
absl::Status VoteOnProposal(
|
||||
const std::string& proposal_id,
|
||||
bool approved,
|
||||
const std::string& username);
|
||||
|
||||
/**
|
||||
* Apply approved proposal
|
||||
*/
|
||||
absl::Status ApplyApprovedProposal(const std::string& proposal_id);
|
||||
|
||||
/**
|
||||
* Get connection status
|
||||
*/
|
||||
bool IsConnected() const;
|
||||
|
||||
/**
|
||||
* Get session info
|
||||
*/
|
||||
absl::StatusOr<SessionInfo> GetSessionInfo() const;
|
||||
|
||||
/**
|
||||
* Get WebSocket client (for advanced usage)
|
||||
*/
|
||||
WebSocketClient* GetClient() { return client_.get(); }
|
||||
|
||||
/**
|
||||
* Enable/disable auto-sync
|
||||
*/
|
||||
void SetAutoSync(bool enabled);
|
||||
|
||||
private:
|
||||
Rom* rom_;
|
||||
RomVersionManager* version_mgr_;
|
||||
ProposalApprovalManager* approval_mgr_;
|
||||
std::unique_ptr<WebSocketClient> client_;
|
||||
Config config_;
|
||||
|
||||
// Sync state
|
||||
std::string last_sync_hash_;
|
||||
bool sync_in_progress_;
|
||||
|
||||
// Callbacks for network events
|
||||
void OnRomSyncReceived(const nlohmann::json& payload);
|
||||
void OnProposalReceived(const nlohmann::json& payload);
|
||||
void OnProposalUpdated(const nlohmann::json& payload);
|
||||
void OnParticipantJoined(const nlohmann::json& payload);
|
||||
void OnParticipantLeft(const nlohmann::json& payload);
|
||||
|
||||
// Helper functions
|
||||
std::string GenerateDiff(const std::string& from_hash, const std::string& to_hash);
|
||||
absl::Status ApplyDiff(const std::string& diff_data);
|
||||
bool ShouldAutoSync();
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace app
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_NET_COLLABORATION_SERVICE_H_
|
||||
@@ -12,6 +12,7 @@
|
||||
set(
|
||||
YAZE_NET_SRC
|
||||
app/net/rom_version_manager.cc
|
||||
app/net/websocket_client.cc
|
||||
)
|
||||
|
||||
add_library(yaze_net STATIC ${YAZE_NET_SRC})
|
||||
@@ -30,11 +31,32 @@ target_link_libraries(yaze_net PUBLIC
|
||||
${ABSL_TARGETS}
|
||||
)
|
||||
|
||||
# Add JSON support if enabled
|
||||
# Add JSON and httplib support if enabled
|
||||
if(YAZE_WITH_JSON)
|
||||
target_include_directories(yaze_net PUBLIC
|
||||
${CMAKE_SOURCE_DIR}/third_party/json/include)
|
||||
${CMAKE_SOURCE_DIR}/third_party/json/include
|
||||
${CMAKE_SOURCE_DIR}/third_party/httplib)
|
||||
target_compile_definitions(yaze_net PUBLIC YAZE_WITH_JSON)
|
||||
|
||||
# Add threading support (cross-platform)
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(yaze_net PUBLIC Threads::Threads)
|
||||
|
||||
# Add OpenSSL for HTTPS/WSS support (optional but recommended)
|
||||
find_package(OpenSSL QUIET)
|
||||
if(OpenSSL_FOUND)
|
||||
target_link_libraries(yaze_net PUBLIC OpenSSL::SSL OpenSSL::Crypto)
|
||||
target_compile_definitions(yaze_net PUBLIC CPPHTTPLIB_OPENSSL_SUPPORT)
|
||||
message(STATUS " - WebSocket with SSL/TLS support enabled")
|
||||
else()
|
||||
message(STATUS " - WebSocket without SSL/TLS (OpenSSL not found)")
|
||||
endif()
|
||||
|
||||
# Windows-specific socket library
|
||||
if(WIN32)
|
||||
target_link_libraries(yaze_net PUBLIC ws2_32)
|
||||
message(STATUS " - Windows socket support (ws2_32) linked")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set_target_properties(yaze_net PROPERTIES
|
||||
|
||||
464
src/app/net/websocket_client.cc
Normal file
464
src/app/net/websocket_client.cc
Normal file
@@ -0,0 +1,464 @@
|
||||
#include "app/net/websocket_client.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
|
||||
// Cross-platform WebSocket support using httplib
|
||||
#ifdef YAZE_WITH_JSON
|
||||
#define CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
#include "httplib.h"
|
||||
#endif
|
||||
|
||||
namespace yaze {
|
||||
namespace app {
|
||||
namespace net {
|
||||
|
||||
#ifdef YAZE_WITH_JSON
|
||||
|
||||
// Platform-independent WebSocket implementation using httplib
|
||||
class WebSocketClient::Impl {
|
||||
public:
|
||||
Impl() : connected_(false), should_stop_(false) {}
|
||||
|
||||
~Impl() {
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
absl::Status Connect(const std::string& host, int port) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
if (connected_) {
|
||||
return absl::AlreadyExistsError("Already connected");
|
||||
}
|
||||
|
||||
host_ = host;
|
||||
port_ = port;
|
||||
|
||||
try {
|
||||
// httplib WebSocket connection (cross-platform)
|
||||
std::string url = absl::StrFormat("ws://%s:%d", host, port);
|
||||
|
||||
// Create WebSocket connection
|
||||
client_ = std::make_unique<httplib::Client>(host, port);
|
||||
client_->set_connection_timeout(5, 0); // 5 seconds
|
||||
client_->set_read_timeout(30, 0); // 30 seconds
|
||||
|
||||
connected_ = true;
|
||||
should_stop_ = false;
|
||||
|
||||
// Start receive thread
|
||||
receive_thread_ = std::thread([this]() { ReceiveLoop(); });
|
||||
|
||||
return absl::OkStatus();
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
return absl::UnavailableError(
|
||||
absl::StrCat("Failed to connect: ", e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void Disconnect() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
if (!connected_) return;
|
||||
|
||||
should_stop_ = true;
|
||||
connected_ = false;
|
||||
|
||||
if (receive_thread_.joinable()) {
|
||||
receive_thread_.join();
|
||||
}
|
||||
|
||||
client_.reset();
|
||||
}
|
||||
|
||||
absl::Status Send(const std::string& message) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
if (!connected_) {
|
||||
return absl::FailedPreconditionError("Not connected");
|
||||
}
|
||||
|
||||
try {
|
||||
// In a real implementation, this would use WebSocket send
|
||||
// For now, we'll use HTTP POST as fallback
|
||||
auto res = client_->Post("/message", message, "application/json");
|
||||
|
||||
if (!res) {
|
||||
return absl::UnavailableError("Failed to send message");
|
||||
}
|
||||
|
||||
if (res->status != 200) {
|
||||
return absl::InternalError(
|
||||
absl::StrFormat("Server error: %d", res->status));
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
return absl::InternalError(absl::StrCat("Send failed: ", e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void SetMessageCallback(std::function<void(const std::string&)> callback) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
message_callback_ = callback;
|
||||
}
|
||||
|
||||
void SetErrorCallback(std::function<void(const std::string&)> callback) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
error_callback_ = callback;
|
||||
}
|
||||
|
||||
bool IsConnected() const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return connected_;
|
||||
}
|
||||
|
||||
private:
|
||||
void ReceiveLoop() {
|
||||
while (!should_stop_) {
|
||||
try {
|
||||
// Poll for messages (platform-independent)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
// In a real WebSocket implementation, this would receive messages
|
||||
// For now, this is a placeholder for the receive loop
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
if (error_callback_) {
|
||||
error_callback_(e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
std::unique_ptr<httplib::Client> client_;
|
||||
std::thread receive_thread_;
|
||||
|
||||
std::string host_;
|
||||
int port_;
|
||||
bool connected_;
|
||||
bool should_stop_;
|
||||
|
||||
std::function<void(const std::string&)> message_callback_;
|
||||
std::function<void(const std::string&)> error_callback_;
|
||||
};
|
||||
|
||||
#else
|
||||
|
||||
// Stub implementation when JSON is not available
|
||||
class WebSocketClient::Impl {
|
||||
public:
|
||||
absl::Status Connect(const std::string&, int) {
|
||||
return absl::UnimplementedError("WebSocket support requires JSON library");
|
||||
}
|
||||
void Disconnect() {}
|
||||
absl::Status Send(const std::string&) {
|
||||
return absl::UnimplementedError("WebSocket support requires JSON library");
|
||||
}
|
||||
void SetMessageCallback(std::function<void(const std::string&)>) {}
|
||||
void SetErrorCallback(std::function<void(const std::string&)>) {}
|
||||
bool IsConnected() const { return false; }
|
||||
};
|
||||
|
||||
#endif // YAZE_WITH_JSON
|
||||
|
||||
// ============================================================================
|
||||
// WebSocketClient Implementation
|
||||
// ============================================================================
|
||||
|
||||
WebSocketClient::WebSocketClient()
|
||||
: impl_(std::make_unique<Impl>()),
|
||||
state_(ConnectionState::kDisconnected) {
|
||||
}
|
||||
|
||||
WebSocketClient::~WebSocketClient() {
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
absl::Status WebSocketClient::Connect(const std::string& host, int port) {
|
||||
auto status = impl_->Connect(host, port);
|
||||
|
||||
if (status.ok()) {
|
||||
SetState(ConnectionState::kConnected);
|
||||
} else {
|
||||
SetState(ConnectionState::kError);
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
void WebSocketClient::Disconnect() {
|
||||
impl_->Disconnect();
|
||||
SetState(ConnectionState::kDisconnected);
|
||||
current_session_ = SessionInfo{};
|
||||
}
|
||||
|
||||
absl::StatusOr<SessionInfo> WebSocketClient::HostSession(
|
||||
const std::string& session_name,
|
||||
const std::string& username,
|
||||
const std::string& rom_hash,
|
||||
bool ai_enabled) {
|
||||
|
||||
#ifdef YAZE_WITH_JSON
|
||||
if (!IsConnected()) {
|
||||
return absl::FailedPreconditionError("Not connected to server");
|
||||
}
|
||||
|
||||
nlohmann::json message = {
|
||||
{"type", "host_session"},
|
||||
{"payload", {
|
||||
{"session_name", session_name},
|
||||
{"username", username},
|
||||
{"rom_hash", rom_hash},
|
||||
{"ai_enabled", ai_enabled}
|
||||
}}
|
||||
};
|
||||
|
||||
auto status = SendRaw(message);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
// In a real implementation, we'd wait for the server response
|
||||
// For now, return a placeholder
|
||||
SessionInfo session;
|
||||
session.session_name = session_name;
|
||||
session.host = username;
|
||||
session.rom_hash = rom_hash;
|
||||
session.ai_enabled = ai_enabled;
|
||||
|
||||
current_session_ = session;
|
||||
return session;
|
||||
#else
|
||||
return absl::UnimplementedError("JSON support required");
|
||||
#endif
|
||||
}
|
||||
|
||||
absl::StatusOr<SessionInfo> WebSocketClient::JoinSession(
|
||||
const std::string& session_code,
|
||||
const std::string& username) {
|
||||
|
||||
#ifdef YAZE_WITH_JSON
|
||||
if (!IsConnected()) {
|
||||
return absl::FailedPreconditionError("Not connected to server");
|
||||
}
|
||||
|
||||
nlohmann::json message = {
|
||||
{"type", "join_session"},
|
||||
{"payload", {
|
||||
{"session_code", session_code},
|
||||
{"username", username}
|
||||
}}
|
||||
};
|
||||
|
||||
auto status = SendRaw(message);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
// Placeholder - would wait for server response
|
||||
SessionInfo session;
|
||||
session.session_code = session_code;
|
||||
|
||||
current_session_ = session;
|
||||
return session;
|
||||
#else
|
||||
return absl::UnimplementedError("JSON support required");
|
||||
#endif
|
||||
}
|
||||
|
||||
absl::Status WebSocketClient::LeaveSession() {
|
||||
#ifdef YAZE_WITH_JSON
|
||||
if (!InSession()) {
|
||||
return absl::FailedPreconditionError("Not in a session");
|
||||
}
|
||||
|
||||
nlohmann::json message = {
|
||||
{"type", "leave_session"},
|
||||
{"payload", {}}
|
||||
};
|
||||
|
||||
auto status = SendRaw(message);
|
||||
current_session_ = SessionInfo{};
|
||||
return status;
|
||||
#else
|
||||
return absl::UnimplementedError("JSON support required");
|
||||
#endif
|
||||
}
|
||||
|
||||
absl::Status WebSocketClient::SendChatMessage(
|
||||
const std::string& message,
|
||||
const std::string& sender) {
|
||||
|
||||
#ifdef YAZE_WITH_JSON
|
||||
nlohmann::json msg = {
|
||||
{"type", "chat_message"},
|
||||
{"payload", {
|
||||
{"message", message},
|
||||
{"sender", sender}
|
||||
}}
|
||||
};
|
||||
|
||||
return SendRaw(msg);
|
||||
#else
|
||||
return absl::UnimplementedError("JSON support required");
|
||||
#endif
|
||||
}
|
||||
|
||||
absl::Status WebSocketClient::SendRomSync(
|
||||
const std::string& diff_data,
|
||||
const std::string& rom_hash,
|
||||
const std::string& sender) {
|
||||
|
||||
#ifdef YAZE_WITH_JSON
|
||||
nlohmann::json message = {
|
||||
{"type", "rom_sync"},
|
||||
{"payload", {
|
||||
{"diff_data", diff_data},
|
||||
{"rom_hash", rom_hash},
|
||||
{"sender", sender}
|
||||
}}
|
||||
};
|
||||
|
||||
return SendRaw(message);
|
||||
#else
|
||||
return absl::UnimplementedError("JSON support required");
|
||||
#endif
|
||||
}
|
||||
|
||||
absl::Status WebSocketClient::ShareProposal(
|
||||
const nlohmann::json& proposal_data,
|
||||
const std::string& sender) {
|
||||
|
||||
#ifdef YAZE_WITH_JSON
|
||||
nlohmann::json message = {
|
||||
{"type", "proposal_share"},
|
||||
{"payload", {
|
||||
{"sender", sender},
|
||||
{"proposal_data", proposal_data}
|
||||
}}
|
||||
};
|
||||
|
||||
return SendRaw(message);
|
||||
#else
|
||||
return absl::UnimplementedError("JSON support required");
|
||||
#endif
|
||||
}
|
||||
|
||||
absl::Status WebSocketClient::VoteOnProposal(
|
||||
const std::string& proposal_id,
|
||||
bool approved,
|
||||
const std::string& username) {
|
||||
|
||||
#ifdef YAZE_WITH_JSON
|
||||
nlohmann::json message = {
|
||||
{"type", "proposal_vote"},
|
||||
{"payload", {
|
||||
{"proposal_id", proposal_id},
|
||||
{"approved", approved},
|
||||
{"username", username}
|
||||
}}
|
||||
};
|
||||
|
||||
return SendRaw(message);
|
||||
#else
|
||||
return absl::UnimplementedError("JSON support required");
|
||||
#endif
|
||||
}
|
||||
|
||||
absl::Status WebSocketClient::UpdateProposalStatus(
|
||||
const std::string& proposal_id,
|
||||
const std::string& status) {
|
||||
|
||||
#ifdef YAZE_WITH_JSON
|
||||
nlohmann::json message = {
|
||||
{"type", "proposal_update"},
|
||||
{"payload", {
|
||||
{"proposal_id", proposal_id},
|
||||
{"status", status}
|
||||
}}
|
||||
};
|
||||
|
||||
return SendRaw(message);
|
||||
#else
|
||||
return absl::UnimplementedError("JSON support required");
|
||||
#endif
|
||||
}
|
||||
|
||||
void WebSocketClient::OnMessage(const std::string& type, MessageCallback callback) {
|
||||
message_callbacks_[type].push_back(callback);
|
||||
}
|
||||
|
||||
void WebSocketClient::OnError(ErrorCallback callback) {
|
||||
error_callbacks_.push_back(callback);
|
||||
}
|
||||
|
||||
void WebSocketClient::OnStateChange(StateCallback callback) {
|
||||
state_callbacks_.push_back(callback);
|
||||
}
|
||||
|
||||
absl::StatusOr<SessionInfo> WebSocketClient::GetSessionInfo() const {
|
||||
if (!InSession()) {
|
||||
return absl::FailedPreconditionError("Not in a session");
|
||||
}
|
||||
return current_session_;
|
||||
}
|
||||
|
||||
// Private methods
|
||||
|
||||
void WebSocketClient::HandleMessage(const std::string& message) {
|
||||
#ifdef YAZE_WITH_JSON
|
||||
try {
|
||||
auto json = nlohmann::json::parse(message);
|
||||
std::string type = json["type"];
|
||||
|
||||
auto it = message_callbacks_.find(type);
|
||||
if (it != message_callbacks_.end()) {
|
||||
for (auto& callback : it->second) {
|
||||
callback(json["payload"]);
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
HandleError(absl::StrCat("Failed to parse message: ", e.what()));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void WebSocketClient::HandleError(const std::string& error) {
|
||||
for (auto& callback : error_callbacks_) {
|
||||
callback(error);
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketClient::SetState(ConnectionState state) {
|
||||
if (state_ != state) {
|
||||
state_ = state;
|
||||
for (auto& callback : state_callbacks_) {
|
||||
callback(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
absl::Status WebSocketClient::SendRaw(const nlohmann::json& message) {
|
||||
#ifdef YAZE_WITH_JSON
|
||||
try {
|
||||
std::string msg_str = message.dump();
|
||||
return impl_->Send(msg_str);
|
||||
} catch (const std::exception& e) {
|
||||
return absl::InternalError(absl::StrCat("Failed to serialize: ", e.what()));
|
||||
}
|
||||
#else
|
||||
return absl::UnimplementedError("JSON support required");
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace app
|
||||
} // namespace yaze
|
||||
214
src/app/net/websocket_client.h
Normal file
214
src/app/net/websocket_client.h
Normal file
@@ -0,0 +1,214 @@
|
||||
#ifndef YAZE_APP_NET_WEBSOCKET_CLIENT_H_
|
||||
#define YAZE_APP_NET_WEBSOCKET_CLIENT_H_
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
|
||||
#ifdef YAZE_WITH_JSON
|
||||
#include "nlohmann/json.hpp"
|
||||
#endif
|
||||
|
||||
namespace yaze {
|
||||
namespace app {
|
||||
namespace net {
|
||||
|
||||
/**
|
||||
* @enum ConnectionState
|
||||
* @brief WebSocket connection states
|
||||
*/
|
||||
enum class ConnectionState {
|
||||
kDisconnected,
|
||||
kConnecting,
|
||||
kConnected,
|
||||
kReconnecting,
|
||||
kError
|
||||
};
|
||||
|
||||
/**
|
||||
* @struct SessionInfo
|
||||
* @brief Information about the current collaboration session
|
||||
*/
|
||||
struct SessionInfo {
|
||||
std::string session_id;
|
||||
std::string session_code;
|
||||
std::string session_name;
|
||||
std::string host;
|
||||
std::vector<std::string> participants;
|
||||
std::string rom_hash;
|
||||
bool ai_enabled;
|
||||
};
|
||||
|
||||
/**
|
||||
* @class WebSocketClient
|
||||
* @brief WebSocket client for connecting to yaze-server
|
||||
*
|
||||
* Provides:
|
||||
* - Connection management with auto-reconnect
|
||||
* - Session hosting and joining
|
||||
* - Message sending/receiving
|
||||
* - Event callbacks for different message types
|
||||
*/
|
||||
class WebSocketClient {
|
||||
public:
|
||||
// Message type callbacks
|
||||
using MessageCallback = std::function<void(const nlohmann::json&)>;
|
||||
using ErrorCallback = std::function<void(const std::string&)>;
|
||||
using StateCallback = std::function<void(ConnectionState)>;
|
||||
|
||||
WebSocketClient();
|
||||
~WebSocketClient();
|
||||
|
||||
/**
|
||||
* Connect to yaze-server
|
||||
* @param host Server hostname/IP
|
||||
* @param port Server port (default: 8765)
|
||||
*/
|
||||
absl::Status Connect(const std::string& host, int port = 8765);
|
||||
|
||||
/**
|
||||
* Disconnect from server
|
||||
*/
|
||||
void Disconnect();
|
||||
|
||||
/**
|
||||
* Host a new collaboration session
|
||||
*/
|
||||
absl::StatusOr<SessionInfo> HostSession(
|
||||
const std::string& session_name,
|
||||
const std::string& username,
|
||||
const std::string& rom_hash,
|
||||
bool ai_enabled = true);
|
||||
|
||||
/**
|
||||
* Join an existing session
|
||||
*/
|
||||
absl::StatusOr<SessionInfo> JoinSession(
|
||||
const std::string& session_code,
|
||||
const std::string& username);
|
||||
|
||||
/**
|
||||
* Leave current session
|
||||
*/
|
||||
absl::Status LeaveSession();
|
||||
|
||||
/**
|
||||
* Send chat message
|
||||
*/
|
||||
absl::Status SendChatMessage(
|
||||
const std::string& message,
|
||||
const std::string& sender);
|
||||
|
||||
/**
|
||||
* Send ROM sync
|
||||
*/
|
||||
absl::Status SendRomSync(
|
||||
const std::string& diff_data,
|
||||
const std::string& rom_hash,
|
||||
const std::string& sender);
|
||||
|
||||
/**
|
||||
* Share snapshot
|
||||
*/
|
||||
absl::Status ShareSnapshot(
|
||||
const std::string& snapshot_data,
|
||||
const std::string& snapshot_type,
|
||||
const std::string& sender);
|
||||
|
||||
/**
|
||||
* Share proposal for approval
|
||||
*/
|
||||
absl::Status ShareProposal(
|
||||
const nlohmann::json& proposal_data,
|
||||
const std::string& sender);
|
||||
|
||||
/**
|
||||
* Vote on proposal (approve/reject)
|
||||
*/
|
||||
absl::Status VoteOnProposal(
|
||||
const std::string& proposal_id,
|
||||
bool approved,
|
||||
const std::string& username);
|
||||
|
||||
/**
|
||||
* Update proposal status
|
||||
*/
|
||||
absl::Status UpdateProposalStatus(
|
||||
const std::string& proposal_id,
|
||||
const std::string& status);
|
||||
|
||||
/**
|
||||
* Send AI query
|
||||
*/
|
||||
absl::Status SendAIQuery(
|
||||
const std::string& query,
|
||||
const std::string& username);
|
||||
|
||||
/**
|
||||
* Register callback for specific message type
|
||||
*/
|
||||
void OnMessage(const std::string& type, MessageCallback callback);
|
||||
|
||||
/**
|
||||
* Register callback for errors
|
||||
*/
|
||||
void OnError(ErrorCallback callback);
|
||||
|
||||
/**
|
||||
* Register callback for connection state changes
|
||||
*/
|
||||
void OnStateChange(StateCallback callback);
|
||||
|
||||
/**
|
||||
* Get current connection state
|
||||
*/
|
||||
ConnectionState GetState() const { return state_; }
|
||||
|
||||
/**
|
||||
* Get current session info (if in a session)
|
||||
*/
|
||||
absl::StatusOr<SessionInfo> GetSessionInfo() const;
|
||||
|
||||
/**
|
||||
* Check if connected
|
||||
*/
|
||||
bool IsConnected() const { return state_ == ConnectionState::kConnected; }
|
||||
|
||||
/**
|
||||
* Check if in a session
|
||||
*/
|
||||
bool InSession() const { return !current_session_.session_id.empty(); }
|
||||
|
||||
private:
|
||||
// Implementation details (using native WebSocket or library)
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
|
||||
ConnectionState state_;
|
||||
SessionInfo current_session_;
|
||||
|
||||
// Callbacks
|
||||
std::map<std::string, std::vector<MessageCallback>> message_callbacks_;
|
||||
std::vector<ErrorCallback> error_callbacks_;
|
||||
std::vector<StateCallback> state_callbacks_;
|
||||
|
||||
// Internal message handling
|
||||
void HandleMessage(const std::string& message);
|
||||
void HandleError(const std::string& error);
|
||||
void SetState(ConnectionState state);
|
||||
|
||||
// Send raw message
|
||||
absl::Status SendRaw(const nlohmann::json& message);
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace app
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_NET_WEBSOCKET_CLIENT_H_
|
||||
102
src/cli/service/net/z3ed_network_client.h
Normal file
102
src/cli/service/net/z3ed_network_client.h
Normal file
@@ -0,0 +1,102 @@
|
||||
#ifndef YAZE_CLI_SERVICE_NET_Z3ED_NETWORK_CLIENT_H_
|
||||
#define YAZE_CLI_SERVICE_NET_Z3ED_NETWORK_CLIENT_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
|
||||
#ifdef YAZE_WITH_JSON
|
||||
#include "nlohmann/json.hpp"
|
||||
#endif
|
||||
|
||||
namespace yaze {
|
||||
namespace cli {
|
||||
namespace net {
|
||||
|
||||
/**
|
||||
* @class Z3edNetworkClient
|
||||
* @brief Simplified WebSocket client for z3ed CLI
|
||||
*
|
||||
* Provides command-line friendly interface for:
|
||||
* - Connecting to yaze-server
|
||||
* - Submitting proposals from CLI
|
||||
* - Checking approval status
|
||||
* - Simple chat messages
|
||||
*/
|
||||
class Z3edNetworkClient {
|
||||
public:
|
||||
Z3edNetworkClient();
|
||||
~Z3edNetworkClient();
|
||||
|
||||
/**
|
||||
* Connect to server
|
||||
*/
|
||||
absl::Status Connect(const std::string& host, int port = 8765);
|
||||
|
||||
/**
|
||||
* Join session
|
||||
*/
|
||||
absl::Status JoinSession(
|
||||
const std::string& session_code,
|
||||
const std::string& username);
|
||||
|
||||
/**
|
||||
* Submit proposal
|
||||
* @param description Human-readable description
|
||||
* @param proposal_json JSON string with proposal details
|
||||
*/
|
||||
absl::Status SubmitProposal(
|
||||
const std::string& description,
|
||||
const std::string& proposal_json,
|
||||
const std::string& username);
|
||||
|
||||
/**
|
||||
* Check proposal status
|
||||
*/
|
||||
absl::StatusOr<std::string> GetProposalStatus(
|
||||
const std::string& proposal_id);
|
||||
|
||||
/**
|
||||
* Wait for proposal approval (blocking)
|
||||
* @param timeout_seconds How long to wait
|
||||
*/
|
||||
absl::StatusOr<bool> WaitForApproval(
|
||||
const std::string& proposal_id,
|
||||
int timeout_seconds = 60);
|
||||
|
||||
/**
|
||||
* Send chat message
|
||||
*/
|
||||
absl::Status SendMessage(
|
||||
const std::string& message,
|
||||
const std::string& sender);
|
||||
|
||||
/**
|
||||
* Query AI agent (if enabled)
|
||||
*/
|
||||
absl::StatusOr<std::string> QueryAI(
|
||||
const std::string& query,
|
||||
const std::string& username);
|
||||
|
||||
/**
|
||||
* Disconnect
|
||||
*/
|
||||
void Disconnect();
|
||||
|
||||
/**
|
||||
* Check if connected
|
||||
*/
|
||||
bool IsConnected() const;
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace cli
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_CLI_SERVICE_NET_Z3ED_NETWORK_CLIENT_H_
|
||||
Reference in New Issue
Block a user