backend-infra-engineer: Release v0.3.2 snapshot

This commit is contained in:
scawful
2025-10-17 12:10:25 -04:00
parent 4371618a9b
commit 3d71417f62
857 changed files with 174954 additions and 45626 deletions

View File

@@ -0,0 +1,440 @@
#include "app/net/collaboration_service.h"
#include <chrono>
#include <thread>
#include "absl/strings/str_format.h"
namespace yaze {
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 yaze

View 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 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 yaze
#endif // YAZE_APP_NET_COLLABORATION_SERVICE_H_

View File

@@ -0,0 +1,105 @@
# ==============================================================================
# Yaze Net Library
# ==============================================================================
# This library contains networking and collaboration functionality:
# - ROM version management
# - Proposal approval system
# - Collaboration utilities
#
# Dependencies: yaze_util, absl
# ==============================================================================
set(
YAZE_NET_SRC
app/net/rom_version_manager.cc
app/net/websocket_client.cc
app/net/collaboration_service.cc
)
if(YAZE_WITH_GRPC)
# Add ROM service implementation (disabled - proto field mismatch)
# list(APPEND YAZE_NET_SRC app/net/rom_service_impl.cc)
endif()
add_library(yaze_net STATIC ${YAZE_NET_SRC})
target_precompile_headers(yaze_net PRIVATE
"$<$<COMPILE_LANGUAGE:CXX>:${CMAKE_SOURCE_DIR}/src/yaze_pch.h>"
)
target_include_directories(yaze_net PUBLIC
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/src/lib
${CMAKE_SOURCE_DIR}/src/lib/imgui
${SDL2_INCLUDE_DIR}
${PROJECT_BINARY_DIR}
)
target_link_libraries(yaze_net PUBLIC
yaze_util
yaze_common
${ABSL_TARGETS}
)
# 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/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)
# Only link OpenSSL if gRPC is NOT enabled (to avoid duplicate symbol errors)
# When gRPC is enabled, it brings its own OpenSSL which we'll use instead
if(NOT YAZE_WITH_GRPC)
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()
else()
# When gRPC is enabled, still enable OpenSSL features but use gRPC's OpenSSL
target_compile_definitions(yaze_net PUBLIC CPPHTTPLIB_OPENSSL_SUPPORT)
message(STATUS " - WebSocket with SSL/TLS support enabled via gRPC's OpenSSL")
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()
# Add gRPC support for ROM service
if(YAZE_WITH_GRPC)
target_add_protobuf(yaze_net ${PROJECT_SOURCE_DIR}/src/protos/rom_service.proto)
target_link_libraries(yaze_net PUBLIC
grpc++
grpc++_reflection
)
if(YAZE_PROTOBUF_TARGETS)
target_link_libraries(yaze_net PUBLIC ${YAZE_PROTOBUF_TARGETS})
if(MSVC AND YAZE_PROTOBUF_WHOLEARCHIVE_TARGETS)
foreach(_yaze_proto_target IN LISTS YAZE_PROTOBUF_WHOLEARCHIVE_TARGETS)
target_link_options(yaze_net PUBLIC /WHOLEARCHIVE:$<TARGET_FILE:${_yaze_proto_target}>)
endforeach()
endif()
endif()
message(STATUS " - gRPC ROM service enabled")
endif()
set_target_properties(yaze_net PROPERTIES
POSITION_INDEPENDENT_CODE ON
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
)
message(STATUS "✓ yaze_net library configured")

View File

@@ -0,0 +1,210 @@
#include "app/net/rom_service_impl.h"
#ifdef YAZE_WITH_GRPC
#include "absl/strings/str_format.h"
#include "app/rom.h"
#include "app/net/rom_version_manager.h"
// Proto namespace alias for convenience
namespace rom_svc = ::yaze::proto;
namespace yaze {
namespace net {
RomServiceImpl::RomServiceImpl(
Rom* rom,
RomVersionManager* version_manager,
ProposalApprovalManager* approval_manager)
: rom_(rom),
version_mgr_(version_manager),
approval_mgr_(approval_manager) {
}
void RomServiceImpl::SetConfig(const Config& config) {
config_ = config;
}
grpc::Status RomServiceImpl::ReadBytes(
grpc::ServerContext* context,
const rom_svc::ReadBytesRequest* request,
rom_svc::ReadBytesResponse* response) {
if (!rom_ || !rom_->is_loaded()) {
return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION, "ROM not loaded");
}
uint32_t address = request->address();
uint32_t length = request->length();
// Validate range
if (address + length > rom_->size()) {
return grpc::Status(
grpc::StatusCode::OUT_OF_RANGE,
absl::StrFormat("Read beyond ROM: 0x%X+%d > %d",
address, length, rom_->size()));
}
// Read data
const auto* data = rom_->data() + address;
response->set_data(data, length);
response->set_success(true);
return grpc::Status::OK;
}
grpc::Status RomServiceImpl::WriteBytes(
grpc::ServerContext* context,
const rom_svc::WriteBytesRequest* request,
rom_svc::WriteBytesResponse* response) {
if (!rom_ || !rom_->is_loaded()) {
return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION, "ROM not loaded");
}
uint32_t address = request->address();
const std::string& data = request->data();
// Validate range
if (address + data.size() > rom_->size()) {
return grpc::Status(
grpc::StatusCode::OUT_OF_RANGE,
absl::StrFormat("Write beyond ROM: 0x%X+%zu > %d",
address, data.size(), rom_->size()));
}
// Check if approval required
if (config_.require_approval_for_writes && approval_mgr_) {
// Create a proposal for this write
std::string proposal_id = absl::StrFormat(
"write_0x%X_%zu_bytes", address, data.size());
if (request->has_proposal_id()) {
proposal_id = request->proposal_id();
}
// Check if proposal is approved
auto status = approval_mgr_->GetProposalStatus(proposal_id);
if (status != ProposalApprovalManager::ApprovalStatus::kApproved) {
response->set_success(false);
response->set_message("Write requires approval");
response->set_proposal_id(proposal_id);
return grpc::Status::OK; // Not an error, just needs approval
}
}
// Create snapshot before write
if (version_mgr_) {
std::string snapshot_desc = absl::StrFormat(
"Before write to 0x%X (%zu bytes)", address, data.size());
auto snapshot_result = version_mgr_->CreateSnapshot(snapshot_desc);
if (snapshot_result.ok()) {
response->set_snapshot_id(std::to_string(snapshot_result.value()));
}
}
// Perform write
std::memcpy(rom_->mutable_data() + address, data.data(), data.size());
response->set_success(true);
response->set_message("Write successful");
return grpc::Status::OK;
}
grpc::Status RomServiceImpl::GetRomInfo(
grpc::ServerContext* context,
const rom_svc::GetRomInfoRequest* request,
rom_svc::GetRomInfoResponse* response) {
if (!rom_ || !rom_->is_loaded()) {
return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION, "ROM not loaded");
}
auto* info = response->mutable_info();
info->set_title(rom_->title());
info->set_size(rom_->size());
info->set_is_loaded(rom_->is_loaded());
info->set_filename(rom_->filename());
return grpc::Status::OK;
}
grpc::Status RomServiceImpl::GetTileData(
grpc::ServerContext* context,
const rom_svc::GetTileDataRequest* request,
rom_svc::GetTileDataResponse* response) {
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED,
"GetTileData not yet implemented");
}
grpc::Status RomServiceImpl::SetTileData(
grpc::ServerContext* context,
const rom_svc::SetTileDataRequest* request,
rom_svc::SetTileDataResponse* response) {
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED,
"SetTileData not yet implemented");
}
grpc::Status RomServiceImpl::GetMapData(
grpc::ServerContext* context,
const rom_svc::GetMapDataRequest* request,
rom_svc::GetMapDataResponse* response) {
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED,
"GetMapData not yet implemented");
}
grpc::Status RomServiceImpl::SetMapData(
grpc::ServerContext* context,
const rom_svc::SetMapDataRequest* request,
rom_svc::SetMapDataResponse* response) {
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED,
"SetMapData not yet implemented");
}
grpc::Status RomServiceImpl::GetSpriteData(
grpc::ServerContext* context,
const rom_svc::GetSpriteDataRequest* request,
rom_svc::GetSpriteDataResponse* response) {
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED,
"GetSpriteData not yet implemented");
}
grpc::Status RomServiceImpl::SetSpriteData(
grpc::ServerContext* context,
const rom_svc::SetSpriteDataRequest* request,
rom_svc::SetSpriteDataResponse* response) {
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED,
"SetSpriteData not yet implemented");
}
grpc::Status RomServiceImpl::GetDialogue(
grpc::ServerContext* context,
const rom_svc::GetDialogueRequest* request,
rom_svc::GetDialogueResponse* response) {
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED,
"GetDialogue not yet implemented");
}
grpc::Status RomServiceImpl::SetDialogue(
grpc::ServerContext* context,
const rom_svc::SetDialogueRequest* request,
rom_svc::SetDialogueResponse* response) {
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED,
"SetDialogue not yet implemented");
}
} // namespace net
} // namespace yaze
#endif // YAZE_WITH_GRPC

