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#include <filesystem>
10
11#include "absl/cleanup/cleanup.h"
12#include "absl/strings/str_format.h"
27#include "app/gui/core/icons.h"
28#include "app/gui/core/input.h"
33#include "core/project.h"
34#include "util/file_util.h"
35#include "util/hex.h"
36#include "util/macro.h"
37#include "zelda3/dungeon/room.h"
38#include "zelda3/game_data.h"
40
41namespace yaze {
42namespace editor {
43
44using ImGui::BeginTable;
45using ImGui::Button;
46using ImGui::EndTable;
47using ImGui::Selectable;
48using ImGui::Separator;
49using ImGui::TableHeadersRow;
50using ImGui::TableNextColumn;
51using ImGui::TableNextRow;
52using ImGui::TableSetupColumn;
53using ImGui::Text;
54
55namespace {
56template <size_t N>
57void CopyStringToBuffer(const std::string& src, char (&dest)[N]) {
58 std::strncpy(dest, src.c_str(), N - 1);
59 dest[N - 1] = '\0';
60}
61
62int ParseIntOrDefault(const std::string& text, int fallback = 0) {
63 if (text.empty()) {
64 return fallback;
65 }
66 errno = 0;
67 char* end = nullptr;
68 long value = std::strtol(text.c_str(), &end, 0);
69 if (end == text.c_str() || errno == ERANGE) {
70 return fallback;
71 }
72 return static_cast<int>(value);
73}
74} // namespace
75
78 return;
79 auto* window_manager = dependencies_.window_manager;
80
81 // Register WindowContent implementations with callbacks
82 // EditorPanels provide both metadata (icon, name, priority) and drawing logic
83 window_manager->RegisterWindowContent(
84 std::make_unique<VanillaSpriteEditorPanel>([this]() {
85 if (rom_ && rom_->is_loaded()) {
86 DrawVanillaSpriteEditor();
87 } else {
88 ImGui::TextDisabled(tr("Load a ROM to view vanilla sprites"));
89 }
90 }));
91
92 window_manager->RegisterWindowContent(
93 std::make_unique<CustomSpriteEditorPanel>(
94 [this]() { DrawCustomSprites(); }));
95 window_manager->RegisterWindowContent(
96 std::make_unique<SpriteCatalogPanel>([this]() { DrawSpriteCatalog(); }));
97}
98
99absl::Status SpriteEditor::Load() {
100 gfx::ScopedTimer timer("SpriteEditor::Load");
102 gfx_buffer_loaded_ = false;
105 room_palette_cache_.reset();
106 if (sheets_loaded_) {
107 const auto slots = zelda3::SpriteSheetSlots(
108 game_data()->spriteset_ids[preview_spriteset_],
110 std::copy(slots.begin(), slots.end(), vanilla_sheets_);
112 std::copy(slots.begin(), slots.end(), current_sheets_);
113 }
115 (void)ReloadSpriteCatalog();
117 return absl::OkStatus();
118}
119
120absl::Status SpriteEditor::Update() {
121 if (rom() && rom()->is_loaded() && !sheets_loaded_) {
122 sheets_loaded_ = true;
123 }
124
125 // Update animation playback for custom sprites
126 float current_time = ImGui::GetTime();
127 float delta_time = current_time - last_frame_time_;
128 last_frame_time_ = current_time;
129 UpdateAnimationPlayback(delta_time);
130
131 // Handle editor-level shortcuts
133
134 // Panel drawing is handled by WorkspaceWindowManager via registered EditorPanels
135 // Each panel's Draw() callback invokes the appropriate draw method
136
137 // Commit any pending undo transaction at the end of the frame
139
140 return status_.ok() ? absl::OkStatus() : status_;
141}
142
144 // Animation playback shortcuts (when custom sprite panel is active)
145 if (ImGui::IsKeyPressed(ImGuiKey_Space, false) &&
146 !ImGui::GetIO().WantTextInput) {
148 }
149
150 // Frame navigation
151 if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket, false)) {
152 if (current_frame_ > 0) {
155 }
156 }
157 if (ImGui::IsKeyPressed(ImGuiKey_RightBracket, false)) {
160 }
161
162 // Sprite navigation
163 if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_UpArrow, false)) {
164 if (current_sprite_id_ > 0) {
166 }
167 }
168 if (ImGui::GetIO().KeyCtrl &&
169 ImGui::IsKeyPressed(ImGuiKey_DownArrow, false)) {
171 }
172}
173
175 // Inspect every pending document before Save can write any named asset.
176 for (const int index : dirty_sprite_indices_) {
177 if (index < 0 || index >= static_cast<int>(custom_sprite_paths_.size()) ||
178 custom_sprite_paths_[index].empty()) {
179 return absl::FailedPreconditionError(
180 "Autosave paused: untitled sprite assets are retained; use Sprite "
181 "Save As to choose their destinations");
182 }
183 }
184 return Save();
185}
186
187absl::Status SpriteEditor::Save() {
188 if (!dirty_sprite_indices_.empty()) {
190 const int selected = current_custom_sprite_index_;
191 const int frame = current_frame_;
192 absl::Cleanup restore = [this, selected, frame] {
194 current_frame_ = frame;
196 };
197 const auto pending = dirty_sprite_indices_;
198 for (const int index : pending) {
200 current_frame_ = 0;
201 const auto path = GetCurrentZsmPath();
202 if (path.empty())
204 else
205 SaveZsmFile(path);
206 if (!status_.ok())
207 return status_;
208 if (dirty_sprite_indices_.count(index))
209 return absl::CancelledError("Sprite Save As was cancelled");
210 }
211 return absl::OkStatus();
212 }
213 return absl::OkStatus();
214}
215
217 // Sidebar handled by EditorManager for card-based editors
218}
219
220// ============================================================
221// Vanilla Sprite Editor
222// ============================================================
223
225 if (sprite_id < 0 || sprite_id >= 256)
226 return;
227 current_sprite_id_ = sprite_id;
229 pending_sprite_tab_ = sprite_id;
230 if (!active_sprites_.contains(sprite_id))
231 active_sprites_.push_back(sprite_id);
232}
233
235 const auto id = static_cast<uint8_t>(current_sprite_id_);
237 id, project() ? project()->hack_manifest.hack_name() : "");
238 return override ? override : zelda3::SpriteOamRegistry::GetLayout(id);
239}
240
242 gui::AutoWidgetScope sprite_scope("Sprite/Vanilla");
243 if (ImGui::BeginTable("##SpriteCanvasTable", 3, ImGuiTableFlags_Resizable,
244 ImVec2(0, 0))) {
245 TableSetupColumn("Sprites List", ImGuiTableColumnFlags_WidthFixed, 256);
246 TableSetupColumn("Canvas", ImGuiTableColumnFlags_WidthStretch,
247 ImGui::GetContentRegionAvail().x);
248 TableSetupColumn("Tile Selector", ImGuiTableColumnFlags_WidthFixed, 256);
249 TableHeadersRow();
250 TableNextRow();
251
252 TableNextColumn();
254
255 TableNextColumn();
256 if (active_sprites_.empty()) {
257 ImGui::TextWrapped("Select a sprite from the list to open its preview.");
258 }
259 if (gui::BeginThemedTabBar("SpriteTabBar", kSpriteTabBarFlags)) {
260 if (ImGui::TabItemButton(ICON_MD_ADD, kSpriteTabFlags)) {
262 }
263
264 for (int n = 0; n < active_sprites_.Size;) {
265 bool open = true;
266
267 const int sprite_id = active_sprites_[n];
268 if (sprite_id < 0 || sprite_id >= 256) {
269 active_sprites_.erase(active_sprites_.Data + n);
270 continue;
271 }
272
273 const auto label =
274 absl::StrFormat("%02X %s###SpriteTab%02X", sprite_id,
275 zelda3::kSpriteDefaultNames[sprite_id], sprite_id);
276 const auto flags = pending_sprite_tab_ == sprite_id
277 ? ImGuiTabItemFlags_SetSelected
278 : ImGuiTabItemFlags_None;
279 const bool selected = ImGui::BeginTabItem(label.c_str(), &open, flags);
281 absl::StrFormat("SpriteTab%02X", sprite_id));
282 if (selected) {
283 if (current_sprite_id_ != sprite_id) {
284 current_sprite_id_ = sprite_id;
286 }
288 ImGui::EndTabItem();
289 }
290
291 if (!open)
292 active_sprites_.erase(active_sprites_.Data + n);
293 else
294 n++;
295 }
296
299 }
300
301 TableNextColumn();
302 if (sheets_loaded_) {
303 DrawCurrentSheets(false);
304 }
305 ImGui::EndTable();
306 }
307}
308
310 if (ImGui::BeginChild(gui::GetID("##SpriteCanvas"),
311 ImGui::GetContentRegionAvail(), true)) {
314
315 // Static layouts are preview references, not a complete game renderer.
316 const auto* layout = CurrentPreviewLayout();
317 ImGui::SetCursorPos(ImVec2(10, 10));
318 ImGui::TextColored(gui::GetWarningColor(), "Experimental static preview");
319 if (!layout) {
320 ImGui::TextWrapped(
321 "No static OAM layout for sprite $%02X. Open or import a ZSM in "
322 "Custom Sprites to preview authored frames.",
324 } else if (layout->graphics_resource) {
325 ImGui::TextWrapped(
326 "%s ($%02X) requires external graphics (%s). This editor does not "
327 "yet preview that graphics source.",
328 layout->name, layout->sprite_id, layout->graphics_resource);
329 } else {
330 RenderVanillaSprite(*layout);
332 const auto& error = !graphics_binding_status_.ok()
335 ImGui::TextWrapped("Preview unavailable: %s",
336 std::string(error.message()).c_str());
337 } else {
340 Text(tr("Sprite: %s (0x%02X)"), layout->name, layout->sprite_id);
341 Text(tr("Tiles: %zu"), layout->tiles.size());
342 if (std::none_of(vanilla_preview_bitmap_.vector().begin(),
344 [](uint8_t pixel) { return (pixel & 0x0F) != 0; })) {
345 ImGui::TextWrapped(
346 "The selected sheets contain no visible pixels for this layout. "
347 "Choose the sprite's spriteset or graphics sheets.");
348 }
349 }
350 ImGui::TextWrapped(
351 "Uses the selected sheets and palette. Animation, behavior, and "
352 "runtime graphics changes are not simulated.");
353 }
354
355 if (ImGui::Button(layout ? tr("Edit preview copy")
356 : tr("Create empty sprite template")))
358 ImGui::TextWrapped(
359 layout
360 ? "Creates a detached static pose. Bind any external graphics "
361 "in Custom Sprites > Graphics sources."
362 : "This ID has no decoded static layout. The template starts "
363 "with an empty frame for authoring; game OAM is not imported.");
364
367 }
368 ImGui::EndChild();
369}
370
372 bool use_asset_binding) {
373 if (game_data() == nullptr || spriteset < 0 ||
374 spriteset >= static_cast<int>(zelda3::kNumSpritesets)) {
375 return;
376 }
377 const auto slots =
378 zelda3::SpriteSheetSlots(game_data()->spriteset_ids[spriteset],
380 if (use_asset_binding && current_custom_sprite())
382 std::copy(slots.begin(), slots.end(),
383 use_asset_binding ? current_sheets_ : vanilla_sheets_);
384 gfx_buffer_loaded_ = false;
386 if (use_asset_binding && current_custom_sprite()) {
388 std::copy(slots.begin(), slots.end(), binding.sheets.begin());
390 }
392}
393
395 const auto* sprite = current_custom_sprite();
396 if (!sprite || current_frame_ < 0 ||
397 current_frame_ >= static_cast<int>(sprite->editor.Frames.size())) {
398 return absl::FailedPreconditionError("Select a sprite frame");
399 }
400 if (!rom_ || !rom_->is_loaded() || !game_data()) {
401 return absl::FailedPreconditionError(
402 "Load a ROM before editing sprite pixels");
403 }
404 if (const auto* binding = current_sprite_binding();
405 binding &&
406 std::any_of(binding->graphics_sources.begin(),
407 binding->graphics_sources.end(),
408 [](const auto& source) { return source.kind != "sheet"; }))
409 return absl::FailedPreconditionError(
410 "This asset uses BIN or raw ROM graphics. Edit BIN pixels in Graphics "
411 "Scratch, export the BIN, then Reload graphics sources here. Raw ROM "
412 "bindings are read-only preview ranges.");
416 if (!graphics_binding_status_.ok())
418 if (!palette_binding_status_.ok())
420 std::array<uint8_t, 8> slots;
421 std::copy_n(current_sheets_, 8, slots.begin());
422 const auto frame = sprite->editor.Frames[current_frame_];
423 const auto palettes = sprite_palettes_;
424 const std::string source =
426 ? absl::StrFormat("room %03X CGRAM rows 8-15; sprite slots %02X",
429 ? absl::StrFormat(
430 "sprite asset binding; preview spriteset %02X",
432 : absl::StrFormat("preview spriteset %02X",
435 frame, current_frame_, selected_tile_index_, slots, palettes, game_data(),
436 sprite->sprName, source);
437 if (!context.ok())
438 return context.status();
439 const auto* owner = game_data();
440 context->render_preview = [frame, slots, palettes, owner]() {
441 return sprite_graphics_context::RenderCapturedFrame(frame, slots, palettes,
442 owner);
443 };
444 auto* manager = static_cast<EditorManager*>(dependencies_.custom_data);
445 if (!manager)
446 return absl::FailedPreconditionError("Editor manager unavailable");
447 const int sprite_index = current_custom_sprite_index_;
448 const int frame_index = current_frame_;
449 const int tile_index = selected_tile_index_;
450 context->return_to_source = [this, manager, sprite_index, frame_index,
451 tile_index]() {
452 if (sprite_index >= 0 &&
453 sprite_index < static_cast<int>(custom_sprites_.size())) {
454 current_custom_sprite_index_ = sprite_index;
455 if (frame_index <
456 static_cast<int>(
457 custom_sprites_[sprite_index].editor.Frames.size())) {
458 current_frame_ = frame_index;
459 selected_tile_index_ = tile_index;
460 }
462 gfx_buffer_loaded_ = false;
463 }
464 manager->SwitchToEditor(EditorType::kSprite, true);
465 };
466 auto* editor_set = manager->GetCurrentEditorSet();
467 if (!editor_set || !editor_set->GetGraphicsEditor()) {
468 return absl::FailedPreconditionError("Graphics editor unavailable");
469 }
470 auto status = manager->EnsureEditorAssetsLoaded(EditorType::kGraphics);
471 if (!status.ok())
472 return status;
473 status = editor_set->GetGraphicsEditor()->OpenInContext(std::move(*context));
474 if (!status.ok())
475 return status;
476 manager->SwitchToEditor(EditorType::kGraphics, true);
477 return absl::OkStatus();
478}
479
480void SpriteEditor::DrawSpritesetPicker(bool use_asset_binding) {
481 if (!ImGui::CollapsingHeader(tr("Spriteset preview"),
482 ImGuiTreeNodeFlags_DefaultOpen)) {
483 return;
484 }
485 if (game_data() == nullptr || rom_ == nullptr || !rom_->is_loaded()) {
486 ImGui::TextDisabled(tr("Load a ROM to preview against spritesets."));
487 return;
488 }
489 if (usage_rom_ != rom_ || !usage_areas_.has_value()) {
493 }
494
495 uint8_t set = static_cast<uint8_t>(preview_spriteset_);
496 if (gui::InputHexByte("Spriteset", &set,
497 static_cast<uint8_t>(zelda3::kNumSpritesets - 1))) {
498 preview_spriteset_ = std::min<int>(set, zelda3::kNumSpritesets - 1);
499 }
500 ImGui::SameLine();
501 if (ImGui::Button(ICON_MD_DONE " Apply to sheets")) {
502 ApplySpritesetToSheets(preview_spriteset_, use_asset_binding);
503 }
505 "Slots 0-3 take the static sprite sheets, slots 4-7 this spriteset's "
506 "values + 0x73 (OAM tiles 0x100-0x1FF).");
507
508 const auto& values = game_data()->spriteset_ids[preview_spriteset_];
509 const auto slots = zelda3::SpriteSheetSlots(
511 ImGui::TextDisabled(
512 "Values %02X %02X %02X %02X -> sheets %02X %02X %02X %02X", values[0],
513 values[1], values[2], values[3], slots[4], slots[5], slots[6], slots[7]);
514
517 auto hex_list = [](const std::vector<int>& ids) {
518 if (ids.empty()) {
519 return std::string("-");
520 }
521 std::string out;
522 for (size_t i = 0; i < ids.size() && i < 24; ++i) {
523 out += absl::StrFormat("%s%02X", i == 0 ? "" : " ", ids[i]);
524 }
525 if (ids.size() > 24) {
526 out += absl::StrFormat(" (+%zu)", ids.size() - 24);
527 }
528 return out;
529 };
530 for (int state = 0; state < 3; ++state) {
531 ImGui::TextWrapped("OW areas, state %d: %s", state,
532 hex_list(usage.ow_areas_by_state[state]).c_str());
533 }
534 ImGui::TextWrapped("Rooms: %s", hex_list(usage.rooms).c_str());
535
536 // Room context: its header picks the spriteset and the CGRAM palette.
537 ImGui::SetNextItemWidth(gui::LayoutHelpers::GetHexInputWidth() * 1.5f);
538 ImGui::InputScalar("Room", ImGuiDataType_U16, &preview_room_, nullptr,
539 nullptr, "%03X", ImGuiInputTextFlags_CharsHexadecimal);
541 std::min<uint16_t>(preview_room_, zelda3::kNumDungeonRooms - 1);
542 ImGui::SameLine();
543 if (ImGui::Button(ICON_MD_MEETING_ROOM " Use room")) {
544 if (auto room = usage_rooms_->find(preview_room_);
545 room != usage_rooms_->end()) {
547 std::min<int>(room->second.spriteset + zelda3::kDungeonSpritesetBase,
549 ApplySpritesetToSheets(preview_spriteset_, use_asset_binding);
551 room_palette_cache_.reset();
552 gfx_buffer_loaded_ = false;
553 }
554 }
555 HOVER_HINT("Use the room's header spriteset (+0x40) and its sprite palette");
556 if (ImGui::Checkbox(tr("Room palette (CGRAM rows 8-15)"),
558 room_palette_cache_.reset();
559 gfx_buffer_loaded_ = false;
561 }
563 "Off: palette rows from the sprite binding (Sprite Asset panel). On: "
564 "the rows the game loads for the room above.");
565
566 if (use_asset_binding)
568 ImGui::Separator();
569}
570
572 const auto* sprite = current_custom_sprite();
573 if (sprite == nullptr || current_frame_ < 0 ||
574 current_frame_ >= static_cast<int>(sprite->editor.Frames.size())) {
575 return;
576 }
577 auto tiles = internal::FrameTiles8x8(sprite->editor.Frames[current_frame_]);
578 if (const auto* binding = current_sprite_binding()) {
579 const auto before = tiles.size();
580 tiles.erase(
581 std::remove_if(tiles.begin(), tiles.end(),
582 [&](int tile) {
583 const int slot = zelda3::SpriteSlotForTile(tile);
584 return slot >= 0 && slot < 8 &&
585 binding->graphics_sources[slot].kind != "sheet";
586 }),
587 tiles.end());
588 if (tiles.size() != before)
589 ImGui::TextWrapped(
590 "%zu tiles use BIN/raw ROM sources; ROM sheet usage checks apply "
591 "only to the remaining tiles.",
592 before - tiles.size());
593 }
594 if (tiles.empty())
595 return;
596 std::array<uint8_t, 8> slots;
597 std::copy_n(current_sheets_, 8, slots.begin());
598 const auto options = core::BuildGfxSheetInventoryOptions(project());
599 std::string key =
600 absl::StrFormat("%d:%d:", current_custom_sprite_index_, current_frame_);
601 for (uint8_t sheet : slots) {
602 key += absl::StrFormat("%02X", sheet);
603 }
604 for (int tile : tiles) {
605 key += absl::StrFormat(",%X", tile);
606 }
607 for (uint16_t sheet : options.reserved_sheets) {
608 key += absl::StrFormat("r%X", sheet);
609 }
610 if (key != tile_check_key_) {
611 tile_check_key_ = key;
613 zelda3::CheckSpriteTiles(*rom_, tiles, slots, options.reserved_sheets);
614 tile_check_count_ = tiles.size();
615 }
616
617 if (tile_issues_.empty()) {
618 ImGui::TextColored(gui::GetSuccessColor(),
619 ICON_MD_CHECK " Frame %d: all %zu 8x8 tiles have art",
621 return;
622 }
623 ImGui::TextColored(gui::GetWarningColor(),
625 " Frame %d: %zu of %zu 8x8 tiles need "
626 "attention",
628 for (size_t i = 0; i < tile_issues_.size() && i < 8; ++i) {
629 const auto& issue = tile_issues_[i];
630 const char* what =
632 ? "blank"
634 ? "reserved sheet"
635 : "sheet unreadable");
636 ImGui::BulletText("tile 0x%03X, slot %d (sheet 0x%02X): %s", issue.tile,
637 issue.slot, issue.sheet, what);
638 }
639 if (tile_issues_.size() > 8) {
640 ImGui::TextDisabled("... %zu more", tile_issues_.size() - 8);
641 }
642}
643
644void SpriteEditor::DrawCurrentSheets(bool use_asset_binding) {
645 if (ImGui::BeginChild(gui::GetID("sheet_label"),
646 ImVec2(ImGui::GetContentRegionAvail().x, 0), true,
647 ImGuiWindowFlags_NoDecoration)) {
648 DrawSpritesetPicker(use_asset_binding);
649 auto* sheets = use_asset_binding ? current_sheets_ : vanilla_sheets_;
650 // Track previous sheet values for change detection
651 bool sheets_changed = false;
652 if (use_asset_binding && current_custom_sprite())
654
655 for (int i = 0; i < 8; i++) {
656 std::string sheet_label = absl::StrFormat("Sheet %d", i);
657 if (gui::InputHexByte(sheet_label.c_str(), &sheets[i])) {
658 sheets_changed = true;
659 }
660 if (i % 2 == 0)
661 ImGui::SameLine();
662 }
663
664 // Reload graphics buffer if sheets changed
665 if (sheets_changed) {
666 gfx_buffer_loaded_ = false;
668 if (use_asset_binding && current_custom_sprite()) {
670 std::copy_n(current_sheets_, 8, binding.sheets.begin());
672 }
673 }
675
680 for (int i = 0; i < 8; i++) {
681 const auto* binding =
682 use_asset_binding ? current_sprite_binding() : nullptr;
683 if (binding && binding->graphics_sources[i].kind != "sheet") {
685 auto& sheet = external_sheet_previews_[i];
686 const std::vector<uint8_t> pixels(
687 sprite_gfx_buffer_.begin() + i * 4096,
688 sprite_gfx_buffer_.begin() + (i + 1) * 4096);
689 const bool create = !sheet.is_active();
690 bool palette_changed = false;
691 if (!sprite_palettes_.empty()) {
692 const auto& palette = sprite_palettes_.palette_ref(0);
693 palette_changed = sheet.palette().size() != palette.size();
694 if (!palette_changed)
695 for (size_t color = 0; color < palette.size(); ++color)
696 palette_changed |=
697 sheet.palette()[color].snes() != palette[color].snes();
698 }
699 if (create || sheet.vector() != pixels || palette_changed) {
700 if (create)
701 sheet.Create(128, 32, 8, pixels);
702 else
703 sheet.set_data(pixels);
704 if (!sprite_palettes_.empty())
705 sheet.SetPalette(sprite_palettes_.palette_ref(0));
709 &sheet);
710 }
711 graphics_sheet_canvas_.DrawBitmap(sheet, 1, (i * 0x40) + 1, 2);
712 continue;
713 }
714 if (sheets[i] >= gfx::Arena::Get().gfx_sheets().size())
715 continue;
716 auto& sheet = gfx::Arena::Get().gfx_sheets().at(sheets[i]);
717 if (sheet.is_active() && !sheet.texture())
718 sheet.CreateTexture();
719 graphics_sheet_canvas_.DrawBitmap(sheet, 1, (i * 0x40) + 1, 2);
720 }
723 }
724 ImGui::EndChild();
725}
726
728 if (ImGui::BeginChild(gui::GetID("##SpritesList"),
729 ImVec2(ImGui::GetContentRegionAvail().x, 0), true,
730 ImGuiWindowFlags_NoDecoration)) {
731 int i = 0;
732 for (const auto each_sprite_name : zelda3::kSpriteDefaultNames) {
734 current_sprite_id_ == i, "Sprite Names", util::HexByte(i),
736 if (ImGui::IsItemClicked()) {
738 }
739 i++;
740 }
741 }
742 ImGui::EndChild();
743}
744
745// ============================================================
746// Custom ZSM Sprite Editor
747// ============================================================
748
750 if (BeginTable("##CustomSpritesTable", 3,
751 ImGuiTableFlags_Resizable | ImGuiTableFlags_Borders,
752 ImVec2(0, 0))) {
753 TableSetupColumn("Sprite Data", ImGuiTableColumnFlags_WidthFixed, 300);
754 TableSetupColumn("Canvas", ImGuiTableColumnFlags_WidthStretch);
755 TableSetupColumn("Tilesheets", ImGuiTableColumnFlags_WidthFixed, 280);
756
757 TableHeadersRow();
758 TableNextRow();
759 TableNextColumn();
760
761 // Keep the canvas visible while scrolling the longer authoring controls.
762 if (ImGui::BeginChild("##SpriteMetadata", ImVec2(0, 0)))
764 ImGui::EndChild();
765
766 TableNextColumn();
768
769 TableNextColumn();
770 DrawCurrentSheets(true);
771
772 EndTable();
773 }
774}
775
777 if (!asset_load_status_.ok())
778 ImGui::TextWrapped("%s", asset_load_status_.ToString().c_str());
779 // File operations toolbar
780 if (ImGui::Button(ICON_MD_ADD " New")) {
782 }
783 ImGui::SameLine();
784 if (ImGui::Button(ICON_MD_FOLDER_OPEN " Open")) {
785 std::string file_path = util::FileDialogWrapper::ShowOpenFileDialog();
786 if (!file_path.empty()) {
787 status_ = OpenSpriteAsset(file_path);
788 }
789 }
790 ImGui::SameLine();
791 if (ImGui::Button(ICON_MD_SAVE " Save")) {
793 const std::string& zsm_path = GetCurrentZsmPath();
794 if (zsm_path.empty()) {
796 } else {
797 SaveZsmFile(zsm_path);
798 }
799 }
800 }
801 ImGui::SameLine();
802 if (ImGui::Button(ICON_MD_SAVE_AS " Save As")) {
804 }
805
806 Separator();
807
808 // Sprite list
809 Text(tr("Loaded Sprites:"));
810 const float avail_y = std::max(0.0f, ImGui::GetContentRegionAvail().y);
811 const float metadata_reserve = std::min(160.0f, avail_y * 0.45f);
812 const float list_budget = std::max(0.0f, avail_y - metadata_reserve);
813 const float preferred_list = std::clamp(avail_y * 0.35f, 180.0f, 280.0f);
814 const float list_height = std::min(preferred_list, list_budget);
815 if (ImGui::BeginChild("SpriteList", ImVec2(0, list_height), true)) {
816 for (size_t i = 0; i < custom_sprites_.size(); i++) {
817 std::string label = custom_sprites_[i].sprName.empty()
818 ? "Unnamed Sprite"
819 : custom_sprites_[i].sprName;
820 ImGui::PushID(static_cast<int>(i));
821 if (Selectable(label.c_str(), current_custom_sprite_index_ == (int)i)) {
822 current_custom_sprite_index_ = static_cast<int>(i);
823 current_frame_ = custom_sprites_[i].editor.Frames.empty() ? -1 : 0;
825 custom_sprites_[i].animations.empty() ? -1 : 0;
828 animation_playing_ = false;
829 frame_timer_ = 0.0f;
833 }
834 ImGui::PopID();
835 }
836 }
837 ImGui::EndChild();
838
839 Separator();
840
841 // Show properties for selected sprite
846 if (gui::BeginThemedTabBar("SpriteDataTabs")) {
847 if (ImGui::BeginTabItem(tr("Properties"))) {
849 ImGui::EndTabItem();
850 }
851 if (ImGui::BeginTabItem(tr("Animations"))) {
853 ImGui::EndTabItem();
854 }
855 if (ImGui::BeginTabItem(tr("Behavior"))) {
857 ImGui::EndTabItem();
858 }
859 if (ImGui::BeginTabItem(tr("Routines"))) {
861 ImGui::EndTabItem();
862 }
864 }
865 } else {
866 Text(tr("No sprite selected"));
867 }
869}
870
871absl::Status SpriteEditor::Copy() {
873 current_custom_sprite_index_ >= static_cast<int>(custom_sprites_.size()))
874 return absl::FailedPreconditionError(
875 "Select a custom sprite frame to copy");
876 const auto& frames =
878 if (current_frame_ < 0 || current_frame_ >= static_cast<int>(frames.size()))
879 return absl::FailedPreconditionError("Select a frame to copy");
881 return absl::OkStatus();
882}
883
884absl::Status SpriteEditor::Cut() {
887 if (sprite.editor.Frames.size() <= 1)
888 return absl::FailedPreconditionError(
889 "Cannot cut the last frame; copied it instead");
893 static_cast<int>(sprite.editor.Frames.size()) - 1);
897 return absl::OkStatus();
898}
899
900absl::Status SpriteEditor::Paste() {
902 current_custom_sprite_index_ >= static_cast<int>(custom_sprites_.size()))
903 return absl::FailedPreconditionError(
904 "Copy a frame and select a destination sprite");
906 if (sprite.editor.Frames.size() >= sprite_authoring::kMaxFrames)
907 return absl::OutOfRangeError("ZSM animations address at most 256 frames");
909 sprite.editor.Frames.push_back(*frame_clipboard_);
910 current_frame_ = static_cast<int>(sprite.editor.Frames.size()) - 1;
914 return absl::OkStatus();
915}
916
919 zsprite::ZSprite new_sprite;
920 new_sprite.Reset();
921 new_sprite.sprName = "New Sprite";
922
923 // Add default frame
924 new_sprite.editor.Frames.emplace_back();
925
926 // Add default animation
927 new_sprite.animations.emplace_back(0, 0, 1, "Idle");
928
929 custom_sprites_.push_back(std::move(new_sprite));
930 custom_sprite_paths_.push_back(std::string());
932 std::copy_n(current_sheets_, 8, binding.sheets.begin());
933 custom_sprite_bindings_.push_back(std::move(binding));
934 source_checked_ = false;
935 current_custom_sprite_index_ = static_cast<int>(custom_sprites_.size()) - 1;
936 current_frame_ = 0;
940 animation_playing_ = false;
941 frame_timer_ = 0.0f;
942 zsm_dirty_ = true;
945}
946
947void SpriteEditor::LoadZsmFile(const std::string& path) {
948 zsprite::ZSprite sprite;
949 status_ = sprite.Load(path);
950 if (status_.ok()) {
951 custom_sprites_.push_back(std::move(sprite));
952 custom_sprite_paths_.push_back(path);
954 binding.zsm_path = path;
955 std::copy_n(current_sheets_, 8, binding.sheets.begin());
956 if (project()) {
957 for (const auto& saved : project()->sprite_assets) {
958 if (project()->GetAbsolutePath(saved.zsm_path) == path) {
959 binding = saved;
960 break;
961 }
962 }
963 }
964 custom_sprite_bindings_.push_back(std::move(binding));
965 current_custom_sprite_index_ = static_cast<int>(custom_sprites_.size()) - 1;
966 current_frame_ = custom_sprites_.back().editor.Frames.empty() ? -1 : 0;
968 custom_sprites_.back().animations.empty() ? -1 : 0;
971 animation_playing_ = false;
972 frame_timer_ = 0.0f;
977 }
978}
979
980void SpriteEditor::SaveZsmFile(const std::string& path) {
985 if (status_.ok()) {
986 SetCurrentZsmPath(path);
988 binding.zsm_path = path;
989 if (project()) {
990 auto& assets = project()->sprite_assets;
991 auto found =
992 std::find_if(assets.begin(), assets.end(), [&](const auto& asset) {
993 return project()->GetAbsolutePath(asset.zsm_path) == path;
994 });
995 if (found == assets.end())
996 assets.push_back(binding);
997 else
998 *found = binding;
1000 }
1003 }
1004 }
1005}
1006
1009 std::string path =
1011 if (!path.empty()) {
1012 SaveZsmFile(path);
1013 }
1014 }
1015}
1016
1017// ============================================================
1018// Properties Panel
1019// ============================================================
1020
1023
1024 // Basic info
1025 Text(tr("Sprite Info"));
1026 Separator();
1027
1028 static char name_buf[256];
1029 CopyStringToBuffer(sprite.sprName, name_buf);
1030 if (ImGui::InputText(tr("Name"), name_buf, sizeof(name_buf))) {
1031 sprite.sprName = name_buf;
1032 sprite.property_sprname.Text = name_buf;
1034 }
1035
1036 static char id_buf[32];
1037 CopyStringToBuffer(sprite.property_sprid.Text, id_buf);
1038 if (ImGui::InputText(tr("Sprite ID"), id_buf, sizeof(id_buf))) {
1039 sprite.property_sprid.Text = id_buf;
1041 }
1042
1043 Separator();
1045
1046 Separator();
1048}
1049
1052
1053 Text(tr("Stats"));
1054
1055 // Use InputInt for numeric values
1056 int prize = ParseIntOrDefault(sprite.property_prize.Text);
1057 if (ImGui::InputInt(tr("Prize"), &prize)) {
1058 sprite.property_prize.Text = std::to_string(std::clamp(prize, 0, 255));
1060 }
1061
1062 int palette = ParseIntOrDefault(sprite.property_palette.Text);
1063 if (ImGui::InputInt(tr("Palette"), &palette)) {
1064 sprite.property_palette.Text = std::to_string(std::clamp(palette, 0, 7));
1066 }
1067
1068 int oamnbr = ParseIntOrDefault(sprite.property_oamnbr.Text);
1069 if (ImGui::InputInt(tr("OAM Count"), &oamnbr)) {
1070 sprite.property_oamnbr.Text = std::to_string(std::clamp(oamnbr, 0, 255));
1072 }
1073
1074 int hitbox = ParseIntOrDefault(sprite.property_hitbox.Text);
1075 if (ImGui::InputInt(tr("Hitbox"), &hitbox)) {
1076 sprite.property_hitbox.Text = std::to_string(std::clamp(hitbox, 0, 255));
1078 }
1079
1080 int health = ParseIntOrDefault(sprite.property_health.Text);
1081 if (ImGui::InputInt(tr("Health"), &health)) {
1082 sprite.property_health.Text = std::to_string(std::clamp(health, 0, 255));
1084 }
1085
1086 int damage = ParseIntOrDefault(sprite.property_damage.Text);
1087 if (ImGui::InputInt(tr("Damage"), &damage)) {
1088 sprite.property_damage.Text = std::to_string(std::clamp(damage, 0, 255));
1090 }
1091}
1092
1095
1096 Text(tr("Behavior Flags"));
1097
1098 // Two columns for boolean properties
1099 if (ImGui::BeginTable("BoolProps", 2, ImGuiTableFlags_None)) {
1100 // Column 1
1101 ImGui::TableNextColumn();
1102 if (ImGui::Checkbox(tr("Blockable"), &sprite.property_blockable.IsChecked))
1104 if (ImGui::Checkbox(tr("Can Fall"), &sprite.property_canfall.IsChecked))
1106 if (ImGui::Checkbox(tr("Collision Layer"),
1107 &sprite.property_collisionlayer.IsChecked))
1109 if (ImGui::Checkbox(tr("Custom Death"),
1110 &sprite.property_customdeath.IsChecked))
1112 if (ImGui::Checkbox(tr("Damage Sound"),
1113 &sprite.property_damagesound.IsChecked))
1115 if (ImGui::Checkbox(tr("Deflect Arrows"),
1116 &sprite.property_deflectarrows.IsChecked))
1118 if (ImGui::Checkbox(tr("Deflect Projectiles"),
1119 &sprite.property_deflectprojectiles.IsChecked))
1121 if (ImGui::Checkbox(tr("Fast"), &sprite.property_fast.IsChecked))
1123 if (ImGui::Checkbox(tr("Harmless"), &sprite.property_harmless.IsChecked))
1125 if (ImGui::Checkbox(tr("Impervious"),
1126 &sprite.property_impervious.IsChecked))
1128
1129 // Column 2
1130 ImGui::TableNextColumn();
1131 if (ImGui::Checkbox(tr("Impervious Arrow"),
1132 &sprite.property_imperviousarrow.IsChecked))
1134 if (ImGui::Checkbox(tr("Impervious Melee"),
1135 &sprite.property_imperviousmelee.IsChecked))
1137 if (ImGui::Checkbox(tr("Interaction"),
1138 &sprite.property_interaction.IsChecked))
1140 if (ImGui::Checkbox(tr("Is Boss"), &sprite.property_isboss.IsChecked))
1142 if (ImGui::Checkbox(tr("Persist"), &sprite.property_persist.IsChecked))
1144 if (ImGui::Checkbox(tr("Shadow"), &sprite.property_shadow.IsChecked))
1146 if (ImGui::Checkbox(tr("Small Shadow"),
1147 &sprite.property_smallshadow.IsChecked))
1149 if (ImGui::Checkbox(tr("Stasis"), &sprite.property_statis.IsChecked))
1151 if (ImGui::Checkbox(tr("Statue"), &sprite.property_statue.IsChecked))
1153 if (ImGui::Checkbox(tr("Water Sprite"),
1154 &sprite.property_watersprite.IsChecked))
1156
1157 ImGui::EndTable();
1158 }
1159}
1160
1161// ============================================================
1162// Animation Panel
1163// ============================================================
1164
1167
1168 if (ImGui::Button(tr("Copy draw tables (ASM)"))) {
1169 auto tables = ExportCurrentSpriteDraw();
1170 draw_export_status_ = tables.status();
1171 if (tables.ok())
1172 ImGui::SetClipboardText(tables->c_str());
1173 }
1174 HOVER_HINT(
1175 "Copy a draw-table candidate. Does not register or patch a sprite.");
1176 if (!draw_export_status_.ok())
1177 ImGui::TextWrapped("%s", draw_export_status_.ToString().c_str());
1178
1179 // Playback controls
1180 if (animation_playing_) {
1181 if (ImGui::Button(ICON_MD_STOP " Stop")) {
1182 animation_playing_ = false;
1183 }
1184 } else {
1185 if (ImGui::Button(ICON_MD_PLAY_ARROW " Play")) {
1186 animation_playing_ = true;
1187 frame_timer_ = 0.0f;
1188 }
1189 }
1190 ImGui::SameLine();
1191 if (ImGui::Button(ICON_MD_SKIP_PREVIOUS)) {
1192 if (current_frame_ > 0)
1194 preview_needs_update_ = true;
1195 }
1196 HOVER_HINT("Previous Frame");
1197 ImGui::SameLine();
1198 if (ImGui::Button(ICON_MD_SKIP_NEXT)) {
1199 if (current_frame_ < (int)sprite.editor.Frames.size() - 1)
1201 preview_needs_update_ = true;
1202 }
1203 HOVER_HINT("Next Frame");
1204 ImGui::SameLine();
1205 Text(tr("Frame: %d / %d"), current_frame_,
1206 (int)sprite.editor.Frames.size() - 1);
1207
1208 Separator();
1209
1210 // Animation list
1211 Text(tr("Animations"));
1212 if (ImGui::Button(ICON_MD_ADD " Add Animation")) {
1213 int frame_count = static_cast<int>(sprite.editor.Frames.size());
1214 sprite.animations.emplace_back(0, std::clamp(frame_count - 1, 0, 255), 1,
1215 "New Animation");
1217 }
1218 HOVER_HINT("Add a new animation sequence");
1219
1220 if (ImGui::BeginChild("AnimList", ImVec2(0, 120), true)) {
1221 for (size_t i = 0; i < sprite.animations.size(); i++) {
1222 auto& anim = sprite.animations[i];
1223 std::string label = anim.frame_name.empty() ? "Unnamed" : anim.frame_name;
1224 ImGui::PushID(static_cast<int>(i));
1225 if (Selectable(label.c_str(), current_animation_index_ == (int)i)) {
1226 current_animation_index_ = static_cast<int>(i);
1227 current_frame_ = anim.frame_start;
1228 frame_timer_ = 0;
1229 preview_needs_update_ = true;
1230 }
1231 ImGui::PopID();
1232 }
1233 }
1234 ImGui::EndChild();
1235
1236 // Edit selected animation
1237 if (current_animation_index_ >= 0 &&
1238 current_animation_index_ < (int)sprite.animations.size()) {
1239 auto& anim = sprite.animations[current_animation_index_];
1240
1241 Separator();
1242 Text(tr("Animation Properties"));
1243
1244 static char anim_name[128];
1245 CopyStringToBuffer(anim.frame_name, anim_name);
1246 if (ImGui::InputText(tr("Name##Anim"), anim_name, sizeof(anim_name))) {
1247 anim.frame_name = anim_name;
1249 }
1250
1251 int start = anim.frame_start;
1252 int end = anim.frame_end;
1253 int speed = anim.frame_speed;
1254
1255 if (ImGui::SliderInt(
1256 tr("Start Frame"), &start, 0,
1257 std::clamp((int)sprite.editor.Frames.size() - 1, 0, 255))) {
1258 anim.frame_start = static_cast<uint8_t>(start);
1259 anim.frame_end = std::max(anim.frame_start, anim.frame_end);
1260 end = anim.frame_end;
1262 }
1263 if (ImGui::SliderInt(
1264 tr("End Frame"), &end, 0,
1265 std::clamp((int)sprite.editor.Frames.size() - 1, 0, 255))) {
1266 anim.frame_end = static_cast<uint8_t>(end);
1267 anim.frame_start = std::min(anim.frame_start, anim.frame_end);
1269 }
1270 if (ImGui::SliderInt(tr("Ticks per frame (60 Hz)"), &speed, 1, 255)) {
1271 anim.frame_speed = static_cast<uint8_t>(speed);
1273 }
1274
1275 auto& actions =
1277 bool used_by_action =
1278 std::any_of(actions.begin(), actions.end(), [&](const auto& action) {
1279 return action.animation == current_animation_index_;
1280 });
1281 ImGui::BeginDisabled(used_by_action);
1282 if (ImGui::Button(tr("Delete Animation")) && sprite.animations.size() > 1) {
1283 for (auto& action : actions)
1284 if (action.animation > current_animation_index_)
1285 --action.animation;
1286 sprite.animations.erase(sprite.animations.begin() +
1289 std::min(current_animation_index_, (int)sprite.animations.size() - 1);
1291 }
1292 ImGui::EndDisabled();
1293 if (used_by_action)
1294 ImGui::TextDisabled(
1295 "Assign another animation to referencing actions before deleting.");
1296 HOVER_HINT("Delete the selected animation");
1297 }
1298
1299 Separator();
1301}
1302
1305
1306 Text(tr("Frames"));
1307 if (ImGui::Button(ICON_MD_ADD " Add Frame")) {
1308 if (sprite_authoring::AppendFrame(sprite)) {
1309 current_frame_ = static_cast<int>(sprite.editor.Frames.size()) - 1;
1312 }
1313 }
1314 HOVER_HINT("Add a new animation frame");
1315 ImGui::SameLine();
1316 if (ImGui::Button(ICON_MD_DELETE " Delete Frame") &&
1317 sprite.editor.Frames.size() > 1 && current_frame_ >= 0) {
1320 std::min(current_frame_, (int)sprite.editor.Frames.size() - 1);
1322 frame_timer_ = 0;
1324 }
1325 }
1326 HOVER_HINT("Delete the current frame and repair animation ranges");
1327 if (ImGui::Button(tr("Duplicate Frame")) && current_frame_ >= 0 &&
1329 current_frame_ = static_cast<int>(sprite.editor.Frames.size()) - 1;
1332 }
1333 ImGui::TextDisabled("Up to 256 frames; duplicate appends a frame.");
1334
1335 // Frame selector
1336 if (ImGui::BeginChild("FrameList", ImVec2(0, 80), true,
1337 ImGuiWindowFlags_HorizontalScrollbar)) {
1338 for (size_t i = 0; i < sprite.editor.Frames.size(); i++) {
1339 ImGui::PushID(static_cast<int>(i));
1340 std::string label = absl::StrFormat("F%d", i);
1341 if (Selectable(label.c_str(), current_frame_ == (int)i,
1342 ImGuiSelectableFlags_None, ImVec2(40, 40))) {
1343 current_frame_ = static_cast<int>(i);
1344 preview_needs_update_ = true;
1345 }
1346 ImGui::SameLine();
1347 ImGui::PopID();
1348 }
1349 }
1350 ImGui::EndChild();
1351
1352 // Edit tiles in current frame
1353 if (current_frame_ >= 0 &&
1354 current_frame_ < (int)sprite.editor.Frames.size()) {
1355 auto& frame = sprite.editor.Frames[current_frame_];
1356
1357 Separator();
1358 Text(tr("Tiles in Frame %d"), current_frame_);
1359
1360 if (ImGui::Button(ICON_MD_ADD " Add Tile")) {
1361 frame.Tiles.emplace_back();
1362 frame.Tiles.back().x = sprite_authoring::kOriginX;
1363 frame.Tiles.back().y = sprite_authoring::kOriginY;
1364 selected_tile_index_ = static_cast<int>(frame.Tiles.size()) - 1;
1366 preview_needs_update_ = true;
1367 }
1368 HOVER_HINT("Add a new tile to this frame");
1369
1370 if (ImGui::BeginChild("TileList", ImVec2(0, 100), true)) {
1371 for (size_t i = 0; i < frame.Tiles.size(); i++) {
1372 auto& tile = frame.Tiles[i];
1373 std::string label = absl::StrFormat("Tile %d (ID: %d)", i, tile.id);
1374 if (Selectable(label.c_str(), selected_tile_index_ == (int)i)) {
1375 selected_tile_index_ = static_cast<int>(i);
1376 }
1377 }
1378 }
1379 ImGui::EndChild();
1380
1381 // Edit selected tile
1382 if (selected_tile_index_ >= 0 &&
1383 selected_tile_index_ < (int)frame.Tiles.size()) {
1384 auto& tile = frame.Tiles[selected_tile_index_];
1385
1386 int tile_id = tile.id;
1387 if (ImGui::InputInt(tr("Tile ID"), &tile_id)) {
1388 tile.id = static_cast<uint16_t>(std::clamp(tile_id, 0, 511));
1390 preview_needs_update_ = true;
1391 }
1392
1393 int x = sprite_authoring::OffsetX(tile),
1394 y = sprite_authoring::OffsetY(tile);
1395 if (ImGui::InputInt(tr("X offset"), &x)) {
1396 tile.x = static_cast<uint8_t>(std::clamp(x, -128, 127) +
1399 preview_needs_update_ = true;
1400 }
1401 if (ImGui::InputInt(tr("Y offset"), &y)) {
1402 tile.y = static_cast<uint8_t>(std::clamp(y, -112, 143) +
1405 preview_needs_update_ = true;
1406 }
1407
1408 int pal = tile.palette;
1409 if (ImGui::SliderInt(tr("Palette##Tile"), &pal, 0, 7)) {
1410 tile.palette = static_cast<uint8_t>(pal);
1412 preview_needs_update_ = true;
1413 }
1414
1415 int priority = tile.priority;
1416 if (ImGui::SliderInt(tr("BG priority"), &priority, 0, 3)) {
1417 tile.priority = static_cast<uint8_t>(priority);
1419 }
1420 ImGui::TextDisabled(
1421 "BG priority is stored; preview has no background layers.");
1422 if (ImGui::Checkbox(tr("16x16"), &tile.size)) {
1424 preview_needs_update_ = true;
1425 }
1426 ImGui::SameLine();
1427 if (ImGui::Checkbox(tr("Flip X"), &tile.mirror_x)) {
1429 preview_needs_update_ = true;
1430 }
1431 ImGui::SameLine();
1432 if (ImGui::Checkbox(tr("Flip Y"), &tile.mirror_y)) {
1434 preview_needs_update_ = true;
1435 }
1436
1437 if (ImGui::Button(tr("Edit selected OAM pixels"))) {
1439 }
1440 if (!graphics_context_status_.ok()) {
1441 ImGui::TextWrapped(
1442 "%s", std::string(graphics_context_status_.message()).c_str());
1443 }
1444
1445 if (ImGui::Button(tr("Duplicate Tile"))) {
1446 const auto copy = tile;
1447 frame.Tiles.push_back(copy);
1448 selected_tile_index_ = static_cast<int>(frame.Tiles.size()) - 1;
1450 }
1451 if (ImGui::Button(tr("Delete Tile"))) {
1452 frame.Tiles.erase(frame.Tiles.begin() + selected_tile_index_);
1455 preview_needs_update_ = true;
1456 }
1457 HOVER_HINT("Delete the selected tile");
1458 }
1459 }
1460}
1461
1465 return;
1466 }
1467
1469 if (current_animation_index_ < 0 ||
1470 current_animation_index_ >= (int)sprite.animations.size()) {
1471 return;
1472 }
1473
1474 auto& anim = sprite.animations[current_animation_index_];
1475
1476 if (sprite_authoring::Advance(anim, sprite.editor.Frames.size(), delta_time,
1478 preview_needs_update_ = true;
1479 }
1480}
1481
1482// ============================================================
1483// User Routines Panel
1484// ============================================================
1485
1488
1489 if (ImGui::Button(ICON_MD_ADD " Add Routine")) {
1490 sprite.userRoutines.emplace_back("New Routine", "; ASM code here\n");
1492 }
1493 HOVER_HINT("Add a new ASM routine");
1494
1495 // Routine list
1496 if (ImGui::BeginChild("RoutineList", ImVec2(0, 100), true)) {
1497 for (size_t i = 0; i < sprite.userRoutines.size(); i++) {
1498 auto& routine = sprite.userRoutines[i];
1499 if (Selectable(routine.name.c_str(), selected_routine_index_ == (int)i)) {
1500 selected_routine_index_ = static_cast<int>(i);
1501 }
1502 }
1503 }
1504 ImGui::EndChild();
1505
1506 // Edit selected routine
1507 if (selected_routine_index_ >= 0 &&
1508 selected_routine_index_ < (int)sprite.userRoutines.size()) {
1509 auto& routine = sprite.userRoutines[selected_routine_index_];
1510
1511 Separator();
1512
1513 static char routine_name[128];
1514 CopyStringToBuffer(routine.name, routine_name);
1515 if (ImGui::InputText(tr("Routine Name"), routine_name,
1516 sizeof(routine_name))) {
1517 routine.name = routine_name;
1519 }
1520
1521 Text(tr("ASM Code:"));
1522
1523 // Multiline text input for code
1524 static char code_buffer[16384];
1525 CopyStringToBuffer(routine.code, code_buffer);
1526 if (ImGui::InputTextMultiline("##RoutineCode", code_buffer,
1527 sizeof(code_buffer), ImVec2(-1, 200))) {
1528 routine.code = code_buffer;
1530 }
1531
1532 if (ImGui::Button(tr("Delete Routine"))) {
1533 sprite.userRoutines.erase(sprite.userRoutines.begin() +
1537 }
1538 HOVER_HINT("Delete the selected routine");
1539 }
1540}
1541
1542// ============================================================
1543// Graphics Pipeline
1544// ============================================================
1545
1547 const bool owner_changed = preview_graphics_owner_ != game_data();
1548 const auto palette_revision =
1550 if (owner_changed || preview_palette_revision_ != palette_revision) {
1551 room_palette_cache_.reset();
1552 preview_palette_revision_ = palette_revision;
1554 preview_needs_update_ = true;
1555 }
1556 bool changed = !gfx_buffer_loaded_ || owner_changed ||
1557 preview_uses_asset_graphics_ != use_asset_binding;
1558 const auto* sheets = use_asset_binding ? current_sheets_ : vanilla_sheets_;
1559 for (int slot = 0; slot < 8; ++slot) {
1560 const auto revision =
1561 game_data() ? game_data()->sheet_store.Revision(sheets[slot]) : 0;
1562 changed |= loaded_preview_sheets_[slot] != sheets[slot] ||
1563 loaded_preview_revisions_[slot] != revision;
1564 }
1565 if (changed) {
1566 LoadSpriteGraphicsBuffer(use_asset_binding);
1567 preview_uses_asset_graphics_ = use_asset_binding;
1569 for (int slot = 0; slot < 8; ++slot) {
1570 loaded_preview_sheets_[slot] = sheets[slot];
1572 game_data() ? game_data()->sheet_store.Revision(sheets[slot]) : 0;
1573 }
1575 preview_needs_update_ = true;
1576 }
1577 LoadSpritePalettes(use_asset_binding);
1578}
1579
1580void SpriteEditor::LoadSpriteGraphicsBuffer(bool use_asset_binding) {
1581 // Combine selected sheets (current_sheets_[0-7]) into single 8BPP buffer
1582 // Layout: 16 tiles per row, 8 rows per sheet, 8 sheets total = 64 tile rows
1583 // Buffer size: 0x10000 bytes (65536)
1584
1585 sprite_gfx_buffer_.assign(0x10000, 0);
1586 graphics_binding_status_ = absl::OkStatus();
1587
1588 // Each sheet is 128x32 pixels (128 bytes per row, 32 rows) = 4096 bytes
1589 // We combine 8 sheets vertically: 128x256 pixels total
1590 const auto* binding = use_asset_binding ? current_sprite_binding() : nullptr;
1591 const auto* sheets = use_asset_binding ? current_sheets_ : vanilla_sheets_;
1592 const auto directory =
1593 project() && !project()->filepath.empty()
1594 ? std::filesystem::path(project()->filepath).parent_path()
1595 : std::filesystem::path{};
1596 for (int sheet_idx = 0; sheet_idx < 8; sheet_idx++) {
1597 const auto source = binding ? binding->graphics_sources[sheet_idx]
1599 auto pixels = sprite_graphics_source::Resolve(source, sheets[sheet_idx],
1600 game_data(), rom_, directory);
1601 if (!pixels.ok()) {
1602 if (graphics_binding_status_.ok())
1604 absl::Status(pixels.status().code(),
1605 absl::StrFormat("Graphics sheet/source in slot %d: %s",
1606 sheet_idx, pixels.status().message()));
1607 continue;
1608 }
1609 std::copy(pixels->begin(), pixels->end(),
1610 sprite_gfx_buffer_.begin() + sheet_idx * 4096);
1611 }
1612
1613 // Update drawer with new buffer
1615 gfx_buffer_loaded_ = true;
1616}
1617
1618void SpriteEditor::LoadSpritePalettes(bool use_asset_binding) {
1619 // Load sprite palettes from ROM palette groups
1620 // ALTTP sprites use a combination of palette groups:
1621 // - Rows 0-1: Global sprite palettes (shared by all sprites)
1622 // - Rows 2-7: Aux palettes (vary by sprite type)
1623 //
1624 // For simplicity, we load global_sprites which contains the main
1625 // sprite palettes. More accurate rendering would require looking up
1626 // which aux palette group each sprite type uses.
1627
1628 if (!rom_ || !rom_->is_loaded() || !game_data()) {
1630 palette_binding_status_ = absl::FailedPreconditionError(
1631 "Load ROM graphics and palettes before previewing sprites");
1632 return;
1633 }
1634
1635 // Build combined sprite palette from global + aux groups
1637
1638 // Add global sprite palettes (typically 2 palettes, 16 colors each)
1639 const auto& global = game_data()->palette_groups.global_sprites;
1640 const auto& aux1 = game_data()->palette_groups.sprites_aux1;
1641 const auto& aux2 = game_data()->palette_groups.sprites_aux2;
1642 const auto& aux3 = game_data()->palette_groups.sprites_aux3;
1644 internal::DefaultSpritePreviewPalettes(global, aux1, aux2, aux3);
1645
1646 palette_binding_status_ = absl::OkStatus();
1647 if (global.empty() && aux1.empty() && aux2.empty() && aux3.empty() &&
1649 palette_binding_status_ = absl::FailedPreconditionError(
1650 "Sprite palette colors are unavailable in this ROM session");
1651 }
1652 if (const auto* binding =
1653 use_asset_binding ? current_sprite_binding() : nullptr) {
1654 auto palettes = sprite_authoring::BindPaletteRows(
1655 *binding, sprite_palettes_, global, aux1, aux2, aux3);
1656 palette_binding_status_ = palettes.status();
1657 if (palettes.ok())
1658 sprite_palettes_ = std::move(*palettes);
1659 else
1661 }
1662 if (preview_use_room_palette_ && game_data() != nullptr) {
1663 if (!room_palette_cache_.has_value() ||
1669 }
1671 palette_binding_status_ = absl::OkStatus();
1672 }
1674}
1675
1677 // Ensure graphics buffer is loaded
1679
1680 // Initialize vanilla preview bitmap if needed. The helper also queues a
1681 // CREATE texture command so canvas_rendering doesn't silently skip the
1682 // draw, and stamps the bitmap as kCompositeOutput.
1685
1687 return;
1688 }
1689
1690 // Clear and render
1692
1693 // Origin is center of bitmap
1694 int origin_x = 64;
1695 int origin_y = 64;
1696
1700 origin_x, origin_y);
1701 }
1702
1703 // Build combined 128-color palette (8 sub-palettes × 16 colors)
1704 // and apply to bitmap for proper color rendering
1705 if (sprite_palettes_.size() > 0) {
1706 gfx::SnesPalette combined_palette;
1707 for (size_t pal_idx = 0; pal_idx < 8 && pal_idx < sprite_palettes_.size();
1708 pal_idx++) {
1709 const auto& sub_pal = sprite_palettes_.palette(pal_idx);
1710 for (size_t col_idx = 0; col_idx < 16 && col_idx < sub_pal.size();
1711 col_idx++) {
1712 combined_palette.AddColor(sub_pal[col_idx]);
1713 }
1714 // Pad to 16 if sub-palette is smaller
1715 while (combined_palette.size() < (pal_idx + 1) * 16) {
1716 combined_palette.AddColor(gfx::SnesColor(0));
1717 }
1718 }
1719 vanilla_preview_bitmap_.SetPalette(combined_palette);
1720 }
1721
1722 // Surface pixels and palette were just mutated above; queue an UPDATE so
1723 // the GPU texture reflects the new state on the next frame. Without this,
1724 // the texture stays frozen at first-CREATE state forever.
1726
1728}
1729
1730// ============================================================
1731// Canvas Rendering
1732// ============================================================
1733
1737 return;
1738 }
1739
1741 if (frame_index < 0 || frame_index >= (int)sprite.editor.Frames.size()) {
1742 return;
1743 }
1744
1745 auto& frame = sprite.editor.Frames[frame_index];
1746
1747 // Ensure graphics buffer is loaded
1749
1750 // The sprite_canvas_ displays a post-render composite (pixels come from
1751 // SpriteDrawer, not direct user paint). Declaring the role lets future
1752 // canvas surfaces (cursor hints, context-menu items) default appropriately.
1754
1755 // Initialize preview bitmap if needed. The helper also queues a CREATE
1756 // texture command so canvas_rendering doesn't silently skip the draw, and
1757 // stamps the bitmap as kCompositeOutput.
1760
1761 // Only render if drawer is ready
1763 // Clear and render to preview bitmap
1765
1766 // Origin is center of canvas (128, 128 for 256x256 bitmap).
1769
1770 // Build combined 128-color palette and apply to bitmap
1771 if (sprite_palettes_.size() > 0) {
1772 gfx::SnesPalette combined_palette;
1773 for (size_t pal_idx = 0; pal_idx < 8 && pal_idx < sprite_palettes_.size();
1774 pal_idx++) {
1775 const auto& sub_pal = sprite_palettes_.palette(pal_idx);
1776 for (size_t col_idx = 0; col_idx < 16 && col_idx < sub_pal.size();
1777 col_idx++) {
1778 combined_palette.AddColor(sub_pal[col_idx]);
1779 }
1780 // Pad to 16 if sub-palette is smaller
1781 while (combined_palette.size() < (pal_idx + 1) * 16) {
1782 combined_palette.AddColor(gfx::SnesColor(0));
1783 }
1784 }
1785 sprite_preview_bitmap_.SetPalette(combined_palette);
1786 }
1787
1788 // Surface pixels and palette were just mutated above; queue an UPDATE so
1789 // the GPU texture reflects the new frame on the next paint.
1791
1792 // Mark as updated
1793 preview_needs_update_ = false;
1794 }
1795
1796 // Draw the preview bitmap on canvas
1799 }
1800
1801 // Draw tile outlines for selection (over the bitmap)
1802 if (show_tile_grid_) {
1803 for (size_t i = 0; i < frame.Tiles.size(); i++) {
1804 const auto& tile = frame.Tiles[i];
1805 int tile_size = tile.size ? 16 : 8;
1806
1807 // Convert signed tile position to canvas position
1808 int signed_x = sprite_authoring::OffsetX(tile);
1809 int signed_y = sprite_authoring::OffsetY(tile);
1810
1811 int canvas_x = 128 + signed_x;
1812 int canvas_y = 128 + signed_y;
1813
1814 // Highlight selected tile
1815 ImVec4 color = (selected_tile_index_ == static_cast<int>(i))
1816 ? ImVec4(0.0f, 1.0f, 0.0f, 0.8f) // Green for selected
1817 : ImVec4(1.0f, 1.0f, 0.0f, 0.3f); // Yellow for others
1818
1819 // Match the 2x bitmap scale used above.
1820 sprite_canvas_.DrawRect(canvas_x * 2, canvas_y * 2, tile_size * 2,
1821 tile_size * 2, color);
1822 }
1823 }
1824}
1825
1827 if (ImGui::BeginChild(gui::GetID("##ZSpriteCanvas"),
1828 ImGui::GetContentRegionAvail(), true)) {
1831
1832 // Render current frame if we have a sprite selected
1837 ImGui::SetCursorPos(ImVec2(10, 34));
1838 const auto& error = !graphics_binding_status_.ok()
1841 ImGui::TextWrapped("Preview unavailable: %s",
1842 std::string(error.message()).c_str());
1843 }
1844 } else {
1845 ImGui::SetCursorPos(ImVec2(10, 10));
1846 ImGui::TextWrapped(
1847 "Open a ZSM or import a draw copy to preview its frames.");
1848 }
1849
1852
1853 // Display current frame info
1856 ImGui::SetCursorPos(ImVec2(10, 10));
1857 char label[256];
1858 std::snprintf(label, sizeof(label), tr("Frame: %d | Tiles: %d"),
1860 current_frame_ >= 0 &&
1861 current_frame_ < (int)sprite.editor.Frames.size()
1862 ? (int)sprite.editor.Frames[current_frame_].Tiles.size()
1863 : 0);
1864 const ImVec2 position = ImGui::GetCursorScreenPos();
1865 const ImVec2 size = ImGui::CalcTextSize(label);
1866 // Preview colors come from the ROM; keep status legible on any palette.
1867 ImGui::GetWindowDrawList()->AddRectFilled(
1868 ImVec2(position.x - 3, position.y - 2),
1869 ImVec2(position.x + size.x + 3, position.y + size.y + 2),
1870 IM_COL32(24, 24, 24, 255), 3.0f);
1871 ImGui::TextColored(ImVec4(1, 1, 1, 1), "%s", label);
1872 }
1873 }
1874 ImGui::EndChild();
1875}
1876
1877// ============================================================
1878// Undo/Redo Helpers
1879// ============================================================
1880
1893
1895 if (snapshot.sprite_index < 0 ||
1896 snapshot.sprite_index >= static_cast<int>(custom_sprites_.size())) {
1897 return;
1898 }
1900 current_frame_ = snapshot.current_frame;
1902 custom_sprites_[snapshot.sprite_index] = snapshot.sprite_data;
1903 custom_sprite_bindings_[snapshot.sprite_index] = snapshot.binding;
1905 preview_needs_update_ = true;
1906 zsm_dirty_ = true;
1908}
1909
1912 return; // Already in a transaction
1913 }
1916}
1917
1920 return;
1921 }
1922
1924 // No mutation happened; discard the pending snapshot so we can
1925 // capture a fresh one next frame.
1926 undo_snapshot_pending_ = false;
1927 return;
1928 }
1929
1930 undo_snapshot_pending_ = false;
1932
1933 auto after = CaptureCurrentSpriteSnapshot();
1934 auto restore_fn = [this](const SpriteSnapshot& snapshot) {
1935 RestoreFromSnapshot(snapshot);
1936 };
1937 undo_manager_.Push(std::make_unique<SpriteEditAction>(
1938 std::move(undo_before_snapshot_), std::move(after),
1939 std::move(restore_fn)));
1940}
1941
1949
1951 if (custom_sprite_paths_.size() < custom_sprites_.size()) {
1953 }
1954}
1955
1956const std::string& SpriteEditor::GetCurrentZsmPath() const {
1957 static const std::string kEmptyPath;
1960 static_cast<int>(custom_sprite_paths_.size())) {
1961 return kEmptyPath;
1962 }
1964}
1965
1966void SpriteEditor::SetCurrentZsmPath(const std::string& path) {
1970 static_cast<int>(custom_sprite_paths_.size())) {
1971 return;
1972 }
1974}
1975
1976} // namespace editor
1977} // namespace yaze
project::ResourceLabelManager * resource_label()
Definition rom.h:242
bool is_loaded() const
Definition rom.h:199
The EditorManager controls the main editor window and manages the various editor classes.
EditorContext context() const
Definition editor.h:376
UndoManager undo_manager_
Definition editor.h:383
zelda3::GameData * game_data() const
Definition editor.h:369
EditorDependencies dependencies_
Definition editor.h:382
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.
std::array< gfx::Bitmap, 8 > external_sheet_previews_
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_
void LoadSpriteGraphicsBuffer(bool use_asset_binding=true)
absl::Status OpenSelectedOamGraphics()
const zsprite::ZSprite * current_custom_sprite() const
std::set< int > dirty_sprite_indices_
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 CreateSpriteAssetFromVanilla(int sprite_id)
absl::Status palette_binding_status_
void DrawCurrentSheets(bool use_asset_binding)
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_
project::YazeProject * project() const
void DrawSpritesetPicker(bool use_asset_binding)
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)
void ApplySpritesetToSheets(int spriteset, bool use_asset_binding)
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 RegisterWindowContent(std::unique_ptr< WindowContent > window)
Register a WindowContent instance for central drawing.
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:39
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:1295
void DrawContextMenu()
Definition canvas.cc:824
bool DrawTileSelector(int size, int size_y=0)
Definition canvas.cc:1231
void DrawRect(int x, int y, int w, int h, ImVec4 color)
Definition canvas.cc:1569
CanvasConfig & GetConfig()
Definition canvas.h:243
void DrawBackground(ImVec2 canvas_size=ImVec2(0, 0), std::optional< ImVec2 > viewport_size=std::nullopt)
Definition canvas.cc:703
void DrawGrid(float grid_step=64.0f, int tile_id_offset=8)
Definition canvas.cc:1627
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
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)
absl::StatusOr< std::vector< uint8_t > > Resolve(const project::SpriteGraphicsSource &source, uint16_t sheet, const zelda3::GameData *data, const Rom *rom, const std::filesystem::path &project_directory)
constexpr ImGuiTabItemFlags kSpriteTabFlags
constexpr ImGuiTabBarFlags kSpriteTabBarFlags
ImGuiID GetID(const std::string &id)
Definition input.cc:693
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
bool InputHexByte(const char *label, uint8_t *data, float input_width, bool no_step)
Definition input.cc:483
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
const SnesPalette & palette_ref(int i) const
void SelectableLabelWithNameEdit(bool selected, const std::string &type, const std::string &key, const std::string &defaultValue)
Definition project.cc:3579
std::array< uint8_t, 8 > sheets
std::vector< SpriteAssetBinding > sprite_assets
Definition project.h:336
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:2675
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.