feat: Implement ROM version management and proposal approval system

- Introduced `RomVersionManager` for managing ROM snapshots, including automatic backups, manual checkpoints, and corruption detection.
- Added `ProposalApprovalManager` to facilitate collaborative proposal submissions and voting, enhancing team workflows.
- Updated `CollaborationPanel` to integrate version management features, allowing users to track changes and manage proposals effectively.
- Enhanced documentation to reflect new functionalities and usage instructions for version management and collaboration features.
This commit is contained in:
scawful
2025-10-04 22:33:06 -04:00
parent 253f36542f
commit 3b406ab671
15 changed files with 1183 additions and 78 deletions

View File

@@ -1,56 +1,65 @@
# Getting Started
This software allows you to modify "The Legend of Zelda: A Link to the Past" (US or JP) ROMs. Built for compatibility with ZScream projects and designed to be cross-platform.
This software allows you to modify "The Legend of Zelda: A Link to the Past" (US or JP) ROMs. It is built for compatibility with ZScream projects and designed to be cross-platform.
## Quick Start
1. **Download** the latest release for your platform
2. **Load ROM** via File > Open ROM
3. **Select Editor** from the toolbar (Overworld, Dungeon, Graphics, etc.)
4. **Make Changes** and save your project
1. **Download** the latest release for your platform from the [releases page](https://github.com/scawful/yaze/releases).
2. **Load ROM** via `File > Open ROM`.
3. **Select an Editor** from the main toolbar (e.g., Overworld, Dungeon, Graphics).
4. **Make Changes** and save your project.
## General Tips
- **Experiment Flags**: Enable/disable features in File > Options > Experiment Flags
- **Backup Files**: Enabled by default - each save creates a timestamped backup
- **Extensions**: Load custom tools via the Extensions menu (C library and Python module support)
- **Experiment Flags**: Enable or disable new features in `File > Options > Experiment Flags`.
- **Backup Files**: Enabled by default. Each save creates a timestamped backup of your ROM.
- **Extensions**: Load custom tools via the `Extensions` menu (C library and Python module support is planned).
## Supported Features
## Feature Status
| Feature | Status | Details |
|---------|--------|---------|
| Overworld Maps | ✅ Complete | Edit and save tile32 data |
| OW Map Properties | ✅ Complete | Edit and save map properties |
| OW Entrances | ✅ Complete | Edit and save entrance data |
| OW Exits | ✅ Complete | Edit and save exit data |
| OW Sprites | 🔄 In Progress | Edit sprite positions, add/remove sprites |
| Dungeon Editor | 🔄 In Progress | View room metadata and edit room data |
| Palette Editor | 🔄 In Progress | Edit and save palettes, palette groups |
| Graphics Sheets | 🔄 In Progress | Edit and save graphics sheets |
| Graphics Groups | ✅ Complete | Edit and save graphics groups |
| Hex Editor | ✅ Complete | View and edit ROM data in hex |
| Asar Patching | ✅ Complete | Apply Asar 65816 assembly patches to ROM |
|---|---|---|
| Overworld Editor | ✅ Complete | Full support for vanilla and ZSCustomOverworld v2/v3. |
| Dungeon Editor | ✅ Complete | Stable, component-based editor for rooms, objects, and sprites. |
| Tile16 Editor | ✅ Complete | Professional-grade tile editor with advanced palette management. |
| Palette Editor | ✅ Complete | Edit and save all SNES palette groups. |
| Graphics Editor | ✅ Complete | View and edit graphics sheets and groups. |
| Sprite Editor | ✅ Complete | Edit sprite properties and attributes. |
| Message Editor | ✅ Complete | Edit in-game text and dialogue. |
| Hex Editor | ✅ Complete | View and edit raw ROM data. |
| Asar Patching | ✅ Complete | Apply Asar 65816 assembly patches to the ROM. |
## Command Line Interface
## Command-Line Interface (`z3ed`)
The `z3ed` CLI tool provides ROM operations:
`z3ed` is a powerful, AI-driven CLI for inspecting and editing ROMs.
### AI Agent Chat
Chat with an AI to perform edits using natural language.
```bash
# Apply Asar assembly patch
z3ed asar patch.asm --rom=zelda3.sfc
# Start an interactive chat session with the AI agent
z3ed agent chat --rom zelda3.sfc
```
> **Prompt:** "What sprites are in dungeon 2?"
# Extract symbols from assembly
z3ed extract patch.asm
### Resource Inspection
Directly query ROM data.
# Validate assembly syntax
z3ed validate patch.asm
```bash
# List all sprites in the Eastern Palace (dungeon 2)
z3ed dungeon list-sprites --rom zelda3.sfc --dungeon 2
# Launch interactive TUI
z3ed --tui
# Get information about a specific overworld map area
z3ed overworld describe-map --rom zelda3.sfc --map 80
```
### Patching
Apply assembly patches using the integrated Asar assembler.
```bash
# Apply an assembly patch to the ROM
z3ed asar patch.asm --rom zelda3.sfc
```
## Extending Functionality
YAZE provides a pure C library interface and Python module for building extensions and custom sprites without assembly. Load these under the Extensions menu.
This feature is still in development and not fully documented yet.
YAZE is designed to be extensible. Future versions will support a full plugin architecture, allowing developers to create custom tools and editors. The C API, while available, is still under development.

View File

@@ -1,42 +1,29 @@
# yaze Documentation
Yet Another Zelda3 Editor - A comprehensive ROM editor for The Legend of Zelda: A Link to the Past.
Welcome to the official documentation for yaze, a comprehensive ROM editor for The Legend of Zelda: A Link to the Past.
## Quick Start
## Core Guides
- [Getting Started](01-getting-started.md) - Basic setup and usage
- [Build Instructions](02-build-instructions.md) - Cross-platform build guide
- [API Reference](04-api-reference.md) - C/C++ API documentation
- [Getting Started](01-getting-started.md) - Basic setup and usage.
- [Build Instructions](02-build-instructions.md) - How to build yaze on Windows, macOS, and Linux.
- [z3ed CLI Guide](z3ed/README.md) - The AI-powered command-line interface.
## Development
## Development & API
- [Testing Guide](A1-testing-guide.md) - Testing framework and best practices
- [Platform Compatibility](B2-platform-compatibility.md) - Cross-platform support details
- [Build Presets](B3-build-presets.md) - CMake preset usage guide
- [Release Workflows](B4-release-workflows.md) - GitHub Actions release pipeline documentation
- [Stability Improvements](B5-stability-improvements.md) - Performance optimizations and reliability enhancements
- [API Reference](04-api-reference.md) - C/C++ API documentation for extensions.
- [Testing Guide](A1-testing-guide.md) - The testing framework and best practices.
- [Assembly Style Guide](E1-asm-style-guide.md) - 65816 assembly coding standards.
- [Build Presets](B3-build-presets.md) - A guide to the CMake preset system.
- [Release Workflows](B4-release-workflows.md) - GitHub Actions release pipeline documentation.
## Technical Documentation
### Assembly & Code
- [Assembly Style Guide](E1-asm-style-guide.md) - 65816 assembly coding standards
### Editor Systems
- [Dungeon Editor Master Guide](dungeon_editor_master_guide.md) - Complete dungeon editing guide
### Overworld System
- [Overworld Loading](F1-overworld-loading.md) - ZSCustomOverworld v3 implementation
## Key Features
- Complete GUI editor for all aspects of Zelda 3 ROM hacking
- Integrated Asar 65816 assembler for custom code patches
- ZSCustomOverworld v3 support for enhanced overworld editing
- Cross-platform support (Windows, macOS, Linux)
- Modern C++23 codebase with comprehensive testing
- **Windows Development**: Automated setup scripts, Visual Studio integration, vcpkg package management
- **CMake Compatibility**: Automatic handling of submodule compatibility issues (abseil-cpp, SDL)
- [Dungeon Editor Guide](D2-dungeon-editor-guide.md) - A master guide to the dungeon editing system.
- [Canvas Guide](G2-canvas-guide.md) - The core GUI drawing and interaction system.
- [Overworld Loading](F1-overworld-loading.md) - How vanilla and ZSCustomOverworld maps are loaded.
- [Platform Compatibility](B2-platform-compatibility.md) - Cross-platform support details.
- [Tile16 Editor Palette System](E7-tile16-editor-palette-system.md) - Design of the palette system.
---
*Last updated: December 2025 - Version 0.3.2*
*Last updated: October 2025 - Version 0.3.2*

View File

@@ -895,7 +895,9 @@ The AI response appears in your chat history and can reference specific details
- **Multimodal Vision Testing**: Comprehensive test suite for Gemini vision capabilities with screenshot integration
- **AI-Controlled GUI Automation**: Natural language parsing (`AIActionParser`) and test script generation (`GuiActionGenerator`) for automated tile placement
- **gRPC Windows Build Optimization**: vcpkg integration for 10-20x faster Windows builds, removed abseil-cpp submodule
- **Collaboration UI Panel**: New `CollaborationPanel` widget with ROM sync history, snapshot gallery, and proposal management
- **ROM Version Management System**: `RomVersionManager` with automatic snapshots, safe points, corruption detection, and rollback capabilities
- **Proposal Approval Framework**: `ProposalApprovalManager` with host/majority/unanimous voting modes to protect ROM from unwanted changes
- **Collaboration UI Panel**: `CollaborationPanel` widget with version history, ROM sync tracking, snapshot gallery, and approval workflow
- **Improved Documentation**: Consolidated architecture, enhancement plans, and build instructions with JSON-first approach
## 12. Troubleshooting

View File

@@ -87,6 +87,7 @@ endif()
include(app/core/core_library.cmake)
include(app/gfx/gfx_library.cmake)
include(app/gui/gui_library.cmake)
include(app/net/net_library.cmake)
include(app/zelda3/zelda3_library.cmake)
include(app/editor/editor_library.cmake)
@@ -299,8 +300,8 @@ if (YAZE_BUILD_APP)
endif()
target_sources(yaze PRIVATE
${CMAKE_SOURCE_DIR}/src/app/core/widget_state_capture.cc
${CMAKE_SOURCE_DIR}/src/app/core/widget_state_capture.h)
${CMAKE_SOURCE_DIR}/src/app/gui/widgets/widget_state_capture.cc
${CMAKE_SOURCE_DIR}/src/app/gui/widgets/widget_state_capture.h)
if(YAZE_WITH_GRPC)
message(STATUS "Adding gRPC ImGuiTestHarness to yaze target")