View File

@@ -0,0 +1,181 @@
#ifndef YAZE_APP_NET_ROM_SERVICE_IMPL_H_
#define YAZE_APP_NET_ROM_SERVICE_IMPL_H_
#include <memory>
#include <string>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#ifdef YAZE_WITH_GRPC
#ifdef _WIN32
#pragma push_macro("DWORD")
#pragma push_macro("ERROR")
#undef DWORD
#undef ERROR
#endif // _WIN32
#include <grpcpp/grpcpp.h>
#include "protos/rom_service.grpc.pb.h"
#ifdef _WIN32
#pragma pop_macro("DWORD")
#pragma pop_macro("ERROR")
#endif // _WIN32
// Note: Proto files will be generated to build directory
#endif
#include "app/rom.h"
#include "app/net/rom_version_manager.h"
namespace yaze {
namespace net {
#ifdef YAZE_WITH_GRPC
/**
* @brief gRPC service implementation for remote ROM manipulation
*
* Enables remote clients (like z3ed CLI) to:
* - Read/write ROM data
* - Submit proposals for collaborative editing
* - Manage ROM versions and snapshots
* - Query ROM structures (overworld, dungeons, sprites)
*
* Thread-safe and designed for concurrent access.
*/
class RomServiceImpl final : public proto::RomService::Service {
public:
/**
* @brief Configuration for the ROM service
*/
struct Config {
bool require_approval_for_writes = true; // Submit writes as proposals
bool enable_version_management = true; // Auto-snapshot before changes
int max_read_size_bytes = 1024 * 1024; // 1MB max per read
bool allow_raw_rom_access = true; // Allow direct byte access
};
/**
* @brief Construct ROM service
* @param rom Pointer to ROM instance (not owned)
* @param version_mgr Pointer to version manager (not owned, optional)
* @param approval_mgr Pointer to approval manager (not owned, optional)
*/
RomServiceImpl(Rom* rom,
RomVersionManager* version_mgr = nullptr,
ProposalApprovalManager* approval_mgr = nullptr);
~RomServiceImpl() override = default;
// Initialize with configuration
void SetConfig(const Config& config);
// =========================================================================
// Basic ROM Operations
// =========================================================================
grpc::Status ReadBytes(
grpc::ServerContext* context,
const proto::ReadBytesRequest* request,
proto::ReadBytesResponse* response) override;
grpc::Status WriteBytes(
grpc::ServerContext* context,
const proto::WriteBytesRequest* request,
proto::WriteBytesResponse* response) override;
grpc::Status GetRomInfo(
grpc::ServerContext* context,
const proto::GetRomInfoRequest* request,
proto::GetRomInfoResponse* response) override;
// =========================================================================
// Overworld Operations
// =========================================================================
grpc::Status ReadOverworldMap(
grpc::ServerContext* context,
const proto::ReadOverworldMapRequest* request,
proto::ReadOverworldMapResponse* response) override;
grpc::Status WriteOverworldTile(
grpc::ServerContext* context,
const proto::WriteOverworldTileRequest* request,
proto::WriteOverworldTileResponse* response) override;
// =========================================================================
// Dungeon Operations
// =========================================================================
grpc::Status ReadDungeonRoom(
grpc::ServerContext* context,
const proto::ReadDungeonRoomRequest* request,
proto::ReadDungeonRoomResponse* response) override;
grpc::Status WriteDungeonTile(
grpc::ServerContext* context,
const proto::WriteDungeonTileRequest* request,
proto::WriteDungeonTileResponse* response) override;
// =========================================================================
// Sprite Operations
// =========================================================================
grpc::Status ReadSprite(
grpc::ServerContext* context,
const proto::ReadSpriteRequest* request,
proto::ReadSpriteResponse* response) override;
// =========================================================================
// Proposal System
// =========================================================================
grpc::Status SubmitRomProposal(
grpc::ServerContext* context,
const proto::SubmitRomProposalRequest* request,
proto::SubmitRomProposalResponse* response) override;
grpc::Status GetProposalStatus(
grpc::ServerContext* context,
const proto::GetProposalStatusRequest* request,
proto::GetProposalStatusResponse* response) override;
// =========================================================================
// Version Management
// =========================================================================
grpc::Status CreateSnapshot(
grpc::ServerContext* context,
const proto::CreateSnapshotRequest* request,
proto::CreateSnapshotResponse* response) override;
grpc::Status RestoreSnapshot(
grpc::ServerContext* context,
const proto::RestoreSnapshotRequest* request,
proto::RestoreSnapshotResponse* response) override;
grpc::Status ListSnapshots(
grpc::ServerContext* context,
const proto::ListSnapshotsRequest* request,
proto::ListSnapshotsResponse* response) override;
private:
Config config_;
Rom* rom_; // Not owned
RomVersionManager* version_mgr_; // Not owned, may be null
ProposalApprovalManager* approval_mgr_; // Not owned, may be null
// Helper to check if ROM is loaded
grpc::Status ValidateRomLoaded();
// Helper to create snapshot before write operations
absl::Status MaybeCreateSnapshot(const std::string& description);
};
#endif // YAZE_WITH_GRPC
} // namespace net
} // namespace yaze
#endif // YAZE_APP_NET_ROM_SERVICE_IMPL_H_

