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#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <cerrno>
6#include <cstdio>
7#include <cstdlib>
8#include <cstring>
9
10#include "absl/strings/str_format.h"
24#include "app/gui/core/icons.h"
25#include "app/gui/core/input.h"
30#include "core/project.h"
31#include "util/file_util.h"
32#include "util/hex.h"
33#include "util/macro.h"
34#include "zelda3/dungeon/room.h"
35#include "zelda3/game_data.h"
37
38namespace yaze {
39namespace editor {
40
41using ImGui::BeginTable;
42using ImGui::Button;
43using ImGui::EndTable;
44using ImGui::Selectable;
45using ImGui::Separator;
46using ImGui::TableHeadersRow;
47using ImGui::TableNextColumn;
48using ImGui::TableNextRow;
49using ImGui::TableSetupColumn;
50using ImGui::Text;
51
52namespace {
53template <size_t N>
54void CopyStringToBuffer(const std::string& src, char (&dest)[N]) {
55 std::strncpy(dest, src.c_str(), N - 1);
56 dest[N - 1] = '\0';
57}
58
59int ParseIntOrDefault(const std::string& text, int fallback = 0) {
60 if (text.empty()) {
61 return fallback;
62 }
63 errno = 0;
64 char* end = nullptr;
65 long value = std::strtol(text.c_str(), &end, 0);
66 if (end == text.c_str() || errno == ERANGE) {
67 return fallback;
68 }
69 return static_cast<int>(value);
70}
71} // namespace
72
75 return;
76 auto* window_manager = dependencies_.window_manager;
77
78 // Register WindowContent implementations with callbacks
79 // EditorPanels provide both metadata (icon, name, priority) and drawing logic
80 window_manager->RegisterWindowContent(
81 std::make_unique<VanillaSpriteEditorPanel>([this]() {
82 if (rom_ && rom_->is_loaded()) {
83 DrawVanillaSpriteEditor();
84 } else {
85 ImGui::TextDisabled(tr("Load a ROM to view vanilla sprites"));
86 }
87 }));
88
89 window_manager->RegisterWindowContent(
90 std::make_unique<CustomSpriteEditorPanel>(
91 [this]() { DrawCustomSprites(); }));
92 window_manager->RegisterWindowContent(
93 std::make_unique<SpriteCatalogPanel>([this]() { DrawSpriteCatalog(); }));
94}
95
96absl::Status SpriteEditor::Load() {
97 gfx::ScopedTimer timer("SpriteEditor::Load");
99 gfx_buffer_loaded_ = false;
102 room_palette_cache_.reset();
104 const auto slots = zelda3::SpriteSheetSlots(
105 game_data()->spriteset_ids[preview_spriteset_],
107 std::copy(slots.begin(), slots.end(), current_sheets_);
108 }
110 (void)ReloadSpriteCatalog();
112 return absl::OkStatus();
113}
114
115absl::Status SpriteEditor::Update() {
116 if (rom() && rom()->is_loaded() && !sheets_loaded_) {
117 sheets_loaded_ = true;
118 }
119
120 // Update animation playback for custom sprites
121 float current_time = ImGui::GetTime();
122 float delta_time = current_time - last_frame_time_;
123 last_frame_time_ = current_time;
124 UpdateAnimationPlayback(delta_time);
125
126 // Handle editor-level shortcuts
128
129 // Panel drawing is handled by WorkspaceWindowManager via registered EditorPanels
130 // Each panel's Draw() callback invokes the appropriate draw method
131
132 // Commit any pending undo transaction at the end of the frame
134
135 return status_.ok() ? absl::OkStatus() : status_;
136}
137
139 // Animation playback shortcuts (when custom sprite panel is active)
140 if (ImGui::IsKeyPressed(ImGuiKey_Space, false) &&
141 !ImGui::GetIO().WantTextInput) {
143 }
144
145 // Frame navigation
146 if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket, false)) {
147 if (current_frame_ > 0) {
150 }
151 }
152 if (ImGui::IsKeyPressed(ImGuiKey_RightBracket, false)) {
155 }
156
157 // Sprite navigation
158 if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_UpArrow, false)) {
159 if (current_sprite_id_ > 0) {
161 }
162 }
163 if (ImGui::GetIO().KeyCtrl &&
164 ImGui::IsKeyPressed(ImGuiKey_DownArrow, false)) {
166 }
167}
168
169absl::Status SpriteEditor::Save() {
171 current_custom_sprite_index_ < static_cast<int>(custom_sprites_.size())) {
172 const std::string& zsm_path = GetCurrentZsmPath();
173 if (zsm_path.empty()) {
175 } else {
176 SaveZsmFile(zsm_path);
177 }
178 }
179 return status_;
180}
181
183 // Sidebar handled by EditorManager for card-based editors
184}
185
186// ============================================================
187// Vanilla Sprite Editor
188// ============================================================
189
191 if (sprite_id < 0 || sprite_id >= 256)
192 return;
193 current_sprite_id_ = sprite_id;
195 pending_sprite_tab_ = sprite_id;
196 if (!active_sprites_.contains(sprite_id))
197 active_sprites_.push_back(sprite_id);
198}
199
201 const auto id = static_cast<uint8_t>(current_sprite_id_);
203 id, project() ? project()->hack_manifest.hack_name() : "");
204 return override ? override : zelda3::SpriteOamRegistry::GetLayout(id);
205}
206
208 gui::AutoWidgetScope sprite_scope("Sprite/Vanilla");
209 if (ImGui::BeginTable("##SpriteCanvasTable", 3, ImGuiTableFlags_Resizable,
210 ImVec2(0, 0))) {
211 TableSetupColumn("Sprites List", ImGuiTableColumnFlags_WidthFixed, 256);
212 TableSetupColumn("Canvas", ImGuiTableColumnFlags_WidthStretch,
213 ImGui::GetContentRegionAvail().x);
214 TableSetupColumn("Tile Selector", ImGuiTableColumnFlags_WidthFixed, 256);
215 TableHeadersRow();
216 TableNextRow();
217
218 TableNextColumn();
220
221 TableNextColumn();
222 if (active_sprites_.empty()) {
223 ImGui::TextWrapped("Select a sprite from the list to open its preview.");
224 }
225 if (gui::BeginThemedTabBar("SpriteTabBar", kSpriteTabBarFlags)) {
226 if (ImGui::TabItemButton(ICON_MD_ADD, kSpriteTabFlags)) {
228 }
229
230 for (int n = 0; n < active_sprites_.Size;) {
231 bool open = true;
232
233 const int sprite_id = active_sprites_[n];
234 if (sprite_id < 0 || sprite_id >= 256) {
235 active_sprites_.erase(active_sprites_.Data + n);
236 continue;
237 }
238
239 const auto label =
240 absl::StrFormat("%02X %s###SpriteTab%02X", sprite_id,
241 zelda3::kSpriteDefaultNames[sprite_id], sprite_id);
242 const auto flags = pending_sprite_tab_ == sprite_id
243 ? ImGuiTabItemFlags_SetSelected
244 : ImGuiTabItemFlags_None;
245 const bool selected = ImGui::BeginTabItem(label.c_str(), &open, flags);
247 absl::StrFormat("SpriteTab%02X", sprite_id));
248 if (selected) {
249 if (current_sprite_id_ != sprite_id) {
250 current_sprite_id_ = sprite_id;
252 }
254 ImGui::EndTabItem();
255 }
256
257 if (!open)
258 active_sprites_.erase(active_sprites_.Data + n);
259 else
260 n++;
261 }
262
265 }
266
267 TableNextColumn();
268 if (sheets_loaded_) {
270 }
271 ImGui::EndTable();
272 }
273}
274
276 if (ImGui::BeginChild(gui::GetID("##SpriteCanvas"),
277 ImGui::GetContentRegionAvail(), true)) {
280
281 // Static layouts are preview references, not a complete game renderer.
282 const auto* layout = CurrentPreviewLayout();
283 ImGui::SetCursorPos(ImVec2(10, 10));
284 ImGui::TextColored(gui::GetWarningColor(), "Experimental static preview");
285 if (!layout) {
286 ImGui::TextWrapped(
287 "No static OAM layout for sprite $%02X. Open or import a ZSM in "
288 "Custom Sprites to preview authored frames.",
290 } else if (layout->graphics_resource) {
291 ImGui::TextWrapped(
292 "%s ($%02X) requires external graphics (%s). This editor does not "
293 "yet preview that graphics source.",
294 layout->name, layout->sprite_id, layout->graphics_resource);
295 } else {
296 RenderVanillaSprite(*layout);
298 const auto& error = !graphics_binding_status_.ok()
301 ImGui::TextWrapped("Preview unavailable: %s",
302 std::string(error.message()).c_str());
303 } else {
306 Text(tr("Sprite: %s (0x%02X)"), layout->name, layout->sprite_id);
307 Text(tr("Tiles: %zu"), layout->tiles.size());
308 if (std::none_of(vanilla_preview_bitmap_.vector().begin(),
310 [](uint8_t pixel) { return (pixel & 0x0F) != 0; })) {
311 ImGui::TextWrapped(
312 "The selected sheets contain no visible pixels for this layout. "
313 "Choose the sprite's spriteset or graphics sheets.");
314 }
315 if (ImGui::Button(tr("Edit preview copy"))) {
317 auto& copy = custom_sprites_.back();
318 copy.sprName = std::string(layout->name) + " (preview copy)";
319 copy.editor.Frames[0] = sprite_authoring::CopyVanillaLayout(*layout);
320 std::copy_n(current_sheets_, 8,
321 custom_sprite_bindings_.back().sheets.begin());
325 visible.push_back("sprite.custom_editor");
327 dependencies_.session_id, visible);
328 }
329 }
330 }
331 ImGui::TextWrapped(
332 "Uses the selected sheets and palette. Animation, behavior, and "
333 "runtime graphics changes are not simulated.");
334 }
335
338 }
339 ImGui::EndChild();
340}
341
343 if (game_data() == nullptr || spriteset < 0 ||
344 spriteset >= static_cast<int>(zelda3::kNumSpritesets)) {
345 return;
346 }
347 const auto slots =
348 zelda3::SpriteSheetSlots(game_data()->spriteset_ids[spriteset],
352 std::copy(slots.begin(), slots.end(), current_sheets_);
353 gfx_buffer_loaded_ = false;
355 if (current_custom_sprite()) {
357 std::copy(slots.begin(), slots.end(), binding.sheets.begin());
359 }
361}
362
364 const auto* sprite = current_custom_sprite();
365 if (!sprite || current_frame_ < 0 ||
366 current_frame_ >= static_cast<int>(sprite->editor.Frames.size())) {
367 return absl::FailedPreconditionError("Select a sprite frame");
368 }
369 if (!rom_ || !rom_->is_loaded() || !game_data()) {
370 return absl::FailedPreconditionError(
371 "Load a ROM before editing sprite pixels");
372 }
376 if (!graphics_binding_status_.ok())
378 if (!palette_binding_status_.ok())
380 std::array<uint8_t, 8> slots;
381 std::copy_n(current_sheets_, 8, slots.begin());
382 const auto frame = sprite->editor.Frames[current_frame_];
383 const auto palettes = sprite_palettes_;
384 const std::string source =
386 ? absl::StrFormat("room %03X CGRAM rows 8-15; sprite slots %02X",
389 ? absl::StrFormat(
390 "sprite asset binding; preview spriteset %02X",
392 : absl::StrFormat("preview spriteset %02X",
395 frame, current_frame_, selected_tile_index_, slots, palettes, game_data(),
396 sprite->sprName, source);
397 if (!context.ok())
398 return context.status();
399 const auto* owner = game_data();
400 context->render_preview = [frame, slots, palettes, owner]() {
401 return sprite_graphics_context::RenderCapturedFrame(frame, slots, palettes,
402 owner);
403 };
404 auto* manager = static_cast<EditorManager*>(dependencies_.custom_data);
405 if (!manager)
406 return absl::FailedPreconditionError("Editor manager unavailable");
407 const int sprite_index = current_custom_sprite_index_;
408 const int frame_index = current_frame_;
409 const int tile_index = selected_tile_index_;
410 context->return_to_source = [this, manager, sprite_index, frame_index,
411 tile_index]() {
412 if (sprite_index >= 0 &&
413 sprite_index < static_cast<int>(custom_sprites_.size())) {
414 current_custom_sprite_index_ = sprite_index;
415 if (frame_index <
416 static_cast<int>(
417 custom_sprites_[sprite_index].editor.Frames.size())) {
418 current_frame_ = frame_index;
419 selected_tile_index_ = tile_index;
420 }
422 gfx_buffer_loaded_ = false;
423 }
424 manager->SwitchToEditor(EditorType::kSprite, true);
425 };
426 auto* editor_set = manager->GetCurrentEditorSet();
427 if (!editor_set || !editor_set->GetGraphicsEditor()) {
428 return absl::FailedPreconditionError("Graphics editor unavailable");
429 }
430 auto status = manager->EnsureEditorAssetsLoaded(EditorType::kGraphics);
431 if (!status.ok())
432 return status;
433 status = editor_set->GetGraphicsEditor()->OpenInContext(std::move(*context));
434 if (!status.ok())
435 return status;
436 manager->SwitchToEditor(EditorType::kGraphics, true);
437 return absl::OkStatus();
438}
439
441 if (!ImGui::CollapsingHeader(tr("Spriteset preview"),
442 ImGuiTreeNodeFlags_DefaultOpen)) {
443 return;
444 }
445 if (game_data() == nullptr || rom_ == nullptr || !rom_->is_loaded()) {
446 ImGui::TextDisabled(tr("Load a ROM to preview against spritesets."));
447 return;
448 }
449 if (usage_rom_ != rom_ || !usage_areas_.has_value()) {
453 }
454
455 uint8_t set = static_cast<uint8_t>(preview_spriteset_);
456 if (gui::InputHexByte("Spriteset", &set,
457 static_cast<uint8_t>(zelda3::kNumSpritesets - 1))) {
458 preview_spriteset_ = std::min<int>(set, zelda3::kNumSpritesets - 1);
459 }
460 ImGui::SameLine();
461 if (ImGui::Button(ICON_MD_DONE " Apply to sheets")) {
463 }
465 "Slots 0-3 take the static sprite sheets, slots 4-7 this spriteset's "
466 "values + 0x73 (OAM tiles 0x100-0x1FF).");
467
468 const auto& values = game_data()->spriteset_ids[preview_spriteset_];
469 const auto slots = zelda3::SpriteSheetSlots(
471 ImGui::TextDisabled(
472 "Values %02X %02X %02X %02X -> sheets %02X %02X %02X %02X", values[0],
473 values[1], values[2], values[3], slots[4], slots[5], slots[6], slots[7]);
474
477 auto hex_list = [](const std::vector<int>& ids) {
478 if (ids.empty()) {
479 return std::string("-");
480 }
481 std::string out;
482 for (size_t i = 0; i < ids.size() && i < 24; ++i) {
483 out += absl::StrFormat("%s%02X", i == 0 ? "" : " ", ids[i]);
484 }
485 if (ids.size() > 24) {
486 out += absl::StrFormat(" (+%zu)", ids.size() - 24);
487 }
488 return out;
489 };
490 for (int state = 0; state < 3; ++state) {
491 ImGui::TextWrapped("OW areas, state %d: %s", state,
492 hex_list(usage.ow_areas_by_state[state]).c_str());
493 }
494 ImGui::TextWrapped("Rooms: %s", hex_list(usage.rooms).c_str());
495
496 // Room context: its header picks the spriteset and the CGRAM palette.
497 ImGui::SetNextItemWidth(gui::LayoutHelpers::GetHexInputWidth() * 1.5f);
498 ImGui::InputScalar("Room", ImGuiDataType_U16, &preview_room_, nullptr,
499 nullptr, "%03X", ImGuiInputTextFlags_CharsHexadecimal);
501 std::min<uint16_t>(preview_room_, zelda3::kNumDungeonRooms - 1);
502 ImGui::SameLine();
503 if (ImGui::Button(ICON_MD_MEETING_ROOM " Use room")) {
504 if (auto room = usage_rooms_->find(preview_room_);
505 room != usage_rooms_->end()) {
507 std::min<int>(room->second.spriteset + zelda3::kDungeonSpritesetBase,
511 room_palette_cache_.reset();
512 gfx_buffer_loaded_ = false;
513 }
514 }
515 HOVER_HINT("Use the room's header spriteset (+0x40) and its sprite palette");
516 if (ImGui::Checkbox(tr("Room palette (CGRAM rows 8-15)"),
518 room_palette_cache_.reset();
519 gfx_buffer_loaded_ = false;
521 }
523 "Off: palette rows from the sprite binding (Sprite Asset panel). On: "
524 "the rows the game loads for the room above.");
525
527 ImGui::Separator();
528}
529
531 const auto* sprite = current_custom_sprite();
532 if (sprite == nullptr || current_frame_ < 0 ||
533 current_frame_ >= static_cast<int>(sprite->editor.Frames.size())) {
534 return;
535 }
536 const auto tiles =
537 internal::FrameTiles8x8(sprite->editor.Frames[current_frame_]);
538 std::array<uint8_t, 8> slots;
539 std::copy_n(current_sheets_, 8, slots.begin());
540 const auto options = core::BuildGfxSheetInventoryOptions(project());
541 std::string key =
542 absl::StrFormat("%d:%d:", current_custom_sprite_index_, current_frame_);
543 for (uint8_t sheet : slots) {
544 key += absl::StrFormat("%02X", sheet);
545 }
546 for (int tile : tiles) {
547 key += absl::StrFormat(",%X", tile);
548 }
549 for (uint16_t sheet : options.reserved_sheets) {
550 key += absl::StrFormat("r%X", sheet);
551 }
552 if (key != tile_check_key_) {
553 tile_check_key_ = key;
555 zelda3::CheckSpriteTiles(*rom_, tiles, slots, options.reserved_sheets);
556 tile_check_count_ = tiles.size();
557 }
558
559 if (tile_issues_.empty()) {
560 ImGui::TextColored(gui::GetSuccessColor(),
561 ICON_MD_CHECK " Frame %d: all %zu 8x8 tiles have art",
563 return;
564 }
565 ImGui::TextColored(gui::GetWarningColor(),
567 " Frame %d: %zu of %zu 8x8 tiles need "
568 "attention",
570 for (size_t i = 0; i < tile_issues_.size() && i < 8; ++i) {
571 const auto& issue = tile_issues_[i];
572 const char* what =
574 ? "blank"
576 ? "reserved sheet"
577 : "sheet unreadable");
578 ImGui::BulletText("tile 0x%03X, slot %d (sheet 0x%02X): %s", issue.tile,
579 issue.slot, issue.sheet, what);
580 }
581 if (tile_issues_.size() > 8) {
582 ImGui::TextDisabled("... %zu more", tile_issues_.size() - 8);
583 }
584}
585
587 if (ImGui::BeginChild(gui::GetID("sheet_label"),
588 ImVec2(ImGui::GetContentRegionAvail().x, 0), true,
589 ImGuiWindowFlags_NoDecoration)) {
591 // Track previous sheet values for change detection
592 bool sheets_changed = false;
595
596 for (int i = 0; i < 8; i++) {
597 std::string sheet_label = absl::StrFormat("Sheet %d", i);
598 if (gui::InputHexByte(sheet_label.c_str(), &current_sheets_[i])) {
599 sheets_changed = true;
600 }
601 if (i % 2 == 0)
602 ImGui::SameLine();
603 }
604
605 // Reload graphics buffer if sheets changed
606 if (sheets_changed) {
607 gfx_buffer_loaded_ = false;
609 if (current_custom_sprite()) {
611 std::copy_n(current_sheets_, 8, binding.sheets.begin());
613 }
614 }
616
621 for (int i = 0; i < 8; i++) {
622 if (current_sheets_[i] >= gfx::Arena::Get().gfx_sheets().size())
623 continue;
624 auto& sheet = gfx::Arena::Get().gfx_sheets().at(current_sheets_[i]);
625 if (sheet.is_active() && !sheet.texture())
626 sheet.CreateTexture();
627 graphics_sheet_canvas_.DrawBitmap(sheet, 1, (i * 0x40) + 1, 2);
628 }
631 }
632 ImGui::EndChild();
633}
634
636 if (ImGui::BeginChild(gui::GetID("##SpritesList"),
637 ImVec2(ImGui::GetContentRegionAvail().x, 0), true,
638 ImGuiWindowFlags_NoDecoration)) {
639 int i = 0;
640 for (const auto each_sprite_name : zelda3::kSpriteDefaultNames) {
642 current_sprite_id_ == i, "Sprite Names", util::HexByte(i),
644 if (ImGui::IsItemClicked()) {
646 }
647 i++;
648 }
649 }
650 ImGui::EndChild();
651}
652
653// ============================================================
654// Custom ZSM Sprite Editor
655// ============================================================
656
658 if (BeginTable("##CustomSpritesTable", 3,
659 ImGuiTableFlags_Resizable | ImGuiTableFlags_Borders,
660 ImVec2(0, 0))) {
661 TableSetupColumn("Sprite Data", ImGuiTableColumnFlags_WidthFixed, 300);
662 TableSetupColumn("Canvas", ImGuiTableColumnFlags_WidthStretch);
663 TableSetupColumn("Tilesheets", ImGuiTableColumnFlags_WidthFixed, 280);
664
665 TableHeadersRow();
666 TableNextRow();
667 TableNextColumn();
668
669 // Keep the canvas visible while scrolling the longer authoring controls.
670 if (ImGui::BeginChild("##SpriteMetadata", ImVec2(0, 0)))
672 ImGui::EndChild();
673
674 TableNextColumn();
676
677 TableNextColumn();
679
680 EndTable();
681 }
682}
683
685 if (!asset_load_status_.ok())
686 ImGui::TextWrapped("%s", asset_load_status_.ToString().c_str());
687 // File operations toolbar
688 if (ImGui::Button(ICON_MD_ADD " New")) {
690 }
691 ImGui::SameLine();
692 if (ImGui::Button(ICON_MD_FOLDER_OPEN " Open")) {
693 std::string file_path = util::FileDialogWrapper::ShowOpenFileDialog();
694 if (!file_path.empty()) {
695 status_ = OpenSpriteAsset(file_path);
696 }
697 }
698 ImGui::SameLine();
699 if (ImGui::Button(ICON_MD_SAVE " Save")) {
701 const std::string& zsm_path = GetCurrentZsmPath();
702 if (zsm_path.empty()) {
704 } else {
705 SaveZsmFile(zsm_path);
706 }
707 }
708 }
709 ImGui::SameLine();
710 if (ImGui::Button(ICON_MD_SAVE_AS " Save As")) {
712 }
713
714 Separator();
715
716 // Sprite list
717 Text(tr("Loaded Sprites:"));
718 const float avail_y = std::max(0.0f, ImGui::GetContentRegionAvail().y);
719 const float metadata_reserve = std::min(160.0f, avail_y * 0.45f);
720 const float list_budget = std::max(0.0f, avail_y - metadata_reserve);
721 const float preferred_list = std::clamp(avail_y * 0.35f, 180.0f, 280.0f);
722 const float list_height = std::min(preferred_list, list_budget);
723 if (ImGui::BeginChild("SpriteList", ImVec2(0, list_height), true)) {
724 for (size_t i = 0; i < custom_sprites_.size(); i++) {
725 std::string label = custom_sprites_[i].sprName.empty()
726 ? "Unnamed Sprite"
727 : custom_sprites_[i].sprName;
728 ImGui::PushID(static_cast<int>(i));
729 if (Selectable(label.c_str(), current_custom_sprite_index_ == (int)i)) {
730 current_custom_sprite_index_ = static_cast<int>(i);
731 current_frame_ = custom_sprites_[i].editor.Frames.empty() ? -1 : 0;
733 custom_sprites_[i].animations.empty() ? -1 : 0;
736 animation_playing_ = false;
737 frame_timer_ = 0.0f;
741 }
742 ImGui::PopID();
743 }
744 }
745 ImGui::EndChild();
746
747 Separator();
748
749 // Show properties for selected sprite
754 if (gui::BeginThemedTabBar("SpriteDataTabs")) {
755 if (ImGui::BeginTabItem(tr("Properties"))) {
757 ImGui::EndTabItem();
758 }
759 if (ImGui::BeginTabItem(tr("Animations"))) {
761 ImGui::EndTabItem();
762 }
763 if (ImGui::BeginTabItem(tr("Behavior"))) {
765 ImGui::EndTabItem();
766 }
767 if (ImGui::BeginTabItem(tr("Routines"))) {
769 ImGui::EndTabItem();
770 }
772 }
773 } else {
774 Text(tr("No sprite selected"));
775 }
777}
778
779absl::Status SpriteEditor::Copy() {
781 current_custom_sprite_index_ >= static_cast<int>(custom_sprites_.size()))
782 return absl::FailedPreconditionError(
783 "Select a custom sprite frame to copy");
784 const auto& frames =
786 if (current_frame_ < 0 || current_frame_ >= static_cast<int>(frames.size()))
787 return absl::FailedPreconditionError("Select a frame to copy");
789 return absl::OkStatus();
790}
791
792absl::Status SpriteEditor::Cut() {
795 if (sprite.editor.Frames.size() <= 1)
796 return absl::FailedPreconditionError(
797 "Cannot cut the last frame; copied it instead");
801 static_cast<int>(sprite.editor.Frames.size()) - 1);
805 return absl::OkStatus();
806}
807
808absl::Status SpriteEditor::Paste() {
810 current_custom_sprite_index_ >= static_cast<int>(custom_sprites_.size()))
811 return absl::FailedPreconditionError(
812 "Copy a frame and select a destination sprite");
814 if (sprite.editor.Frames.size() >= sprite_authoring::kMaxFrames)
815 return absl::OutOfRangeError("ZSM animations address at most 256 frames");
817 sprite.editor.Frames.push_back(*frame_clipboard_);
818 current_frame_ = static_cast<int>(sprite.editor.Frames.size()) - 1;
822 return absl::OkStatus();
823}
824
826 zsprite::ZSprite new_sprite;
827 new_sprite.Reset();
828 new_sprite.sprName = "New Sprite";
829
830 // Add default frame
831 new_sprite.editor.Frames.emplace_back();
832
833 // Add default animation
834 new_sprite.animations.emplace_back(0, 0, 1, "Idle");
835
836 custom_sprites_.push_back(std::move(new_sprite));
837 custom_sprite_paths_.push_back(std::string());
839 std::copy_n(current_sheets_, 8, binding.sheets.begin());
840 custom_sprite_bindings_.push_back(std::move(binding));
841 source_checked_ = false;
842 current_custom_sprite_index_ = static_cast<int>(custom_sprites_.size()) - 1;
843 current_frame_ = 0;
847 animation_playing_ = false;
848 frame_timer_ = 0.0f;
849 zsm_dirty_ = true;
851}
852
853void SpriteEditor::LoadZsmFile(const std::string& path) {
854 zsprite::ZSprite sprite;
855 status_ = sprite.Load(path);
856 if (status_.ok()) {
857 custom_sprites_.push_back(std::move(sprite));
858 custom_sprite_paths_.push_back(path);
860 binding.zsm_path = path;
861 std::copy_n(current_sheets_, 8, binding.sheets.begin());
862 if (project()) {
863 for (const auto& saved : project()->sprite_assets) {
864 if (project()->GetAbsolutePath(saved.zsm_path) == path) {
865 binding = saved;
866 break;
867 }
868 }
869 }
870 custom_sprite_bindings_.push_back(std::move(binding));
871 current_custom_sprite_index_ = static_cast<int>(custom_sprites_.size()) - 1;
872 current_frame_ = custom_sprites_.back().editor.Frames.empty() ? -1 : 0;
874 custom_sprites_.back().animations.empty() ? -1 : 0;
877 animation_playing_ = false;
878 frame_timer_ = 0.0f;
879 zsm_dirty_ = false;
883 }
884}
885
886void SpriteEditor::SaveZsmFile(const std::string& path) {
890 if (status_.ok()) {
891 SetCurrentZsmPath(path);
893 binding.zsm_path = path;
894 if (project()) {
895 auto& assets = project()->sprite_assets;
896 auto found =
897 std::find_if(assets.begin(), assets.end(), [&](const auto& asset) {
898 return project()->GetAbsolutePath(asset.zsm_path) == path;
899 });
900 if (found == assets.end())
901 assets.push_back(binding);
902 else
903 *found = binding;
904 }
905 zsm_dirty_ = false;
906 }
907 }
908}
909
912 std::string path =
914 if (!path.empty()) {
915 SaveZsmFile(path);
916 }
917 }
918}
919
920// ============================================================
921// Properties Panel
922// ============================================================
923
926
927 // Basic info
928 Text(tr("Sprite Info"));
929 Separator();
930
931 static char name_buf[256];
932 CopyStringToBuffer(sprite.sprName, name_buf);
933 if (ImGui::InputText(tr("Name"), name_buf, sizeof(name_buf))) {
934 sprite.sprName = name_buf;
935 sprite.property_sprname.Text = name_buf;
937 }
938
939 static char id_buf[32];
940 CopyStringToBuffer(sprite.property_sprid.Text, id_buf);
941 if (ImGui::InputText(tr("Sprite ID"), id_buf, sizeof(id_buf))) {
942 sprite.property_sprid.Text = id_buf;
944 }
945
946 Separator();
948
949 Separator();
951}
952
955
956 Text(tr("Stats"));
957
958 // Use InputInt for numeric values
959 int prize = ParseIntOrDefault(sprite.property_prize.Text);
960 if (ImGui::InputInt(tr("Prize"), &prize)) {
961 sprite.property_prize.Text = std::to_string(std::clamp(prize, 0, 255));
963 }
964
965 int palette = ParseIntOrDefault(sprite.property_palette.Text);
966 if (ImGui::InputInt(tr("Palette"), &palette)) {
967 sprite.property_palette.Text = std::to_string(std::clamp(palette, 0, 7));
969 }
970
971 int oamnbr = ParseIntOrDefault(sprite.property_oamnbr.Text);
972 if (ImGui::InputInt(tr("OAM Count"), &oamnbr)) {
973 sprite.property_oamnbr.Text = std::to_string(std::clamp(oamnbr, 0, 255));
975 }
976
977 int hitbox = ParseIntOrDefault(sprite.property_hitbox.Text);
978 if (ImGui::InputInt(tr("Hitbox"), &hitbox)) {
979 sprite.property_hitbox.Text = std::to_string(std::clamp(hitbox, 0, 255));
981 }
982
983 int health = ParseIntOrDefault(sprite.property_health.Text);
984 if (ImGui::InputInt(tr("Health"), &health)) {
985 sprite.property_health.Text = std::to_string(std::clamp(health, 0, 255));
987 }
988
989 int damage = ParseIntOrDefault(sprite.property_damage.Text);
990 if (ImGui::InputInt(tr("Damage"), &damage)) {
991 sprite.property_damage.Text = std::to_string(std::clamp(damage, 0, 255));
993 }
994}
995
998
999 Text(tr("Behavior Flags"));
1000
1001 // Two columns for boolean properties
1002 if (ImGui::BeginTable("BoolProps", 2, ImGuiTableFlags_None)) {
1003 // Column 1
1004 ImGui::TableNextColumn();
1005 if (ImGui::Checkbox(tr("Blockable"), &sprite.property_blockable.IsChecked))
1007 if (ImGui::Checkbox(tr("Can Fall"), &sprite.property_canfall.IsChecked))
1009 if (ImGui::Checkbox(tr("Collision Layer"),
1010 &sprite.property_collisionlayer.IsChecked))
1012 if (ImGui::Checkbox(tr("Custom Death"),
1013 &sprite.property_customdeath.IsChecked))
1015 if (ImGui::Checkbox(tr("Damage Sound"),
1016 &sprite.property_damagesound.IsChecked))
1018 if (ImGui::Checkbox(tr("Deflect Arrows"),
1019 &sprite.property_deflectarrows.IsChecked))
1021 if (ImGui::Checkbox(tr("Deflect Projectiles"),
1022 &sprite.property_deflectprojectiles.IsChecked))
1024 if (ImGui::Checkbox(tr("Fast"), &sprite.property_fast.IsChecked))
1026 if (ImGui::Checkbox(tr("Harmless"), &sprite.property_harmless.IsChecked))
1028 if (ImGui::Checkbox(tr("Impervious"),
1029 &sprite.property_impervious.IsChecked))
1031
1032 // Column 2
1033 ImGui::TableNextColumn();
1034 if (ImGui::Checkbox(tr("Impervious Arrow"),
1035 &sprite.property_imperviousarrow.IsChecked))
1037 if (ImGui::Checkbox(tr("Impervious Melee"),
1038 &sprite.property_imperviousmelee.IsChecked))
1040 if (ImGui::Checkbox(tr("Interaction"),
1041 &sprite.property_interaction.IsChecked))
1043 if (ImGui::Checkbox(tr("Is Boss"), &sprite.property_isboss.IsChecked))
1045 if (ImGui::Checkbox(tr("Persist"), &sprite.property_persist.IsChecked))
1047 if (ImGui::Checkbox(tr("Shadow"), &sprite.property_shadow.IsChecked))
1049 if (ImGui::Checkbox(tr("Small Shadow"),
1050 &sprite.property_smallshadow.IsChecked))
1052 if (ImGui::Checkbox(tr("Stasis"), &sprite.property_statis.IsChecked))
1054 if (ImGui::Checkbox(tr("Statue"), &sprite.property_statue.IsChecked))
1056 if (ImGui::Checkbox(tr("Water Sprite"),
1057 &sprite.property_watersprite.IsChecked))
1059
1060 ImGui::EndTable();
1061 }
1062}
1063
1064// ============================================================
1065// Animation Panel
1066// ============================================================
1067
1070
1071 if (ImGui::Button(tr("Copy draw tables (ASM)"))) {
1072 auto tables = ExportCurrentSpriteDraw();
1073 draw_export_status_ = tables.status();
1074 if (tables.ok())
1075 ImGui::SetClipboardText(tables->c_str());
1076 }
1077 HOVER_HINT(
1078 "Copy a draw-table candidate. Does not register or patch a sprite.");
1079 if (!draw_export_status_.ok())
1080 ImGui::TextWrapped("%s", draw_export_status_.ToString().c_str());
1081
1082 // Playback controls
1083 if (animation_playing_) {
1084 if (ImGui::Button(ICON_MD_STOP " Stop")) {
1085 animation_playing_ = false;
1086 }
1087 } else {
1088 if (ImGui::Button(ICON_MD_PLAY_ARROW " Play")) {
1089 animation_playing_ = true;
1090 frame_timer_ = 0.0f;
1091 }
1092 }
1093 ImGui::SameLine();
1094 if (ImGui::Button(ICON_MD_SKIP_PREVIOUS)) {
1095 if (current_frame_ > 0)
1097 preview_needs_update_ = true;
1098 }
1099 HOVER_HINT("Previous Frame");
1100 ImGui::SameLine();
1101 if (ImGui::Button(ICON_MD_SKIP_NEXT)) {
1102 if (current_frame_ < (int)sprite.editor.Frames.size() - 1)
1104 preview_needs_update_ = true;
1105 }
1106 HOVER_HINT("Next Frame");
1107 ImGui::SameLine();
1108 Text(tr("Frame: %d / %d"), current_frame_,
1109 (int)sprite.editor.Frames.size() - 1);
1110
1111 Separator();
1112
1113 // Animation list
1114 Text(tr("Animations"));
1115 if (ImGui::Button(ICON_MD_ADD " Add Animation")) {
1116 int frame_count = static_cast<int>(sprite.editor.Frames.size());
1117 sprite.animations.emplace_back(0, std::clamp(frame_count - 1, 0, 255), 1,
1118 "New Animation");
1120 }
1121 HOVER_HINT("Add a new animation sequence");
1122
1123 if (ImGui::BeginChild("AnimList", ImVec2(0, 120), true)) {
1124 for (size_t i = 0; i < sprite.animations.size(); i++) {
1125 auto& anim = sprite.animations[i];
1126 std::string label = anim.frame_name.empty() ? "Unnamed" : anim.frame_name;
1127 ImGui::PushID(static_cast<int>(i));
1128 if (Selectable(label.c_str(), current_animation_index_ == (int)i)) {
1129 current_animation_index_ = static_cast<int>(i);
1130 current_frame_ = anim.frame_start;
1131 frame_timer_ = 0;
1132 preview_needs_update_ = true;
1133 }
1134 ImGui::PopID();
1135 }
1136 }
1137 ImGui::EndChild();
1138
1139 // Edit selected animation
1140 if (current_animation_index_ >= 0 &&
1141 current_animation_index_ < (int)sprite.animations.size()) {
1142 auto& anim = sprite.animations[current_animation_index_];
1143
1144 Separator();
1145 Text(tr("Animation Properties"));
1146
1147 static char anim_name[128];
1148 CopyStringToBuffer(anim.frame_name, anim_name);
1149 if (ImGui::InputText(tr("Name##Anim"), anim_name, sizeof(anim_name))) {
1150 anim.frame_name = anim_name;
1152 }
1153
1154 int start = anim.frame_start;
1155 int end = anim.frame_end;
1156 int speed = anim.frame_speed;
1157
1158 if (ImGui::SliderInt(
1159 tr("Start Frame"), &start, 0,
1160 std::clamp((int)sprite.editor.Frames.size() - 1, 0, 255))) {
1161 anim.frame_start = static_cast<uint8_t>(start);
1162 anim.frame_end = std::max(anim.frame_start, anim.frame_end);
1163 end = anim.frame_end;
1165 }
1166 if (ImGui::SliderInt(
1167 tr("End Frame"), &end, 0,
1168 std::clamp((int)sprite.editor.Frames.size() - 1, 0, 255))) {
1169 anim.frame_end = static_cast<uint8_t>(end);
1170 anim.frame_start = std::min(anim.frame_start, anim.frame_end);
1172 }
1173 if (ImGui::SliderInt(tr("Ticks per frame (60 Hz)"), &speed, 1, 255)) {
1174 anim.frame_speed = static_cast<uint8_t>(speed);
1176 }
1177
1178 auto& actions =
1180 bool used_by_action =
1181 std::any_of(actions.begin(), actions.end(), [&](const auto& action) {
1182 return action.animation == current_animation_index_;
1183 });
1184 ImGui::BeginDisabled(used_by_action);
1185 if (ImGui::Button(tr("Delete Animation")) && sprite.animations.size() > 1) {
1186 for (auto& action : actions)
1187 if (action.animation > current_animation_index_)
1188 --action.animation;
1189 sprite.animations.erase(sprite.animations.begin() +
1192 std::min(current_animation_index_, (int)sprite.animations.size() - 1);
1194 }
1195 ImGui::EndDisabled();
1196 if (used_by_action)
1197 ImGui::TextDisabled(
1198 "Assign another animation to referencing actions before deleting.");
1199 HOVER_HINT("Delete the selected animation");
1200 }
1201
1202 Separator();
1204}
1205
1208
1209 Text(tr("Frames"));
1210 if (ImGui::Button(ICON_MD_ADD " Add Frame")) {
1211 if (sprite_authoring::AppendFrame(sprite)) {
1212 current_frame_ = static_cast<int>(sprite.editor.Frames.size()) - 1;
1215 }
1216 }
1217 HOVER_HINT("Add a new animation frame");
1218 ImGui::SameLine();
1219 if (ImGui::Button(ICON_MD_DELETE " Delete Frame") &&
1220 sprite.editor.Frames.size() > 1 && current_frame_ >= 0) {
1223 std::min(current_frame_, (int)sprite.editor.Frames.size() - 1);
1225 frame_timer_ = 0;
1227 }
1228 }
1229 HOVER_HINT("Delete the current frame and repair animation ranges");
1230 if (ImGui::Button(tr("Duplicate Frame")) && current_frame_ >= 0 &&
1232 current_frame_ = static_cast<int>(sprite.editor.Frames.size()) - 1;
1235 }
1236 ImGui::TextDisabled("Up to 256 frames; duplicate appends a frame.");
1237
1238 // Frame selector
1239 if (ImGui::BeginChild("FrameList", ImVec2(0, 80), true,
1240 ImGuiWindowFlags_HorizontalScrollbar)) {
1241 for (size_t i = 0; i < sprite.editor.Frames.size(); i++) {
1242 ImGui::PushID(static_cast<int>(i));
1243 std::string label = absl::StrFormat("F%d", i);
1244 if (Selectable(label.c_str(), current_frame_ == (int)i,
1245 ImGuiSelectableFlags_None, ImVec2(40, 40))) {
1246 current_frame_ = static_cast<int>(i);
1247 preview_needs_update_ = true;
1248 }
1249 ImGui::SameLine();
1250 ImGui::PopID();
1251 }
1252 }
1253 ImGui::EndChild();
1254
1255 // Edit tiles in current frame
1256 if (current_frame_ >= 0 &&
1257 current_frame_ < (int)sprite.editor.Frames.size()) {
1258 auto& frame = sprite.editor.Frames[current_frame_];
1259
1260 Separator();
1261 Text(tr("Tiles in Frame %d"), current_frame_);
1262
1263 if (ImGui::Button(ICON_MD_ADD " Add Tile")) {
1264 frame.Tiles.emplace_back();
1265 frame.Tiles.back().x = sprite_authoring::kOriginX;
1266 frame.Tiles.back().y = sprite_authoring::kOriginY;
1267 selected_tile_index_ = static_cast<int>(frame.Tiles.size()) - 1;
1269 preview_needs_update_ = true;
1270 }
1271 HOVER_HINT("Add a new tile to this frame");
1272
1273 if (ImGui::BeginChild("TileList", ImVec2(0, 100), true)) {
1274 for (size_t i = 0; i < frame.Tiles.size(); i++) {
1275 auto& tile = frame.Tiles[i];
1276 std::string label = absl::StrFormat("Tile %d (ID: %d)", i, tile.id);
1277 if (Selectable(label.c_str(), selected_tile_index_ == (int)i)) {
1278 selected_tile_index_ = static_cast<int>(i);
1279 }
1280 }
1281 }
1282 ImGui::EndChild();
1283
1284 // Edit selected tile
1285 if (selected_tile_index_ >= 0 &&
1286 selected_tile_index_ < (int)frame.Tiles.size()) {
1287 auto& tile = frame.Tiles[selected_tile_index_];
1288
1289 int tile_id = tile.id;
1290 if (ImGui::InputInt(tr("Tile ID"), &tile_id)) {
1291 tile.id = static_cast<uint16_t>(std::clamp(tile_id, 0, 511));
1293 preview_needs_update_ = true;
1294 }
1295
1296 int x = sprite_authoring::OffsetX(tile),
1297 y = sprite_authoring::OffsetY(tile);
1298 if (ImGui::InputInt(tr("X offset"), &x)) {
1299 tile.x = static_cast<uint8_t>(std::clamp(x, -128, 127) +
1302 preview_needs_update_ = true;
1303 }
1304 if (ImGui::InputInt(tr("Y offset"), &y)) {
1305 tile.y = static_cast<uint8_t>(std::clamp(y, -112, 143) +
1308 preview_needs_update_ = true;
1309 }
1310
1311 int pal = tile.palette;
1312 if (ImGui::SliderInt(tr("Palette##Tile"), &pal, 0, 7)) {
1313 tile.palette = static_cast<uint8_t>(pal);
1315 preview_needs_update_ = true;
1316 }
1317
1318 int priority = tile.priority;
1319 if (ImGui::SliderInt(tr("BG priority"), &priority, 0, 3)) {
1320 tile.priority = static_cast<uint8_t>(priority);
1322 }
1323 ImGui::TextDisabled(
1324 "BG priority is stored; preview has no background layers.");
1325 if (ImGui::Checkbox(tr("16x16"), &tile.size)) {
1327 preview_needs_update_ = true;
1328 }
1329 ImGui::SameLine();
1330 if (ImGui::Checkbox(tr("Flip X"), &tile.mirror_x)) {
1332 preview_needs_update_ = true;
1333 }
1334 ImGui::SameLine();
1335 if (ImGui::Checkbox(tr("Flip Y"), &tile.mirror_y)) {
1337 preview_needs_update_ = true;
1338 }
1339
1340 if (ImGui::Button(tr("Edit selected OAM pixels"))) {
1342 }
1343 if (!graphics_context_status_.ok()) {
1344 ImGui::TextWrapped(
1345 "%s", std::string(graphics_context_status_.message()).c_str());
1346 }
1347
1348 if (ImGui::Button(tr("Duplicate Tile"))) {
1349 const auto copy = tile;
1350 frame.Tiles.push_back(copy);
1351 selected_tile_index_ = static_cast<int>(frame.Tiles.size()) - 1;
1353 }
1354 if (ImGui::Button(tr("Delete Tile"))) {
1355 frame.Tiles.erase(frame.Tiles.begin() + selected_tile_index_);
1358 preview_needs_update_ = true;
1359 }
1360 HOVER_HINT("Delete the selected tile");
1361 }
1362 }
1363}
1364
1368 return;
1369 }
1370
1372 if (current_animation_index_ < 0 ||
1373 current_animation_index_ >= (int)sprite.animations.size()) {
1374 return;
1375 }
1376
1377 auto& anim = sprite.animations[current_animation_index_];
1378
1379 if (sprite_authoring::Advance(anim, sprite.editor.Frames.size(), delta_time,
1381 preview_needs_update_ = true;
1382 }
1383}
1384
1385// ============================================================
1386// User Routines Panel
1387// ============================================================
1388
1391
1392 if (ImGui::Button(ICON_MD_ADD " Add Routine")) {
1393 sprite.userRoutines.emplace_back("New Routine", "; ASM code here\n");
1395 }
1396 HOVER_HINT("Add a new ASM routine");
1397
1398 // Routine list
1399 if (ImGui::BeginChild("RoutineList", ImVec2(0, 100), true)) {
1400 for (size_t i = 0; i < sprite.userRoutines.size(); i++) {
1401 auto& routine = sprite.userRoutines[i];
1402 if (Selectable(routine.name.c_str(), selected_routine_index_ == (int)i)) {
1403 selected_routine_index_ = static_cast<int>(i);
1404 }
1405 }
1406 }
1407 ImGui::EndChild();
1408
1409 // Edit selected routine
1410 if (selected_routine_index_ >= 0 &&
1411 selected_routine_index_ < (int)sprite.userRoutines.size()) {
1412 auto& routine = sprite.userRoutines[selected_routine_index_];
1413
1414 Separator();
1415
1416 static char routine_name[128];
1417 CopyStringToBuffer(routine.name, routine_name);
1418 if (ImGui::InputText(tr("Routine Name"), routine_name,
1419 sizeof(routine_name))) {
1420 routine.name = routine_name;
1422 }
1423
1424 Text(tr("ASM Code:"));
1425
1426 // Multiline text input for code
1427 static char code_buffer[16384];
1428 CopyStringToBuffer(routine.code, code_buffer);
1429 if (ImGui::InputTextMultiline("##RoutineCode", code_buffer,
1430 sizeof(code_buffer), ImVec2(-1, 200))) {
1431 routine.code = code_buffer;
1433 }
1434
1435 if (ImGui::Button(tr("Delete Routine"))) {
1436 sprite.userRoutines.erase(sprite.userRoutines.begin() +
1440 }
1441 HOVER_HINT("Delete the selected routine");
1442 }
1443}
1444
1445// ============================================================
1446// Graphics Pipeline
1447// ============================================================
1448
1450 const bool owner_changed = preview_graphics_owner_ != game_data();
1451 const auto palette_revision =
1453 if (owner_changed || preview_palette_revision_ != palette_revision) {
1454 room_palette_cache_.reset();
1455 preview_palette_revision_ = palette_revision;
1457 preview_needs_update_ = true;
1458 }
1459 bool changed = !gfx_buffer_loaded_ || owner_changed;
1460 for (int slot = 0; slot < 8; ++slot) {
1461 const auto revision =
1463 : 0;
1464 changed |= loaded_preview_sheets_[slot] != current_sheets_[slot] ||
1465 loaded_preview_revisions_[slot] != revision;
1466 }
1467 if (changed) {
1470 for (int slot = 0; slot < 8; ++slot) {
1474 : 0;
1475 }
1477 preview_needs_update_ = true;
1478 }
1479 LoadSpritePalettes(use_asset_binding);
1480}
1481
1483 // Combine selected sheets (current_sheets_[0-7]) into single 8BPP buffer
1484 // Layout: 16 tiles per row, 8 rows per sheet, 8 sheets total = 64 tile rows
1485 // Buffer size: 0x10000 bytes (65536)
1486
1487 sprite_gfx_buffer_.assign(0x10000, 0);
1488 graphics_binding_status_ = absl::OkStatus();
1489
1490 // Each sheet is 128x32 pixels (128 bytes per row, 32 rows) = 4096 bytes
1491 // We combine 8 sheets vertically: 128x256 pixels total
1492 constexpr int kSheetWidth = 128;
1493 constexpr int kSheetHeight = 32;
1494 constexpr int kRowStride = 128;
1495
1496 for (int sheet_idx = 0; sheet_idx < 8; sheet_idx++) {
1497 uint8_t sheet_id = current_sheets_[sheet_idx];
1498 if (game_data() == nullptr ||
1499 !game_data()->sheet_store.HasSheet(sheet_id)) {
1500 graphics_binding_status_ = absl::FailedPreconditionError(absl::StrFormat(
1501 "Graphics sheet $%02X in slot %d is unavailable in this ROM session",
1502 sheet_id, sheet_idx));
1503 continue;
1504 }
1505 const auto sheet = game_data()->sheet_store.Sheet(sheet_id);
1506
1507 // Copy sheet data to buffer at appropriate offset
1508 // Each sheet occupies 8 tile rows (8 * 8 scanlines = 64 scanlines)
1509 // Offset = sheet_idx * (8 tile rows * 1024 bytes per tile row)
1510 // But sheets are 32 pixels tall (4 tile rows), so:
1511 // Offset = sheet_idx * 4 * 1024 = sheet_idx * 4096
1512 int dest_offset = sheet_idx * (kSheetHeight * kRowStride);
1513
1514 const uint8_t* src_data = sheet.data();
1515 size_t copy_size =
1516 std::min(sheet.size(), static_cast<size_t>(kSheetWidth * kSheetHeight));
1517
1518 if (dest_offset + copy_size <= sprite_gfx_buffer_.size()) {
1519 std::memcpy(sprite_gfx_buffer_.data() + dest_offset, src_data, copy_size);
1520 }
1521 }
1522
1523 // Update drawer with new buffer
1525 gfx_buffer_loaded_ = true;
1526}
1527
1528void SpriteEditor::LoadSpritePalettes(bool use_asset_binding) {
1529 // Load sprite palettes from ROM palette groups
1530 // ALTTP sprites use a combination of palette groups:
1531 // - Rows 0-1: Global sprite palettes (shared by all sprites)
1532 // - Rows 2-7: Aux palettes (vary by sprite type)
1533 //
1534 // For simplicity, we load global_sprites which contains the main
1535 // sprite palettes. More accurate rendering would require looking up
1536 // which aux palette group each sprite type uses.
1537
1538 if (!rom_ || !rom_->is_loaded() || !game_data()) {
1540 palette_binding_status_ = absl::FailedPreconditionError(
1541 "Load ROM graphics and palettes before previewing sprites");
1542 return;
1543 }
1544
1545 // Build combined sprite palette from global + aux groups
1547
1548 // Add global sprite palettes (typically 2 palettes, 16 colors each)
1549 const auto& global = game_data()->palette_groups.global_sprites;
1550 const auto& aux1 = game_data()->palette_groups.sprites_aux1;
1551 const auto& aux2 = game_data()->palette_groups.sprites_aux2;
1552 const auto& aux3 = game_data()->palette_groups.sprites_aux3;
1554 internal::DefaultSpritePreviewPalettes(global, aux1, aux2, aux3);
1555
1556 palette_binding_status_ = absl::OkStatus();
1557 if (global.empty() && aux1.empty() && aux2.empty() && aux3.empty() &&
1559 palette_binding_status_ = absl::FailedPreconditionError(
1560 "Sprite palette colors are unavailable in this ROM session");
1561 }
1562 if (const auto* binding =
1563 use_asset_binding ? current_sprite_binding() : nullptr) {
1564 auto palettes = sprite_authoring::BindPaletteRows(
1565 *binding, sprite_palettes_, global, aux1, aux2, aux3);
1566 palette_binding_status_ = palettes.status();
1567 if (palettes.ok())
1568 sprite_palettes_ = std::move(*palettes);
1569 else
1571 }
1572 if (preview_use_room_palette_ && game_data() != nullptr) {
1573 if (!room_palette_cache_.has_value() ||
1579 }
1581 palette_binding_status_ = absl::OkStatus();
1582 }
1584}
1585
1587 // Ensure graphics buffer is loaded
1589
1590 // Initialize vanilla preview bitmap if needed. The helper also queues a
1591 // CREATE texture command so canvas_rendering doesn't silently skip the
1592 // draw, and stamps the bitmap as kCompositeOutput.
1595
1597 return;
1598 }
1599
1600 // Clear and render
1602
1603 // Origin is center of bitmap
1604 int origin_x = 64;
1605 int origin_y = 64;
1606
1610 origin_x, origin_y);
1611 }
1612
1613 // Build combined 128-color palette (8 sub-palettes × 16 colors)
1614 // and apply to bitmap for proper color rendering
1615 if (sprite_palettes_.size() > 0) {
1616 gfx::SnesPalette combined_palette;
1617 for (size_t pal_idx = 0; pal_idx < 8 && pal_idx < sprite_palettes_.size();
1618 pal_idx++) {
1619 const auto& sub_pal = sprite_palettes_.palette(pal_idx);
1620 for (size_t col_idx = 0; col_idx < 16 && col_idx < sub_pal.size();
1621 col_idx++) {
1622 combined_palette.AddColor(sub_pal[col_idx]);
1623 }
1624 // Pad to 16 if sub-palette is smaller
1625 while (combined_palette.size() < (pal_idx + 1) * 16) {
1626 combined_palette.AddColor(gfx::SnesColor(0));
1627 }
1628 }
1629 vanilla_preview_bitmap_.SetPalette(combined_palette);
1630 }
1631
1632 // Surface pixels and palette were just mutated above; queue an UPDATE so
1633 // the GPU texture reflects the new state on the next frame. Without this,
1634 // the texture stays frozen at first-CREATE state forever.
1636
1638}
1639
1640// ============================================================
1641// Canvas Rendering
1642// ============================================================
1643
1647 return;
1648 }
1649
1651 if (frame_index < 0 || frame_index >= (int)sprite.editor.Frames.size()) {
1652 return;
1653 }
1654
1655 auto& frame = sprite.editor.Frames[frame_index];
1656
1657 // Ensure graphics buffer is loaded
1659
1660 // The sprite_canvas_ displays a post-render composite (pixels come from
1661 // SpriteDrawer, not direct user paint). Declaring the role lets future
1662 // canvas surfaces (cursor hints, context-menu items) default appropriately.
1664
1665 // Initialize preview bitmap if needed. The helper also queues a CREATE
1666 // texture command so canvas_rendering doesn't silently skip the draw, and
1667 // stamps the bitmap as kCompositeOutput.
1670
1671 // Only render if drawer is ready
1673 // Clear and render to preview bitmap
1675
1676 // Origin is center of canvas (128, 128 for 256x256 bitmap).
1679
1680 // Build combined 128-color palette and apply to bitmap
1681 if (sprite_palettes_.size() > 0) {
1682 gfx::SnesPalette combined_palette;
1683 for (size_t pal_idx = 0; pal_idx < 8 && pal_idx < sprite_palettes_.size();
1684 pal_idx++) {
1685 const auto& sub_pal = sprite_palettes_.palette(pal_idx);
1686 for (size_t col_idx = 0; col_idx < 16 && col_idx < sub_pal.size();
1687 col_idx++) {
1688 combined_palette.AddColor(sub_pal[col_idx]);
1689 }
1690 // Pad to 16 if sub-palette is smaller
1691 while (combined_palette.size() < (pal_idx + 1) * 16) {
1692 combined_palette.AddColor(gfx::SnesColor(0));
1693 }
1694 }
1695 sprite_preview_bitmap_.SetPalette(combined_palette);
1696 }
1697
1698 // Surface pixels and palette were just mutated above; queue an UPDATE so
1699 // the GPU texture reflects the new frame on the next paint.
1701
1702 // Mark as updated
1703 preview_needs_update_ = false;
1704 }
1705
1706 // Draw the preview bitmap on canvas
1709 }
1710
1711 // Draw tile outlines for selection (over the bitmap)
1712 if (show_tile_grid_) {
1713 for (size_t i = 0; i < frame.Tiles.size(); i++) {
1714 const auto& tile = frame.Tiles[i];
1715 int tile_size = tile.size ? 16 : 8;
1716
1717 // Convert signed tile position to canvas position
1718 int signed_x = sprite_authoring::OffsetX(tile);
1719 int signed_y = sprite_authoring::OffsetY(tile);
1720
1721 int canvas_x = 128 + signed_x;
1722 int canvas_y = 128 + signed_y;
1723
1724 // Highlight selected tile
1725 ImVec4 color = (selected_tile_index_ == static_cast<int>(i))
1726 ? ImVec4(0.0f, 1.0f, 0.0f, 0.8f) // Green for selected
1727 : ImVec4(1.0f, 1.0f, 0.0f, 0.3f); // Yellow for others
1728
1729 // Match the 2x bitmap scale used above.
1730 sprite_canvas_.DrawRect(canvas_x * 2, canvas_y * 2, tile_size * 2,
1731 tile_size * 2, color);
1732 }
1733 }
1734}
1735
1737 if (ImGui::BeginChild(gui::GetID("##ZSpriteCanvas"),
1738 ImGui::GetContentRegionAvail(), true)) {
1741
1742 // Render current frame if we have a sprite selected
1747 ImGui::SetCursorPos(ImVec2(10, 34));
1748 const auto& error = !graphics_binding_status_.ok()
1751 ImGui::TextWrapped("Preview unavailable: %s",
1752 std::string(error.message()).c_str());
1753 }
1754 } else {
1755 ImGui::SetCursorPos(ImVec2(10, 10));
1756 ImGui::TextWrapped(
1757 "Open a ZSM or import a draw copy to preview its frames.");
1758 }
1759
1762
1763 // Display current frame info
1766 ImGui::SetCursorPos(ImVec2(10, 10));
1767 char label[256];
1768 std::snprintf(label, sizeof(label), tr("Frame: %d | Tiles: %d"),
1770 current_frame_ >= 0 &&
1771 current_frame_ < (int)sprite.editor.Frames.size()
1772 ? (int)sprite.editor.Frames[current_frame_].Tiles.size()
1773 : 0);
1774 const ImVec2 position = ImGui::GetCursorScreenPos();
1775 const ImVec2 size = ImGui::CalcTextSize(label);
1776 // Preview colors come from the ROM; keep status legible on any palette.
1777 ImGui::GetWindowDrawList()->AddRectFilled(
1778 ImVec2(position.x - 3, position.y - 2),
1779 ImVec2(position.x + size.x + 3, position.y + size.y + 2),
1780 IM_COL32(24, 24, 24, 255), 3.0f);
1781 ImGui::TextColored(ImVec4(1, 1, 1, 1), "%s", label);
1782 }
1783 }
1784 ImGui::EndChild();
1785}
1786
1787// ============================================================
1788// Undo/Redo Helpers
1789// ============================================================
1790
1803
1805 if (snapshot.sprite_index < 0 ||
1806 snapshot.sprite_index >= static_cast<int>(custom_sprites_.size())) {
1807 return;
1808 }
1810 current_frame_ = snapshot.current_frame;
1812 custom_sprites_[snapshot.sprite_index] = snapshot.sprite_data;
1813 custom_sprite_bindings_[snapshot.sprite_index] = snapshot.binding;
1815 preview_needs_update_ = true;
1816 zsm_dirty_ = true;
1817}
1818
1821 return; // Already in a transaction
1822 }
1825}
1826
1829 return;
1830 }
1831
1833 // No mutation happened; discard the pending snapshot so we can
1834 // capture a fresh one next frame.
1835 undo_snapshot_pending_ = false;
1836 return;
1837 }
1838
1839 undo_snapshot_pending_ = false;
1841
1842 auto after = CaptureCurrentSpriteSnapshot();
1843 auto restore_fn = [this](const SpriteSnapshot& snapshot) {
1844 RestoreFromSnapshot(snapshot);
1845 };
1846 undo_manager_.Push(std::make_unique<SpriteEditAction>(
1847 std::move(undo_before_snapshot_), std::move(after),
1848 std::move(restore_fn)));
1849}
1850
1857
1859 if (custom_sprite_paths_.size() < custom_sprites_.size()) {
1861 }
1862}
1863
1864const std::string& SpriteEditor::GetCurrentZsmPath() const {
1865 static const std::string kEmptyPath;
1868 static_cast<int>(custom_sprite_paths_.size())) {
1869 return kEmptyPath;
1870 }
1872}
1873
1874void SpriteEditor::SetCurrentZsmPath(const std::string& path) {
1878 static_cast<int>(custom_sprite_paths_.size())) {
1879 return;
1880 }
1882}
1883
1884} // namespace editor
1885} // namespace yaze
project::ResourceLabelManager * resource_label()
Definition rom.h:225
bool is_loaded() const
Definition rom.h:187
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
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 SetGraphicsBuffer(const uint8_t *buffer)
Set the graphics buffer for tile lookup.
ImVector< int > active_sprites_
absl::Status OpenSpriteAsset(const std::string &path)
void UpdateAnimationPlayback(float delta_time)
std::vector< project::SpriteAssetBinding > custom_sprite_bindings_
std::vector< zelda3::SpriteTileIssue > tile_issues_
absl::Status OpenSelectedOamGraphics()
void ApplySpritesetToSheets(int spriteset)
const zsprite::ZSprite * current_custom_sprite() const
void RefreshSpritePreviewGraphics(bool use_asset_binding)
std::array< uint64_t, 8 > loaded_preview_revisions_
void RenderZSpriteFrame(int frame_index)
absl::Status Paste() override
std::array< uint8_t, 8 > loaded_preview_sheets_
absl::StatusOr< std::string > ExportCurrentSpriteDraw()
void LoadSpritePalettes(bool use_asset_binding=true)
std::optional< zsprite::Frame > frame_clipboard_
absl::Status Update() override
gfx::PaletteGroup sprite_palettes_
void LoadZsmFile(const std::string &path)
absl::Status Cut() override
void RenderVanillaSprite(const zelda3::SpriteOamLayout &layout)
absl::Status palette_binding_status_
std::optional< std::map< int, zelda3::RoomGfxInfo > > usage_rooms_
const std::string & GetCurrentZsmPath() const
void RestoreFromSnapshot(const SpriteSnapshot &snapshot)
std::vector< zsprite::ZSprite > custom_sprites_
std::vector< uint8_t > sprite_gfx_buffer_
const project::SpriteAssetBinding * current_sprite_binding() const
void SetCurrentZsmPath(const std::string &path)
absl::Status Copy() override
const zelda3::GameData * preview_graphics_owner_
absl::Status Save() override
std::optional< std::map< int, zelda3::OverworldAreaGfxInfo > > usage_areas_
SpriteSnapshot undo_before_snapshot_
void SaveZsmFile(const std::string &path)
SpriteSnapshot CaptureCurrentSpriteSnapshot() const
absl::Status graphics_binding_status_
absl::Status graphics_context_status_
std::optional< gfx::PaletteGroup > room_palette_cache_
absl::Status Load() override
std::vector< std::string > custom_sprite_paths_
void SelectVanillaSprite(int sprite_id)
const zelda3::SpriteOamLayout * CurrentPreviewLayout() const
void Push(std::unique_ptr< UndoAction > action)
void SetVisibleWindows(size_t session_id, const std::vector< std::string > &window_ids)
Set which panels should be visible for a session.
void RegisterWindowContent(std::unique_ptr< WindowContent > window)
Register a WindowContent instance for central drawing.
std::vector< std::string > GetVisibleWindowIds(size_t session_id) const
Get list of currently visible panel IDs for a session.
GraphicsSheets & gfx_sheets()
Definition arena.h:178
static Arena & Get()
Definition arena.cc:24
const std::vector< uint8_t > & vector() const
Definition bitmap.h:411
bool is_active() const
Definition bitmap.h:414
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:452
uint64_t ColorRevision(const zelda3::GameData *game_data) const
static PaletteManager & Get()
Get the singleton instance.
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)
RAII scope that enables automatic widget registration.
void DrawBitmap(Bitmap &bitmap, int border_offset, float scale)
Definition canvas.cc:1293
void DrawContextMenu()
Definition canvas.cc:823
bool DrawTileSelector(int size, int size_y=0)
Definition canvas.cc:1229
void DrawRect(int x, int y, int w, int h, ImVec4 color)
Definition canvas.cc:1563
CanvasConfig & GetConfig()
Definition canvas.h:243
void DrawBackground(ImVec2 canvas_size=ImVec2(0, 0), std::optional< ImVec2 > viewport_size=std::nullopt)
Definition canvas.cc:702
void DrawGrid(float grid_step=64.0f, int tile_id_offset=8)
Definition canvas.cc:1620
static float GetHexInputWidth()
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...
uint64_t Revision(uint16_t sheet) const
absl::Span< const uint8_t > Sheet(uint16_t sheet) const
The sheet's 4096 pixels, or an empty span when it is not held.
static const SpriteOamLayout * GetLayout(uint8_t sprite_id)
Get the OAM layout for a sprite ID.
static const SpriteOamLayout * GetPreviewOverride(uint8_t sprite_id, std::string_view hack_name)
#define ICON_MD_MEETING_ROOM
Definition icons.h:1194
#define ICON_MD_FOLDER_OPEN
Definition icons.h:813
#define ICON_MD_WARNING
Definition icons.h:2123
#define ICON_MD_PLAY_ARROW
Definition icons.h:1479
#define ICON_MD_CHECK
Definition icons.h:397
#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_DONE
Definition icons.h:607
#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
zelda3::GfxSheetInventoryOptions BuildGfxSheetInventoryOptions(const project::YazeProject *project)
int ParseIntOrDefault(const std::string &text, int fallback=0)
void EnsureSpritePreviewBitmapReady(gfx::Bitmap &bmp, int width, int height, int depth, const std::vector< uint8_t > &gfx_buffer)
void PublishSpritePreviewPixels(gfx::Bitmap &bmp)
std::vector< int > FrameTiles8x8(const zsprite::Frame &frame)
gfx::PaletteGroup SpritePalettesFromCgram(const std::array< SDL_Color, 256 > &cgram)
gfx::PaletteGroup DefaultSpritePreviewPalettes(const gfx::PaletteGroup &global, const gfx::PaletteGroup &aux1, const gfx::PaletteGroup &aux2, const gfx::PaletteGroup &aux3)
int OffsetX(const zsprite::OamTile &tile)
bool Advance(const zsprite::AnimationGroup &animation, size_t frame_count, float delta, int &frame, float &remainder)
absl::StatusOr< gfx::PaletteGroup > BindPaletteRows(const project::SpriteAssetBinding &binding, const gfx::PaletteGroup &defaults, const gfx::PaletteGroup &global, const gfx::PaletteGroup &aux1, const gfx::PaletteGroup &aux2, const gfx::PaletteGroup &aux3)
int OffsetY(const zsprite::OamTile &tile)
bool AppendFrame(zsprite::ZSprite &sprite, int copy_from=-1)
zsprite::Frame CopyVanillaLayout(const zelda3::SpriteOamLayout &layout)
bool DeleteFrame(zsprite::ZSprite &sprite, int index)
absl::StatusOr< GraphicsEditContext > Make(const zsprite::Frame &frame, int frame_index, int oam_index, const std::array< uint8_t, 8 > &slots, const gfx::PaletteGroup &palettes, const zelda3::GameData *owner, const std::string &source_label, const std::string &sheet_source)
absl::StatusOr< usage_preview::PreviewImage > RenderCapturedFrame(const zsprite::Frame &frame, const std::array< uint8_t, 8 > &slots, const gfx::PaletteGroup &palettes, const zelda3::GameData *owner)
constexpr ImGuiTabItemFlags kSpriteTabFlags
constexpr ImGuiTabBarFlags kSpriteTabBarFlags
ImVec4 GetSuccessColor()
Definition ui_helpers.cc:49
void AutoRegisterLastItem(const std::string &widget_type, const std::string &explicit_label, const std::string &description)
Automatically register the last ImGui item.
bool BeginThemedTabBar(const char *id, ImGuiTabBarFlags flags)
A stylized tab bar with "Mission Control" branding.
void EndThemedTabBar()
ImVec4 GetWarningColor()
Definition ui_helpers.cc:54
ImGuiID GetID(const std::string &id)
Definition input.cc:704
bool InputHexByte(const char *label, uint8_t *data, float input_width, bool no_step)
Definition input.cc:510
std::string HexByte(uint8_t byte, HexStringParams params)
Definition hex.cc:33
constexpr int kNumDungeonRooms
GfxGroupUsage FindSpritesetUsage(int spriteset, const std::map< int, OverworldAreaGfxInfo > &areas, const std::map< int, RoomGfxInfo > &rooms)
Room LoadRoomHeaderFromRom(Rom *rom, int room_id)
Definition room.cc:680
std::map< int, OverworldAreaGfxInfo > CollectOverworldAreaGfx(Rom &rom, GameData *game_data)
constexpr uint16_t kDungeonSpritesetBase
std::map< int, RoomGfxInfo > CollectRoomGfx(const Rom &rom)
const std::string kSpriteDefaultNames[256]
Definition sprite.cc:16
constexpr uint32_t kNumSpritesets
Definition game_data.h:34
constexpr bool IsUnderworldSpriteset(int spriteset)
Dungeon rooms use spritesets 0x40-0x8F (room header value + 0x40).
std::array< uint8_t, 8 > SpriteSheetSlots(const std::array< uint8_t, 4 > &spriteset_values, bool underworld)
The eight sheets for a spriteset's four values.
std::array< SDL_Color, 256 > BuildDungeonSpriteRenderPalette(const Room &room, const GameData *game_data)
Definition room.cc:200
std::vector< SpriteTileIssue > CheckSpriteTiles(const Rom &rom, const std::vector< int > &tiles, const std::array< uint8_t, 8 > &slots, const std::set< uint16_t > &reserved_sheets)
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
WorkspaceWindowManager * window_manager
Definition editor.h:182
Snapshot of a custom ZSprite's editable state for undo/redo.
project::SpriteAssetBinding binding
std::vector< Frame > Frames
Definition zsprite.h:125
void Reset()
Reset all sprite data to defaults.
Definition zsprite.h:405
absl::Status Load(const std::string &filename)
Load a ZSM file from disk.
Definition zsprite.h:139
std::vector< AnimationGroup > animations
Definition zsprite.h:445
auto palette(int i) const
void SelectableLabelWithNameEdit(bool selected, const std::string &type, const std::string &key, const std::string &defaultValue)
Definition project.cc:2915
std::array< uint8_t, 8 > sheets
std::vector< SpriteAssetBinding > sprite_assets
Definition project.h:333
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:2028
std::array< std::array< uint8_t, 4 >, kNumSpritesets > spriteset_ids
Definition game_data.h:99
gfx::PaletteGroupMap palette_groups
Definition game_data.h:95
Complete OAM layout for a vanilla sprite.
Automatic widget registration helpers for ImGui Test Engine integration.