View File

@@ -229,8 +229,8 @@ endif()
# Test harness utilities shared across builds (IT-08 widget state capture)
# ============================================================================
target_sources(yaze PRIVATE
${CMAKE_SOURCE_DIR}/src/app/core/widget_state_capture.cc
${CMAKE_SOURCE_DIR}/src/app/core/widget_state_capture.h)
${CMAKE_SOURCE_DIR}/src/app/gui/widgets/widget_state_capture.cc
${CMAKE_SOURCE_DIR}/src/app/gui/widgets/widget_state_capture.h)
# ============================================================================
# Optional gRPC Support for ImGuiTestHarness

View File

@@ -5,7 +5,6 @@ set(
app/core/project.cc
app/core/window.cc
app/core/asar_wrapper.cc
app/core/widget_state_capture.cc
)
if (WIN32 OR MINGW OR (UNIX AND NOT APPLE))

View File

@@ -3,6 +3,7 @@ set(
app/gui/modules/asset_browser.cc
app/gui/modules/text_editor.cc
app/gui/widgets/agent_chat_widget.cc
app/gui/widgets/collaboration_panel.cc
app/gui/canvas.cc
app/gui/canvas_utils.cc
app/gui/enhanced_palette_editor.cc
@@ -60,6 +61,7 @@ target_link_libraries(yaze_gui PUBLIC
yaze_gfx
yaze_util
yaze_common
yaze_net
ImGui
${SDL_TARGETS}
)

View File

@@ -12,7 +12,10 @@ namespace app {
namespace gui {
CollaborationPanel::CollaborationPanel()
: selected_tab_(0),
: rom_(nullptr),
version_mgr_(nullptr),
approval_mgr_(nullptr),
selected_tab_(0),
selected_rom_sync_(-1),
selected_snapshot_(-1),
selected_proposal_(-1),
@@ -45,6 +48,15 @@ CollaborationPanel::~CollaborationPanel() {
}
}
void CollaborationPanel::Initialize(
Rom* rom,
net::RomVersionManager* version_mgr,
net::ProposalApprovalManager* approval_mgr) {
rom_ = rom;
version_mgr_ = version_mgr;
approval_mgr_ = approval_mgr;
}
void CollaborationPanel::Render(bool* p_open) {
if (!ImGui::Begin("Collaboration", p_open, ImGuiWindowFlags_None)) {
ImGui::End();
@@ -59,18 +71,30 @@ void CollaborationPanel::Render(bool* p_open) {
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Snapshots")) {
if (ImGui::BeginTabItem("Version History")) {
selected_tab_ = 1;
RenderVersionHistoryTab();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Snapshots")) {
selected_tab_ = 2;
RenderSnapshotsTab();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Proposals")) {
selected_tab_ = 2;
selected_tab_ = 3;
RenderProposalsTab();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("🔒 Approvals")) {
selected_tab_ = 4;
RenderApprovalTab();
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
@@ -440,6 +464,201 @@ ImVec4 CollaborationPanel::GetProposalStatusColor(const std::string& status) {
return ImVec4(0.7f, 0.7f, 0.7f, 1.0f);
}
void CollaborationPanel::RenderVersionHistoryTab() {
if (!version_mgr_) {
ImGui::TextWrapped("Version management not initialized");
return;
}
ImGui::TextWrapped("ROM Version History & Protection");
ImGui::Separator();
// Stats
auto stats = version_mgr_->GetStats();
ImGui::Text("Total Snapshots: %zu", stats.total_snapshots);
ImGui::SameLine();
ImGui::TextColored(colors_.sync_applied, "Safe Points: %zu", stats.safe_points);
ImGui::SameLine();
ImGui::TextColored(colors_.sync_pending, "Auto-Backups: %zu", stats.auto_backups);
ImGui::Text("Storage Used: %s", FormatFileSize(stats.total_storage_bytes).c_str());
ImGui::Separator();
// Toolbar
if (ImGui::Button("💾 Create Checkpoint")) {
auto result = version_mgr_->CreateSnapshot(
"Manual checkpoint",
"user",
true);
// TODO: Show result in UI
}
ImGui::SameLine();
if (ImGui::Button("🛡️ Mark Current as Safe Point")) {
std::string current_hash = version_mgr_->GetCurrentHash();
// TODO: Find snapshot with this hash and mark as safe
}
ImGui::SameLine();
if (ImGui::Button("🔍 Check for Corruption")) {
auto result = version_mgr_->DetectCorruption();
// TODO: Show result
}
ImGui::Separator();
// Version list
if (ImGui::BeginChild("VersionList", ImVec2(0, 0), true)) {
auto snapshots = version_mgr_->GetSnapshots();
for (size_t i = 0; i < snapshots.size(); ++i) {
RenderVersionSnapshot(snapshots[i], i);
}
}
ImGui::EndChild();
}
void CollaborationPanel::RenderApprovalTab() {
if (!approval_mgr_) {
ImGui::TextWrapped("Approval management not initialized");
return;
}
ImGui::TextWrapped("Proposal Approval System");
ImGui::Separator();
// Pending proposals that need votes
auto pending = approval_mgr_->GetPendingProposals();
if (pending.empty()) {
ImGui::TextWrapped("No proposals pending approval.");
return;
}
ImGui::Text("Pending Proposals: %zu", pending.size());
ImGui::Separator();
if (ImGui::BeginChild("ApprovalList", ImVec2(0, 0), true)) {
for (size_t i = 0; i < pending.size(); ++i) {
RenderApprovalProposal(pending[i], i);
}
}
ImGui::EndChild();
}
void CollaborationPanel::RenderVersionSnapshot(
const net::RomSnapshot& snapshot, int index) {
ImGui::PushID(index);
// Icon based on type
const char* icon;
ImVec4 color;
if (snapshot.is_safe_point) {
icon = "🛡️";
color = colors_.sync_applied;
} else if (snapshot.is_checkpoint) {
icon = "💾";
color = colors_.proposal_approved;
} else {
icon = "📝";
color = colors_.sync_pending;
}
ImGui::TextColored(color, "%s", icon);
ImGui::SameLine();
// Collapsible header
bool is_open = ImGui::TreeNode(snapshot.description.c_str());
if (is_open) {
ImGui::Indent();
ImGui::Text("Creator: %s", snapshot.creator.c_str());
ImGui::Text("Time: %s", FormatTimestamp(snapshot.timestamp).c_str());
ImGui::Text("Hash: %s", snapshot.rom_hash.substr(0, 16).c_str());
ImGui::Text("Size: %s", FormatFileSize(snapshot.compressed_size).c_str());
if (snapshot.is_safe_point) {
ImGui::TextColored(colors_.sync_applied, "✓ Safe Point (Host Verified)");
}
ImGui::Separator();
// Actions
if (ImGui::Button("↩️ Restore This Version")) {
auto result = version_mgr_->RestoreSnapshot(snapshot.snapshot_id);
// TODO: Show result
}
ImGui::SameLine();
if (!snapshot.is_safe_point && ImGui::Button("🛡️ Mark as Safe")) {
version_mgr_->MarkAsSafePoint(snapshot.snapshot_id);
}
ImGui::SameLine();
if (!snapshot.is_safe_point && ImGui::Button("🗑️ Delete")) {
version_mgr_->DeleteSnapshot(snapshot.snapshot_id);
}
ImGui::Unindent();
ImGui::TreePop();
}
ImGui::Separator();
ImGui::PopID();
}
void CollaborationPanel::RenderApprovalProposal(
const net::ProposalApprovalManager::ApprovalStatus& status, int index) {
ImGui::PushID(index);
// Status indicator
ImGui::TextColored(colors_.proposal_pending, "[⏳]");
ImGui::SameLine();
// Proposal ID (shortened)
std::string short_id = status.proposal_id.substr(0, 8);
bool is_open = ImGui::TreeNode(absl::StrFormat("Proposal %s", short_id.c_str()).c_str());
if (is_open) {
ImGui::Indent();
ImGui::Text("Created: %s", FormatTimestamp(status.created_at).c_str());
ImGui::Text("Snapshot Before: %s", status.snapshot_before.substr(0, 8).c_str());
ImGui::Separator();
ImGui::TextWrapped("Votes:");
for (const auto& [username, approved] : status.votes) {
ImVec4 vote_color = approved ? colors_.proposal_approved : colors_.proposal_rejected;
const char* vote_icon = approved ? "" : "";
ImGui::TextColored(vote_color, " %s %s", vote_icon, username.c_str());
}
ImGui::Separator();
// Voting actions
if (ImGui::Button("✓ Approve")) {
// TODO: Send approval vote
// approval_mgr_->VoteOnProposal(status.proposal_id, "current_user", true);
}
ImGui::SameLine();
if (ImGui::Button("✗ Reject")) {
// TODO: Send rejection vote
// approval_mgr_->VoteOnProposal(status.proposal_id, "current_user", false);
}
ImGui::SameLine();
if (ImGui::Button("↩️ Rollback")) {
// Restore snapshot from before this proposal
version_mgr_->RestoreSnapshot(status.snapshot_before);
}
ImGui::Unindent();
ImGui::TreePop();
}
ImGui::Separator();
ImGui::PopID();
}
} // namespace gui
} // namespace app
} // namespace yaze

View File

@@ -6,6 +6,8 @@
#include <vector>
#include "absl/status/status.h"
#include "app/net/rom_version_manager.h"
#include "app/rom.h"
#include "imgui/imgui.h"
#ifdef YAZE_WITH_JSON
@@ -80,6 +82,12 @@ class CollaborationPanel {
CollaborationPanel();
~CollaborationPanel();
/**
* Initialize with ROM and version manager
*/
void Initialize(Rom* rom, net::RomVersionManager* version_mgr,
net::ProposalApprovalManager* approval_mgr);
/**
* Render the collaboration panel
*/
@@ -119,10 +127,19 @@ class CollaborationPanel {
void RenderRomSyncTab();
void RenderSnapshotsTab();
void RenderProposalsTab();
void RenderVersionHistoryTab();
void RenderApprovalTab();
void RenderRomSyncEntry(const RomSyncEntry& entry, int index);
void RenderSnapshotEntry(const SnapshotEntry& entry, int index);
void RenderProposalEntry(const ProposalEntry& entry, int index);
void RenderVersionSnapshot(const net::RomSnapshot& snapshot, int index);
void RenderApprovalProposal(const net::ProposalApprovalManager::ApprovalStatus& status, int index);
// Integration components
Rom* rom_;
net::RomVersionManager* version_mgr_;
net::ProposalApprovalManager* approval_mgr_;
// Tab selection
int selected_tab_;

View File

@@ -1,4 +1,4 @@
#include "app/core/widget_state_capture.h"
#include "app/gui/widgets/widget_state_capture.h"
#include "absl/strings/str_format.h"
#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE

View File

@@ -1,5 +1,5 @@
#ifndef YAZE_CORE_WIDGET_STATE_CAPTURE_H
#define YAZE_CORE_WIDGET_STATE_CAPTURE_H
#ifndef YAZE_APP_GUI_WIDGETS_WIDGET_STATE_CAPTURE_H_
#define YAZE_APP_GUI_WIDGETS_WIDGET_STATE_CAPTURE_H_
#include <string>
#include <vector>
@@ -51,4 +51,4 @@ std::string SerializeWidgetStateToJson(const WidgetState& state);
} // namespace core
} // namespace yaze
#endif // YAZE_CORE_WIDGET_STATE_CAPTURE_H
#endif // YAZE_APP_GUI_WIDGETS_WIDGET_STATE_CAPTURE_H_

View File

@@ -0,0 +1,43 @@
# ==============================================================================
# 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
)
add_library(yaze_net STATIC ${YAZE_NET_SRC})
target_include_directories(yaze_net PUBLIC
${CMAKE_SOURCE_DIR}/src
${PROJECT_BINARY_DIR}
)
target_link_libraries(yaze_net PUBLIC
yaze_util
yaze_common
${ABSL_TARGETS}
)
# Add JSON support if enabled
if(YAZE_WITH_JSON)
target_include_directories(yaze_net PUBLIC
${CMAKE_SOURCE_DIR}/third_party/json/include)
target_compile_definitions(yaze_net PUBLIC YAZE_WITH_JSON)
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,534 @@
#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 app {
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());
rom_->set_modified(true);
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 app
} // 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 app {
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 app
} // namespace yaze
#endif // YAZE_APP_NET_ROM_VERSION_MANAGER_H_

View File

@@ -12,7 +12,7 @@
#include "absl/time/clock.h"
#include "absl/time/time.h"
#include "app/core/service/screenshot_utils.h"
#include "app/core/widget_state_capture.h"
#include "app/gui/widgets/widget_state_capture.h"
#include "app/core/features.h"
#include "app/core/platform/file_dialog.h"
#include "app/gfx/arena.h"