View File

@@ -0,0 +1,533 @@
#include "app/net/rom_version_manager.h"
#include <algorithm>
#include <chrono>
#include <cstring>
#include "absl/strings/str_format.h"
#include "absl/strings/str_cat.h"
// For compression (placeholder - would use zlib or similar)
#include <vector>
#ifdef YAZE_WITH_JSON
#include "nlohmann/json.hpp"
#endif
namespace yaze {
namespace net {
namespace {
// Simple hash function (in production, use SHA256)
std::string ComputeHash(const std::vector<uint8_t>& data) {
uint32_t hash = 0;
for (size_t i = 0; i < data.size(); ++i) {
hash = hash * 31 + data[i];
}
return absl::StrFormat("%08x", hash);
}
// Generate unique ID
std::string GenerateId() {
auto now = std::chrono::system_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()).count();
return absl::StrFormat("snap_%lld", ms);
}
int64_t GetCurrentTimestamp() {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
}
} // namespace
// ============================================================================
// RomVersionManager Implementation
// ============================================================================
RomVersionManager::RomVersionManager(Rom* rom)
: rom_(rom),
last_backup_time_(0) {
}
RomVersionManager::~RomVersionManager() {
// Cleanup if needed
}
absl::Status RomVersionManager::Initialize(const Config& config) {
config_ = config;
// Create initial snapshot
auto initial_result = CreateSnapshot(
"Initial state",
"system",
true);
if (!initial_result.ok()) {
return initial_result.status();
}
// Mark as safe point
return MarkAsSafePoint(*initial_result);
}
absl::StatusOr<std::string> RomVersionManager::CreateSnapshot(
const std::string& description,
const std::string& creator,
bool is_checkpoint) {
if (!rom_ || !rom_->is_loaded()) {
return absl::FailedPreconditionError("ROM not loaded");
}
// Get ROM data
std::vector<uint8_t> rom_data(rom_->size());
std::memcpy(rom_data.data(), rom_->data(), rom_->size());
// Create snapshot
RomSnapshot snapshot;
snapshot.snapshot_id = GenerateId();
snapshot.description = description;
snapshot.timestamp = GetCurrentTimestamp();
snapshot.rom_hash = ComputeHash(rom_data);
snapshot.creator = creator;
snapshot.is_checkpoint = is_checkpoint;
snapshot.is_safe_point = false;
// Compress if enabled
if (config_.compress_snapshots) {
snapshot.rom_data = CompressData(rom_data);
snapshot.compressed_size = snapshot.rom_data.size();
} else {
snapshot.rom_data = std::move(rom_data);
snapshot.compressed_size = snapshot.rom_data.size();
}
#ifdef YAZE_WITH_JSON
snapshot.metadata = nlohmann::json::object();
snapshot.metadata["size"] = rom_->size();
snapshot.metadata["auto_backup"] = !is_checkpoint;
#endif
// Store snapshot
snapshots_[snapshot.snapshot_id] = std::move(snapshot);
last_known_hash_ = snapshots_[snapshot.snapshot_id].rom_hash;
// Cleanup if needed
if (snapshots_.size() > config_.max_snapshots) {
CleanupOldSnapshots();
}
return snapshots_[snapshot.snapshot_id].snapshot_id;
}
absl::Status RomVersionManager::RestoreSnapshot(const std::string& snapshot_id) {
auto it = snapshots_.find(snapshot_id);
if (it == snapshots_.end()) {
return absl::NotFoundError(absl::StrCat("Snapshot not found: ", snapshot_id));
}
if (!rom_ || !rom_->is_loaded()) {
return absl::FailedPreconditionError("ROM not loaded");
}
const RomSnapshot& snapshot = it->second;
// Decompress if needed
std::vector<uint8_t> rom_data;
if (config_.compress_snapshots) {
rom_data = DecompressData(snapshot.rom_data);
} else {
rom_data = snapshot.rom_data;
}
// Verify size matches
if (rom_data.size() != rom_->size()) {
return absl::DataLossError("Snapshot size mismatch");
}
// Create backup before restore
auto backup_result = CreateSnapshot(
"Pre-restore backup",
"system",
false);
if (!backup_result.ok()) {
return absl::InternalError("Failed to create pre-restore backup");
}
// Restore ROM data
std::memcpy(rom_->mutable_data(), rom_data.data(), rom_data.size());
last_known_hash_ = snapshot.rom_hash;
return absl::OkStatus();
}
absl::Status RomVersionManager::MarkAsSafePoint(const std::string& snapshot_id) {
auto it = snapshots_.find(snapshot_id);
if (it == snapshots_.end()) {
return absl::NotFoundError("Snapshot not found");
}
it->second.is_safe_point = true;
return absl::OkStatus();
}
std::vector<RomSnapshot> RomVersionManager::GetSnapshots(bool safe_points_only) const {
std::vector<RomSnapshot> result;
for (const auto& [id, snapshot] : snapshots_) {
if (!safe_points_only || snapshot.is_safe_point) {
result.push_back(snapshot);
}
}
// Sort by timestamp (newest first)
std::sort(result.begin(), result.end(),
[](const RomSnapshot& a, const RomSnapshot& b) {
return a.timestamp > b.timestamp;
});
return result;
}
absl::StatusOr<RomSnapshot> RomVersionManager::GetSnapshot(
const std::string& snapshot_id) const {
auto it = snapshots_.find(snapshot_id);
if (it == snapshots_.end()) {
return absl::NotFoundError("Snapshot not found");
}
return it->second;
}
absl::Status RomVersionManager::DeleteSnapshot(const std::string& snapshot_id) {
auto it = snapshots_.find(snapshot_id);
if (it == snapshots_.end()) {
return absl::NotFoundError("Snapshot not found");
}
// Don't allow deleting safe points
if (it->second.is_safe_point) {
return absl::FailedPreconditionError("Cannot delete safe point");
}
snapshots_.erase(it);
return absl::OkStatus();
}
absl::StatusOr<bool> RomVersionManager::DetectCorruption() {
if (!config_.enable_corruption_detection) {
return false;
}
if (!rom_ || !rom_->is_loaded()) {
return absl::FailedPreconditionError("ROM not loaded");
}
// Compute current hash
std::vector<uint8_t> current_data(rom_->size());
std::memcpy(current_data.data(), rom_->data(), rom_->size());
std::string current_hash = ComputeHash(current_data);
// Basic integrity checks
auto integrity_status = ValidateRomIntegrity();
if (!integrity_status.ok()) {
return true; // Corruption detected
}
// Check against last known good hash (if modified unexpectedly)
if (!last_known_hash_.empty() && current_hash != last_known_hash_) {
// ROM changed without going through version manager
// This might be intentional, so just flag it
return false;
}
return false;
}
absl::Status RomVersionManager::AutoRecover() {
// Find most recent safe point
auto snapshots = GetSnapshots(true);
if (snapshots.empty()) {
return absl::NotFoundError("No safe points available for recovery");
}
// Restore from most recent safe point
return RestoreSnapshot(snapshots[0].snapshot_id);
}
std::string RomVersionManager::GetCurrentHash() const {
if (!rom_ || !rom_->is_loaded()) {
return "";
}
std::vector<uint8_t> data(rom_->size());
std::memcpy(data.data(), rom_->data(), rom_->size());
return ComputeHash(data);
}
absl::Status RomVersionManager::CleanupOldSnapshots() {
// Keep safe points and checkpoints
// Remove oldest auto-backups first
std::vector<std::pair<int64_t, std::string>> auto_backups;
for (const auto& [id, snapshot] : snapshots_) {
if (!snapshot.is_safe_point && !snapshot.is_checkpoint) {
auto_backups.push_back({snapshot.timestamp, id});
}
}
// Sort by timestamp (oldest first)
std::sort(auto_backups.begin(), auto_backups.end());
// Delete oldest until within limits
while (snapshots_.size() > config_.max_snapshots && !auto_backups.empty()) {
snapshots_.erase(auto_backups.front().second);
auto_backups.erase(auto_backups.begin());
}
// Check storage limit
while (GetTotalStorageUsed() > config_.max_storage_mb * 1024 * 1024 &&
!auto_backups.empty()) {
snapshots_.erase(auto_backups.front().second);
auto_backups.erase(auto_backups.begin());
}
return absl::OkStatus();
}
RomVersionManager::Stats RomVersionManager::GetStats() const {
Stats stats = {};
stats.total_snapshots = snapshots_.size();
for (const auto& [id, snapshot] : snapshots_) {
if (snapshot.is_safe_point) stats.safe_points++;
if (snapshot.is_checkpoint) stats.manual_checkpoints++;
if (!snapshot.is_checkpoint) stats.auto_backups++;
stats.total_storage_bytes += snapshot.compressed_size;
if (stats.oldest_snapshot_timestamp == 0 ||
snapshot.timestamp < stats.oldest_snapshot_timestamp) {
stats.oldest_snapshot_timestamp = snapshot.timestamp;
}
if (snapshot.timestamp > stats.newest_snapshot_timestamp) {
stats.newest_snapshot_timestamp = snapshot.timestamp;
}
}
return stats;
}
// Private helper methods
std::string RomVersionManager::ComputeRomHash() const {
if (!rom_ || !rom_->is_loaded()) {
return "";
}
std::vector<uint8_t> data(rom_->size());
std::memcpy(data.data(), rom_->data(), rom_->size());
return ComputeHash(data);
}
std::vector<uint8_t> RomVersionManager::CompressData(
const std::vector<uint8_t>& data) const {
// Placeholder: In production, use zlib or similar
// For now, just return the data as-is
return data;
}
std::vector<uint8_t> RomVersionManager::DecompressData(
const std::vector<uint8_t>& compressed) const {
// Placeholder: In production, use zlib or similar
return compressed;
}
absl::Status RomVersionManager::ValidateRomIntegrity() const {
if (!rom_ || !rom_->is_loaded()) {
return absl::FailedPreconditionError("ROM not loaded");
}
// Basic checks
if (rom_->size() == 0) {
return absl::DataLossError("ROM size is zero");
}
// Check for valid SNES header
// (This is a simplified check - real validation would be more thorough)
if (rom_->size() < 0x8000) {
return absl::DataLossError("ROM too small to be valid");
}
return absl::OkStatus();
}
size_t RomVersionManager::GetTotalStorageUsed() const {
size_t total = 0;
for (const auto& [id, snapshot] : snapshots_) {
total += snapshot.compressed_size;
}
return total;
}
// ============================================================================
// ProposalApprovalManager Implementation
// ============================================================================
ProposalApprovalManager::ProposalApprovalManager(RomVersionManager* version_mgr)
: version_mgr_(version_mgr),
mode_(ApprovalMode::kHostOnly) {
}
void ProposalApprovalManager::SetApprovalMode(ApprovalMode mode) {
mode_ = mode;
}
void ProposalApprovalManager::SetHost(const std::string& host_username) {
host_username_ = host_username;
}
absl::Status ProposalApprovalManager::SubmitProposal(
const std::string& proposal_id,
const std::string& sender,
const std::string& description,
const nlohmann::json& proposal_data) {
ApprovalStatus status;
status.proposal_id = proposal_id;
status.status = "pending";
status.created_at = GetCurrentTimestamp();
status.decided_at = 0;
// Create snapshot before potential application
auto snapshot_result = version_mgr_->CreateSnapshot(
absl::StrCat("Before proposal: ", description),
sender,
false);
if (!snapshot_result.ok()) {
return snapshot_result.status();
}
status.snapshot_before = *snapshot_result;
proposals_[proposal_id] = status;
return absl::OkStatus();
}
absl::Status ProposalApprovalManager::VoteOnProposal(
const std::string& proposal_id,
const std::string& username,
bool approved) {
auto it = proposals_.find(proposal_id);
if (it == proposals_.end()) {
return absl::NotFoundError("Proposal not found");
}
ApprovalStatus& status = it->second;
if (status.status != "pending") {
return absl::FailedPreconditionError("Proposal already decided");
}
// Record vote
status.votes[username] = approved;
// Check if decision can be made
if (CheckApprovalThreshold(status)) {
status.status = "approved";
status.decided_at = GetCurrentTimestamp();
} else {
// Check if rejection threshold reached
size_t rejection_count = 0;
for (const auto& [user, vote] : status.votes) {
if (!vote) rejection_count++;
}
// If host rejected (in host-only mode), reject immediately
if (mode_ == ApprovalMode::kHostOnly &&
username == host_username_ && !approved) {
status.status = "rejected";
status.decided_at = GetCurrentTimestamp();
}
}
return absl::OkStatus();
}
bool ProposalApprovalManager::CheckApprovalThreshold(
const ApprovalStatus& status) const {
switch (mode_) {
case ApprovalMode::kHostOnly:
// Only host vote matters
if (status.votes.find(host_username_) != status.votes.end()) {
return status.votes.at(host_username_);
}
return false;
case ApprovalMode::kMajorityVote: {
size_t approval_count = 0;
for (const auto& [user, approved] : status.votes) {
if (approved) approval_count++;
}
return approval_count > participants_.size() / 2;
}
case ApprovalMode::kUnanimous: {
if (status.votes.size() < participants_.size()) {
return false; // Not everyone voted yet
}
for (const auto& [user, approved] : status.votes) {
if (!approved) return false;
}
return true;
}
case ApprovalMode::kAutoApprove:
return true;
}
return false;
}
bool ProposalApprovalManager::IsProposalApproved(
const std::string& proposal_id) const {
auto it = proposals_.find(proposal_id);
if (it == proposals_.end()) {
return false;
}
return it->second.status == "approved";
}
std::vector<ProposalApprovalManager::ApprovalStatus>
ProposalApprovalManager::GetPendingProposals() const {
std::vector<ApprovalStatus> pending;
for (const auto& [id, status] : proposals_) {
if (status.status == "pending") {
pending.push_back(status);
}
}
return pending;
}
absl::StatusOr<ProposalApprovalManager::ApprovalStatus>
ProposalApprovalManager::GetProposalStatus(
const std::string& proposal_id) const {
auto it = proposals_.find(proposal_id);
if (it == proposals_.end()) {
return absl::NotFoundError("Proposal not found");
}
return it->second;
}
} // namespace net
} // namespace yaze

