yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
collaboration_panel.cc
Go to the documentation of this file.
2#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <ctime>
6#include <iomanip>
7#include <sstream>
8
9#include "absl/strings/str_format.h"
10#include "imgui/imgui.h"
11
12namespace yaze {
13
14namespace gui {
15
17 : rom_(nullptr),
18 version_mgr_(nullptr),
19 approval_mgr_(nullptr),
20 selected_tab_(0),
21 selected_rom_sync_(-1),
22 selected_snapshot_(-1),
23 selected_proposal_(-1),
24 show_sync_details_(false),
25 show_snapshot_preview_(true),
26 auto_scroll_(true),
27 filter_pending_only_(false) {
28 // Initialize search filter
29 search_filter_[0] = '\0';
30
31 // Initialize colors
32 colors_.sync_applied = ImVec4(0.2f, 0.8f, 0.2f, 1.0f);
33 colors_.sync_pending = ImVec4(0.8f, 0.8f, 0.2f, 1.0f);
34 colors_.sync_error = ImVec4(0.8f, 0.2f, 0.2f, 1.0f);
35 colors_.proposal_pending = ImVec4(0.7f, 0.7f, 0.7f, 1.0f);
36 colors_.proposal_approved = ImVec4(0.2f, 0.8f, 0.2f, 1.0f);
37 colors_.proposal_rejected = ImVec4(0.8f, 0.3f, 0.3f, 1.0f);
38 colors_.proposal_applied = ImVec4(0.2f, 0.6f, 0.8f, 1.0f);
39}
40
42 // Cleanup any OpenGL textures
43 for (auto& snapshot : snapshots_) {
44 if (snapshot.texture_id) {
45 // Note: Actual texture cleanup would depend on your renderer
46 // This is a placeholder
47 snapshot.texture_id = nullptr;
48 }
49 }
50}
51
53 Rom* rom, net::RomVersionManager* version_mgr,
54 net::ProposalApprovalManager* approval_mgr) {
55 rom_ = rom;
56 version_mgr_ = version_mgr;
57 approval_mgr_ = approval_mgr;
58}
59
60void CollaborationPanel::Render(bool* p_open) {
61 if (!ImGui::Begin("Collaboration", p_open, ImGuiWindowFlags_None)) {
62 ImGui::End();
63 return;
64 }
65
66 // Tabs for different collaboration features
67 if (ImGui::BeginTabBar("CollaborationTabs")) {
68 if (ImGui::BeginTabItem(tr("ROM Sync"))) {
69 selected_tab_ = 0;
71 ImGui::EndTabItem();
72 }
73
74 if (ImGui::BeginTabItem(tr("Version History"))) {
75 selected_tab_ = 1;
77 ImGui::EndTabItem();
78 }
79
80 if (ImGui::BeginTabItem(tr("Snapshots"))) {
81 selected_tab_ = 2;
83 ImGui::EndTabItem();
84 }
85
86 if (ImGui::BeginTabItem(tr("Proposals"))) {
87 selected_tab_ = 3;
89 ImGui::EndTabItem();
90 }
91
92 if (ImGui::BeginTabItem(tr("🔒 Approvals"))) {
93 selected_tab_ = 4;
95 ImGui::EndTabItem();
96 }
97
98 ImGui::EndTabBar();
99 }
100
101 ImGui::End();
102}
103
105 ImGui::TextWrapped(tr("ROM Synchronization History"));
106 ImGui::Separator();
107
108 // Toolbar
109 if (ImGui::Button(tr("Clear History"))) {
110 rom_syncs_.clear();
111 }
112 ImGui::SameLine();
113 ImGui::Checkbox(tr("Auto-scroll"), &auto_scroll_);
114 ImGui::SameLine();
115 ImGui::Checkbox(tr("Show Details"), &show_sync_details_);
116
117 ImGui::Separator();
118
119 // Stats
120 int applied_count = 0;
121 int pending_count = 0;
122 int error_count = 0;
123
124 for (const auto& sync : rom_syncs_) {
125 if (sync.applied)
126 applied_count++;
127 else if (!sync.error_message.empty())
128 error_count++;
129 else
130 pending_count++;
131 }
132
133 ImGui::Text(tr("Total: %zu | "), rom_syncs_.size());
134 ImGui::SameLine();
135 ImGui::TextColored(colors_.sync_applied, tr("Applied: %d"), applied_count);
136 ImGui::SameLine();
137 ImGui::TextColored(colors_.sync_pending, tr("Pending: %d"), pending_count);
138 ImGui::SameLine();
139 ImGui::TextColored(colors_.sync_error, tr("Errors: %d"), error_count);
140
141 ImGui::Separator();
142
143 // Sync list
144 if (ImGui::BeginChild("SyncList", ImVec2(0, 0), true)) {
145 for (size_t i = 0; i < rom_syncs_.size(); ++i) {
147 }
148
149 if (auto_scroll_ && ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) {
150 ImGui::SetScrollHereY(1.0f);
151 }
152 }
153 ImGui::EndChild();
154}
155
157 ImGui::TextWrapped(tr("Shared Snapshots Gallery"));
158 ImGui::Separator();
159
160 // Toolbar
161 if (ImGui::Button(tr("Clear Gallery"))) {
162 snapshots_.clear();
163 }
164 ImGui::SameLine();
165 ImGui::Checkbox(tr("Show Preview"), &show_snapshot_preview_);
166 ImGui::SameLine();
167 ImGui::InputText(tr("Search"), search_filter_, sizeof(search_filter_));
168
169 ImGui::Separator();
170
171 // Snapshot grid
172 if (ImGui::BeginChild("SnapshotGrid", ImVec2(0, 0), true)) {
173 float thumbnail_size = 150.0f;
174 float padding = 10.0f;
175 float cell_size = thumbnail_size + padding;
176
177 int columns =
178 std::max(1, (int)((ImGui::GetContentRegionAvail().x) / cell_size));
179
180 for (size_t i = 0; i < snapshots_.size(); ++i) {
181 // Filter by search
182 if (search_filter_[0] != '\0') {
183 std::string search_lower = search_filter_;
184 std::string sender_lower = snapshots_[i].sender;
185 std::transform(search_lower.begin(), search_lower.end(),
186 search_lower.begin(), ::tolower);
187 std::transform(sender_lower.begin(), sender_lower.end(),
188 sender_lower.begin(), ::tolower);
189
190 if (sender_lower.find(search_lower) == std::string::npos &&
191 snapshots_[i].snapshot_type.find(search_lower) ==
192 std::string::npos) {
193 continue;
194 }
195 }
196
198
199 // Grid layout
200 if ((i + 1) % columns != 0 && i < snapshots_.size() - 1) {
201 ImGui::SameLine();
202 }
203 }
204 }
205 ImGui::EndChild();
206}
207
209 ImGui::TextWrapped(tr("AI Proposals & Suggestions"));
210 ImGui::Separator();
211
212 // Toolbar
213 if (ImGui::Button(tr("Clear All"))) {
214 proposals_.clear();
215 }
216 ImGui::SameLine();
217 ImGui::Checkbox(tr("Pending Only"), &filter_pending_only_);
218 ImGui::SameLine();
219 ImGui::InputText(tr("Search"), search_filter_, sizeof(search_filter_));
220
221 ImGui::Separator();
222
223 // Stats
224 int pending = 0, approved = 0, rejected = 0, applied = 0;
225 for (const auto& proposal : proposals_) {
226 if (proposal.status == "pending")
227 pending++;
228 else if (proposal.status == "approved")
229 approved++;
230 else if (proposal.status == "rejected")
231 rejected++;
232 else if (proposal.status == "applied")
233 applied++;
234 }
235
236 ImGui::Text(tr("Total: %zu"), proposals_.size());
237 ImGui::SameLine();
238 ImGui::TextColored(colors_.proposal_pending, tr(" | Pending: %d"), pending);
239 ImGui::SameLine();
240 ImGui::TextColored(colors_.proposal_approved, tr(" | Approved: %d"),
241 approved);
242 ImGui::SameLine();
243 ImGui::TextColored(colors_.proposal_rejected, tr(" | Rejected: %d"),
244 rejected);
245 ImGui::SameLine();
246 ImGui::TextColored(colors_.proposal_applied, tr(" | Applied: %d"), applied);
247
248 ImGui::Separator();
249
250 // Proposal list
251 if (ImGui::BeginChild("ProposalList", ImVec2(0, 0), true)) {
252 for (size_t i = 0; i < proposals_.size(); ++i) {
253 // Filter
254 if (filter_pending_only_ && proposals_[i].status != "pending") {
255 continue;
256 }
257
258 if (search_filter_[0] != '\0') {
259 std::string search_lower = search_filter_;
260 std::string sender_lower = proposals_[i].sender;
261 std::string desc_lower = proposals_[i].description;
262 std::transform(search_lower.begin(), search_lower.end(),
263 search_lower.begin(), ::tolower);
264 std::transform(sender_lower.begin(), sender_lower.end(),
265 sender_lower.begin(), ::tolower);
266 std::transform(desc_lower.begin(), desc_lower.end(), desc_lower.begin(),
267 ::tolower);
268
269 if (sender_lower.find(search_lower) == std::string::npos &&
270 desc_lower.find(search_lower) == std::string::npos) {
271 continue;
272 }
273 }
274
276 }
277 }
278 ImGui::EndChild();
279}
280
282 int index) {
283 ImGui::PushID(index);
284
285 // Status indicator
286 ImVec4 status_color;
287 const char* status_icon;
288
289 if (entry.applied) {
290 status_color = colors_.sync_applied;
291 status_icon = "[✓]";
292 } else if (!entry.error_message.empty()) {
293 status_color = colors_.sync_error;
294 status_icon = "[✗]";
295 } else {
296 status_color = colors_.sync_pending;
297 status_icon = "[◷]";
298 }
299
300 ImGui::TextColored(status_color, "%s", status_icon);
301 ImGui::SameLine();
302
303 // Entry info
304 ImGui::Text("%s - %s (%s)", FormatTimestamp(entry.timestamp).c_str(),
305 entry.sender.c_str(), FormatFileSize(entry.diff_size).c_str());
306
307 // Details on hover or if enabled
308 if (show_sync_details_ || ImGui::IsItemHovered()) {
309 ImGui::Indent();
310 ImGui::TextWrapped(tr("ROM Hash: %s"),
311 entry.rom_hash.substr(0, 16).c_str());
312 if (!entry.error_message.empty()) {
313 ImGui::TextColored(colors_.sync_error, tr("Error: %s"),
314 entry.error_message.c_str());
315 }
316 ImGui::Unindent();
317 }
318
319 ImGui::Separator();
320 ImGui::PopID();
321}
322
324 int index) {
325 ImGui::PushID(index);
326
327 ImGui::BeginGroup();
328
329 // Thumbnail placeholder or actual image
330 if (show_snapshot_preview_ && entry.is_image && entry.texture_id) {
331 ImGui::Image(entry.texture_id, ImVec2(150, 150));
332 } else {
333 // Placeholder
334 ImGui::BeginChild("SnapshotPlaceholder", ImVec2(150, 150), true);
335 ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 50);
336 ImGui::TextWrapped("%s", entry.snapshot_type.c_str());
337 ImGui::EndChild();
338 }
339
340 // Info
341 ImGui::TextWrapped("%s", entry.sender.c_str());
342 ImGui::Text("%s", FormatTimestamp(entry.timestamp).c_str());
343 ImGui::Text("%s", FormatFileSize(entry.data_size).c_str());
344
345 // Actions
346 if (ImGui::SmallButton(tr("View"))) {
347 selected_snapshot_ = index;
348 // TODO: Open snapshot viewer
349 }
350 ImGui::SameLine();
351 if (ImGui::SmallButton(tr("Export"))) {
352 // TODO: Export snapshot to file
353 }
354
355 ImGui::EndGroup();
356
357 ImGui::PopID();
358}
359
361 int index) {
362 ImGui::PushID(index);
363
364 // Status icon and color
365 const char* icon = GetProposalStatusIcon(entry.status);
366 ImVec4 color = GetProposalStatusColor(entry.status);
367
368 ImGui::TextColored(color, "%s", icon);
369 ImGui::SameLine();
370
371 // Collapsible header
372 bool is_open = ImGui::TreeNode(entry.description.c_str());
373
374 if (is_open) {
375 ImGui::Indent();
376
377 ImGui::Text(tr("From: %s"), entry.sender.c_str());
378 ImGui::Text(tr("Time: %s"), FormatTimestamp(entry.timestamp).c_str());
379 ImGui::Text(tr("Status: %s"), entry.status.c_str());
380
381 ImGui::Separator();
382
383 // Proposal data
384 ImGui::TextWrapped("%s", entry.proposal_data.c_str());
385
386 // Actions for pending proposals
387 if (entry.status == "pending") {
388 ImGui::Separator();
389 if (ImGui::Button(tr("✓ Approve"))) {
390 // TODO: Send approval to server
391 }
392 ImGui::SameLine();
393 if (ImGui::Button(tr("✗ Reject"))) {
394 // TODO: Send rejection to server
395 }
396 ImGui::SameLine();
397 if (ImGui::Button(tr("â–ļ Apply Now"))) {
398 // TODO: Execute proposal
399 }
400 }
401
402 ImGui::Unindent();
403 ImGui::TreePop();
404 }
405
406 ImGui::Separator();
407 ImGui::PopID();
408}
409
411 rom_syncs_.push_back(entry);
412}
413
415 snapshots_.push_back(entry);
416}
417
419 proposals_.push_back(entry);
420}
421
422void CollaborationPanel::UpdateProposalStatus(const std::string& proposal_id,
423 const std::string& status) {
424 for (auto& proposal : proposals_) {
425 if (proposal.proposal_id == proposal_id) {
426 proposal.status = status;
427 break;
428 }
429 }
430}
431
433 rom_syncs_.clear();
434 snapshots_.clear();
435 proposals_.clear();
436}
437
438ProposalEntry* CollaborationPanel::GetProposal(const std::string& proposal_id) {
439 for (auto& proposal : proposals_) {
440 if (proposal.proposal_id == proposal_id) {
441 return &proposal;
442 }
443 }
444 return nullptr;
445}
446
447std::string CollaborationPanel::FormatTimestamp(int64_t timestamp) {
448 std::time_t time = timestamp / 1000; // Convert ms to seconds
449 std::tm* tm = std::localtime(&time);
450
451 char buffer[32];
452 std::strftime(buffer, sizeof(buffer), "%H:%M:%S", tm);
453 return std::string(buffer);
454}
455
456std::string CollaborationPanel::FormatFileSize(size_t bytes) {
457 const char* units[] = {"B", "KB", "MB", "GB"};
458 int unit_index = 0;
459 double size = static_cast<double>(bytes);
460
461 while (size >= 1024.0 && unit_index < 3) {
462 size /= 1024.0;
463 unit_index++;
464 }
465
466 char buffer[32];
467 snprintf(buffer, sizeof(buffer), "%.1f %s", size, units[unit_index]);
468 return std::string(buffer);
469}
470
472 const std::string& status) {
473 if (status == "pending")
474 return "[◷]";
475 if (status == "approved")
476 return "[✓]";
477 if (status == "rejected")
478 return "[✗]";
479 if (status == "applied")
480 return "[âœĻ]";
481 return "[?]";
482}
483
484ImVec4 CollaborationPanel::GetProposalStatusColor(const std::string& status) {
485 if (status == "pending")
486 return colors_.proposal_pending;
487 if (status == "approved")
488 return colors_.proposal_approved;
489 if (status == "rejected")
490 return colors_.proposal_rejected;
491 if (status == "applied")
492 return colors_.proposal_applied;
493 return ImVec4(0.7f, 0.7f, 0.7f, 1.0f);
494}
495
497 if (!version_mgr_) {
498 ImGui::TextWrapped(tr("Version management not initialized"));
499 return;
500 }
501
502 ImGui::TextWrapped(tr("ROM Version History & Protection"));
503 ImGui::Separator();
504
505 // Stats
506 auto stats = version_mgr_->GetStats();
507 ImGui::Text(tr("Total Snapshots: %zu"), stats.total_snapshots);
508 ImGui::SameLine();
509 ImGui::TextColored(colors_.sync_applied, tr("Safe Points: %zu"),
510 stats.safe_points);
511 ImGui::SameLine();
512 ImGui::TextColored(colors_.sync_pending, tr("Auto-Backups: %zu"),
513 stats.auto_backups);
514
515 ImGui::Text(tr("Storage Used: %s"),
516 FormatFileSize(stats.total_storage_bytes).c_str());
517
518 ImGui::Separator();
519
520 // Toolbar
521 if (ImGui::Button(tr("💾 Create Checkpoint"))) {
522 auto result =
523 version_mgr_->CreateSnapshot("Manual checkpoint", "user", true);
524 // TODO: Show result in UI
525 }
526 ImGui::SameLine();
527 if (ImGui::Button(tr("đŸ›Ąī¸ Mark Current as Safe Point"))) {
528 std::string current_hash = version_mgr_->GetCurrentHash();
529 // TODO: Find snapshot with this hash and mark as safe
530 }
531 ImGui::SameLine();
532 if (ImGui::Button(tr("🔍 Check for Corruption"))) {
533 auto result = version_mgr_->DetectCorruption();
534 // TODO: Show result
535 }
536
537 ImGui::Separator();
538
539 // Version list
540 if (ImGui::BeginChild("VersionList", ImVec2(0, 0), true)) {
541 auto snapshots = version_mgr_->GetSnapshots();
542
543 for (size_t i = 0; i < snapshots.size(); ++i) {
544 RenderVersionSnapshot(snapshots[i], i);
545 }
546 }
547 ImGui::EndChild();
548}
549
551 if (!approval_mgr_) {
552 ImGui::TextWrapped(tr("Approval management not initialized"));
553 return;
554 }
555
556 ImGui::TextWrapped(tr("Proposal Approval System"));
557 ImGui::Separator();
558
559 // Pending proposals that need votes
560 auto pending = approval_mgr_->GetPendingProposals();
561
562 if (pending.empty()) {
563 ImGui::TextWrapped(tr("No proposals pending approval."));
564 return;
565 }
566
567 ImGui::Text(tr("Pending Proposals: %zu"), pending.size());
568 ImGui::Separator();
569
570 if (ImGui::BeginChild("ApprovalList", ImVec2(0, 0), true)) {
571 for (size_t i = 0; i < pending.size(); ++i) {
572 RenderApprovalProposal(pending[i], i);
573 }
574 }
575 ImGui::EndChild();
576}
577
579 int index) {
580 ImGui::PushID(index);
581
582 // Icon based on type
583 const char* icon;
584 ImVec4 color;
585
586 if (snapshot.is_safe_point) {
587 icon = "đŸ›Ąī¸";
588 color = colors_.sync_applied;
589 } else if (snapshot.is_checkpoint) {
590 icon = "💾";
591 color = colors_.proposal_approved;
592 } else {
593 icon = "📝";
594 color = colors_.sync_pending;
595 }
596
597 ImGui::TextColored(color, "%s", icon);
598 ImGui::SameLine();
599
600 // Collapsible header
601 bool is_open = ImGui::TreeNode(snapshot.description.c_str());
602
603 if (is_open) {
604 ImGui::Indent();
605
606 ImGui::Text(tr("Creator: %s"), snapshot.creator.c_str());
607 ImGui::Text(tr("Time: %s"), FormatTimestamp(snapshot.timestamp).c_str());
608 ImGui::Text(tr("Hash: %s"), snapshot.rom_hash.substr(0, 16).c_str());
609 ImGui::Text(tr("Size: %s"),
610 FormatFileSize(snapshot.compressed_size).c_str());
611
612 if (snapshot.is_safe_point) {
613 ImGui::TextColored(colors_.sync_applied,
614 tr("✓ Safe Point (Host Verified)"));
615 }
616
617 ImGui::Separator();
618
619 // Actions
620 if (ImGui::Button(tr("â†Šī¸ Restore This Version"))) {
621 auto result = version_mgr_->RestoreSnapshot(snapshot.snapshot_id);
622 // TODO: Show result
623 }
624 ImGui::SameLine();
625 if (!snapshot.is_safe_point && ImGui::Button(tr("đŸ›Ąī¸ Mark as Safe"))) {
627 }
628 ImGui::SameLine();
629 if (!snapshot.is_safe_point && ImGui::Button(tr("đŸ—‘ī¸ Delete"))) {
631 }
632
633 ImGui::Unindent();
634 ImGui::TreePop();
635 }
636
637 ImGui::Separator();
638 ImGui::PopID();
639}
640
642 const net::ProposalApprovalManager::ApprovalStatus& status, int index) {
643 ImGui::PushID(index);
644
645 // Status indicator
646 ImGui::TextColored(colors_.proposal_pending, "[âŗ]");
647 ImGui::SameLine();
648
649 // Proposal ID (shortened)
650 std::string short_id = status.proposal_id.substr(0, 8);
651 bool is_open =
652 ImGui::TreeNode(absl::StrFormat("Proposal %s", short_id.c_str()).c_str());
653
654 if (is_open) {
655 ImGui::Indent();
656
657 ImGui::Text(tr("Created: %s"), FormatTimestamp(status.created_at).c_str());
658 ImGui::Text(tr("Snapshot Before: %s"),
659 status.snapshot_before.substr(0, 8).c_str());
660
661 ImGui::Separator();
662 ImGui::TextWrapped(tr("Votes:"));
663
664 for (const auto& [username, approved] : status.votes) {
665 ImVec4 vote_color =
666 approved ? colors_.proposal_approved : colors_.proposal_rejected;
667 const char* vote_icon = approved ? "✓" : "✗";
668 ImGui::TextColored(vote_color, " %s %s", vote_icon, username.c_str());
669 }
670
671 ImGui::Separator();
672
673 // Voting actions
674 if (ImGui::Button(tr("✓ Approve"))) {
675 // TODO: Send approval vote
676 // approval_mgr_->VoteOnProposal(status.proposal_id, "current_user",
677 // true);
678 }
679 ImGui::SameLine();
680 if (ImGui::Button(tr("✗ Reject"))) {
681 // TODO: Send rejection vote
682 // approval_mgr_->VoteOnProposal(status.proposal_id, "current_user",
683 // false);
684 }
685 ImGui::SameLine();
686 if (ImGui::Button(tr("â†Šī¸ Rollback"))) {
687 // Restore snapshot from before this proposal
689 }
690
691 ImGui::Unindent();
692 ImGui::TreePop();
693 }
694
695 ImGui::Separator();
696 ImGui::PopID();
697}
698
699} // namespace gui
700
701} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
std::string FormatFileSize(size_t bytes)
ProposalEntry * GetProposal(const std::string &proposal_id)
net::RomVersionManager * version_mgr_
void RenderVersionSnapshot(const net::RomSnapshot &snapshot, int index)
void RenderProposalEntry(const ProposalEntry &entry, int index)
std::string FormatTimestamp(int64_t timestamp)
void AddSnapshot(const SnapshotEntry &entry)
void AddProposal(const ProposalEntry &entry)
void UpdateProposalStatus(const std::string &proposal_id, const std::string &status)
const char * GetProposalStatusIcon(const std::string &status)
std::vector< ProposalEntry > proposals_
void RenderSnapshotEntry(const SnapshotEntry &entry, int index)
void AddRomSync(const RomSyncEntry &entry)
void RenderApprovalProposal(const net::ProposalApprovalManager::ApprovalStatus &status, int index)
struct yaze::gui::CollaborationPanel::@0 colors_
ImVec4 GetProposalStatusColor(const std::string &status)
void Render(bool *p_open=nullptr)
std::vector< RomSyncEntry > rom_syncs_
net::ProposalApprovalManager * approval_mgr_
void RenderRomSyncEntry(const RomSyncEntry &entry, int index)
std::vector< SnapshotEntry > snapshots_
void Initialize(Rom *rom, net::RomVersionManager *version_mgr, net::ProposalApprovalManager *approval_mgr)
Manages proposal approval workflow for collaborative sessions.
std::vector< ApprovalStatus > GetPendingProposals() const
Manages ROM versioning, snapshots, and rollback capabilities.
absl::StatusOr< std::string > CreateSnapshot(const std::string &description, const std::string &creator, bool is_checkpoint=false)
absl::StatusOr< bool > DetectCorruption()
absl::Status RestoreSnapshot(const std::string &snapshot_id)
absl::Status MarkAsSafePoint(const std::string &snapshot_id)
absl::Status DeleteSnapshot(const std::string &snapshot_id)
std::vector< RomSnapshot > GetSnapshots(bool safe_points_only=false) const
Represents an AI-generated proposal.
Represents a ROM synchronization event.
Represents a shared snapshot (image, map state, etc.)
Represents a versioned snapshot of ROM state.