yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
graphics_editor.cc
Go to the documentation of this file.
1// Related header
2#include "graphics_editor.h"
5#include "util/i18n/tr.h"
6
7// C++ standard library headers
8#include <algorithm>
9#include <filesystem>
10#include <set>
11
12// Third-party library headers
13#include "absl/status/status.h"
14#include "absl/status/statusor.h"
15#include "absl/strings/str_cat.h"
16#include "absl/strings/str_format.h"
17#include "absl/strings/str_join.h"
18#include "imgui/imgui.h"
19#include "imgui/misc/cpp/imgui_stdlib.h"
20
21// Project headers
27#include "app/gfx/core/bitmap.h"
35#include "app/gui/core/color.h"
36#include "app/gui/core/icons.h"
37#include "app/gui/core/input.h"
38#include "app/gui/core/style.h"
42#include "app/platform/window.h"
45#include "core/project.h"
46#include "core/rom_settings.h"
47#include "rom/rom.h"
48#include "rom/snes.h"
49#include "util/file_util.h"
50#include "util/log.h"
51#include "util/macro.h"
53
54namespace yaze {
55namespace editor {
56
58using ImGui::Button;
59using ImGui::InputInt;
60using ImGui::InputText;
61using ImGui::SameLine;
62
65 return;
66 auto* window_manager = dependencies_.window_manager;
67
68 // Pixel edits go to this session's sheet store (graphics_sheet_sync.h).
70
71 // Initialize panel components
72 sheet_browser_panel_ = std::make_unique<SheetBrowserPanel>(&state_);
73 sheet_browser_panel_->SetDataSources(
75 [this]() -> const project::YazeProject* { return project(); });
76 sheet_browser_panel_->SetUndoManager(&undo_manager_);
77 sheet_browser_panel_->SetSavePlanner([this]() { return PlanGraphicsSave(); });
78 sheet_browser_panel_->SetLabelCallbacks(
79 [this](uint16_t sheet, const std::string& label) {
80 return EditProject([&](project::YazeProject& target) {
81 core::SetGraphicsSheetLabel(target, sheet, label);
82 });
83 },
84 [this](const std::string& csv_text) -> absl::StatusOr<int> {
85 int written = 0;
86 bool counted = false;
88 const int count = core::ImportSpritesetSheetLabels(target, csv_text);
89 if (!counted) {
90 written = count;
91 counted = true;
92 }
93 }));
94 return written;
95 });
97 std::make_unique<PixelEditorPanel>(&state_, rom_, &undo_manager_);
100 };
101 pixel_editor_panel_->SetHistoryCallbacks([this] { return Undo(); },
102 [this] { return Redo(); });
103 pixel_editor_panel_->SetPaletteReorderCallback(
104 [this](const std::vector<size_t>& targets,
105 const PalettePermutation& permutation) {
106 return ReorderSourcePalette(targets, permutation);
107 });
108 pixel_editor_panel_->SetSheetBrowserCallbacks(
109 [this]() {
111 "graphics.sheet_browser_v2");
112 },
113 [this](bool visible) {
114 auto* workspace = dependencies_.window_manager;
115 if (visible)
116 workspace->OpenWindow("graphics.sheet_browser_v2");
117 else
118 workspace->CloseWindow("graphics.sheet_browser_v2");
119 });
120 palette_controls_panel_ = std::make_unique<PaletteControlsPanel>(&state_);
121 link_sprite_panel_ = std::make_unique<LinkSpritePanel>(&state_, rom_);
122 gfx_group_panel_ = std::make_unique<GfxGroupEditor>();
124 gfx_group_panel_->SetRom(rom_);
125 gfx_group_panel_->SetGameData(game_data_);
126 gfx_group_panel_->SetUndoManager(&undo_manager_);
127 gfx_group_panel_->SetMainUsageCallback(main_graphics_usage_);
128 gfx_group_panel_->SetRoomsetUsageCallback(roomset_graphics_usage_);
129 gfx_group_panel_->SetHostSurfaceHint(
130 "Gfx Groups: blockset/roomset/spriteset selection syncs with the "
131 "Overworld "
132 "editor for this ROM session (each surface has its own preview "
133 "canvases).");
134 paletteset_panel_ = std::make_unique<PalettesetEditorPanel>();
135 paletteset_panel_->SetRom(rom_);
136 paletteset_panel_->SetGameData(game_data_);
137
138 polyhedral_panel_ = std::make_unique<PolyhedralEditorPanel>(rom_);
139 polyhedral_panel_->SetRom(rom_);
140 usage_preview_view_ = std::make_unique<UsagePreviewView>(&state_);
141 usage_preview_view_->SetRom(rom_);
142 usage_preview_view_->SetGameData(game_data_);
143 usage_preview_view_->SetLiveRoomProvider(room_usage_provider_);
144 pixel_editor_panel_->SetSourcePreviewCallback(
145 [this]() { usage_preview_view_->DrawSourcePreview(); });
146
147 sheet_browser_panel_->Initialize();
148 pixel_editor_panel_->Initialize();
149 link_sprite_panel_->Initialize();
150
151 // Register panels using WindowContent system with callbacks
152 window_manager->RegisterWindowContent(
153 std::make_unique<GraphicsSheetBrowserPanel>([this]() {
155 status_ = sheet_browser_panel_->Update();
156 }
157 }));
158
159 window_manager->RegisterWindowContent(
160 std::make_unique<GraphicsPixelEditorPanel>([this]() {
162 status_ = pixel_editor_panel_->Update();
163 }
164 }));
165
166 window_manager->RegisterWindowContent(
167 std::make_unique<GraphicsPaletteControlsPanel>([this]() {
170 }
171 }));
172
173 window_manager->RegisterWindowContent(
174 std::make_unique<GraphicsUsagePreviewPanel>([this]() {
176 const auto* current_project = project();
177 usage_preview_view_->SetHackName(
178 current_project != nullptr
179 ? std::string(current_project->hack_manifest.hack_name())
180 : std::string());
181 usage_preview_view_->Draw();
182 }
183 }));
184
185 window_manager->RegisterWindowContent(
186 std::make_unique<GraphicsLinkSpritePanel>([this]() {
187 if (link_sprite_panel_) {
188 status_ = link_sprite_panel_->Update();
189 }
190 }));
191
192 window_manager->RegisterWindowContent(
193 std::make_unique<GraphicsGfxGroupPanel>([this]() {
194 if (gfx_group_panel_) {
195 status_ = gfx_group_panel_->Update();
196 }
197 }));
198
199 // Paletteset editor panel (separated from GfxGroupEditor for better UX)
200 window_manager->RegisterWindowContent(
201 std::make_unique<GraphicsPalettesetPanel>([this]() {
202 if (paletteset_panel_) {
203 status_ = paletteset_panel_->Update();
204 }
205 }));
206
207 window_manager->RegisterWindowContent(
208 std::make_unique<GraphicsPrototypeViewerPanel>(
209 [this]() { DrawPrototypeViewer(); }));
210
211 window_manager->RegisterWindowContent(
212 std::make_unique<GraphicsPolyhedralPanel>([this]() {
213 if (polyhedral_panel_) {
214 bool open = true;
215 polyhedral_panel_->Draw(&open);
216 }
217 }));
218}
219
220absl::Status GraphicsEditor::Load() {
221 gfx::ScopedTimer timer("GraphicsEditor::Load");
223 usage_preview_view_->Reset();
225 state_.preview_palettes.clear();
226
227 // Initialize all graphics sheets with appropriate palettes from ROM
228 // This ensures textures are created for editing
229 if (rom()->is_loaded()) {
230 auto& sheets = gfx::Arena::Get().gfx_sheets();
231
232 // Apply default palettes to all sheets based on common SNES ROM structure
233 // Sheets 0-112: Use overworld/dungeon palettes
234 // Sheets 113-127: Use sprite palettes
235 // Sheets 128-222: Use auxiliary/menu palettes
236
237 LOG_INFO("GraphicsEditor", "Initializing textures for %d graphics sheets",
239
240 int sheets_queued = 0;
241 for (int i = 0; i < zelda3::kNumGfxSheets; i++) {
242 if (!sheets[i].is_active() || !sheets[i].surface()) {
243 continue; // Skip inactive or surface-less sheets
244 }
245
246 // Palettes are now applied during ROM loading in LoadAllGraphicsData()
247 // Just queue texture creation for sheets that don't have textures yet
248 if (!sheets[i].texture()) {
249 // Fix: Ensure default palettes are applied if missing
250 // This handles the case where sheets are loaded but have no palette assigned
251 if (sheets[i].palette().empty()) {
252 // Default palette assignment logic
253 if (i <= 112) {
254 // Overworld/Dungeon sheets - use Dungeon Main palette (Group 0, Index 0)
255 if (game_data() &&
256 game_data()->palette_groups.dungeon_main.size() > 0) {
257 sheets[i].SetPaletteWithTransparent(
258 game_data()->palette_groups.dungeon_main.palette(0), 0);
259 }
260 } else if (i >= 113 && i <= 127) {
261 // Sprite sheets - use Sprites Aux1 palette (Group 4, Index 0)
262 if (game_data() &&
263 game_data()->palette_groups.sprites_aux1.size() > 0) {
264 sheets[i].SetPaletteWithTransparent(
265 game_data()->palette_groups.sprites_aux1.palette(0), 0);
266 }
267 } else {
268 // Menu/Aux sheets - use HUD palette if available, or fallback
269 if (game_data() && game_data()->palette_groups.hud.size() > 0) {
270 sheets[i].SetPaletteWithTransparent(
271 game_data()->palette_groups.hud.palette(0), 0);
272 }
273 }
274 }
275
278 sheets_queued++;
279 }
280 }
281
282 LOG_INFO("GraphicsEditor", "Queued texture creation for %d graphics sheets",
283 sheets_queued);
284 }
285
286 return absl::OkStatus();
287}
288
290 if (!status_bar)
291 return;
292
293 StatusBarSegmentOptions sheet_opts;
294 sheet_opts.tooltip = absl::StrFormat(
295 "Sheet %d (0x%02X) — use Command Palette to jump between sheets",
297 status_bar->SetCustomSegment(
298 "Sheet", absl::StrFormat("0x%02X", state_.current_sheet_id),
299 std::move(sheet_opts));
300
301 if (!state_.selected_sheets.empty()) {
302 status_bar->SetSelection(static_cast<int>(state_.selected_sheets.size()));
303 }
305 StatusBarSegmentOptions modified_opts;
306 modified_opts.tooltip = absl::StrFormat(
307 "%zu modified sheet%s pending save", state_.modified_sheets.size(),
308 state_.modified_sheets.size() == 1 ? "" : "s");
309 status_bar->SetCustomSegment(
310 "Modified", absl::StrFormat("%zu", state_.modified_sheets.size()),
311 std::move(modified_opts));
312 }
313}
314
316 EditorContextSnapshot snapshot;
317 snapshot.category = "Graphics";
318 snapshot.semantic_owner =
319 absl::StrFormat("graphics.sheet.%02X", state_.current_sheet_id);
320 snapshot.title = absl::StrFormat("Sheet %02X", state_.current_sheet_id);
323 snapshot.subtitle = state_.source_context->label;
325 snapshot.metadata.push_back(
326 {"gfx.source_capture", "Source mapping",
327 "Captured when opened; preview uses live pixels"});
328 }
329 if (const auto* target = state_.SourceTarget()) {
330 snapshot.metadata.push_back(
331 {"gfx.selected_tile", "Source tile", target->label});
332 std::string colors;
333 for (size_t i = 0; i < target->palette.size(); ++i) {
334 if (i != 0)
335 colors += " ";
336 colors += absl::StrFormat("%04X", target->palette[i].snes());
337 }
338 snapshot.metadata.push_back(
339 {"gfx.raw_palette", "Raw index colors (SNES)", colors});
340 }
341 if (state_.game_data != nullptr) {
342 snapshot.metadata.push_back(
343 {"gfx.current_sheet_revision", "Current sheet revision",
344 std::to_string(
346 }
347 return snapshot;
348}
349
351 const std::function<void(project::YazeProject&)>& edit) {
352 auto* editor_manager = static_cast<EditorManager*>(dependencies_.custom_data);
353 project::YazeProject* snapshot = project();
354 if (editor_manager == nullptr || snapshot == nullptr ||
355 !editor_manager->IsCurrentProjectContextOwnedBySession(
357 return absl::FailedPreconditionError(
358 "Sheet labels need the project that owns this ROM to be active");
359 }
360 project::YazeProject* active = editor_manager->GetCurrentProject();
361 if (active == nullptr || !active->project_opened()) {
362 return absl::FailedPreconditionError(
363 "Open a .yaze project to store sheet labels");
364 }
365 edit(*active);
366 if (snapshot != active) {
367 edit(*snapshot);
368 }
369 editor_manager->MarkCurrentProjectDirty();
370 return absl::OkStatus();
371}
372
373absl::StatusOr<GraphicsEditor::PreparedGraphicsSave>
375 if (!rom_ || !rom_->is_loaded()) {
376 return absl::FailedPreconditionError("ROM not loaded");
377 }
378 if (game_data() == nullptr) {
379 return absl::FailedPreconditionError("Game data not loaded");
380 }
381 // The edited sheets live in the shared Arena, which holds the last loaded
382 // ROM's sheets. Never write another open ROM's pixels into this one.
383 if (gfx::Arena::Get().gfx_sheets_owner() != game_data()) {
384 return absl::FailedPreconditionError(
385 "Graphics sheets in memory belong to another open ROM. Switch back to "
386 "the ROM they were edited in, or reopen this ROM, before saving "
387 "graphics");
388 }
389
390 PreparedGraphicsSave prepared;
391 if (const auto* project = this->project(); project != nullptr) {
393 }
394
395 auto& sheets = gfx::Arena::Get().gfx_sheets();
396 for (uint16_t sheet_id : state_.modified_sheets) {
397 if (sheet_id >= zelda3::kNumGfxSheets) {
398 prepared.refusals[sheet_id] = "out of range";
399 } else if (prepared.policy.reserved_sheets.count(sheet_id) != 0) {
400 prepared.refusals[sheet_id] = "reserved by the project";
401 } else if (zelda3::GetGfxSheetStorageKind(sheet_id) ==
403 prepared.refusals[sheet_id] = "2bpp, read-only";
404 } else if (!sheets[sheet_id].is_active()) {
405 prepared.refusals[sheet_id] = "not loaded";
406 } else if (std::any_of(sheets[sheet_id].vector().begin(),
407 sheets[sheet_id].vector().end(),
408 [](uint8_t index) { return index > 7; })) {
409 // 3bpp sheets store colors 0-7; packing masks higher indices silently.
410 prepared.refusals[sheet_id] = "uses colors above index 7";
411 } else {
412 prepared.sheet_ids.push_back(sheet_id);
413 }
414 }
415
416 const auto version_constants =
417 zelda3::kVersionConstantsMap.at(game_data()->version);
420 version_constants.kOverworldGfxPtr1);
423 version_constants.kOverworldGfxPtr2);
426 version_constants.kOverworldGfxPtr3);
427 return prepared;
428}
429
430absl::Status GraphicsEditor::Save() {
432 return absl::FailedPreconditionError(
433 "Commit or cancel the floating paste in Graphics before saving "
434 "(Enter to commit, Esc to cancel)");
435 if (pixel_editor_panel_ && pixel_editor_panel_->HasActiveStroke())
436 return absl::FailedPreconditionError(
437 "Finish the pixel stroke before saving");
438 if (!rom_ || !rom_->is_loaded()) {
439 return absl::FailedPreconditionError("ROM not loaded");
440 }
441
442 // Only save sheets that have been modified
443 if (!state_.HasUnsavedChanges()) {
444 LOG_INFO("GraphicsEditor", "No modified sheets to save");
445 return absl::OkStatus();
446 }
448
449 // Refuse the whole batch before writing when any sheet cannot be saved, so
450 // a save never lands only part of the user's edits.
451 if (!prepared.refusals.empty()) {
452 std::vector<std::string> refused;
453 for (const auto& [sheet_id, reason] : prepared.refusals) {
454 refused.push_back(absl::StrFormat("0x%02X (%s)", sheet_id, reason));
455 }
456 return absl::FailedPreconditionError(absl::StrFormat(
457 "Graphics save refused for sheet(s) %s. Discard those edits to save "
458 "the rest.",
459 absl::StrJoin(refused, ", ")));
460 }
461
462 LOG_INFO("GraphicsEditor", "Saving %zu modified graphics sheets",
463 prepared.sheet_ids.size());
464 auto& sheets = gfx::Arena::Get().gfx_sheets();
465
466 // Each WriteGfxSheet call restores its own bytes on failure; this snapshot
467 // also undoes sheets written earlier in the batch.
468 const std::vector<uint8_t> rom_snapshot = rom_->vector();
469 const bool rom_was_dirty = rom_->dirty();
470 auto restore = [&]() {
471 rom_->mutable_vector() = rom_snapshot;
472 rom_->set_dirty(rom_was_dirty);
473 };
474 std::set<uint16_t> saved_sheets;
475 for (uint16_t sheet_id : prepared.sheet_ids) {
476 const auto snes_data =
477 gfx::IndexedToSnesSheet(sheets[sheet_id].vector(), /*bpp=*/3);
478 auto result = zelda3::WriteGfxSheet(*rom_, sheet_id, snes_data,
479 prepared.policy, prepared.tables);
480 if (!result.ok()) {
481 restore();
482 return absl::Status(result.status().code(),
483 absl::StrFormat("Graphics sheet 0x%02X not saved: %s",
484 sheet_id, result.status().message()));
485 }
486 LOG_INFO("GraphicsEditor", "Saved sheet %02X (%zu bytes, %s) at 0x%06X",
487 sheet_id, result->new_stored_size,
488 result->placement == zelda3::GfxSheetPlacement::kRelocated
489 ? "relocated"
490 : "in place",
491 result->new_pc);
492 saved_sheets.insert(sheet_id);
493 }
494
495 // Every written sheet must decode to the edited pixels, with the game's
496 // decoder and with the editor's loader, before the ROM goes to disk (the
497 // caller writes the file only when this returns OK).
498 for (uint16_t sheet_id : prepared.sheet_ids) {
499 const absl::Status verified = verify_written_sheet_(
500 *rom_, sheet_id, sheets[sheet_id].vector(), prepared.tables);
501 if (!verified.ok()) {
502 restore();
503 return absl::Status(
504 verified.code(),
505 absl::StrFormat("Graphics save stopped; nothing was written: %s",
506 verified.message()));
507 }
508 }
509
510 // Clear modified tracking after successful save
511 state_.ClearModifiedSheets(saved_sheets);
512 return absl::OkStatus();
513}
514
515absl::StatusOr<std::vector<GraphicsSavePlanEntry>>
518 std::vector<GraphicsSavePlanEntry> entries;
519 for (const auto& [sheet_id, reason] : prepared.refusals) {
521 entry.sheet_id = sheet_id;
522 entry.refusal = reason;
523 entries.push_back(std::move(entry));
524 }
525 // Run the real writer on a scratch copy; the ROM itself is untouched.
526 Rom scratch;
528 auto& sheets = gfx::Arena::Get().gfx_sheets();
529 for (uint16_t sheet_id : prepared.sheet_ids) {
531 entry.sheet_id = sheet_id;
532 const auto& pixels = sheets[sheet_id].vector();
533 auto result = zelda3::WriteGfxSheet(
534 scratch, sheet_id, gfx::IndexedToSnesSheet(pixels, /*bpp=*/3),
535 prepared.policy, prepared.tables);
536 if (!result.ok()) {
537 entry.refusal = std::string(result.status().message());
538 } else {
539 entry.placement = result->placement;
540 entry.old_pc = result->old_pc;
541 entry.new_pc = result->new_pc;
542 entry.old_stored_size = result->old_stored_size;
543 entry.new_stored_size = result->new_stored_size;
544 const absl::Status verified =
545 verify_written_sheet_(scratch, sheet_id, pixels, prepared.tables);
546 if (!verified.ok()) {
547 entry.refusal = std::string(verified.message());
548 }
549 }
550 entries.push_back(std::move(entry));
551 }
552 std::sort(entries.begin(), entries.end(), [](const auto& a, const auto& b) {
553 return a.sheet_id < b.sheet_id;
554 });
555 return entries;
556}
557
559 // Panels are now drawn via WorkspaceWindowManager::DrawAllVisiblePanels()
560 // This Update() only handles editor-level state and keyboard shortcuts
561
562 // Show store changes (undo, redo, imports) in the Arena's display copies.
565
566 // Keyboard shortcuts (tools, zoom, grid, sheet navigation) are registered
567 // in shortcut_configurator.cc, scoped to EditorType::kGraphics.
568
570 return absl::OkStatus();
571}
572
573absl::Status GraphicsEditor::Undo() {
575 pixel_editor_panel_->FinishReleasedGesture();
576 if (pixel_editor_panel_ && pixel_editor_panel_->HasActiveStroke())
577 return absl::FailedPreconditionError("Finish the pixel stroke before Undo");
578 return undo_manager_.Undo();
579}
580
581absl::Status GraphicsEditor::Redo() {
583 pixel_editor_panel_->FinishReleasedGesture();
584 if (pixel_editor_panel_ && pixel_editor_panel_->HasActiveStroke())
585 return absl::FailedPreconditionError("Finish the pixel stroke before Redo");
586 return undo_manager_.Redo();
587}
588
590 if (!rom_ || !rom_->is_loaded()) {
591 ImGui::TextWrapped(tr(
592 "No ROM loaded — CGX/SCR/COL/BIN and clipboard tools work without one. "
593 "Load a ROM when you want vanilla palette presets or to save graphics "
594 "back into a cartridge image."));
595 ImGui::Spacing();
596 }
597 if (!prototype_import_feedback_.empty()) {
598 ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.35f, 1.0f), "%s",
600 if (ImGui::SmallButton(tr("Dismiss##prototype_import_feedback"))) {
602 }
603 ImGui::Separator();
604 }
605
607 ImGui::Begin("Memory Editor", &open_memory_editor_);
609 ImGui::End();
610 }
611
612 constexpr ImGuiTableFlags kGfxEditFlags = ImGuiTableFlags_Reorderable |
613 ImGuiTableFlags_Resizable |
614 ImGuiTableFlags_SizingStretchSame;
615
616 BEGIN_TABLE("#gfxEditTable", 4, kGfxEditFlags)
617 SETUP_COLUMN("File Import (BIN, CGX, ROM)")
618 SETUP_COLUMN("Palette (COL)")
619 ImGui::TableSetupColumn("Tilemaps and Objects (SCR, PNL, OBJ)",
620 ImGuiTableColumnFlags_WidthFixed);
621 SETUP_COLUMN("Graphics Preview")
623 NEXT_COLUMN() {
628 }
629
630 NEXT_COLUMN() {
632 }
633
637 scr_loaded_, false, 0);
639
641 if (super_donkey_) {
642 // Super Donkey prototype graphics
643 for (size_t i = 0; i < num_sheets_to_load_ && i < gfx_sheets_.size(); i++) {
644 if (gfx_sheets_[i].is_active() && gfx_sheets_[i].texture()) {
645 ImGui::Image((ImTextureID)(intptr_t)gfx_sheets_[i].texture(),
646 ImVec2(128, 32));
647 if ((i + 1) % 4 != 0) {
648 ImGui::SameLine();
649 }
650 }
651 }
652 } else if (cgx_loaded_ && col_file_) {
653 // Load the CGX graphics
656 cgx_loaded_, true, 5);
657 } else {
658 // Load the BIN/Clipboard Graphics
661 gfx_loaded_, true, 2);
662 }
663 END_TABLE()
664}
665
666// =============================================================================
667// Prototype Viewer Import Methods
668// =============================================================================
669
671 gui::TextWithSeparators("Cgx Import");
672 InputInt(tr("BPP"), &current_bpp_);
673
674 InputText("##CGXFile", &cgx_file_name_);
675 SameLine();
676
677 if (ImGui::Button(tr("Open CGX"))) {
679 cgx_file_name_ = filename;
680 cgx_file_path_ = std::filesystem::absolute(filename).string();
681 is_open_ = true;
682 cgx_loaded_ = true;
683 }
684
685 if (ImGui::Button(tr("Copy CGX Path"))) {
686 ImGui::SetClipboardText(cgx_file_path_.c_str());
687 }
688
689 if (ImGui::Button(tr("Load CGX Data"))) {
692 if (!status_.ok()) {
693 prototype_import_feedback_ = absl::StrCat("[CGX] ", status_.message());
694 return absl::OkStatus();
695 }
697
698 cgx_bitmap_.Create(0x80, 0x200, 8, decoded_cgx_);
699 if (col_file_) {
703 }
704 }
705
706 return absl::OkStatus();
707}
708
710 InputText("##ScrFile", &scr_file_name_);
711
712 if (ImGui::Button(tr("Open SCR"))) {
714 scr_file_name_ = filename;
715 scr_file_path_ = std::filesystem::absolute(filename).string();
716 is_open_ = true;
717 scr_loaded_ = true;
718 }
719
720 InputInt(tr("SCR Mod"), &scr_mod_value_);
721
722 if (ImGui::Button(tr("Load Scr Data"))) {
724 if (!status_.ok()) {
725 prototype_import_feedback_ = absl::StrCat("[SCR] ", status_.message());
726 return absl::OkStatus();
727 }
728
729 decoded_scr_data_.resize(0x100 * 0x100);
732 if (!status_.ok()) {
734 absl::StrCat("[SCR draw] ", status_.message());
735 return absl::OkStatus();
736 }
738
739 scr_bitmap_.Create(0x100, 0x100, 8, decoded_scr_data_);
740 if (scr_loaded_) {
744 }
745 }
746
747 return absl::OkStatus();
748}
749
751 gui::TextWithSeparators("COL Import");
752 InputText("##ColFile", &col_file_name_);
753 SameLine();
754
755 if (ImGui::Button(tr("Open COL"))) {
757 col_file_name_ = filename;
758 col_file_path_ = std::filesystem::absolute(filename).string();
760 auto col_data_ = gfx::GetColFileData(temp_rom_.mutable_data());
761 if (col_file_palette_group_.size() != 0) {
763 }
764 auto col_file_palette_group_status =
766 if (col_file_palette_group_status.ok()) {
767 col_file_palette_group_ = col_file_palette_group_status.value();
768 }
770
771 // gigaleak dev format based code
773 col_file_ = true;
774 is_open_ = true;
775 }
776 HOVER_HINT(".COL, .BAK");
777
778 if (ImGui::Button(tr("Copy Col Path"))) {
779 ImGui::SetClipboardText(col_file_path_.c_str());
780 }
781
782 if (rom()->is_loaded()) {
783 gui::TextWithSeparators("ROM Palette");
784 gui::InputHex("Palette Index", &current_palette_index_);
785 ImGui::Combo(tr("Palette"), &current_palette_, kPaletteGroupAddressesKeys,
786 IM_ARRAYSIZE(kPaletteGroupAddressesKeys));
787 }
788
789 if (col_file_palette_.size() != 0) {
792 }
793
794 return absl::OkStatus();
795}
796
798 gui::TextWithSeparators("OBJ Import");
799
800 InputText("##ObjFile", &obj_file_path_);
801 SameLine();
802
803 if (ImGui::Button(tr("Open OBJ"))) {
805 obj_file_path_ = std::filesystem::absolute(filename).string();
807 is_open_ = true;
808 obj_loaded_ = true;
809 }
810 HOVER_HINT(".OBJ, .BAK");
811
812 return absl::OkStatus();
813}
814
816 gui::TextWithSeparators("Tilemap Import");
817
818 InputText("##TMapFile", &tilemap_file_path_);
819 SameLine();
820
821 if (ImGui::Button(tr("Open Tilemap"))) {
823 tilemap_file_path_ = std::filesystem::absolute(filename).string();
826
827 // Extract the high and low bytes from the file.
828 auto decomp_sheet = gfx::lc_lz2::DecompressV2(tilemap_rom_.data(), 0, 0x800,
831 tilemap_loaded_ = true;
832 is_open_ = true;
833 }
834 HOVER_HINT(".DAT, .BIN, .HEX");
835
836 return absl::OkStatus();
837}
838
840 gui::TextWithSeparators("BIN Import");
841
842 InputText("##ROMFile", &file_path_);
843 SameLine();
844
845 if (ImGui::Button(tr("Open BIN"))) {
847 file_path_ = filename;
849 is_open_ = true;
850 }
851 HOVER_HINT(".BIN, .HEX");
852
853 if (Button(tr("Copy File Path"))) {
854 ImGui::SetClipboardText(file_path_.c_str());
855 }
856
857 gui::InputHex("BIN Offset", &current_offset_);
858 gui::InputHex("BIN Size", &bin_size_);
859
860 if (Button(tr("Decompress BIN"))) {
861 if (file_path_.empty()) {
862 return absl::InvalidArgumentError(
863 "Please select a file before decompressing.");
864 }
866 }
867
868 return absl::OkStatus();
869}
870
872 gui::TextWithSeparators("Clipboard Import");
873 if (Button(tr("Paste From Clipboard"))) {
874 const char* text = ImGui::GetClipboardText();
875 if (text) {
876 const auto clipboard_data =
877 std::vector<uint8_t>(text, text + strlen(text));
878 ImGui::MemFree((void*)text);
879 status_ = temp_rom_.LoadFromData(clipboard_data);
880 is_open_ = true;
881 open_memory_editor_ = true;
882 }
883 }
886 gui::InputHex("Num Sheets", &num_sheets_to_load_);
887
888 if (Button(tr("Decompress Clipboard Data"))) {
889 if (temp_rom_.is_loaded()) {
890 status_ = DecompressImportData(0x40000);
891 } else {
892 status_ = absl::InvalidArgumentError(
893 "Please paste data into the clipboard before "
894 "decompressing.");
895 }
896 }
897
898 return absl::OkStatus();
899}
900
902 gui::TextWithSeparators("Experimental");
903 if (Button(tr("Decompress Super Donkey Full"))) {
904 if (file_path_.empty()) {
905 return absl::InvalidArgumentError(
906 "Please select `super_donkey_1.bin` before "
907 "importing.");
908 }
910 }
911 ImGui::SetItemTooltip(
912 tr("Requires `super_donkey_1.bin` to be imported under the "
913 "BIN import section."));
914 return absl::OkStatus();
915}
916
918 std::string title = "Memory Editor";
919 if (is_open_) {
920 static yaze::gui::MemoryEditorWidget mem_edit;
921 mem_edit.DrawWindow(title.c_str(), temp_rom_.mutable_data(),
922 temp_rom_.size());
923 }
924 return absl::OkStatus();
925}
926
930 size, 1, temp_rom_.size()));
931
932 auto converted_sheet = gfx::SnesTo8bppSheet(import_data_, 3);
934 converted_sheet);
935
936 if (rom()->is_loaded() && game_data()) {
937 auto palette_group = game_data()->palette_groups.overworld_animated;
938 z3_rom_palette_ = palette_group[current_palette_];
939 if (col_file_) {
941 } else {
943 }
944 }
945
947 &bin_bitmap_);
948 gfx_loaded_ = true;
949
950 return absl::OkStatus();
951}
952
954 int i = 0;
955 for (const auto& offset : kSuperDonkeyTiles) {
956 int offset_value =
957 std::stoi(offset, nullptr, 16); // convert hex string to int
958 ASSIGN_OR_RETURN(auto decompressed_data,
960 0x1000, 1, temp_rom_.size()));
961 auto converted_sheet = gfx::SnesTo8bppSheet(decompressed_data, 3);
963 gfx::kTilesheetDepth, converted_sheet);
964 if (col_file_) {
965 gfx_sheets_[i].SetPalette(
967 } else {
968 // ROM palette
969 if (!game_data()) {
970 return absl::FailedPreconditionError("GameData not available");
971 }
972 auto palette_group = game_data()->palette_groups.get_group(
973 kPaletteGroupAddressesKeys[current_palette_]);
974 z3_rom_palette_ = palette_group->palette(current_palette_index_);
975 gfx_sheets_[i].SetPalette(z3_rom_palette_);
976 }
977
980 i++;
981 }
982
983 for (const auto& offset : kSuperDonkeySprites) {
984 int offset_value =
985 std::stoi(offset, nullptr, 16); // convert hex string to int
986 ASSIGN_OR_RETURN(auto decompressed_data,
988 0x1000, 1, temp_rom_.size()));
989 auto converted_sheet = gfx::SnesTo8bppSheet(decompressed_data, 3);
991 gfx::kTilesheetDepth, converted_sheet);
992 if (col_file_) {
993 gfx_sheets_[i].SetPalette(
995 } else {
996 // ROM palette
997 if (game_data()) {
998 auto palette_group = game_data()->palette_groups.get_group(
999 kPaletteGroupAddressesKeys[current_palette_]);
1000 z3_rom_palette_ = palette_group->palette(current_palette_index_);
1001 gfx_sheets_[i].SetPalette(z3_rom_palette_);
1002 }
1003 }
1004
1007 i++;
1008 }
1009 super_donkey_ = true;
1011
1012 return absl::OkStatus();
1013}
1014
1020
1026
1030
1032 state_.ZoomIn();
1033}
1034
1038
1042
1043absl::Status GraphicsEditor::Cut() {
1044 if (!pixel_editor_panel_) {
1045 return absl::FailedPreconditionError("Pixel editor is not open");
1046 }
1047 pixel_editor_panel_->CutToSystemClipboard();
1048 return absl::OkStatus();
1049}
1050
1051absl::Status GraphicsEditor::Copy() {
1052 if (!pixel_editor_panel_) {
1053 return absl::FailedPreconditionError("Pixel editor is not open");
1054 }
1055 pixel_editor_panel_->CopyToSystemClipboard();
1056 return absl::OkStatus();
1057}
1058
1060 if (!pixel_editor_panel_) {
1061 return absl::FailedPreconditionError("Pixel editor is not open");
1062 }
1063 pixel_editor_panel_->PasteFromSystemClipboard();
1064 return absl::OkStatus();
1065}
1066
1071
1072void GraphicsEditor::SelectSheet(uint16_t sheet_id) {
1073 if (sheet_id >= zelda3::kNumGfxSheets) {
1074 return;
1075 }
1076 state_.SelectSheet(sheet_id);
1077 state_.requested_sheet_tab = sheet_id;
1078}
1079
1081 if (context.owner == nullptr || context.owner != state_.game_data) {
1082 return absl::FailedPreconditionError(
1083 "Graphics context belongs to a different ROM session");
1084 }
1085 if (context.targets.empty()) {
1086 return absl::InvalidArgumentError("Graphics context has no editable tiles");
1087 }
1088 for (const auto& target : context.targets) {
1089 if (target.sheet_id >= zelda3::kNumGfxSheets || target.tile_index >= 64 ||
1090 target.palette.size() != 8 ||
1091 zelda3::GetGfxSheetStorageKind(target.sheet_id) ==
1093 !state_.game_data->sheet_store.HasSheet(target.sheet_id)) {
1094 return absl::InvalidArgumentError(
1095 "Graphics context needs loaded 3bpp sheets and eight indexed colors");
1096 }
1097 }
1099 return absl::FailedPreconditionError(
1100 "Commit or cancel the floating paste before opening another source");
1101 }
1104 state_.source_context = std::move(context);
1108 pixel_editor_panel_->ShowSourcePreview();
1110 dependencies_.window_manager->OpenWindow("graphics.pixel_editor");
1111 // Source preview is embedded. Deliberately opened comparison windows keep
1112 // their visibility, pins and layout; entering Graphics adds no pop-out.
1113 }
1114 return absl::OkStatus();
1115}
1116
1118 const std::vector<size_t>& targets,
1119 const std::array<uint8_t, 8>& permutation) {
1120 if (pixel_editor_panel_ && pixel_editor_panel_->HasActiveStroke())
1121 return absl::FailedPreconditionError("Finish the pixel stroke first");
1123 permutation));
1125 return absl::OkStatus();
1126}
1127
1128void GraphicsEditor::HighlightTile(uint16_t sheet_id, uint16_t tile_index,
1129 const std::string& label,
1130 double duration_secs) {
1131 if (sheet_id >= zelda3::kNumGfxSheets) {
1132 return;
1133 }
1134 state_.HighlightTile(sheet_id, tile_index, label, duration_secs);
1135}
1136
1137} // namespace editor
1138} // 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:49
absl::Status LoadFromFile(const std::string &filename, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:491
void set_dirty(bool dirty)
Definition rom.h:189
auto mutable_data()
Definition rom.h:202
const auto & vector() const
Definition rom.h:205
auto data() const
Definition rom.h:201
auto size() const
Definition rom.h:200
bool dirty() const
Definition rom.h:188
auto & mutable_vector()
Definition rom.h:206
absl::Status LoadFromData(const std::vector< uint8_t > &data, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:606
bool is_loaded() const
Definition rom.h:187
const std::string & hack_name() const
static RomSettings & Get()
uint32_t GetAddressOr(const std::string &key, uint32_t default_value) const
The EditorManager controls the main editor window and manages the various editor classes.
EditorContext context() const
Definition editor.h:364
UndoManager undo_manager_
Definition editor.h:371
project::YazeProject * project() const
Definition editor.h:358
zelda3::GameData * game_data() const
Definition editor.h:357
EditorDependencies dependencies_
Definition editor.h:370
std::map< uint16_t, GraphicsPaletteSelection > preview_palettes
std::optional< GraphicsEditContext > source_context
std::optional< uint16_t > requested_sheet_tab
void HighlightTile(uint16_t sheet_id, uint16_t tile_index, const std::string &label="", double duration_secs=3.0)
Highlight a tile in the current sheet for quick visual focus.
std::function< void()> on_before_sheet_selected
bool HasUnsavedChanges() const
Check if any sheets have unsaved changes.
void SelectSheet(uint16_t sheet_id)
Select a sheet for editing.
const GraphicsEditTarget * SourceTarget() const
void SetTool(PixelTool tool)
Set the current editing tool.
void ClearModifiedSheets()
Clear modification tracking (after save)
usage_preview::LiveRoomProvider room_usage_provider_
std::vector< uint8_t > scr_data_
absl::Status Save() override
void HighlightTile(uint16_t sheet_id, uint16_t tile_index, const std::string &label="", double duration_secs=3.0)
absl::Status ReorderSourcePalette(const std::vector< size_t > &targets, const std::array< uint8_t, 8 > &permutation)
GfxGroupEditor::RoomsetUsageCallback roomset_graphics_usage_
void SetPixelTool(PixelTool tool)
gfx::PaletteGroup col_file_palette_group_
void SelectSheet(uint16_t sheet_id)
absl::Status Load() override
std::unique_ptr< GfxGroupEditor > gfx_group_panel_
std::unique_ptr< PaletteControlsPanel > palette_controls_panel_
std::vector< uint8_t > decoded_cgx_
EditorContextSnapshot BuildContextSnapshot() const override
absl::Status Copy() override
GfxGroupEditor::MainUsageCallback main_graphics_usage_
absl::Status EditProject(const std::function< void(project::YazeProject &)> &edit)
std::vector< uint8_t > cgx_data_
std::unique_ptr< PixelEditorPanel > pixel_editor_panel_
absl::Status Redo() override
std::unique_ptr< PolyhedralEditorPanel > polyhedral_panel_
std::unique_ptr< PalettesetEditorPanel > paletteset_panel_
std::function< absl::Status(const Rom &, uint16_t, const std::vector< uint8_t > &, const zelda3::GfxSheetPointerTables &) verify_written_sheet_)
absl::Status Cut() override
std::vector< uint8_t > import_data_
absl::Status Paste() override
absl::StatusOr< std::vector< GraphicsSavePlanEntry > > PlanGraphicsSave()
Graphics save preflight: every dirty sheet written to a scratch copy of the ROM (placement,...
std::vector< SDL_Color > decoded_col_
absl::Status Undo() override
absl::StatusOr< PreparedGraphicsSave > PrepareGraphicsSave()
absl::Status Update() override
absl::Status OpenInContext(GraphicsEditContext context)
std::vector< uint8_t > extra_cgx_data_
std::array< gfx::Bitmap, zelda3::kNumGfxSheets > gfx_sheets_
std::vector< uint8_t > decoded_scr_data_
std::unique_ptr< LinkSpritePanel > link_sprite_panel_
absl::Status DecompressImportData(int size)
void ContributeStatus(StatusBar *status_bar) override
std::unique_ptr< UsagePreviewView > usage_preview_view_
std::unique_ptr< SheetBrowserPanel > sheet_browser_panel_
A session-aware status bar displayed at the bottom of the application.
Definition status_bar.h:57
void SetSelection(int count, int width=0, int height=0)
Set selection information.
void SetCustomSegment(const std::string &key, const std::string &value)
Set a custom segment with key-value pair.
absl::Status Redo()
Redo the top action. Returns error if stack is empty.
absl::Status Undo()
Undo the top action. Returns error if stack is empty.
bool IsWindowOpen(size_t session_id, const std::string &base_window_id) const
bool OpenWindow(size_t session_id, const std::string &base_window_id)
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:39
GraphicsSheets & gfx_sheets()
Definition arena.h:178
static Arena & Get()
Definition arena.cc:24
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:70
void Create(int width, int height, int depth, std::span< uint8_t > data)
Create a bitmap with the given dimensions and data.
Definition bitmap.cc:198
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:452
RAII timer for automatic timing management.
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
CanvasConfig & GetConfig()
Definition canvas.h:243
static std::string ShowOpenFileDialog()
ShowOpenFileDialog opens a file dialog and returns the selected filepath. Uses global feature flag to...
bool HasSheet(uint16_t sheet) const
True when the buffer holds all 4096 bytes of sheet.
uint64_t Revision(uint16_t sheet) const
Locates graphics sheets in the ROM and writes edited sheets back.
#define LOG_INFO(category, format,...)
Definition log.h:136
#define SETUP_COLUMN(l)
Definition macro.h:12
#define END_TABLE()
Definition macro.h:20
#define TABLE_HEADERS()
Definition macro.h:14
#define BEGIN_TABLE(l, n, f)
Definition macro.h:11
#define NEXT_COLUMN()
Definition macro.h:18
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
#define CLEAR_AND_RETURN_STATUS(status)
Definition macro.h:97
#define HOVER_HINT(string)
Definition macro.h:24
constexpr char kOverworldGfxPtr3[]
constexpr char kOverworldGfxPtr1[]
constexpr char kOverworldGfxPtr2[]
absl::StatusOr< zelda3::GfxSheetWritePolicy > BuildGfxSheetWritePolicy(const HackManifest &manifest)
void SetGraphicsSheetLabel(project::YazeProject &project, uint16_t sheet, const std::string &label)
int ImportSpritesetSheetLabels(project::YazeProject &project, const std::string &csv_text, bool overwrite)
int SyncArenaFromStore(GraphicsEditorState &state)
std::array< uint8_t, 8 > PalettePermutation
const std::string kSuperDonkeySprites[]
void AppendResolvedGraphicsContext(EditorContextSnapshot &snapshot, const zelda3::ResolvedGraphicsContext &context)
void AttachSheetStore(GraphicsEditorState &state, zelda3::GameData *game_data)
const std::string kSuperDonkeyTiles[]
void RefreshGraphicsPreviewPalettes(GraphicsEditorState &state)
PixelTool
Pixel editing tool types for the graphics editor.
absl::Status ApplyPalettePixelPermutation(GraphicsEditorState &state, UndoManager &history, const std::vector< size_t > &targets, const PalettePermutation &permutation)
absl::StatusOr< std::vector< uint8_t > > DecompressV2(const uint8_t *data, int offset, int size, int mode, size_t rom_size)
Decompresses a buffer of data using the LC_LZ2 algorithm.
constexpr int kNintendoMode1
Definition compression.h:54
absl::Status LoadScr(std::string_view filename, uint8_t input_value, std::vector< uint8_t > &map_data)
Load Scr file (screen data)
std::vector< SDL_Color > DecodeColFile(const std::string_view filename)
Decode color file.
constexpr int kTilesheetHeight
Definition snes_tile.h:17
constexpr int kTilesheetWidth
Definition snes_tile.h:16
absl::Status LoadCgx(uint8_t bpp, std::string_view filename, std::vector< uint8_t > &cgx_data, std::vector< uint8_t > &cgx_loaded, std::vector< uint8_t > &cgx_header)
Load Cgx file (graphical content)
constexpr const char * kPaletteGroupAddressesKeys[]
absl::Status DrawScrWithCgx(uint8_t bpp, std::vector< uint8_t > &map_bitmap_data, std::vector< uint8_t > &map_data, std::vector< uint8_t > &cgx_loaded)
Draw screen tilemap with graphical data.
constexpr int kTilesheetDepth
Definition snes_tile.h:18
std::vector< uint8_t > SnesTo8bppSheet(std::span< const uint8_t > sheet, int bpp, int num_sheets)
Definition snes_tile.cc:133
std::vector< uint8_t > IndexedToSnesSheet(std::span< const uint8_t > sheet, int bpp, int num_sheets)
Definition snes_tile.cc:204
absl::StatusOr< PaletteGroup > CreatePaletteGroupFromColFile(std::vector< SnesColor > &palette_rows)
std::vector< SnesColor > GetColFileData(uint8_t *data)
Definition snes_color.cc:74
bool InputHex(const char *label, uint64_t *data)
Definition input.cc:470
void SelectablePalettePipeline(uint64_t &palette_id, bool &refresh_graphics, gfx::SnesPalette &palette)
Definition color.cc:338
void BitmapCanvasPipeline(Canvas &canvas, gfx::Bitmap &bitmap, int width, int height, int tile_size, bool is_loaded, bool scrollbar, int canvas_id)
void TextWithSeparators(const absl::string_view &text)
Definition style.cc:1344
absl::StatusOr< GfxSheetWriteResult > WriteGfxSheet(Rom &rom, uint16_t sheet_id, const std::vector< uint8_t > &snes_3bpp, const GfxSheetWritePolicy &policy, const GfxSheetPointerTables &tables)
Writes one edited 3bpp sheet to the ROM buffer.
constexpr uint32_t kNumGfxSheets
Definition game_data.h:28
GfxSheetStorageKind GetGfxSheetStorageKind(uint16_t sheet_id)
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
std::vector< EditorContextValue > metadata
WorkspaceWindowManager * window_manager
Definition editor.h:182
GfxGroupWorkspaceState * gfx_group_workspace
Definition editor.h:179
zelda3::GfxSheetPlacement placement
Optional behavior for an interactive status bar segment.
Definition status_bar.h:27
PaletteGroup * get_group(const std::string &group_name)
void DrawWindow(const char *title, void *mem_data, size_t mem_size, size_t base_display_addr=0x0000)
Modern project structure with comprehensive settings consolidation.
Definition project.h:287
core::HackManifest hack_manifest
Definition project.h:360
gfx::PaletteGroupMap palette_groups
Definition game_data.h:95
zelda3_version version
Definition game_data.h:84