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:
@@ -1,56 +1,65 @@
|
|||||||
# Getting Started
|
# 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
|
## Quick Start
|
||||||
|
|
||||||
1. **Download** the latest release for your platform
|
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
|
2. **Load ROM** via `File > Open ROM`.
|
||||||
3. **Select Editor** from the toolbar (Overworld, Dungeon, Graphics, etc.)
|
3. **Select an Editor** from the main toolbar (e.g., Overworld, Dungeon, Graphics).
|
||||||
4. **Make Changes** and save your project
|
4. **Make Changes** and save your project.
|
||||||
|
|
||||||
## General Tips
|
## General Tips
|
||||||
|
|
||||||
- **Experiment Flags**: Enable/disable features in File > Options > Experiment Flags
|
- **Experiment Flags**: Enable or disable new features in `File > Options > Experiment Flags`.
|
||||||
- **Backup Files**: Enabled by default - each save creates a timestamped backup
|
- **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)
|
- **Extensions**: Load custom tools via the `Extensions` menu (C library and Python module support is planned).
|
||||||
|
|
||||||
## Supported Features
|
## Feature Status
|
||||||
|
|
||||||
| Feature | Status | Details |
|
| Feature | Status | Details |
|
||||||
|---------|--------|---------|
|
|---|---|---|
|
||||||
| Overworld Maps | ✅ Complete | Edit and save tile32 data |
|
| Overworld Editor | ✅ Complete | Full support for vanilla and ZSCustomOverworld v2/v3. |
|
||||||
| OW Map Properties | ✅ Complete | Edit and save map properties |
|
| Dungeon Editor | ✅ Complete | Stable, component-based editor for rooms, objects, and sprites. |
|
||||||
| OW Entrances | ✅ Complete | Edit and save entrance data |
|
| Tile16 Editor | ✅ Complete | Professional-grade tile editor with advanced palette management. |
|
||||||
| OW Exits | ✅ Complete | Edit and save exit data |
|
| Palette Editor | ✅ Complete | Edit and save all SNES palette groups. |
|
||||||
| OW Sprites | 🔄 In Progress | Edit sprite positions, add/remove sprites |
|
| Graphics Editor | ✅ Complete | View and edit graphics sheets and groups. |
|
||||||
| Dungeon Editor | 🔄 In Progress | View room metadata and edit room data |
|
| Sprite Editor | ✅ Complete | Edit sprite properties and attributes. |
|
||||||
| Palette Editor | 🔄 In Progress | Edit and save palettes, palette groups |
|
| Message Editor | ✅ Complete | Edit in-game text and dialogue. |
|
||||||
| Graphics Sheets | 🔄 In Progress | Edit and save graphics sheets |
|
| Hex Editor | ✅ Complete | View and edit raw ROM data. |
|
||||||
| Graphics Groups | ✅ Complete | Edit and save graphics groups |
|
| Asar Patching | ✅ Complete | Apply Asar 65816 assembly patches to the ROM. |
|
||||||
| Hex Editor | ✅ Complete | View and edit ROM data in hex |
|
|
||||||
| Asar Patching | ✅ Complete | Apply Asar 65816 assembly patches to 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
|
```bash
|
||||||
# Apply Asar assembly patch
|
# Start an interactive chat session with the AI agent
|
||||||
z3ed asar patch.asm --rom=zelda3.sfc
|
z3ed agent chat --rom zelda3.sfc
|
||||||
|
```
|
||||||
|
> **Prompt:** "What sprites are in dungeon 2?"
|
||||||
|
|
||||||
# Extract symbols from assembly
|
### Resource Inspection
|
||||||
z3ed extract patch.asm
|
Directly query ROM data.
|
||||||
|
|
||||||
# Validate assembly syntax
|
```bash
|
||||||
z3ed validate patch.asm
|
# List all sprites in the Eastern Palace (dungeon 2)
|
||||||
|
z3ed dungeon list-sprites --rom zelda3.sfc --dungeon 2
|
||||||
|
|
||||||
# Launch interactive TUI
|
# Get information about a specific overworld map area
|
||||||
z3ed --tui
|
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
|
## 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.
|
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.
|
||||||
|
|
||||||
This feature is still in development and not fully documented yet.
|
|
||||||
|
|||||||
@@ -1,42 +1,29 @@
|
|||||||
# yaze Documentation
|
# 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
|
- [Getting Started](01-getting-started.md) - Basic setup and usage.
|
||||||
- [Build Instructions](02-build-instructions.md) - Cross-platform build guide
|
- [Build Instructions](02-build-instructions.md) - How to build yaze on Windows, macOS, and Linux.
|
||||||
- [API Reference](04-api-reference.md) - C/C++ API documentation
|
- [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
|
- [API Reference](04-api-reference.md) - C/C++ API documentation for extensions.
|
||||||
- [Platform Compatibility](B2-platform-compatibility.md) - Cross-platform support details
|
- [Testing Guide](A1-testing-guide.md) - The testing framework and best practices.
|
||||||
- [Build Presets](B3-build-presets.md) - CMake preset usage guide
|
- [Assembly Style Guide](E1-asm-style-guide.md) - 65816 assembly coding standards.
|
||||||
- [Release Workflows](B4-release-workflows.md) - GitHub Actions release pipeline documentation
|
- [Build Presets](B3-build-presets.md) - A guide to the CMake preset system.
|
||||||
- [Stability Improvements](B5-stability-improvements.md) - Performance optimizations and reliability enhancements
|
- [Release Workflows](B4-release-workflows.md) - GitHub Actions release pipeline documentation.
|
||||||
|
|
||||||
## Technical Documentation
|
## Technical Documentation
|
||||||
|
|
||||||
### Assembly & Code
|
- [Dungeon Editor Guide](D2-dungeon-editor-guide.md) - A master guide to the dungeon editing system.
|
||||||
- [Assembly Style Guide](E1-asm-style-guide.md) - 65816 assembly coding standards
|
- [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.
|
||||||
### Editor Systems
|
- [Platform Compatibility](B2-platform-compatibility.md) - Cross-platform support details.
|
||||||
- [Dungeon Editor Master Guide](dungeon_editor_master_guide.md) - Complete dungeon editing guide
|
- [Tile16 Editor Palette System](E7-tile16-editor-palette-system.md) - Design of the palette system.
|
||||||
|
|
||||||
### 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)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Last updated: December 2025 - Version 0.3.2*
|
*Last updated: October 2025 - Version 0.3.2*
|
||||||
|
|||||||
@@ -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
|
- **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
|
- **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
|
- **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
|
- **Improved Documentation**: Consolidated architecture, enhancement plans, and build instructions with JSON-first approach
|
||||||
|
|
||||||
## 12. Troubleshooting
|
## 12. Troubleshooting
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ endif()
|
|||||||
include(app/core/core_library.cmake)
|
include(app/core/core_library.cmake)
|
||||||
include(app/gfx/gfx_library.cmake)
|
include(app/gfx/gfx_library.cmake)
|
||||||
include(app/gui/gui_library.cmake)
|
include(app/gui/gui_library.cmake)
|
||||||
|
include(app/net/net_library.cmake)
|
||||||
include(app/zelda3/zelda3_library.cmake)
|
include(app/zelda3/zelda3_library.cmake)
|
||||||
include(app/editor/editor_library.cmake)
|
include(app/editor/editor_library.cmake)
|
||||||
|
|
||||||
@@ -299,8 +300,8 @@ if (YAZE_BUILD_APP)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
target_sources(yaze PRIVATE
|
target_sources(yaze PRIVATE
|
||||||
${CMAKE_SOURCE_DIR}/src/app/core/widget_state_capture.cc
|
${CMAKE_SOURCE_DIR}/src/app/gui/widgets/widget_state_capture.cc
|
||||||
${CMAKE_SOURCE_DIR}/src/app/core/widget_state_capture.h)
|
${CMAKE_SOURCE_DIR}/src/app/gui/widgets/widget_state_capture.h)
|
||||||
|
|
||||||
if(YAZE_WITH_GRPC)
|
if(YAZE_WITH_GRPC)
|
||||||
message(STATUS "Adding gRPC ImGuiTestHarness to yaze target")
|
message(STATUS "Adding gRPC ImGuiTestHarness to yaze target")
|
||||||
|
|||||||
@@ -229,8 +229,8 @@ endif()
|
|||||||
# Test harness utilities shared across builds (IT-08 widget state capture)
|
# Test harness utilities shared across builds (IT-08 widget state capture)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
target_sources(yaze PRIVATE
|
target_sources(yaze PRIVATE
|
||||||
${CMAKE_SOURCE_DIR}/src/app/core/widget_state_capture.cc
|
${CMAKE_SOURCE_DIR}/src/app/gui/widgets/widget_state_capture.cc
|
||||||
${CMAKE_SOURCE_DIR}/src/app/core/widget_state_capture.h)
|
${CMAKE_SOURCE_DIR}/src/app/gui/widgets/widget_state_capture.h)
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Optional gRPC Support for ImGuiTestHarness
|
# Optional gRPC Support for ImGuiTestHarness
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ set(
|
|||||||
app/core/project.cc
|
app/core/project.cc
|
||||||
app/core/window.cc
|
app/core/window.cc
|
||||||
app/core/asar_wrapper.cc
|
app/core/asar_wrapper.cc
|
||||||
app/core/widget_state_capture.cc
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (WIN32 OR MINGW OR (UNIX AND NOT APPLE))
|
if (WIN32 OR MINGW OR (UNIX AND NOT APPLE))
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ set(
|
|||||||
app/gui/modules/asset_browser.cc
|
app/gui/modules/asset_browser.cc
|
||||||
app/gui/modules/text_editor.cc
|
app/gui/modules/text_editor.cc
|
||||||
app/gui/widgets/agent_chat_widget.cc
|
app/gui/widgets/agent_chat_widget.cc
|
||||||
|
app/gui/widgets/collaboration_panel.cc
|
||||||
app/gui/canvas.cc
|
app/gui/canvas.cc
|
||||||
app/gui/canvas_utils.cc
|
app/gui/canvas_utils.cc
|
||||||
app/gui/enhanced_palette_editor.cc
|
app/gui/enhanced_palette_editor.cc
|
||||||
@@ -60,6 +61,7 @@ target_link_libraries(yaze_gui PUBLIC
|
|||||||
yaze_gfx
|
yaze_gfx
|
||||||
yaze_util
|
yaze_util
|
||||||
yaze_common
|
yaze_common
|
||||||
|
yaze_net
|
||||||
ImGui
|
ImGui
|
||||||
${SDL_TARGETS}
|
${SDL_TARGETS}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ namespace app {
|
|||||||
namespace gui {
|
namespace gui {
|
||||||
|
|
||||||
CollaborationPanel::CollaborationPanel()
|
CollaborationPanel::CollaborationPanel()
|
||||||
: selected_tab_(0),
|
: rom_(nullptr),
|
||||||
|
version_mgr_(nullptr),
|
||||||
|
approval_mgr_(nullptr),
|
||||||
|
selected_tab_(0),
|
||||||
selected_rom_sync_(-1),
|
selected_rom_sync_(-1),
|
||||||
selected_snapshot_(-1),
|
selected_snapshot_(-1),
|
||||||
selected_proposal_(-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) {
|
void CollaborationPanel::Render(bool* p_open) {
|
||||||
if (!ImGui::Begin("Collaboration", p_open, ImGuiWindowFlags_None)) {
|
if (!ImGui::Begin("Collaboration", p_open, ImGuiWindowFlags_None)) {
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
@@ -59,18 +71,30 @@ void CollaborationPanel::Render(bool* p_open) {
|
|||||||
ImGui::EndTabItem();
|
ImGui::EndTabItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ImGui::BeginTabItem("Snapshots")) {
|
if (ImGui::BeginTabItem("Version History")) {
|
||||||
selected_tab_ = 1;
|
selected_tab_ = 1;
|
||||||
|
RenderVersionHistoryTab();
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::BeginTabItem("Snapshots")) {
|
||||||
|
selected_tab_ = 2;
|
||||||
RenderSnapshotsTab();
|
RenderSnapshotsTab();
|
||||||
ImGui::EndTabItem();
|
ImGui::EndTabItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ImGui::BeginTabItem("Proposals")) {
|
if (ImGui::BeginTabItem("Proposals")) {
|
||||||
selected_tab_ = 2;
|
selected_tab_ = 3;
|
||||||
RenderProposalsTab();
|
RenderProposalsTab();
|
||||||
ImGui::EndTabItem();
|
ImGui::EndTabItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ImGui::BeginTabItem("🔒 Approvals")) {
|
||||||
|
selected_tab_ = 4;
|
||||||
|
RenderApprovalTab();
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::EndTabBar();
|
ImGui::EndTabBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,6 +464,201 @@ ImVec4 CollaborationPanel::GetProposalStatusColor(const std::string& status) {
|
|||||||
return ImVec4(0.7f, 0.7f, 0.7f, 1.0f);
|
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 gui
|
||||||
} // namespace app
|
} // namespace app
|
||||||
} // namespace yaze
|
} // namespace yaze
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "absl/status/status.h"
|
#include "absl/status/status.h"
|
||||||
|
#include "app/net/rom_version_manager.h"
|
||||||
|
#include "app/rom.h"
|
||||||
#include "imgui/imgui.h"
|
#include "imgui/imgui.h"
|
||||||
|
|
||||||
#ifdef YAZE_WITH_JSON
|
#ifdef YAZE_WITH_JSON
|
||||||
@@ -80,6 +82,12 @@ class CollaborationPanel {
|
|||||||
CollaborationPanel();
|
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
|
* Render the collaboration panel
|
||||||
*/
|
*/
|
||||||
@@ -119,10 +127,19 @@ class CollaborationPanel {
|
|||||||
void RenderRomSyncTab();
|
void RenderRomSyncTab();
|
||||||
void RenderSnapshotsTab();
|
void RenderSnapshotsTab();
|
||||||
void RenderProposalsTab();
|
void RenderProposalsTab();
|
||||||
|
void RenderVersionHistoryTab();
|
||||||
|
void RenderApprovalTab();
|
||||||
|
|
||||||
void RenderRomSyncEntry(const RomSyncEntry& entry, int index);
|
void RenderRomSyncEntry(const RomSyncEntry& entry, int index);
|
||||||
void RenderSnapshotEntry(const SnapshotEntry& entry, int index);
|
void RenderSnapshotEntry(const SnapshotEntry& entry, int index);
|
||||||
void RenderProposalEntry(const ProposalEntry& 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
|
// Tab selection
|
||||||
int selected_tab_;
|
int selected_tab_;
|
||||||
|
|||||||
@@ -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"
|
#include "absl/strings/str_format.h"
|
||||||
#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
|
#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#ifndef YAZE_CORE_WIDGET_STATE_CAPTURE_H
|
#ifndef YAZE_APP_GUI_WIDGETS_WIDGET_STATE_CAPTURE_H_
|
||||||
#define YAZE_CORE_WIDGET_STATE_CAPTURE_H
|
#define YAZE_APP_GUI_WIDGETS_WIDGET_STATE_CAPTURE_H_
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -51,4 +51,4 @@ std::string SerializeWidgetStateToJson(const WidgetState& state);
|
|||||||
} // namespace core
|
} // namespace core
|
||||||
} // namespace yaze
|
} // namespace yaze
|
||||||
|
|
||||||
#endif // YAZE_CORE_WIDGET_STATE_CAPTURE_H
|
#endif // YAZE_APP_GUI_WIDGETS_WIDGET_STATE_CAPTURE_H_
|
||||||
43
src/app/net/net_library.cmake
Normal file
43
src/app/net/net_library.cmake
Normal 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")
|
||||||
534
src/app/net/rom_version_manager.cc
Normal file
534
src/app/net/rom_version_manager.cc
Normal 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
|
||||||
292
src/app/net/rom_version_manager.h
Normal file
292
src/app/net/rom_version_manager.h
Normal 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_
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
#include "absl/time/clock.h"
|
#include "absl/time/clock.h"
|
||||||
#include "absl/time/time.h"
|
#include "absl/time/time.h"
|
||||||
#include "app/core/service/screenshot_utils.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/features.h"
|
||||||
#include "app/core/platform/file_dialog.h"
|
#include "app/core/platform/file_dialog.h"
|
||||||
#include "app/gfx/arena.h"
|
#include "app/gfx/arena.h"
|
||||||
|
|||||||
Reference in New Issue
Block a user