yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
sprite_editor.cc
Go to the documentation of this file.
1#include "sprite_editor.h"
2
3#include <algorithm>
4#include <cerrno>
5#include <cstdlib>
6#include <cstring>
7
8#include "absl/strings/str_format.h"
14#include "app/gui/core/icons.h"
15#include "app/gui/core/input.h"
18#include "util/file_util.h"
19#include "util/hex.h"
20#include "util/macro.h"
22
23namespace yaze {
24namespace editor {
25
26using ImGui::BeginTable;
27using ImGui::Button;
28using ImGui::EndTable;
29using ImGui::Selectable;
30using ImGui::Separator;
31using ImGui::TableHeadersRow;
32using ImGui::TableNextColumn;
33using ImGui::TableNextRow;
34using ImGui::TableSetupColumn;
35using ImGui::Text;
36
37namespace {
38template <size_t N>
39void CopyStringToBuffer(const std::string& src, char (&dest)[N]) {
40 std::strncpy(dest, src.c_str(), N - 1);
41 dest[N - 1] = '\0';
42}
43
44int ParseIntOrDefault(const std::string& text, int fallback = 0) {
45 if (text.empty()) {
46 return fallback;
47 }
48 errno = 0;
49 char* end = nullptr;
50 long value = std::strtol(text.c_str(), &end, 0);
51 if (end == text.c_str() || errno == ERANGE) {
52 return fallback;
53 }
54 return static_cast<int>(value);
55}
56} // namespace
57
60 return;
61 auto* panel_manager = dependencies_.panel_manager;
62
63 // Register EditorPanel implementations with callbacks
64 // EditorPanels provide both metadata (icon, name, priority) and drawing logic
65 panel_manager->RegisterEditorPanel(
66 std::make_unique<VanillaSpriteEditorPanel>([this]() {
67 if (rom_ && rom_->is_loaded()) {
68 DrawVanillaSpriteEditor();
69 } else {
70 ImGui::TextDisabled("Load a ROM to view vanilla sprites");
71 }
72 }));
73
74 panel_manager->RegisterEditorPanel(std::make_unique<CustomSpriteEditorPanel>(
75 [this]() { DrawCustomSprites(); }));
76}
77
78absl::Status SpriteEditor::Load() {
79 gfx::ScopedTimer timer("SpriteEditor::Load");
80 return absl::OkStatus();
81}
82
83absl::Status SpriteEditor::Update() {
84 if (rom()->is_loaded() && !sheets_loaded_) {
85 sheets_loaded_ = true;
86 }
87
88 // Update animation playback for custom sprites
89 float current_time = ImGui::GetTime();
90 float delta_time = current_time - last_frame_time_;
91 last_frame_time_ = current_time;
92 UpdateAnimationPlayback(delta_time);
93
94 // Handle editor-level shortcuts
96
97 // Panel drawing is handled by PanelManager via registered EditorPanels
98 // Each panel's Draw() callback invokes the appropriate draw method
99
100 return status_.ok() ? absl::OkStatus() : status_;
101}
102
104 // Animation playback shortcuts (when custom sprite panel is active)
105 if (ImGui::IsKeyPressed(ImGuiKey_Space, false) &&
106 !ImGui::GetIO().WantTextInput) {
108 }
109
110 // Frame navigation
111 if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket, false)) {
112 if (current_frame_ > 0) {
115 }
116 }
117 if (ImGui::IsKeyPressed(ImGuiKey_RightBracket, false)) {
120 }
121
122 // Sprite navigation
123 if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_UpArrow, false)) {
124 if (current_sprite_id_ > 0) {
127 }
128 }
129 if (ImGui::GetIO().KeyCtrl &&
130 ImGui::IsKeyPressed(ImGuiKey_DownArrow, false)) {
133 }
134}
135
136absl::Status SpriteEditor::Save() {
138 current_custom_sprite_index_ < static_cast<int>(custom_sprites_.size())) {
139 const std::string& zsm_path = GetCurrentZsmPath();
140 if (zsm_path.empty()) {
142 } else {
143 SaveZsmFile(zsm_path);
144 }
145 }
146 return absl::OkStatus();
147}
148
150 // Sidebar handled by EditorManager for card-based editors
151}
152
153// ============================================================
154// Vanilla Sprite Editor
155// ============================================================
156
158 if (ImGui::BeginTable("##SpriteCanvasTable", 3, ImGuiTableFlags_Resizable,
159 ImVec2(0, 0))) {
160 TableSetupColumn("Sprites List", ImGuiTableColumnFlags_WidthFixed, 256);
161 TableSetupColumn("Canvas", ImGuiTableColumnFlags_WidthStretch,
162 ImGui::GetContentRegionAvail().x);
163 TableSetupColumn("Tile Selector", ImGuiTableColumnFlags_WidthFixed, 256);
164 TableHeadersRow();
165 TableNextRow();
166
167 TableNextColumn();
169
170 TableNextColumn();
171 static int next_tab_id = 0;
172
173 if (gui::BeginThemedTabBar("SpriteTabBar", kSpriteTabBarFlags)) {
174 if (ImGui::TabItemButton(ICON_MD_ADD, kSpriteTabFlags)) {
175 if (std::find(active_sprites_.begin(), active_sprites_.end(),
177 next_tab_id++;
178 }
179 active_sprites_.push_back(next_tab_id++);
180 }
181
182 for (int n = 0; n < active_sprites_.Size;) {
183 bool open = true;
184
185 if (active_sprites_[n] > sizeof(zelda3::kSpriteDefaultNames) / 4) {
186 active_sprites_.erase(active_sprites_.Data + n);
187 continue;
188 }
189
190 if (ImGui::BeginTabItem(
192 ImGuiTabItemFlags_None)) {
194 ImGui::EndTabItem();
195 }
196
197 if (!open)
198 active_sprites_.erase(active_sprites_.Data + n);
199 else
200 n++;
201 }
202
204 }
205
206 TableNextColumn();
207 if (sheets_loaded_) {
209 }
210 ImGui::EndTable();
211 }
212}
213
215 static bool flip_x = false;
216 static bool flip_y = false;
217 if (ImGui::BeginChild(gui::GetID("##SpriteCanvas"),
218 ImGui::GetContentRegionAvail(), true)) {
221
222 // Render vanilla sprite if layout exists
223 if (current_sprite_id_ >= 0) {
224 const auto* layout = zelda3::SpriteOamRegistry::GetLayout(
225 static_cast<uint8_t>(current_sprite_id_));
226 if (layout) {
227 // Load required sheets for this sprite
228 LoadSheetsForSprite(layout->required_sheets);
229 RenderVanillaSprite(*layout);
230
231 // Draw the preview bitmap centered on canvas
234 }
235
236 // Show sprite info
237 ImGui::SetCursorPos(ImVec2(10, 10));
238 Text("Sprite: %s (0x%02X)", layout->name, layout->sprite_id);
239 Text("Tiles: %zu", layout->tiles.size());
240 }
241 }
242
245
246 if (ImGui::BeginTable("##OAMTable", 7, ImGuiTableFlags_Resizable,
247 ImVec2(0, 0))) {
248 TableSetupColumn("X", ImGuiTableColumnFlags_WidthStretch);
249 TableSetupColumn("Y", ImGuiTableColumnFlags_WidthStretch);
250 TableSetupColumn("Tile", ImGuiTableColumnFlags_WidthStretch);
251 TableSetupColumn("Palette", ImGuiTableColumnFlags_WidthStretch);
252 TableSetupColumn("Priority", ImGuiTableColumnFlags_WidthStretch);
253 TableSetupColumn("Flip X", ImGuiTableColumnFlags_WidthStretch);
254 TableSetupColumn("Flip Y", ImGuiTableColumnFlags_WidthStretch);
255 TableHeadersRow();
256 TableNextRow();
257
258 TableNextColumn();
260
261 TableNextColumn();
263
264 TableNextColumn();
266
267 TableNextColumn();
269
270 TableNextColumn();
272
273 TableNextColumn();
274 if (ImGui::Checkbox("##XFlip", &flip_x)) {
275 oam_config_.flip_x = flip_x;
276 }
277
278 TableNextColumn();
279 if (ImGui::Checkbox("##YFlip", &flip_y)) {
280 oam_config_.flip_y = flip_y;
281 }
282
283 ImGui::EndTable();
284 }
285
287 }
288 ImGui::EndChild();
289}
290
292 if (ImGui::BeginChild(gui::GetID("sheet_label"),
293 ImVec2(ImGui::GetContentRegionAvail().x, 0), true,
294 ImGuiWindowFlags_NoDecoration)) {
295 // Track previous sheet values for change detection
296 static uint8_t prev_sheets[8] = {0};
297 bool sheets_changed = false;
298
299 for (int i = 0; i < 8; i++) {
300 std::string sheet_label = absl::StrFormat("Sheet %d", i);
301 if (gui::InputHexByte(sheet_label.c_str(), &current_sheets_[i])) {
302 sheets_changed = true;
303 }
304 if (i % 2 == 0)
305 ImGui::SameLine();
306 }
307
308 // Reload graphics buffer if sheets changed
309 if (sheets_changed || std::memcmp(prev_sheets, current_sheets_, 8) != 0) {
310 std::memcpy(prev_sheets, current_sheets_, 8);
311 gfx_buffer_loaded_ = false;
313 }
314
318 for (int i = 0; i < 8; i++) {
320 gfx::Arena::Get().gfx_sheets().at(current_sheets_[i]), 1,
321 (i * 0x40) + 1, 2);
322 }
325 }
326 ImGui::EndChild();
327}
328
330 if (ImGui::BeginChild(gui::GetID("##SpritesList"),
331 ImVec2(ImGui::GetContentRegionAvail().x, 0), true,
332 ImGuiWindowFlags_NoDecoration)) {
333 int i = 0;
334 for (const auto each_sprite_name : zelda3::kSpriteDefaultNames) {
336 current_sprite_id_ == i, "Sprite Names", util::HexByte(i),
338 if (ImGui::IsItemClicked()) {
339 if (current_sprite_id_ != i) {
342 }
343 if (!active_sprites_.contains(i)) {
344 active_sprites_.push_back(i);
345 }
346 }
347 i++;
348 }
349 }
350 ImGui::EndChild();
351}
352
354 if (ImGui::Button("Add Frame")) {
355 // Add a new frame
356 }
357 if (ImGui::Button("Remove Frame")) {
358 // Remove the current frame
359 }
360}
361
362// ============================================================
363// Custom ZSM Sprite Editor
364// ============================================================
365
367 if (BeginTable("##CustomSpritesTable", 3,
368 ImGuiTableFlags_Resizable | ImGuiTableFlags_Borders,
369 ImVec2(0, 0))) {
370 TableSetupColumn("Sprite Data", ImGuiTableColumnFlags_WidthFixed, 300);
371 TableSetupColumn("Canvas", ImGuiTableColumnFlags_WidthStretch);
372 TableSetupColumn("Tilesheets", ImGuiTableColumnFlags_WidthFixed, 280);
373
374 TableHeadersRow();
375 TableNextRow();
376 TableNextColumn();
377
379
380 TableNextColumn();
382
383 TableNextColumn();
385
386 EndTable();
387 }
388}
389
391 // File operations toolbar
392 if (ImGui::Button(ICON_MD_ADD " New")) {
394 }
395 ImGui::SameLine();
396 if (ImGui::Button(ICON_MD_FOLDER_OPEN " Open")) {
397 std::string file_path = util::FileDialogWrapper::ShowOpenFileDialog();
398 if (!file_path.empty()) {
399 LoadZsmFile(file_path);
400 }
401 }
402 ImGui::SameLine();
403 if (ImGui::Button(ICON_MD_SAVE " Save")) {
405 const std::string& zsm_path = GetCurrentZsmPath();
406 if (zsm_path.empty()) {
408 } else {
409 SaveZsmFile(zsm_path);
410 }
411 }
412 }
413 ImGui::SameLine();
414 if (ImGui::Button(ICON_MD_SAVE_AS " Save As")) {
416 }
417
418 Separator();
419
420 // Sprite list
421 Text("Loaded Sprites:");
422 if (ImGui::BeginChild("SpriteList", ImVec2(0, 100), true)) {
423 for (size_t i = 0; i < custom_sprites_.size(); i++) {
424 std::string label = custom_sprites_[i].sprName.empty()
425 ? "Unnamed Sprite"
426 : custom_sprites_[i].sprName;
427 if (Selectable(label.c_str(), current_custom_sprite_index_ == (int)i)) {
428 current_custom_sprite_index_ = static_cast<int>(i);
429 current_frame_ = custom_sprites_[i].editor.Frames.empty() ? -1 : 0;
431 custom_sprites_[i].animations.empty() ? -1 : 0;
434 animation_playing_ = false;
435 frame_timer_ = 0.0f;
437 }
438 }
439 }
440 ImGui::EndChild();
441
442 Separator();
443
444 // Show properties for selected sprite
447 if (gui::BeginThemedTabBar("SpriteDataTabs")) {
448 if (ImGui::BeginTabItem("Properties")) {
450 ImGui::EndTabItem();
451 }
452 if (ImGui::BeginTabItem("Animations")) {
454 ImGui::EndTabItem();
455 }
456 if (ImGui::BeginTabItem("Routines")) {
458 ImGui::EndTabItem();
459 }
461 }
462 } else {
463 Text("No sprite selected");
464 }
465}
466
468 zsprite::ZSprite new_sprite;
469 new_sprite.Reset();
470 new_sprite.sprName = "New Sprite";
471
472 // Add default frame
473 new_sprite.editor.Frames.emplace_back();
474
475 // Add default animation
476 new_sprite.animations.emplace_back(0, 0, 1, "Idle");
477
478 custom_sprites_.push_back(std::move(new_sprite));
479 custom_sprite_paths_.push_back(std::string());
480 current_custom_sprite_index_ = static_cast<int>(custom_sprites_.size()) - 1;
481 current_frame_ = 0;
485 animation_playing_ = false;
486 frame_timer_ = 0.0f;
487 zsm_dirty_ = true;
489}
490
491void SpriteEditor::LoadZsmFile(const std::string& path) {
492 zsprite::ZSprite sprite;
493 status_ = sprite.Load(path);
494 if (status_.ok()) {
495 custom_sprites_.push_back(std::move(sprite));
496 custom_sprite_paths_.push_back(path);
497 current_custom_sprite_index_ = static_cast<int>(custom_sprites_.size()) - 1;
498 current_frame_ = custom_sprites_.back().editor.Frames.empty() ? -1 : 0;
500 custom_sprites_.back().animations.empty() ? -1 : 0;
503 animation_playing_ = false;
504 frame_timer_ = 0.0f;
505 zsm_dirty_ = false;
507 }
508}
509
510void SpriteEditor::SaveZsmFile(const std::string& path) {
514 if (status_.ok()) {
515 SetCurrentZsmPath(path);
516 zsm_dirty_ = false;
517 }
518 }
519}
520
523 std::string path =
525 if (!path.empty()) {
526 SaveZsmFile(path);
527 }
528 }
529}
530
531// ============================================================
532// Properties Panel
533// ============================================================
534
537
538 // Basic info
539 Text("Sprite Info");
540 Separator();
541
542 static char name_buf[256];
543 CopyStringToBuffer(sprite.sprName, name_buf);
544 if (ImGui::InputText("Name", name_buf, sizeof(name_buf))) {
545 sprite.sprName = name_buf;
546 sprite.property_sprname.Text = name_buf;
547 zsm_dirty_ = true;
548 }
549
550 static char id_buf[32];
551 CopyStringToBuffer(sprite.property_sprid.Text, id_buf);
552 if (ImGui::InputText("Sprite ID", id_buf, sizeof(id_buf))) {
553 sprite.property_sprid.Text = id_buf;
554 zsm_dirty_ = true;
555 }
556
557 Separator();
559
560 Separator();
562}
563
566
567 Text("Stats");
568
569 // Use InputInt for numeric values
570 int prize = ParseIntOrDefault(sprite.property_prize.Text);
571 if (ImGui::InputInt("Prize", &prize)) {
572 sprite.property_prize.Text = std::to_string(std::clamp(prize, 0, 255));
573 zsm_dirty_ = true;
574 }
575
576 int palette = ParseIntOrDefault(sprite.property_palette.Text);
577 if (ImGui::InputInt("Palette", &palette)) {
578 sprite.property_palette.Text = std::to_string(std::clamp(palette, 0, 7));
579 zsm_dirty_ = true;
580 }
581
582 int oamnbr = ParseIntOrDefault(sprite.property_oamnbr.Text);
583 if (ImGui::InputInt("OAM Count", &oamnbr)) {
584 sprite.property_oamnbr.Text = std::to_string(std::clamp(oamnbr, 0, 255));
585 zsm_dirty_ = true;
586 }
587
588 int hitbox = ParseIntOrDefault(sprite.property_hitbox.Text);
589 if (ImGui::InputInt("Hitbox", &hitbox)) {
590 sprite.property_hitbox.Text = std::to_string(std::clamp(hitbox, 0, 255));
591 zsm_dirty_ = true;
592 }
593
594 int health = ParseIntOrDefault(sprite.property_health.Text);
595 if (ImGui::InputInt("Health", &health)) {
596 sprite.property_health.Text = std::to_string(std::clamp(health, 0, 255));
597 zsm_dirty_ = true;
598 }
599
600 int damage = ParseIntOrDefault(sprite.property_damage.Text);
601 if (ImGui::InputInt("Damage", &damage)) {
602 sprite.property_damage.Text = std::to_string(std::clamp(damage, 0, 255));
603 zsm_dirty_ = true;
604 }
605}
606
609
610 Text("Behavior Flags");
611
612 // Two columns for boolean properties
613 if (ImGui::BeginTable("BoolProps", 2, ImGuiTableFlags_None)) {
614 // Column 1
615 ImGui::TableNextColumn();
616 if (ImGui::Checkbox("Blockable", &sprite.property_blockable.IsChecked))
617 zsm_dirty_ = true;
618 if (ImGui::Checkbox("Can Fall", &sprite.property_canfall.IsChecked))
619 zsm_dirty_ = true;
620 if (ImGui::Checkbox("Collision Layer",
621 &sprite.property_collisionlayer.IsChecked))
622 zsm_dirty_ = true;
623 if (ImGui::Checkbox("Custom Death", &sprite.property_customdeath.IsChecked))
624 zsm_dirty_ = true;
625 if (ImGui::Checkbox("Damage Sound", &sprite.property_damagesound.IsChecked))
626 zsm_dirty_ = true;
627 if (ImGui::Checkbox("Deflect Arrows",
628 &sprite.property_deflectarrows.IsChecked))
629 zsm_dirty_ = true;
630 if (ImGui::Checkbox("Deflect Projectiles",
631 &sprite.property_deflectprojectiles.IsChecked))
632 zsm_dirty_ = true;
633 if (ImGui::Checkbox("Fast", &sprite.property_fast.IsChecked))
634 zsm_dirty_ = true;
635 if (ImGui::Checkbox("Harmless", &sprite.property_harmless.IsChecked))
636 zsm_dirty_ = true;
637 if (ImGui::Checkbox("Impervious", &sprite.property_impervious.IsChecked))
638 zsm_dirty_ = true;
639
640 // Column 2
641 ImGui::TableNextColumn();
642 if (ImGui::Checkbox("Impervious Arrow",
643 &sprite.property_imperviousarrow.IsChecked))
644 zsm_dirty_ = true;
645 if (ImGui::Checkbox("Impervious Melee",
646 &sprite.property_imperviousmelee.IsChecked))
647 zsm_dirty_ = true;
648 if (ImGui::Checkbox("Interaction", &sprite.property_interaction.IsChecked))
649 zsm_dirty_ = true;
650 if (ImGui::Checkbox("Is Boss", &sprite.property_isboss.IsChecked))
651 zsm_dirty_ = true;
652 if (ImGui::Checkbox("Persist", &sprite.property_persist.IsChecked))
653 zsm_dirty_ = true;
654 if (ImGui::Checkbox("Shadow", &sprite.property_shadow.IsChecked))
655 zsm_dirty_ = true;
656 if (ImGui::Checkbox("Small Shadow", &sprite.property_smallshadow.IsChecked))
657 zsm_dirty_ = true;
658 if (ImGui::Checkbox("Stasis", &sprite.property_statis.IsChecked))
659 zsm_dirty_ = true;
660 if (ImGui::Checkbox("Statue", &sprite.property_statue.IsChecked))
661 zsm_dirty_ = true;
662 if (ImGui::Checkbox("Water Sprite", &sprite.property_watersprite.IsChecked))
663 zsm_dirty_ = true;
664
665 ImGui::EndTable();
666 }
667}
668
669// ============================================================
670// Animation Panel
671// ============================================================
672
675
676 // Playback controls
677 if (animation_playing_) {
678 if (ImGui::Button(ICON_MD_STOP " Stop")) {
679 animation_playing_ = false;
680 }
681 } else {
682 if (ImGui::Button(ICON_MD_PLAY_ARROW " Play")) {
683 animation_playing_ = true;
684 frame_timer_ = 0.0f;
685 }
686 }
687 ImGui::SameLine();
688 if (ImGui::Button(ICON_MD_SKIP_PREVIOUS)) {
689 if (current_frame_ > 0)
692 }
693 HOVER_HINT("Previous Frame");
694 ImGui::SameLine();
695 if (ImGui::Button(ICON_MD_SKIP_NEXT)) {
696 if (current_frame_ < (int)sprite.editor.Frames.size() - 1)
699 }
700 HOVER_HINT("Next Frame");
701 ImGui::SameLine();
702 Text("Frame: %d / %d", current_frame_, (int)sprite.editor.Frames.size() - 1);
703
704 Separator();
705
706 // Animation list
707 Text("Animations");
708 if (ImGui::Button(ICON_MD_ADD " Add Animation")) {
709 int frame_count = static_cast<int>(sprite.editor.Frames.size());
710 sprite.animations.emplace_back(0, frame_count > 0 ? frame_count - 1 : 0, 1,
711 "New Animation");
712 zsm_dirty_ = true;
713 }
714 HOVER_HINT("Add a new animation sequence");
715
716 if (ImGui::BeginChild("AnimList", ImVec2(0, 120), true)) {
717 for (size_t i = 0; i < sprite.animations.size(); i++) {
718 auto& anim = sprite.animations[i];
719 std::string label = anim.frame_name.empty() ? "Unnamed" : anim.frame_name;
720 if (Selectable(label.c_str(), current_animation_index_ == (int)i)) {
721 current_animation_index_ = static_cast<int>(i);
722 current_frame_ = anim.frame_start;
724 }
725 }
726 }
727 ImGui::EndChild();
728
729 // Edit selected animation
730 if (current_animation_index_ >= 0 &&
731 current_animation_index_ < (int)sprite.animations.size()) {
732 auto& anim = sprite.animations[current_animation_index_];
733
734 Separator();
735 Text("Animation Properties");
736
737 static char anim_name[128];
738 CopyStringToBuffer(anim.frame_name, anim_name);
739 if (ImGui::InputText("Name##Anim", anim_name, sizeof(anim_name))) {
740 anim.frame_name = anim_name;
741 zsm_dirty_ = true;
742 }
743
744 int start = anim.frame_start;
745 int end = anim.frame_end;
746 int speed = anim.frame_speed;
747
748 if (ImGui::SliderInt("Start Frame", &start, 0,
749 std::max(0, (int)sprite.editor.Frames.size() - 1))) {
750 anim.frame_start = static_cast<uint8_t>(start);
751 zsm_dirty_ = true;
752 }
753 if (ImGui::SliderInt("End Frame", &end, 0,
754 std::max(0, (int)sprite.editor.Frames.size() - 1))) {
755 anim.frame_end = static_cast<uint8_t>(end);
756 zsm_dirty_ = true;
757 }
758 if (ImGui::SliderInt("Speed", &speed, 1, 16)) {
759 anim.frame_speed = static_cast<uint8_t>(speed);
760 zsm_dirty_ = true;
761 }
762
763 if (ImGui::Button("Delete Animation") && sprite.animations.size() > 1) {
764 sprite.animations.erase(sprite.animations.begin() +
767 std::min(current_animation_index_, (int)sprite.animations.size() - 1);
768 zsm_dirty_ = true;
769 }
770 HOVER_HINT("Delete the selected animation");
771 }
772
773 Separator();
775}
776
779
780 Text("Frames");
781 if (ImGui::Button(ICON_MD_ADD " Add Frame")) {
782 sprite.editor.Frames.emplace_back();
783 zsm_dirty_ = true;
784 }
785 HOVER_HINT("Add a new animation frame");
786 ImGui::SameLine();
787 if (ImGui::Button(ICON_MD_DELETE " Delete Frame") &&
788 sprite.editor.Frames.size() > 1 && current_frame_ >= 0) {
789 sprite.editor.Frames.erase(sprite.editor.Frames.begin() + current_frame_);
791 std::min(current_frame_, (int)sprite.editor.Frames.size() - 1);
792 zsm_dirty_ = true;
794 }
795 HOVER_HINT("Delete the current frame");
796
797 // Frame selector
798 if (ImGui::BeginChild("FrameList", ImVec2(0, 80), true,
799 ImGuiWindowFlags_HorizontalScrollbar)) {
800 for (size_t i = 0; i < sprite.editor.Frames.size(); i++) {
801 ImGui::PushID(static_cast<int>(i));
802 std::string label = absl::StrFormat("F%d", i);
803 if (Selectable(label.c_str(), current_frame_ == (int)i,
804 ImGuiSelectableFlags_None, ImVec2(40, 40))) {
805 current_frame_ = static_cast<int>(i);
807 }
808 ImGui::SameLine();
809 ImGui::PopID();
810 }
811 }
812 ImGui::EndChild();
813
814 // Edit tiles in current frame
815 if (current_frame_ >= 0 &&
816 current_frame_ < (int)sprite.editor.Frames.size()) {
817 auto& frame = sprite.editor.Frames[current_frame_];
818
819 Separator();
820 Text("Tiles in Frame %d", current_frame_);
821
822 if (ImGui::Button(ICON_MD_ADD " Add Tile")) {
823 frame.Tiles.emplace_back();
824 zsm_dirty_ = true;
826 }
827 HOVER_HINT("Add a new tile to this frame");
828
829 if (ImGui::BeginChild("TileList", ImVec2(0, 100), true)) {
830 for (size_t i = 0; i < frame.Tiles.size(); i++) {
831 auto& tile = frame.Tiles[i];
832 std::string label = absl::StrFormat("Tile %d (ID: %d)", i, tile.id);
833 if (Selectable(label.c_str(), selected_tile_index_ == (int)i)) {
834 selected_tile_index_ = static_cast<int>(i);
835 }
836 }
837 }
838 ImGui::EndChild();
839
840 // Edit selected tile
841 if (selected_tile_index_ >= 0 &&
842 selected_tile_index_ < (int)frame.Tiles.size()) {
843 auto& tile = frame.Tiles[selected_tile_index_];
844
845 int tile_id = tile.id;
846 if (ImGui::InputInt("Tile ID", &tile_id)) {
847 tile.id = static_cast<uint16_t>(std::clamp(tile_id, 0, 511));
848 zsm_dirty_ = true;
850 }
851
852 int x = tile.x, y = tile.y;
853 if (ImGui::InputInt("X", &x)) {
854 tile.x = static_cast<uint8_t>(std::clamp(x, 0, 251));
855 zsm_dirty_ = true;
857 }
858 if (ImGui::InputInt("Y", &y)) {
859 tile.y = static_cast<uint8_t>(std::clamp(y, 0, 219));
860 zsm_dirty_ = true;
862 }
863
864 int pal = tile.palette;
865 if (ImGui::SliderInt("Palette##Tile", &pal, 0, 7)) {
866 tile.palette = static_cast<uint8_t>(pal);
867 zsm_dirty_ = true;
869 }
870
871 if (ImGui::Checkbox("16x16", &tile.size)) {
872 zsm_dirty_ = true;
874 }
875 ImGui::SameLine();
876 if (ImGui::Checkbox("Flip X", &tile.mirror_x)) {
877 zsm_dirty_ = true;
879 }
880 ImGui::SameLine();
881 if (ImGui::Checkbox("Flip Y", &tile.mirror_y)) {
882 zsm_dirty_ = true;
884 }
885
886 if (ImGui::Button("Delete Tile")) {
887 frame.Tiles.erase(frame.Tiles.begin() + selected_tile_index_);
889 zsm_dirty_ = true;
891 }
892 HOVER_HINT("Delete the selected tile");
893 }
894 }
895}
896
900 return;
901 }
902
904 if (current_animation_index_ < 0 ||
905 current_animation_index_ >= (int)sprite.animations.size()) {
906 return;
907 }
908
909 auto& anim = sprite.animations[current_animation_index_];
910
911 frame_timer_ += delta_time;
912 float frame_duration = anim.frame_speed / 60.0f;
913
914 if (frame_timer_ >= frame_duration) {
915 frame_timer_ = 0;
917 if (current_frame_ > anim.frame_end) {
918 current_frame_ = anim.frame_start;
919 }
921 }
922}
923
924// ============================================================
925// User Routines Panel
926// ============================================================
927
930
931 if (ImGui::Button(ICON_MD_ADD " Add Routine")) {
932 sprite.userRoutines.emplace_back("New Routine", "; ASM code here\n");
933 zsm_dirty_ = true;
934 }
935 HOVER_HINT("Add a new ASM routine");
936
937 // Routine list
938 if (ImGui::BeginChild("RoutineList", ImVec2(0, 100), true)) {
939 for (size_t i = 0; i < sprite.userRoutines.size(); i++) {
940 auto& routine = sprite.userRoutines[i];
941 if (Selectable(routine.name.c_str(), selected_routine_index_ == (int)i)) {
942 selected_routine_index_ = static_cast<int>(i);
943 }
944 }
945 }
946 ImGui::EndChild();
947
948 // Edit selected routine
949 if (selected_routine_index_ >= 0 &&
950 selected_routine_index_ < (int)sprite.userRoutines.size()) {
951 auto& routine = sprite.userRoutines[selected_routine_index_];
952
953 Separator();
954
955 static char routine_name[128];
956 CopyStringToBuffer(routine.name, routine_name);
957 if (ImGui::InputText("Routine Name", routine_name, sizeof(routine_name))) {
958 routine.name = routine_name;
959 zsm_dirty_ = true;
960 }
961
962 Text("ASM Code:");
963
964 // Multiline text input for code
965 static char code_buffer[16384];
966 CopyStringToBuffer(routine.code, code_buffer);
967 if (ImGui::InputTextMultiline("##RoutineCode", code_buffer,
968 sizeof(code_buffer), ImVec2(-1, 200))) {
969 routine.code = code_buffer;
970 zsm_dirty_ = true;
971 }
972
973 if (ImGui::Button("Delete Routine")) {
974 sprite.userRoutines.erase(sprite.userRoutines.begin() +
977 zsm_dirty_ = true;
978 }
979 HOVER_HINT("Delete the selected routine");
980 }
981}
982
983// ============================================================
984// Graphics Pipeline
985// ============================================================
986
988 // Combine selected sheets (current_sheets_[0-7]) into single 8BPP buffer
989 // Layout: 16 tiles per row, 8 rows per sheet, 8 sheets total = 64 tile rows
990 // Buffer size: 0x10000 bytes (65536)
991
992 sprite_gfx_buffer_.resize(0x10000, 0);
993
994 // Each sheet is 128x32 pixels (128 bytes per row, 32 rows) = 4096 bytes
995 // We combine 8 sheets vertically: 128x256 pixels total
996 constexpr int kSheetWidth = 128;
997 constexpr int kSheetHeight = 32;
998 constexpr int kRowStride = 128;
999
1000 for (int sheet_idx = 0; sheet_idx < 8; sheet_idx++) {
1001 uint8_t sheet_id = current_sheets_[sheet_idx];
1002 if (sheet_id >= gfx::Arena::Get().gfx_sheets().size()) {
1003 continue;
1004 }
1005
1006 auto& sheet = gfx::Arena::Get().gfx_sheets().at(sheet_id);
1007 if (!sheet.is_active() || sheet.size() == 0) {
1008 continue;
1009 }
1010
1011 // Copy sheet data to buffer at appropriate offset
1012 // Each sheet occupies 8 tile rows (8 * 8 scanlines = 64 scanlines)
1013 // Offset = sheet_idx * (8 tile rows * 1024 bytes per tile row)
1014 // But sheets are 32 pixels tall (4 tile rows), so:
1015 // Offset = sheet_idx * 4 * 1024 = sheet_idx * 4096
1016 int dest_offset = sheet_idx * (kSheetHeight * kRowStride);
1017
1018 const uint8_t* src_data = sheet.data();
1019 size_t copy_size =
1020 std::min(sheet.size(), static_cast<size_t>(kSheetWidth * kSheetHeight));
1021
1022 if (dest_offset + copy_size <= sprite_gfx_buffer_.size()) {
1023 std::memcpy(sprite_gfx_buffer_.data() + dest_offset, src_data, copy_size);
1024 }
1025 }
1026
1027 // Update drawer with new buffer
1029 gfx_buffer_loaded_ = true;
1030}
1031
1033 // Load sprite palettes from ROM palette groups
1034 // ALTTP sprites use a combination of palette groups:
1035 // - Rows 0-1: Global sprite palettes (shared by all sprites)
1036 // - Rows 2-7: Aux palettes (vary by sprite type)
1037 //
1038 // For simplicity, we load global_sprites which contains the main
1039 // sprite palettes. More accurate rendering would require looking up
1040 // which aux palette group each sprite type uses.
1041
1042 if (!rom_ || !rom_->is_loaded()) {
1043 return;
1044 }
1045
1046 // Build combined sprite palette from global + aux groups
1048
1049 // Add global sprite palettes (typically 2 palettes, 16 colors each)
1050 if (!game_data())
1051 return;
1052 const auto& global = game_data()->palette_groups.global_sprites;
1053 for (size_t i = 0; i < global.size() && i < 8; i++) {
1054 sprite_palettes_.AddPalette(global.palette(i));
1055 }
1056
1057 // If we don't have 8 palettes yet, fill with aux palettes
1058 const auto& aux1 = game_data()->palette_groups.sprites_aux1;
1059 const auto& aux2 = game_data()->palette_groups.sprites_aux2;
1060 const auto& aux3 = game_data()->palette_groups.sprites_aux3;
1061
1062 // Pad to 8 palettes total for proper OAM palette mapping
1063 while (sprite_palettes_.size() < 8) {
1064 if (sprite_palettes_.size() < 4 && aux1.size() > 0) {
1066 aux1.palette(sprite_palettes_.size() % aux1.size()));
1067 } else if (sprite_palettes_.size() < 6 && aux2.size() > 0) {
1069 aux2.palette((sprite_palettes_.size() - 4) % aux2.size()));
1070 } else if (aux3.size() > 0) {
1072 aux3.palette((sprite_palettes_.size() - 6) % aux3.size()));
1073 } else {
1074 // Fallback: add empty palette
1076 }
1077 }
1078
1080}
1081
1082void SpriteEditor::LoadSheetsForSprite(const std::array<uint8_t, 4>& sheets) {
1083 // Load the required sheets for a vanilla sprite
1084 bool changed = false;
1085 for (int i = 0; i < 4; i++) {
1086 if (sheets[i] != 0 && current_sheets_[i] != sheets[i]) {
1087 current_sheets_[i] = sheets[i];
1088 changed = true;
1089 }
1090 }
1091
1092 if (changed) {
1093 gfx_buffer_loaded_ = false;
1095 }
1096}
1097
1099 // Ensure graphics buffer is loaded
1103 }
1104
1105 // Initialize vanilla preview bitmap if needed
1109 }
1110
1112 return;
1113 }
1114
1115 // Clear and render
1117
1118 // Origin is center of bitmap
1119 int origin_x = 64;
1120 int origin_y = 64;
1121
1122 // Convert SpriteOamLayout tiles to zsprite::OamTile and draw
1123 for (const auto& entry : layout.tiles) {
1124 zsprite::OamTile tile;
1125 tile.x = static_cast<uint8_t>(entry.x_offset + 128); // Convert to unsigned
1126 tile.y = static_cast<uint8_t>(entry.y_offset + 128);
1127 tile.id = entry.tile_id;
1128 tile.palette = entry.palette;
1129 tile.size = entry.size_16x16;
1130 tile.mirror_x = entry.flip_x;
1131 tile.mirror_y = entry.flip_y;
1132 tile.priority = 0;
1133
1135 origin_y);
1136 }
1137
1138 // Build combined 128-color palette (8 sub-palettes × 16 colors)
1139 // and apply to bitmap for proper color rendering
1140 if (sprite_palettes_.size() > 0) {
1141 gfx::SnesPalette combined_palette;
1142 for (size_t pal_idx = 0; pal_idx < 8 && pal_idx < sprite_palettes_.size();
1143 pal_idx++) {
1144 const auto& sub_pal = sprite_palettes_.palette(pal_idx);
1145 for (size_t col_idx = 0; col_idx < 16 && col_idx < sub_pal.size();
1146 col_idx++) {
1147 combined_palette.AddColor(sub_pal[col_idx]);
1148 }
1149 // Pad to 16 if sub-palette is smaller
1150 while (combined_palette.size() < (pal_idx + 1) * 16) {
1151 combined_palette.AddColor(gfx::SnesColor(0));
1152 }
1153 }
1154 vanilla_preview_bitmap_.SetPalette(combined_palette);
1155 }
1156
1158}
1159
1160// ============================================================
1161// Canvas Rendering
1162// ============================================================
1163
1167 return;
1168 }
1169
1171 if (frame_index < 0 || frame_index >= (int)sprite.editor.Frames.size()) {
1172 return;
1173 }
1174
1175 auto& frame = sprite.editor.Frames[frame_index];
1176
1177 // Ensure graphics buffer is loaded
1181 }
1182
1183 // Initialize preview bitmap if needed
1187 }
1188
1189 // Only render if drawer is ready
1191 // Clear and render to preview bitmap
1193
1194 // Origin is center of canvas (128, 128 for 256x256 bitmap)
1196
1197 // Build combined 128-color palette and apply to bitmap
1198 if (sprite_palettes_.size() > 0) {
1199 gfx::SnesPalette combined_palette;
1200 for (size_t pal_idx = 0; pal_idx < 8 && pal_idx < sprite_palettes_.size();
1201 pal_idx++) {
1202 const auto& sub_pal = sprite_palettes_.palette(pal_idx);
1203 for (size_t col_idx = 0; col_idx < 16 && col_idx < sub_pal.size();
1204 col_idx++) {
1205 combined_palette.AddColor(sub_pal[col_idx]);
1206 }
1207 // Pad to 16 if sub-palette is smaller
1208 while (combined_palette.size() < (pal_idx + 1) * 16) {
1209 combined_palette.AddColor(gfx::SnesColor(0));
1210 }
1211 }
1212 sprite_preview_bitmap_.SetPalette(combined_palette);
1213 }
1214
1215 // Mark as updated
1216 preview_needs_update_ = false;
1217 }
1218
1219 // Draw the preview bitmap on canvas
1222 }
1223
1224 // Draw tile outlines for selection (over the bitmap)
1225 if (show_tile_grid_) {
1226 for (size_t i = 0; i < frame.Tiles.size(); i++) {
1227 const auto& tile = frame.Tiles[i];
1228 int tile_size = tile.size ? 16 : 8;
1229
1230 // Convert signed tile position to canvas position
1231 int8_t signed_x = static_cast<int8_t>(tile.x);
1232 int8_t signed_y = static_cast<int8_t>(tile.y);
1233
1234 int canvas_x = 128 + signed_x;
1235 int canvas_y = 128 + signed_y;
1236
1237 // Highlight selected tile
1238 ImVec4 color = (selected_tile_index_ == static_cast<int>(i))
1239 ? ImVec4(0.0f, 1.0f, 0.0f, 0.8f) // Green for selected
1240 : ImVec4(1.0f, 1.0f, 0.0f, 0.3f); // Yellow for others
1241
1242 sprite_canvas_.DrawRect(canvas_x, canvas_y, tile_size, tile_size, color);
1243 }
1244 }
1245}
1246
1248 if (ImGui::BeginChild(gui::GetID("##ZSpriteCanvas"),
1249 ImGui::GetContentRegionAvail(), true)) {
1252
1253 // Render current frame if we have a sprite selected
1257 }
1258
1261
1262 // Display current frame info
1265 ImGui::SetCursorPos(ImVec2(10, 10));
1266 Text("Frame: %d | Tiles: %d", current_frame_,
1267 current_frame_ < (int)sprite.editor.Frames.size()
1268 ? (int)sprite.editor.Frames[current_frame_].Tiles.size()
1269 : 0);
1270 }
1271 }
1272 ImGui::EndChild();
1273}
1274
1276 if (custom_sprite_paths_.size() < custom_sprites_.size()) {
1278 }
1279}
1280
1281const std::string& SpriteEditor::GetCurrentZsmPath() const {
1282 static const std::string kEmptyPath;
1285 static_cast<int>(custom_sprite_paths_.size())) {
1286 return kEmptyPath;
1287 }
1289}
1290
1291void SpriteEditor::SetCurrentZsmPath(const std::string& path) {
1295 static_cast<int>(custom_sprite_paths_.size())) {
1296 return;
1297 }
1299}
1300
1301} // namespace editor
1302} // namespace yaze
project::ResourceLabelManager * resource_label()
Definition rom.h:150
bool is_loaded() const
Definition rom.h:132
zelda3::GameData * game_data() const
Definition editor.h:297
EditorDependencies dependencies_
Definition editor.h:306
void RegisterEditorPanel(std::unique_ptr< EditorPanel > panel)
Register an EditorPanel instance for central drawing.
void SetPalettes(const gfx::PaletteGroup *palettes)
Set the palette group for color mapping.
void ClearBitmap(gfx::Bitmap &bitmap)
Clear the bitmap with transparent color.
void DrawFrame(gfx::Bitmap &bitmap, const zsprite::Frame &frame, int origin_x, int origin_y)
Draw all tiles in a ZSM frame.
bool IsReady() const
Check if drawer is ready to render.
void DrawOamTile(gfx::Bitmap &bitmap, const zsprite::OamTile &tile, int origin_x, int origin_y)
Draw a single ZSM OAM tile to bitmap.
void SetGraphicsBuffer(const uint8_t *buffer)
Set the graphics buffer for tile lookup.
ImVector< int > active_sprites_
void UpdateAnimationPlayback(float delta_time)
void RenderZSpriteFrame(int frame_index)
void LoadSheetsForSprite(const std::array< uint8_t, 4 > &sheets)
absl::Status Update() override
gfx::PaletteGroup sprite_palettes_
void LoadZsmFile(const std::string &path)
void RenderVanillaSprite(const zelda3::SpriteOamLayout &layout)
const std::string & GetCurrentZsmPath() const
std::vector< zsprite::ZSprite > custom_sprites_
std::vector< uint8_t > sprite_gfx_buffer_
void SetCurrentZsmPath(const std::string &path)
absl::Status Save() override
void SaveZsmFile(const std::string &path)
absl::Status Load() override
std::vector< std::string > custom_sprite_paths_
std::array< gfx::Bitmap, 223 > & gfx_sheets()
Get reference to all graphics sheets.
Definition arena.h:152
static Arena & Get()
Definition arena.cc:21
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:201
void Reformat(int format)
Reformat the bitmap to use a different pixel format.
Definition bitmap.cc:277
bool is_active() const
Definition bitmap.h:384
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:384
RAII timer for automatic timing management.
SNES Color container.
Definition snes_color.h:110
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
void AddColor(const SnesColor &color)
void DrawBitmap(Bitmap &bitmap, int border_offset, float scale)
Definition canvas.cc:1157
void DrawContextMenu()
Definition canvas.cc:684
bool DrawTileSelector(int size, int size_y=0)
Definition canvas.cc:1093
void DrawRect(int x, int y, int w, int h, ImVec4 color)
Definition canvas.cc:1423
void DrawBackground(ImVec2 canvas_size=ImVec2(0, 0))
Definition canvas.cc:590
void DrawGrid(float grid_step=64.0f, int tile_id_offset=8)
Definition canvas.cc:1480
static std::string ShowSaveFileDialog(const std::string &default_name="", const std::string &default_extension="")
ShowSaveFileDialog opens a save file dialog and returns the selected filepath. Uses global feature fl...
static std::string ShowOpenFileDialog()
ShowOpenFileDialog opens a file dialog and returns the selected filepath. Uses global feature flag to...
static const SpriteOamLayout * GetLayout(uint8_t sprite_id)
Get the OAM layout for a sprite ID.
#define ICON_MD_FOLDER_OPEN
Definition icons.h:813
#define ICON_MD_PLAY_ARROW
Definition icons.h:1479
#define ICON_MD_SAVE_AS
Definition icons.h:1646
#define ICON_MD_STOP
Definition icons.h:1862
#define ICON_MD_ADD
Definition icons.h:86
#define ICON_MD_SKIP_NEXT
Definition icons.h:1773
#define ICON_MD_SAVE
Definition icons.h:1644
#define ICON_MD_DELETE
Definition icons.h:530
#define ICON_MD_SKIP_PREVIOUS
Definition icons.h:1774
#define HOVER_HINT(string)
Definition macro.h:24
int ParseIntOrDefault(const std::string &text, int fallback=0)
constexpr ImGuiTabItemFlags kSpriteTabFlags
constexpr ImGuiTabBarFlags kSpriteTabBarFlags
bool InputHexWord(const char *label, uint16_t *data, float input_width, bool no_step)
Definition input.cc:344
bool BeginThemedTabBar(const char *id, ImGuiTabBarFlags flags)
A stylized tab bar with "Mission Control" branding.
void EndThemedTabBar()
ImGuiID GetID(const std::string &id)
Definition input.cc:573
bool InputHexByte(const char *label, uint8_t *data, float input_width, bool no_step)
Definition input.cc:370
std::string HexByte(uint8_t byte, HexStringParams params)
Definition hex.cc:30
const std::string kSpriteDefaultNames[256]
Definition sprite.cc:13
std::vector< Frame > Frames
Definition zsprite.h:120
void Reset()
Reset all sprite data to defaults.
Definition zsprite.h:352
absl::Status Load(const std::string &filename)
Load a ZSM file from disk.
Definition zsprite.h:134
std::vector< AnimationGroup > animations
Definition zsprite.h:392
auto palette(int i) const
void AddPalette(SnesPalette pal)
void SelectableLabelWithNameEdit(bool selected, const std::string &type, const std::string &key, const std::string &defaultValue)
Definition project.cc:1941
gfx::PaletteGroupMap palette_groups
Definition game_data.h:89
Complete OAM layout for a vanilla sprite.
std::vector< SpriteOamEntry > tiles