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 <cstring>
5
6#include "absl/strings/str_format.h"
12#include "app/gui/core/icons.h"
13#include "app/gui/core/input.h"
15#include "util/file_util.h"
16#include "util/hex.h"
18
19namespace yaze {
20namespace editor {
21
22using ImGui::BeginTable;
23using ImGui::Button;
24using ImGui::EndTable;
25using ImGui::Selectable;
26using ImGui::Separator;
27using ImGui::TableHeadersRow;
28using ImGui::TableNextColumn;
29using ImGui::TableNextRow;
30using ImGui::TableSetupColumn;
31using ImGui::Text;
32
35 return;
36 auto* panel_manager = dependencies_.panel_manager;
37
38 // Register EditorPanel implementations with callbacks
39 // EditorPanels provide both metadata (icon, name, priority) and drawing logic
40 panel_manager->RegisterEditorPanel(
41 std::make_unique<VanillaSpriteEditorPanel>([this]() {
42 if (rom_ && rom_->is_loaded()) {
43 DrawVanillaSpriteEditor();
44 } else {
45 ImGui::TextDisabled("Load a ROM to view vanilla sprites");
46 }
47 }));
48
49 panel_manager->RegisterEditorPanel(std::make_unique<CustomSpriteEditorPanel>(
50 [this]() { DrawCustomSprites(); }));
51}
52
53absl::Status SpriteEditor::Load() {
54 gfx::ScopedTimer timer("SpriteEditor::Load");
55 return absl::OkStatus();
56}
57
58absl::Status SpriteEditor::Update() {
59 if (rom()->is_loaded() && !sheets_loaded_) {
60 sheets_loaded_ = true;
61 }
62
63 // Update animation playback for custom sprites
64 float current_time = ImGui::GetTime();
65 float delta_time = current_time - last_frame_time_;
66 last_frame_time_ = current_time;
67 UpdateAnimationPlayback(delta_time);
68
69 // Handle editor-level shortcuts
71
72 // Panel drawing is handled by PanelManager via registered EditorPanels
73 // Each panel's Draw() callback invokes the appropriate draw method
74
75 return status_.ok() ? absl::OkStatus() : status_;
76}
77
79 // Animation playback shortcuts (when custom sprite panel is active)
80 if (ImGui::IsKeyPressed(ImGuiKey_Space, false) &&
81 !ImGui::GetIO().WantTextInput) {
83 }
84
85 // Frame navigation
86 if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket, false)) {
87 if (current_frame_ > 0) {
90 }
91 }
92 if (ImGui::IsKeyPressed(ImGuiKey_RightBracket, false)) {
95 }
96
97 // Sprite navigation
98 if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_UpArrow, false)) {
99 if (current_sprite_id_ > 0) {
102 }
103 }
104 if (ImGui::GetIO().KeyCtrl &&
105 ImGui::IsKeyPressed(ImGuiKey_DownArrow, false)) {
108 }
109}
110
111absl::Status SpriteEditor::Save() {
113 current_custom_sprite_index_ < static_cast<int>(custom_sprites_.size())) {
114 if (current_zsm_path_.empty()) {
116 } else {
118 }
119 }
120 return absl::OkStatus();
121}
122
124 // Sidebar handled by EditorManager for card-based editors
125}
126
127// ============================================================
128// Vanilla Sprite Editor
129// ============================================================
130
132 if (ImGui::BeginTable("##SpriteCanvasTable", 3, ImGuiTableFlags_Resizable,
133 ImVec2(0, 0))) {
134 TableSetupColumn("Sprites List", ImGuiTableColumnFlags_WidthFixed, 256);
135 TableSetupColumn("Canvas", ImGuiTableColumnFlags_WidthStretch,
136 ImGui::GetContentRegionAvail().x);
137 TableSetupColumn("Tile Selector", ImGuiTableColumnFlags_WidthFixed, 256);
138 TableHeadersRow();
139 TableNextRow();
140
141 TableNextColumn();
143
144 TableNextColumn();
145 static int next_tab_id = 0;
146
147 if (ImGui::BeginTabBar("SpriteTabBar", kSpriteTabBarFlags)) {
148 if (ImGui::TabItemButton(ICON_MD_ADD, kSpriteTabBarFlags)) {
149 if (std::find(active_sprites_.begin(), active_sprites_.end(),
151 next_tab_id++;
152 }
153 active_sprites_.push_back(next_tab_id++);
154 }
155
156 for (int n = 0; n < active_sprites_.Size;) {
157 bool open = true;
158
159 if (active_sprites_[n] > sizeof(zelda3::kSpriteDefaultNames) / 4) {
160 active_sprites_.erase(active_sprites_.Data + n);
161 continue;
162 }
163
164 if (ImGui::BeginTabItem(
166 ImGuiTabItemFlags_None)) {
168 ImGui::EndTabItem();
169 }
170
171 if (!open)
172 active_sprites_.erase(active_sprites_.Data + n);
173 else
174 n++;
175 }
176
177 ImGui::EndTabBar();
178 }
179
180 TableNextColumn();
181 if (sheets_loaded_) {
183 }
184 ImGui::EndTable();
185 }
186}
187
189 static bool flip_x = false;
190 static bool flip_y = false;
191 if (ImGui::BeginChild(gui::GetID("##SpriteCanvas"),
192 ImGui::GetContentRegionAvail(), true)) {
195
196 // Render vanilla sprite if layout exists
197 if (current_sprite_id_ >= 0) {
198 const auto* layout = zelda3::SpriteOamRegistry::GetLayout(
199 static_cast<uint8_t>(current_sprite_id_));
200 if (layout) {
201 // Load required sheets for this sprite
202 LoadSheetsForSprite(layout->required_sheets);
203 RenderVanillaSprite(*layout);
204
205 // Draw the preview bitmap centered on canvas
208 }
209
210 // Show sprite info
211 ImGui::SetCursorPos(ImVec2(10, 10));
212 Text("Sprite: %s (0x%02X)", layout->name, layout->sprite_id);
213 Text("Tiles: %zu", layout->tiles.size());
214 }
215 }
216
219
220 if (ImGui::BeginTable("##OAMTable", 7, ImGuiTableFlags_Resizable,
221 ImVec2(0, 0))) {
222 TableSetupColumn("X", ImGuiTableColumnFlags_WidthStretch);
223 TableSetupColumn("Y", ImGuiTableColumnFlags_WidthStretch);
224 TableSetupColumn("Tile", ImGuiTableColumnFlags_WidthStretch);
225 TableSetupColumn("Palette", ImGuiTableColumnFlags_WidthStretch);
226 TableSetupColumn("Priority", ImGuiTableColumnFlags_WidthStretch);
227 TableSetupColumn("Flip X", ImGuiTableColumnFlags_WidthStretch);
228 TableSetupColumn("Flip Y", ImGuiTableColumnFlags_WidthStretch);
229 TableHeadersRow();
230 TableNextRow();
231
232 TableNextColumn();
234
235 TableNextColumn();
237
238 TableNextColumn();
240
241 TableNextColumn();
243
244 TableNextColumn();
246
247 TableNextColumn();
248 if (ImGui::Checkbox("##XFlip", &flip_x)) {
249 oam_config_.flip_x = flip_x;
250 }
251
252 TableNextColumn();
253 if (ImGui::Checkbox("##YFlip", &flip_y)) {
254 oam_config_.flip_y = flip_y;
255 }
256
257 ImGui::EndTable();
258 }
259
261 }
262 ImGui::EndChild();
263}
264
266 if (ImGui::BeginChild(gui::GetID("sheet_label"),
267 ImVec2(ImGui::GetContentRegionAvail().x, 0), true,
268 ImGuiWindowFlags_NoDecoration)) {
269 // Track previous sheet values for change detection
270 static uint8_t prev_sheets[8] = {0};
271 bool sheets_changed = false;
272
273 for (int i = 0; i < 8; i++) {
274 std::string sheet_label = absl::StrFormat("Sheet %d", i);
275 if (gui::InputHexByte(sheet_label.c_str(), &current_sheets_[i])) {
276 sheets_changed = true;
277 }
278 if (i % 2 == 0)
279 ImGui::SameLine();
280 }
281
282 // Reload graphics buffer if sheets changed
283 if (sheets_changed || std::memcmp(prev_sheets, current_sheets_, 8) != 0) {
284 std::memcpy(prev_sheets, current_sheets_, 8);
285 gfx_buffer_loaded_ = false;
287 }
288
292 for (int i = 0; i < 8; i++) {
294 gfx::Arena::Get().gfx_sheets().at(current_sheets_[i]), 1,
295 (i * 0x40) + 1, 2);
296 }
299 }
300 ImGui::EndChild();
301}
302
304 if (ImGui::BeginChild(gui::GetID("##SpritesList"),
305 ImVec2(ImGui::GetContentRegionAvail().x, 0), true,
306 ImGuiWindowFlags_NoDecoration)) {
307 int i = 0;
308 for (const auto each_sprite_name : zelda3::kSpriteDefaultNames) {
310 current_sprite_id_ == i, "Sprite Names", util::HexByte(i),
312 if (ImGui::IsItemClicked()) {
313 if (current_sprite_id_ != i) {
316 }
317 if (!active_sprites_.contains(i)) {
318 active_sprites_.push_back(i);
319 }
320 }
321 i++;
322 }
323 }
324 ImGui::EndChild();
325}
326
328 if (ImGui::Button("Add Frame")) {
329 // Add a new frame
330 }
331 if (ImGui::Button("Remove Frame")) {
332 // Remove the current frame
333 }
334}
335
336// ============================================================
337// Custom ZSM Sprite Editor
338// ============================================================
339
341 if (BeginTable("##CustomSpritesTable", 3,
342 ImGuiTableFlags_Resizable | ImGuiTableFlags_Borders,
343 ImVec2(0, 0))) {
344 TableSetupColumn("Sprite Data", ImGuiTableColumnFlags_WidthFixed, 300);
345 TableSetupColumn("Canvas", ImGuiTableColumnFlags_WidthStretch);
346 TableSetupColumn("Tilesheets", ImGuiTableColumnFlags_WidthFixed, 280);
347
348 TableHeadersRow();
349 TableNextRow();
350 TableNextColumn();
351
353
354 TableNextColumn();
356
357 TableNextColumn();
359
360 EndTable();
361 }
362}
363
365 // File operations toolbar
366 if (ImGui::Button(ICON_MD_ADD " New")) {
368 }
369 ImGui::SameLine();
370 if (ImGui::Button(ICON_MD_FOLDER_OPEN " Open")) {
371 std::string file_path = util::FileDialogWrapper::ShowOpenFileDialog();
372 if (!file_path.empty()) {
373 LoadZsmFile(file_path);
374 }
375 }
376 ImGui::SameLine();
377 if (ImGui::Button(ICON_MD_SAVE " Save")) {
379 if (current_zsm_path_.empty()) {
381 } else {
383 }
384 }
385 }
386 ImGui::SameLine();
387 if (ImGui::Button(ICON_MD_SAVE_AS " Save As")) {
389 }
390
391 Separator();
392
393 // Sprite list
394 Text("Loaded Sprites:");
395 if (ImGui::BeginChild("SpriteList", ImVec2(0, 100), true)) {
396 for (size_t i = 0; i < custom_sprites_.size(); i++) {
397 std::string label = custom_sprites_[i].sprName.empty()
398 ? "Unnamed Sprite"
399 : custom_sprites_[i].sprName;
400 if (Selectable(label.c_str(), current_custom_sprite_index_ == (int)i)) {
401 current_custom_sprite_index_ = static_cast<int>(i);
403 }
404 }
405 }
406 ImGui::EndChild();
407
408 Separator();
409
410 // Show properties for selected sprite
413 if (ImGui::BeginTabBar("SpriteDataTabs")) {
414 if (ImGui::BeginTabItem("Properties")) {
416 ImGui::EndTabItem();
417 }
418 if (ImGui::BeginTabItem("Animations")) {
420 ImGui::EndTabItem();
421 }
422 if (ImGui::BeginTabItem("Routines")) {
424 ImGui::EndTabItem();
425 }
426 ImGui::EndTabBar();
427 }
428 } else {
429 Text("No sprite selected");
430 }
431}
432
434 zsprite::ZSprite new_sprite;
435 new_sprite.Reset();
436 new_sprite.sprName = "New Sprite";
437
438 // Add default frame
439 new_sprite.editor.Frames.emplace_back();
440
441 // Add default animation
442 new_sprite.animations.emplace_back(0, 0, 1, "Idle");
443
444 custom_sprites_.push_back(std::move(new_sprite));
445 current_custom_sprite_index_ = static_cast<int>(custom_sprites_.size()) - 1;
446 current_zsm_path_.clear();
447 zsm_dirty_ = true;
449}
450
451void SpriteEditor::LoadZsmFile(const std::string& path) {
452 zsprite::ZSprite sprite;
453 status_ = sprite.Load(path);
454 if (status_.ok()) {
455 custom_sprites_.push_back(std::move(sprite));
456 current_custom_sprite_index_ = static_cast<int>(custom_sprites_.size()) - 1;
457 current_zsm_path_ = path;
458 zsm_dirty_ = false;
460 }
461}
462
463void SpriteEditor::SaveZsmFile(const std::string& path) {
467 if (status_.ok()) {
468 current_zsm_path_ = path;
469 zsm_dirty_ = false;
470 }
471 }
472}
473
476 std::string path =
478 if (!path.empty()) {
479 SaveZsmFile(path);
480 }
481 }
482}
483
484// ============================================================
485// Properties Panel
486// ============================================================
487
490
491 // Basic info
492 Text("Sprite Info");
493 Separator();
494
495 static char name_buf[256];
496 strncpy(name_buf, sprite.sprName.c_str(), sizeof(name_buf) - 1);
497 if (ImGui::InputText("Name", name_buf, sizeof(name_buf))) {
498 sprite.sprName = name_buf;
499 sprite.property_sprname.Text = name_buf;
500 zsm_dirty_ = true;
501 }
502
503 static char id_buf[32];
504 strncpy(id_buf, sprite.property_sprid.Text.c_str(), sizeof(id_buf) - 1);
505 if (ImGui::InputText("Sprite ID", id_buf, sizeof(id_buf))) {
506 sprite.property_sprid.Text = id_buf;
507 zsm_dirty_ = true;
508 }
509
510 Separator();
512
513 Separator();
515}
516
519
520 Text("Stats");
521
522 // Use InputInt for numeric values
523 int prize = sprite.property_prize.Text.empty()
524 ? 0
525 : std::stoi(sprite.property_prize.Text);
526 if (ImGui::InputInt("Prize", &prize)) {
527 sprite.property_prize.Text = std::to_string(std::clamp(prize, 0, 255));
528 zsm_dirty_ = true;
529 }
530
531 int palette = sprite.property_palette.Text.empty()
532 ? 0
533 : std::stoi(sprite.property_palette.Text);
534 if (ImGui::InputInt("Palette", &palette)) {
535 sprite.property_palette.Text = std::to_string(std::clamp(palette, 0, 7));
536 zsm_dirty_ = true;
537 }
538
539 int oamnbr = sprite.property_oamnbr.Text.empty()
540 ? 0
541 : std::stoi(sprite.property_oamnbr.Text);
542 if (ImGui::InputInt("OAM Count", &oamnbr)) {
543 sprite.property_oamnbr.Text = std::to_string(std::clamp(oamnbr, 0, 255));
544 zsm_dirty_ = true;
545 }
546
547 int hitbox = sprite.property_hitbox.Text.empty()
548 ? 0
549 : std::stoi(sprite.property_hitbox.Text);
550 if (ImGui::InputInt("Hitbox", &hitbox)) {
551 sprite.property_hitbox.Text = std::to_string(std::clamp(hitbox, 0, 255));
552 zsm_dirty_ = true;
553 }
554
555 int health = sprite.property_health.Text.empty()
556 ? 0
557 : std::stoi(sprite.property_health.Text);
558 if (ImGui::InputInt("Health", &health)) {
559 sprite.property_health.Text = std::to_string(std::clamp(health, 0, 255));
560 zsm_dirty_ = true;
561 }
562
563 int damage = sprite.property_damage.Text.empty()
564 ? 0
565 : std::stoi(sprite.property_damage.Text);
566 if (ImGui::InputInt("Damage", &damage)) {
567 sprite.property_damage.Text = std::to_string(std::clamp(damage, 0, 255));
568 zsm_dirty_ = true;
569 }
570}
571
574
575 Text("Behavior Flags");
576
577 // Two columns for boolean properties
578 if (ImGui::BeginTable("BoolProps", 2, ImGuiTableFlags_None)) {
579 // Column 1
580 ImGui::TableNextColumn();
581 if (ImGui::Checkbox("Blockable", &sprite.property_blockable.IsChecked))
582 zsm_dirty_ = true;
583 if (ImGui::Checkbox("Can Fall", &sprite.property_canfall.IsChecked))
584 zsm_dirty_ = true;
585 if (ImGui::Checkbox("Collision Layer",
586 &sprite.property_collisionlayer.IsChecked))
587 zsm_dirty_ = true;
588 if (ImGui::Checkbox("Custom Death", &sprite.property_customdeath.IsChecked))
589 zsm_dirty_ = true;
590 if (ImGui::Checkbox("Damage Sound", &sprite.property_damagesound.IsChecked))
591 zsm_dirty_ = true;
592 if (ImGui::Checkbox("Deflect Arrows",
593 &sprite.property_deflectarrows.IsChecked))
594 zsm_dirty_ = true;
595 if (ImGui::Checkbox("Deflect Projectiles",
596 &sprite.property_deflectprojectiles.IsChecked))
597 zsm_dirty_ = true;
598 if (ImGui::Checkbox("Fast", &sprite.property_fast.IsChecked))
599 zsm_dirty_ = true;
600 if (ImGui::Checkbox("Harmless", &sprite.property_harmless.IsChecked))
601 zsm_dirty_ = true;
602 if (ImGui::Checkbox("Impervious", &sprite.property_impervious.IsChecked))
603 zsm_dirty_ = true;
604
605 // Column 2
606 ImGui::TableNextColumn();
607 if (ImGui::Checkbox("Impervious Arrow",
608 &sprite.property_imperviousarrow.IsChecked))
609 zsm_dirty_ = true;
610 if (ImGui::Checkbox("Impervious Melee",
611 &sprite.property_imperviousmelee.IsChecked))
612 zsm_dirty_ = true;
613 if (ImGui::Checkbox("Interaction", &sprite.property_interaction.IsChecked))
614 zsm_dirty_ = true;
615 if (ImGui::Checkbox("Is Boss", &sprite.property_isboss.IsChecked))
616 zsm_dirty_ = true;
617 if (ImGui::Checkbox("Persist", &sprite.property_persist.IsChecked))
618 zsm_dirty_ = true;
619 if (ImGui::Checkbox("Shadow", &sprite.property_shadow.IsChecked))
620 zsm_dirty_ = true;
621 if (ImGui::Checkbox("Small Shadow", &sprite.property_smallshadow.IsChecked))
622 zsm_dirty_ = true;
623 if (ImGui::Checkbox("Stasis", &sprite.property_statis.IsChecked))
624 zsm_dirty_ = true;
625 if (ImGui::Checkbox("Statue", &sprite.property_statue.IsChecked))
626 zsm_dirty_ = true;
627 if (ImGui::Checkbox("Water Sprite", &sprite.property_watersprite.IsChecked))
628 zsm_dirty_ = true;
629
630 ImGui::EndTable();
631 }
632}
633
634// ============================================================
635// Animation Panel
636// ============================================================
637
640
641 // Playback controls
642 if (animation_playing_) {
643 if (ImGui::Button(ICON_MD_STOP " Stop")) {
644 animation_playing_ = false;
645 }
646 } else {
647 if (ImGui::Button(ICON_MD_PLAY_ARROW " Play")) {
648 animation_playing_ = true;
649 frame_timer_ = 0.0f;
650 }
651 }
652 ImGui::SameLine();
653 if (ImGui::Button(ICON_MD_SKIP_PREVIOUS)) {
654 if (current_frame_ > 0)
657 }
658 ImGui::SameLine();
659 if (ImGui::Button(ICON_MD_SKIP_NEXT)) {
660 if (current_frame_ < (int)sprite.editor.Frames.size() - 1)
663 }
664 ImGui::SameLine();
665 Text("Frame: %d / %d", current_frame_, (int)sprite.editor.Frames.size() - 1);
666
667 Separator();
668
669 // Animation list
670 Text("Animations");
671 if (ImGui::Button(ICON_MD_ADD " Add Animation")) {
672 int frame_count = static_cast<int>(sprite.editor.Frames.size());
673 sprite.animations.emplace_back(0, frame_count > 0 ? frame_count - 1 : 0, 1,
674 "New Animation");
675 zsm_dirty_ = true;
676 }
677
678 if (ImGui::BeginChild("AnimList", ImVec2(0, 120), true)) {
679 for (size_t i = 0; i < sprite.animations.size(); i++) {
680 auto& anim = sprite.animations[i];
681 std::string label = anim.frame_name.empty() ? "Unnamed" : anim.frame_name;
682 if (Selectable(label.c_str(), current_animation_index_ == (int)i)) {
683 current_animation_index_ = static_cast<int>(i);
684 current_frame_ = anim.frame_start;
686 }
687 }
688 }
689 ImGui::EndChild();
690
691 // Edit selected animation
692 if (current_animation_index_ >= 0 &&
693 current_animation_index_ < (int)sprite.animations.size()) {
694 auto& anim = sprite.animations[current_animation_index_];
695
696 Separator();
697 Text("Animation Properties");
698
699 static char anim_name[128];
700 strncpy(anim_name, anim.frame_name.c_str(), sizeof(anim_name) - 1);
701 if (ImGui::InputText("Name##Anim", anim_name, sizeof(anim_name))) {
702 anim.frame_name = anim_name;
703 zsm_dirty_ = true;
704 }
705
706 int start = anim.frame_start;
707 int end = anim.frame_end;
708 int speed = anim.frame_speed;
709
710 if (ImGui::SliderInt("Start Frame", &start, 0,
711 std::max(0, (int)sprite.editor.Frames.size() - 1))) {
712 anim.frame_start = static_cast<uint8_t>(start);
713 zsm_dirty_ = true;
714 }
715 if (ImGui::SliderInt("End Frame", &end, 0,
716 std::max(0, (int)sprite.editor.Frames.size() - 1))) {
717 anim.frame_end = static_cast<uint8_t>(end);
718 zsm_dirty_ = true;
719 }
720 if (ImGui::SliderInt("Speed", &speed, 1, 16)) {
721 anim.frame_speed = static_cast<uint8_t>(speed);
722 zsm_dirty_ = true;
723 }
724
725 if (ImGui::Button("Delete Animation") && sprite.animations.size() > 1) {
726 sprite.animations.erase(sprite.animations.begin() +
729 std::min(current_animation_index_, (int)sprite.animations.size() - 1);
730 zsm_dirty_ = true;
731 }
732 }
733
734 Separator();
736}
737
740
741 Text("Frames");
742 if (ImGui::Button(ICON_MD_ADD " Add Frame")) {
743 sprite.editor.Frames.emplace_back();
744 zsm_dirty_ = true;
745 }
746 ImGui::SameLine();
747 if (ImGui::Button(ICON_MD_DELETE " Delete Frame") &&
748 sprite.editor.Frames.size() > 1 && current_frame_ >= 0) {
749 sprite.editor.Frames.erase(sprite.editor.Frames.begin() + current_frame_);
751 std::min(current_frame_, (int)sprite.editor.Frames.size() - 1);
752 zsm_dirty_ = true;
754 }
755
756 // Frame selector
757 if (ImGui::BeginChild("FrameList", ImVec2(0, 80), true,
758 ImGuiWindowFlags_HorizontalScrollbar)) {
759 for (size_t i = 0; i < sprite.editor.Frames.size(); i++) {
760 ImGui::PushID(static_cast<int>(i));
761 std::string label = absl::StrFormat("F%d", i);
762 if (Selectable(label.c_str(), current_frame_ == (int)i,
763 ImGuiSelectableFlags_None, ImVec2(40, 40))) {
764 current_frame_ = static_cast<int>(i);
766 }
767 ImGui::SameLine();
768 ImGui::PopID();
769 }
770 }
771 ImGui::EndChild();
772
773 // Edit tiles in current frame
774 if (current_frame_ >= 0 &&
775 current_frame_ < (int)sprite.editor.Frames.size()) {
776 auto& frame = sprite.editor.Frames[current_frame_];
777
778 Separator();
779 Text("Tiles in Frame %d", current_frame_);
780
781 if (ImGui::Button(ICON_MD_ADD " Add Tile")) {
782 frame.Tiles.emplace_back();
783 zsm_dirty_ = true;
785 }
786
787 if (ImGui::BeginChild("TileList", ImVec2(0, 100), true)) {
788 for (size_t i = 0; i < frame.Tiles.size(); i++) {
789 auto& tile = frame.Tiles[i];
790 std::string label = absl::StrFormat("Tile %d (ID: %d)", i, tile.id);
791 if (Selectable(label.c_str(), selected_tile_index_ == (int)i)) {
792 selected_tile_index_ = static_cast<int>(i);
793 }
794 }
795 }
796 ImGui::EndChild();
797
798 // Edit selected tile
799 if (selected_tile_index_ >= 0 &&
800 selected_tile_index_ < (int)frame.Tiles.size()) {
801 auto& tile = frame.Tiles[selected_tile_index_];
802
803 int tile_id = tile.id;
804 if (ImGui::InputInt("Tile ID", &tile_id)) {
805 tile.id = static_cast<uint16_t>(std::clamp(tile_id, 0, 511));
806 zsm_dirty_ = true;
808 }
809
810 int x = tile.x, y = tile.y;
811 if (ImGui::InputInt("X", &x)) {
812 tile.x = static_cast<uint8_t>(std::clamp(x, 0, 251));
813 zsm_dirty_ = true;
815 }
816 if (ImGui::InputInt("Y", &y)) {
817 tile.y = static_cast<uint8_t>(std::clamp(y, 0, 219));
818 zsm_dirty_ = true;
820 }
821
822 int pal = tile.palette;
823 if (ImGui::SliderInt("Palette##Tile", &pal, 0, 7)) {
824 tile.palette = static_cast<uint8_t>(pal);
825 zsm_dirty_ = true;
827 }
828
829 if (ImGui::Checkbox("16x16", &tile.size)) {
830 zsm_dirty_ = true;
832 }
833 ImGui::SameLine();
834 if (ImGui::Checkbox("Flip X", &tile.mirror_x)) {
835 zsm_dirty_ = true;
837 }
838 ImGui::SameLine();
839 if (ImGui::Checkbox("Flip Y", &tile.mirror_y)) {
840 zsm_dirty_ = true;
842 }
843
844 if (ImGui::Button("Delete Tile")) {
845 frame.Tiles.erase(frame.Tiles.begin() + selected_tile_index_);
847 zsm_dirty_ = true;
849 }
850 }
851 }
852}
853
857 return;
858 }
859
861 if (current_animation_index_ < 0 ||
862 current_animation_index_ >= (int)sprite.animations.size()) {
863 return;
864 }
865
866 auto& anim = sprite.animations[current_animation_index_];
867
868 frame_timer_ += delta_time;
869 float frame_duration = anim.frame_speed / 60.0f;
870
871 if (frame_timer_ >= frame_duration) {
872 frame_timer_ = 0;
874 if (current_frame_ > anim.frame_end) {
875 current_frame_ = anim.frame_start;
876 }
878 }
879}
880
881// ============================================================
882// User Routines Panel
883// ============================================================
884
887
888 if (ImGui::Button(ICON_MD_ADD " Add Routine")) {
889 sprite.userRoutines.emplace_back("New Routine", "; ASM code here\n");
890 zsm_dirty_ = true;
891 }
892
893 // Routine list
894 if (ImGui::BeginChild("RoutineList", ImVec2(0, 100), true)) {
895 for (size_t i = 0; i < sprite.userRoutines.size(); i++) {
896 auto& routine = sprite.userRoutines[i];
897 if (Selectable(routine.name.c_str(), selected_routine_index_ == (int)i)) {
898 selected_routine_index_ = static_cast<int>(i);
899 }
900 }
901 }
902 ImGui::EndChild();
903
904 // Edit selected routine
905 if (selected_routine_index_ >= 0 &&
906 selected_routine_index_ < (int)sprite.userRoutines.size()) {
907 auto& routine = sprite.userRoutines[selected_routine_index_];
908
909 Separator();
910
911 static char routine_name[128];
912 strncpy(routine_name, routine.name.c_str(), sizeof(routine_name) - 1);
913 if (ImGui::InputText("Routine Name", routine_name, sizeof(routine_name))) {
914 routine.name = routine_name;
915 zsm_dirty_ = true;
916 }
917
918 Text("ASM Code:");
919
920 // Multiline text input for code
921 static char code_buffer[16384];
922 strncpy(code_buffer, routine.code.c_str(), sizeof(code_buffer) - 1);
923 code_buffer[sizeof(code_buffer) - 1] = '\0';
924 if (ImGui::InputTextMultiline("##RoutineCode", code_buffer,
925 sizeof(code_buffer), ImVec2(-1, 200))) {
926 routine.code = code_buffer;
927 zsm_dirty_ = true;
928 }
929
930 if (ImGui::Button("Delete Routine")) {
931 sprite.userRoutines.erase(sprite.userRoutines.begin() +
934 zsm_dirty_ = true;
935 }
936 }
937}
938
939// ============================================================
940// Graphics Pipeline
941// ============================================================
942
944 // Combine selected sheets (current_sheets_[0-7]) into single 8BPP buffer
945 // Layout: 16 tiles per row, 8 rows per sheet, 8 sheets total = 64 tile rows
946 // Buffer size: 0x10000 bytes (65536)
947
948 sprite_gfx_buffer_.resize(0x10000, 0);
949
950 // Each sheet is 128x32 pixels (128 bytes per row, 32 rows) = 4096 bytes
951 // We combine 8 sheets vertically: 128x256 pixels total
952 constexpr int kSheetWidth = 128;
953 constexpr int kSheetHeight = 32;
954 constexpr int kRowStride = 128;
955
956 for (int sheet_idx = 0; sheet_idx < 8; sheet_idx++) {
957 uint8_t sheet_id = current_sheets_[sheet_idx];
958 if (sheet_id >= gfx::Arena::Get().gfx_sheets().size()) {
959 continue;
960 }
961
962 auto& sheet = gfx::Arena::Get().gfx_sheets().at(sheet_id);
963 if (!sheet.is_active() || sheet.size() == 0) {
964 continue;
965 }
966
967 // Copy sheet data to buffer at appropriate offset
968 // Each sheet occupies 8 tile rows (8 * 8 scanlines = 64 scanlines)
969 // Offset = sheet_idx * (8 tile rows * 1024 bytes per tile row)
970 // But sheets are 32 pixels tall (4 tile rows), so:
971 // Offset = sheet_idx * 4 * 1024 = sheet_idx * 4096
972 int dest_offset = sheet_idx * (kSheetHeight * kRowStride);
973
974 const uint8_t* src_data = sheet.data();
975 size_t copy_size =
976 std::min(sheet.size(), static_cast<size_t>(kSheetWidth * kSheetHeight));
977
978 if (dest_offset + copy_size <= sprite_gfx_buffer_.size()) {
979 std::memcpy(sprite_gfx_buffer_.data() + dest_offset, src_data, copy_size);
980 }
981 }
982
983 // Update drawer with new buffer
985 gfx_buffer_loaded_ = true;
986}
987
989 // Load sprite palettes from ROM palette groups
990 // ALTTP sprites use a combination of palette groups:
991 // - Rows 0-1: Global sprite palettes (shared by all sprites)
992 // - Rows 2-7: Aux palettes (vary by sprite type)
993 //
994 // For simplicity, we load global_sprites which contains the main
995 // sprite palettes. More accurate rendering would require looking up
996 // which aux palette group each sprite type uses.
997
998 if (!rom_ || !rom_->is_loaded()) {
999 return;
1000 }
1001
1002 // Build combined sprite palette from global + aux groups
1004
1005 // Add global sprite palettes (typically 2 palettes, 16 colors each)
1006 if (!game_data())
1007 return;
1008 const auto& global = game_data()->palette_groups.global_sprites;
1009 for (size_t i = 0; i < global.size() && i < 8; i++) {
1010 sprite_palettes_.AddPalette(global.palette(i));
1011 }
1012
1013 // If we don't have 8 palettes yet, fill with aux palettes
1014 const auto& aux1 = game_data()->palette_groups.sprites_aux1;
1015 const auto& aux2 = game_data()->palette_groups.sprites_aux2;
1016 const auto& aux3 = game_data()->palette_groups.sprites_aux3;
1017
1018 // Pad to 8 palettes total for proper OAM palette mapping
1019 while (sprite_palettes_.size() < 8) {
1020 if (sprite_palettes_.size() < 4 && aux1.size() > 0) {
1022 aux1.palette(sprite_palettes_.size() % aux1.size()));
1023 } else if (sprite_palettes_.size() < 6 && aux2.size() > 0) {
1025 aux2.palette((sprite_palettes_.size() - 4) % aux2.size()));
1026 } else if (aux3.size() > 0) {
1028 aux3.palette((sprite_palettes_.size() - 6) % aux3.size()));
1029 } else {
1030 // Fallback: add empty palette
1032 }
1033 }
1034
1036}
1037
1038void SpriteEditor::LoadSheetsForSprite(const std::array<uint8_t, 4>& sheets) {
1039 // Load the required sheets for a vanilla sprite
1040 bool changed = false;
1041 for (int i = 0; i < 4; i++) {
1042 if (sheets[i] != 0 && current_sheets_[i] != sheets[i]) {
1043 current_sheets_[i] = sheets[i];
1044 changed = true;
1045 }
1046 }
1047
1048 if (changed) {
1049 gfx_buffer_loaded_ = false;
1051 }
1052}
1053
1055 // Ensure graphics buffer is loaded
1059 }
1060
1061 // Initialize vanilla preview bitmap if needed
1065 }
1066
1068 return;
1069 }
1070
1071 // Clear and render
1073
1074 // Origin is center of bitmap
1075 int origin_x = 64;
1076 int origin_y = 64;
1077
1078 // Convert SpriteOamLayout tiles to zsprite::OamTile and draw
1079 for (const auto& entry : layout.tiles) {
1080 zsprite::OamTile tile;
1081 tile.x = static_cast<uint8_t>(entry.x_offset + 128); // Convert to unsigned
1082 tile.y = static_cast<uint8_t>(entry.y_offset + 128);
1083 tile.id = entry.tile_id;
1084 tile.palette = entry.palette;
1085 tile.size = entry.size_16x16;
1086 tile.mirror_x = entry.flip_x;
1087 tile.mirror_y = entry.flip_y;
1088 tile.priority = 0;
1089
1091 origin_y);
1092 }
1093
1094 // Build combined 128-color palette (8 sub-palettes × 16 colors)
1095 // and apply to bitmap for proper color rendering
1096 if (sprite_palettes_.size() > 0) {
1097 gfx::SnesPalette combined_palette;
1098 for (size_t pal_idx = 0; pal_idx < 8 && pal_idx < sprite_palettes_.size();
1099 pal_idx++) {
1100 const auto& sub_pal = sprite_palettes_.palette(pal_idx);
1101 for (size_t col_idx = 0; col_idx < 16 && col_idx < sub_pal.size();
1102 col_idx++) {
1103 combined_palette.AddColor(sub_pal[col_idx]);
1104 }
1105 // Pad to 16 if sub-palette is smaller
1106 while (combined_palette.size() < (pal_idx + 1) * 16) {
1107 combined_palette.AddColor(gfx::SnesColor(0));
1108 }
1109 }
1110 vanilla_preview_bitmap_.SetPalette(combined_palette);
1111 }
1112
1114}
1115
1116// ============================================================
1117// Canvas Rendering
1118// ============================================================
1119
1123 return;
1124 }
1125
1127 if (frame_index < 0 || frame_index >= (int)sprite.editor.Frames.size()) {
1128 return;
1129 }
1130
1131 auto& frame = sprite.editor.Frames[frame_index];
1132
1133 // Ensure graphics buffer is loaded
1137 }
1138
1139 // Initialize preview bitmap if needed
1143 }
1144
1145 // Only render if drawer is ready
1147 // Clear and render to preview bitmap
1149
1150 // Origin is center of canvas (128, 128 for 256x256 bitmap)
1152
1153 // Build combined 128-color palette and apply to bitmap
1154 if (sprite_palettes_.size() > 0) {
1155 gfx::SnesPalette combined_palette;
1156 for (size_t pal_idx = 0; pal_idx < 8 && pal_idx < sprite_palettes_.size();
1157 pal_idx++) {
1158 const auto& sub_pal = sprite_palettes_.palette(pal_idx);
1159 for (size_t col_idx = 0; col_idx < 16 && col_idx < sub_pal.size();
1160 col_idx++) {
1161 combined_palette.AddColor(sub_pal[col_idx]);
1162 }
1163 // Pad to 16 if sub-palette is smaller
1164 while (combined_palette.size() < (pal_idx + 1) * 16) {
1165 combined_palette.AddColor(gfx::SnesColor(0));
1166 }
1167 }
1168 sprite_preview_bitmap_.SetPalette(combined_palette);
1169 }
1170
1171 // Mark as updated
1172 preview_needs_update_ = false;
1173 }
1174
1175 // Draw the preview bitmap on canvas
1178 }
1179
1180 // Draw tile outlines for selection (over the bitmap)
1181 if (show_tile_grid_) {
1182 for (size_t i = 0; i < frame.Tiles.size(); i++) {
1183 const auto& tile = frame.Tiles[i];
1184 int tile_size = tile.size ? 16 : 8;
1185
1186 // Convert signed tile position to canvas position
1187 int8_t signed_x = static_cast<int8_t>(tile.x);
1188 int8_t signed_y = static_cast<int8_t>(tile.y);
1189
1190 int canvas_x = 128 + signed_x;
1191 int canvas_y = 128 + signed_y;
1192
1193 // Highlight selected tile
1194 ImVec4 color = (selected_tile_index_ == static_cast<int>(i))
1195 ? ImVec4(0.0f, 1.0f, 0.0f, 0.8f) // Green for selected
1196 : ImVec4(1.0f, 1.0f, 0.0f, 0.3f); // Yellow for others
1197
1198 sprite_canvas_.DrawRect(canvas_x, canvas_y, tile_size, tile_size, color);
1199 }
1200 }
1201}
1202
1204 if (ImGui::BeginChild(gui::GetID("##ZSpriteCanvas"),
1205 ImGui::GetContentRegionAvail(), true)) {
1208
1209 // Render current frame if we have a sprite selected
1213 }
1214
1217
1218 // Display current frame info
1221 ImGui::SetCursorPos(ImVec2(10, 10));
1222 Text("Frame: %d | Tiles: %d", current_frame_,
1223 current_frame_ < (int)sprite.editor.Frames.size()
1224 ? (int)sprite.editor.Frames[current_frame_].Tiles.size()
1225 : 0);
1226 }
1227 }
1228 ImGui::EndChild();
1229}
1230
1231} // namespace editor
1232} // namespace yaze
project::ResourceLabelManager * resource_label()
Definition rom.h:146
bool is_loaded() const
Definition rom.h:128
zelda3::GameData * game_data() const
Definition editor.h:228
EditorDependencies dependencies_
Definition editor.h:237
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)
std::vector< zsprite::ZSprite > custom_sprites_
std::vector< uint8_t > sprite_gfx_buffer_
absl::Status Save() override
void SaveZsmFile(const std::string &path)
absl::Status Load() override
std::array< gfx::Bitmap, 223 > & gfx_sheets()
Get reference to all graphics sheets.
Definition arena.h:102
static Arena & Get()
Definition arena.cc:20
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:1075
void DrawContextMenu()
Definition canvas.cc:602
bool DrawTileSelector(int size, int size_y=0)
Definition canvas.cc:1011
void DrawRect(int x, int y, int w, int h, ImVec4 color)
Definition canvas.cc:1341
void DrawBackground(ImVec2 canvas_size=ImVec2(0, 0))
Definition canvas.cc:547
void DrawGrid(float grid_step=64.0f, int tile_id_offset=8)
Definition canvas.cc:1398
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
constexpr ImGuiTabBarFlags kSpriteTabBarFlags
bool InputHexWord(const char *label, uint16_t *data, float input_width, bool no_step)
Definition input.cc:344
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:1310
gfx::PaletteGroupMap palette_groups
Definition game_data.h:89
Complete OAM layout for a vanilla sprite.
std::vector< SpriteOamEntry > tiles