View File

@@ -0,0 +1,292 @@
#ifndef YAZE_APP_NET_ROM_VERSION_MANAGER_H_
#define YAZE_APP_NET_ROM_VERSION_MANAGER_H_
#include <map>
#include <memory>
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "app/rom.h"
#ifdef YAZE_WITH_JSON
#include "nlohmann/json.hpp"
#endif
namespace yaze {
namespace net {
/**
* @struct RomSnapshot
* @brief Represents a versioned snapshot of ROM state
*/
struct RomSnapshot {
std::string snapshot_id;
std::string description;
int64_t timestamp;
std::string rom_hash;
std::vector<uint8_t> rom_data;
size_t compressed_size;
// Metadata
std::string creator;
bool is_checkpoint; // Manual checkpoint vs auto-backup
bool is_safe_point; // Marked as "known good" by host
#ifdef YAZE_WITH_JSON
nlohmann::json metadata; // Custom metadata (proposals applied, etc.)
#endif
};
/**
* @struct VersionDiff
* @brief Represents differences between two ROM versions
*/
struct VersionDiff {
std::string from_snapshot_id;
std::string to_snapshot_id;
std::vector<std::pair<size_t, std::vector<uint8_t>>> changes; // offset, data
size_t total_bytes_changed;
std::vector<std::string> proposals_applied; // IDs of proposals in this diff
};
/**
* @class RomVersionManager
* @brief Manages ROM versioning, snapshots, and rollback capabilities
*
* Provides:
* - Automatic periodic snapshots
* - Manual checkpoints
* - Rollback to any previous version
* - Diff generation between versions
* - Corruption detection and recovery
*/
class RomVersionManager {
public:
struct Config {
bool enable_auto_backup = true;
int auto_backup_interval_seconds = 300; // 5 minutes
size_t max_snapshots = 50;
size_t max_storage_mb = 500; // 500MB max for all snapshots
bool compress_snapshots = true;
bool enable_corruption_detection = true;
};
explicit RomVersionManager(Rom* rom);
~RomVersionManager();
/**
* Initialize version management
*/
absl::Status Initialize(const Config& config);
/**
* Create a snapshot of current ROM state
*/
absl::StatusOr<std::string> CreateSnapshot(
const std::string& description,
const std::string& creator,
bool is_checkpoint = false);
/**
* Restore ROM to a previous snapshot
*/
absl::Status RestoreSnapshot(const std::string& snapshot_id);
/**
* Mark a snapshot as a safe point (host-verified)
*/
absl::Status MarkAsSafePoint(const std::string& snapshot_id);
/**
* Get all snapshots, sorted by timestamp
*/
std::vector<RomSnapshot> GetSnapshots(bool safe_points_only = false) const;
/**
* Get a specific snapshot
*/
absl::StatusOr<RomSnapshot> GetSnapshot(const std::string& snapshot_id) const;
/**
* Delete a snapshot
*/
absl::Status DeleteSnapshot(const std::string& snapshot_id);
/**
* Generate diff between two snapshots
*/
absl::StatusOr<VersionDiff> GenerateDiff(
const std::string& from_id,
const std::string& to_id) const;
/**
* Check for ROM corruption
*/
absl::StatusOr<bool> DetectCorruption();
/**
* Auto-recover from corruption using nearest safe point
*/
absl::Status AutoRecover();
/**
* Export snapshot to file
*/
absl::Status ExportSnapshot(
const std::string& snapshot_id,
const std::string& filepath);
/**
* Import snapshot from file
*/
absl::Status ImportSnapshot(const std::string& filepath);
/**
* Get current ROM hash
*/
std::string GetCurrentHash() const;
/**
* Cleanup old snapshots based on policy
*/
absl::Status CleanupOldSnapshots();
/**
* Get statistics
*/
struct Stats {
size_t total_snapshots;
size_t safe_points;
size_t auto_backups;
size_t manual_checkpoints;
size_t total_storage_bytes;
int64_t oldest_snapshot_timestamp;
int64_t newest_snapshot_timestamp;
};
Stats GetStats() const;
private:
Rom* rom_;
Config config_;
std::map<std::string, RomSnapshot> snapshots_;
std::string last_known_hash_;
int64_t last_backup_time_;
// Helper functions
std::string ComputeRomHash() const;
std::vector<uint8_t> CompressData(const std::vector<uint8_t>& data) const;
std::vector<uint8_t> DecompressData(const std::vector<uint8_t>& compressed) const;
absl::Status ValidateRomIntegrity() const;
size_t GetTotalStorageUsed() const;
void PruneOldSnapshots();
};
/**
* @class ProposalApprovalManager
* @brief Manages proposal approval workflow for collaborative sessions
*
* Features:
* - Host approval required for all changes
* - Participant voting system
* - Automatic rollback on rejection
* - Change tracking and audit log
*/
class ProposalApprovalManager {
public:
enum class ApprovalMode {
kHostOnly, // Only host can approve
kMajorityVote, // Majority of participants must approve
kUnanimous, // All participants must approve
kAutoApprove // No approval needed (dangerous!)
};
struct ApprovalStatus {
std::string proposal_id;
std::string status; // "pending", "approved", "rejected", "applied"
std::map<std::string, bool> votes; // username -> approved
int64_t created_at;
int64_t decided_at;
std::string snapshot_before; // Snapshot ID before applying
std::string snapshot_after; // Snapshot ID after applying
};
explicit ProposalApprovalManager(RomVersionManager* version_mgr);
/**
* Set approval mode for the session
*/
void SetApprovalMode(ApprovalMode mode);
/**
* Set host username
*/
void SetHost(const std::string& host_username);
/**
* Submit a proposal for approval
*/
absl::Status SubmitProposal(
const std::string& proposal_id,
const std::string& sender,
const std::string& description,
const nlohmann::json& proposal_data);
/**
* Vote on a proposal
*/
absl::Status VoteOnProposal(
const std::string& proposal_id,
const std::string& username,
bool approved);
/**
* Apply an approved proposal
*/
absl::Status ApplyProposal(
const std::string& proposal_id,
Rom* rom);
/**
* Reject and rollback a proposal
*/
absl::Status RejectProposal(const std::string& proposal_id);
/**
* Get proposal status
*/
absl::StatusOr<ApprovalStatus> GetProposalStatus(
const std::string& proposal_id) const;
/**
* Get all pending proposals
*/
std::vector<ApprovalStatus> GetPendingProposals() const;
/**
* Check if proposal is approved
*/
bool IsProposalApproved(const std::string& proposal_id) const;
/**
* Get audit log
*/
std::vector<ApprovalStatus> GetAuditLog() const;
private:
RomVersionManager* version_mgr_;
ApprovalMode mode_;
std::string host_username_;
std::map<std::string, ApprovalStatus> proposals_;
std::vector<std::string> participants_;
bool CheckApprovalThreshold(const ApprovalStatus& status) const;
};
} // namespace net
} // namespace yaze
#endif // YAZE_APP_NET_ROM_VERSION_MANAGER_H_

View 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 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 yaze

View 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 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 yaze
#endif // YAZE_APP_NET_WEBSOCKET_CLIENT_H_