yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
sprite_editor_panel.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cctype>
5#include <utility>
6
7#include "absl/strings/str_format.h"
15#include "app/gui/core/icons.h"
17#include "imgui/imgui.h"
18#include "util/i18n/tr.h"
19#include "zelda3/dungeon/room.h"
22
23namespace yaze {
24namespace editor {
25
27 DungeonRoomStore* rooms,
28 DungeonCanvasViewer* canvas_viewer)
29 : current_room_id_(current_room_id),
30 rooms_(rooms),
31 canvas_viewer_(canvas_viewer) {}
32
33std::string SpriteEditorPanel::GetId() const {
34 return "dungeon.sprite_editor";
35}
36
38 return "Sprite Editor";
39}
40
41std::string SpriteEditorPanel::GetIcon() const {
42 return ICON_MD_PERSON;
43}
44
46 return "Dungeon";
47}
48
50 return 65;
51}
52
54 return "Editors";
55}
56
57void SpriteEditorPanel::Draw(bool* /*p_open*/) {
58 DrawContent(true);
59}
60
62 gui::AutoWidgetScope scope("Dungeon/SpriteEditor");
63 if (!current_room_id_ || !rooms_ || *current_room_id_ < 0 ||
64 *current_room_id_ >= static_cast<int>(rooms_->size()))
65 return;
66 if (canvas_viewer_ &&
69 constexpr const char* categories[] = {"All sprites", "Used here"};
71 auto& room = (*rooms_)[*current_room_id_];
72 std::vector<std::pair<int, std::string>> entries;
73 for (int id = 0; id < 256; ++id) {
74 if (browser_state_.category == 1 &&
75 std::none_of(room.GetSprites().begin(), room.GetSprites().end(),
76 [id](const auto& sprite) { return sprite.id() == id; }))
77 continue;
78 auto name = SpriteName(id);
80 entries.emplace_back(id, std::move(name));
81 }
82 if (browser_state_.sort == 1)
83 std::stable_sort(entries.begin(), entries.end(),
84 [](const auto& a, const auto& b) {
85 return workbench::DungeonBrowserFold(a.second) <
86 workbench::DungeonBrowserFold(b.second);
87 });
88 const bool can_place =
94 room, canvas_viewer_ ? canvas_viewer_->game_data() : nullptr);
95 if (ImGui::BeginChild("##SpriteLibrary", ImVec2(0, 0), false)) {
96 ImGui::BeginDisabled(!can_place);
97 for (const auto& [id, name] : entries) {
98 ImGui::PushID(id);
99 const bool picked = workbench::DrawDungeonBrowserRow(
100 id, name, selected_sprite_id_ == id,
101 [&, id = id](ImVec2 pos, ImVec2 size) {
102 const auto& preview = GetOrCreateSpritePreview(id, room);
104 ImGui::GetWindowDrawList(), preview.pixels, preview.bounds, pos,
105 ImVec2(pos.x + size.x, pos.y + size.y), colors);
106 });
107 gui::AutoRegisterLastItem("selectable",
108 absl::StrFormat("place_sprite_%02X", id));
109 if (picked && can_place) {
112 }
113 ImGui::PopID();
114 }
115 ImGui::EndDisabled();
116 if (entries.empty())
117 ImGui::TextDisabled("No matches");
118 }
119 ImGui::EndChild();
120}
121
122void SpriteEditorPanel::DrawContent(bool show_room_sprites) {
123 gui::AutoWidgetScope automation_scope("Dungeon/SpriteEditor");
124 if (!current_room_id_ || !rooms_ || *current_room_id_ < 0 ||
125 *current_room_id_ >= static_cast<int>(rooms_->size())) {
126 ImGui::TextDisabled("No room selected");
127 return;
128 }
130 // Standalone selection stays above the library and room list. Both hosts
131 // share these fields and their validation; Workbench owns its inspector.
132 if (show_room_sprites && canvas_viewer_ &&
134 const auto& selected = canvas_viewer_->object_interaction()
137 if (selected.size() == 1 && selected.front().type == EntityType::Sprite) {
139 }
140 }
141 DrawSpriteSelector(show_room_sprites);
142 if (show_room_sprites) {
143 ImGui::Separator();
145 }
146}
147
151
153 std::function<void(const zelda3::Sprite&)> callback) {
154 sprite_placed_callback_ = std::move(callback);
155}
156
158 std::function<void()> callback) {
159 open_selection_inspector_callback_ = std::move(callback);
160}
161
163 const bool placing =
167 if (placing) {
171 .GetSpriteId();
172 ImGui::Text("Placing sprite %02X", selected_sprite_id_);
173 if (ImGui::IsItemHovered())
174 ImGui::SetTooltip("%s", SpriteName(selected_sprite_id_).c_str());
175 ImGui::SameLine();
176 const bool done = ImGui::SmallButton("Done (Esc)");
177 gui::AutoRegisterLastItem("button", "finish_sprite_placement");
178 if (done)
180 } else {
181 ImGui::TextDisabled("Click a sprite, then the canvas.");
182 }
183}
184
187 const zelda3::Room& room) {
188 const project::YazeProject* project =
190 const std::string_view hack_name =
191 project != nullptr && project->hack_manifest.loaded()
192 ? std::string_view(project->hack_manifest.hack_name())
193 : std::string_view{};
194 const bool resources_changed = sprite_preview_resources_.SetContext(
195 project ? project->filepath : std::string{},
196 project ? project->GetAbsolutePath(project->assets_folder)
197 : std::string{},
198 hack_name);
199 if (resources_changed ||
201 sprite_preview_cache_.clear();
203 }
204 auto it = sprite_preview_cache_.find(sprite_id);
205 if (it != sprite_preview_cache_.end()) {
206 return it->second;
207 }
208
209 CachedSpritePreview cached;
210 zelda3::Sprite sprite;
211 sprite.set_id(static_cast<uint8_t>(sprite_id));
212
213 const auto* preview_layout =
215 if (!preview_layout) {
216 preview_layout = zelda3::SpriteOamRegistry::GetLayout(sprite_id);
217 }
218
219 const auto& room_gfx = room.get_gfx_buffer();
220 const std::span<const uint8_t> room_gfx_span(room_gfx.data(),
221 room_gfx.size());
223 room_gfx_span, preview_layout,
224 sprite_preview_resources_.GetGraphics(preview_layout));
225 if (sprite.preview_graphics() != nullptr &&
226 !sprite.preview_graphics()->empty()) {
227 cached.pixels = *sprite.preview_graphics();
228 cached.bounds = sprite.preview_bounds();
229 }
230 auto [inserted_it, _] =
231 sprite_preview_cache_.emplace(sprite_id, std::move(cached));
232 return inserted_it->second;
233}
234
235void SpriteEditorPanel::DrawSpriteSelector(bool show_room_sprites) {
236 ImGui::SetNextItemWidth(-1);
237 ImGui::InputTextWithHint("##Search", "Search sprites by name or hex ID...",
239 gui::AutoRegisterLastItem("input_text", "sprite_search");
240
241 const float available = std::max(1.0f, ImGui::GetContentRegionAvail().y);
242 const float list_height =
243 show_room_sprites ? std::max(1.0f, available * 0.55f) : available;
244 const bool can_place =
249 if (ImGui::BeginChild("##SpriteLibrary", ImVec2(0, list_height), true)) {
250 const auto& theme = AgentUI::GetTheme();
251 auto& room = (*rooms_)[*current_room_id_];
252 const auto sprite_colors = zelda3::BuildDungeonSpriteRenderPalette(
253 room, canvas_viewer_ ? canvas_viewer_->game_data() : nullptr);
254 ImDrawList* draw_list = ImGui::GetWindowDrawList();
255
256 ImGui::BeginDisabled(!can_place);
257 for (int id = 0; id < 256; ++id) {
258 if (!MatchesFilter(id))
259 continue;
260 const std::string name = SpriteName(id);
261 ImGui::PushID(id);
262
263 const float row_height = 28.0f;
264 const ImVec2 p0 = ImGui::GetCursorScreenPos();
265 const bool is_selected = selected_sprite_id_ == id;
266 const bool selected =
267 ImGui::Selectable("##row", is_selected, 0, ImVec2(0, row_height));
268 gui::AutoRegisterLastItem("selectable",
269 absl::StrFormat("place_sprite_%02X", id));
270 if (selected) {
273 }
274
275 const auto& preview = GetOrCreateSpritePreview(id, room);
276 const ImVec2 preview_min(p0.x + 4.0f, p0.y + 2.0f);
277 const ImVec2 preview_max(p0.x + 28.0f, p0.y + 26.0f);
278 bool drew_preview = false;
279 if (preview.bounds.w > 0 && preview.bounds.h > 0 &&
280 !preview.pixels.empty()) {
282 draw_list, preview.pixels, preview.bounds, preview_min, preview_max,
283 sprite_colors);
284 }
285 if (!drew_preview) {
286 draw_list->AddRect(preview_min, preview_max,
287 ImGui::GetColorU32(theme.status_inactive), 2.0f);
288 draw_list->AddText(ImVec2(preview_min.x + 5.0f, preview_min.y + 4.0f),
289 ImGui::GetColorU32(theme.status_inactive),
291 }
292
293 const std::string label = absl::StrFormat("%02X %s", id, name);
294 ImGui::LogText("%s\n", label.c_str());
295 draw_list->AddText(
296 ImVec2(p0.x + 34.0f, p0.y + 6.0f),
297 ImGui::GetColorU32(is_selected ? theme.text_primary
298 : theme.text_secondary_gray),
299 label.c_str());
300
301 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
302 ImGui::SetTooltip("%s (%02X)\n%s", name.c_str(), id,
303 can_place
304 ? "Click, then click the room to place."
305 : "Select an editable room to place sprites.");
306 ImGui::PopID();
307 }
308 ImGui::EndDisabled();
309 }
310 ImGui::EndChild();
311}
312
314 const auto& theme = AgentUI::GetTheme();
315 auto& room = (*rooms_)[*current_room_id_];
316 auto& sprites = room.GetSprites();
317 int selected_sprite_list_index = -1;
318 if (canvas_viewer_) {
319 const auto& selected_entities = canvas_viewer_->object_interaction()
322 if (selected_entities.size() == 1 &&
323 selected_entities.front().type == EntityType::Sprite) {
324 selected_sprite_list_index =
325 static_cast<int>(selected_entities.front().index);
326 }
327 }
328
329 // Sprite count with limit warning
330 int sprite_count = static_cast<int>(sprites.size());
331 ImVec4 count_color =
332 sprite_count > 16 ? theme.text_error_red : theme.text_primary;
333 ImGui::TextColored(count_color, ICON_MD_LIST " Room Sprites: %d/16",
334 sprite_count);
335 if (selected_sprite_list_index >= 0 && open_selection_inspector_callback_) {
336 ImGui::SameLine();
337 if (ImGui::SmallButton(ICON_MD_OPEN_IN_NEW " Inspect Selected")) {
339 }
340 }
341
342 if (sprite_count > 16) {
343 ImGui::SameLine();
344 ImGui::TextColored(theme.text_warning_yellow, ICON_MD_WARNING);
345 if (ImGui::IsItemHovered()) {
346 ImGui::SetTooltip(
347 tr("Room exceeds sprite limit (16 max)!\n"
348 "This may cause game crashes."));
349 }
350 }
351
352 if (sprites.empty()) {
353 ImGui::TextColored(theme.text_secondary_gray,
354 ICON_MD_INFO " No sprites in this room");
355 return;
356 }
357
358 // Split view: list on top, properties below
359 float available = ImGui::GetContentRegionAvail().y;
360 float list_height = std::max(100.0f, available * 0.4f);
361
362 const auto sprite_colors = zelda3::BuildDungeonSpriteRenderPalette(
363 room, canvas_viewer_ ? canvas_viewer_->game_data() : nullptr);
364 ImGui::BeginChild("##SpriteList", ImVec2(0, list_height), true);
365 ImDrawList* draw_list = ImGui::GetWindowDrawList();
366 for (size_t i = 0; i < sprites.size(); ++i) {
367 const auto& sprite = sprites[i];
368 bool is_selected = (selected_sprite_list_index == static_cast<int>(i));
369
370 ImGui::PushID(static_cast<int>(i));
371
372 const float row_height = 24.0f;
373 const ImVec2 p0 = ImGui::GetCursorScreenPos();
374 const bool row_selected =
375 ImGui::Selectable("##row", is_selected, 0, ImVec2(0, row_height));
376 gui::AutoRegisterLastItem("selectable",
377 absl::StrFormat("sprite_row_%zu", i),
378 "Select a sprite in the current dungeon room");
379 if (row_selected) {
380 if (canvas_viewer_) {
381 canvas_viewer_->object_interaction().SelectEntity(EntityType::Sprite,
382 i);
383 }
384 }
385
386 const auto& preview = GetOrCreateSpritePreview(sprite.id(), room);
387 const ImVec2 preview_min(p0.x + 2.0f, p0.y + 1.0f);
388 const ImVec2 preview_max(p0.x + 24.0f, p0.y + 23.0f);
389 if (preview.bounds.w > 0 && preview.bounds.h > 0 &&
390 !preview.pixels.empty()) {
391 DungeonRenderingHelpers::DrawSpritePreview(draw_list, preview.pixels,
392 preview.bounds, preview_min,
393 preview_max, sprite_colors);
394 }
395
396 std::string label =
397 absl::StrFormat("[%02X] %s", sprite.id(), SpriteName(sprite.id()));
398 if (sprite.key_drop() == 1) {
399 label += " " ICON_MD_KEY;
400 } else if (sprite.key_drop() == 2) {
401 label += " " ICON_MD_VPN_KEY;
402 }
403 if (sprite.IsOverlord()) {
404 label += " " ICON_MD_STAR;
405 }
406
407 draw_list->AddText(
408 ImVec2(p0.x + 28.0f, p0.y + 4.0f),
409 ImGui::GetColorU32(is_selected ? theme.text_primary
410 : theme.text_secondary_gray),
411 label.c_str());
412
413 std::string pos_text =
414 absl::StrFormat("(%d,%d) L%d", sprite.x(), sprite.y(), sprite.layer());
415 ImVec2 pos_size = ImGui::CalcTextSize(pos_text.c_str());
416 const float right_margin = 8.0f;
417 const float pos_x =
418 std::max(p0.x + 180.0f, p0.x + ImGui::GetContentRegionAvail().x -
419 pos_size.x - right_margin);
420 draw_list->AddText(ImVec2(pos_x, p0.y + 4.0f),
421 ImGui::GetColorU32(theme.text_secondary_gray),
422 pos_text.c_str());
423
424 ImGui::PopID();
425 }
426 ImGui::EndChild();
427}
428
429std::string SpriteEditorPanel::SpriteName(int sprite_id) const {
431 canvas_viewer_ ? canvas_viewer_->project() : nullptr);
432 return labels.GetLabel(zelda3::ResourceType::kSprite, sprite_id);
433}
434
435bool SpriteEditorPanel::MatchesFilter(int sprite_id) const {
436 if (search_filter_[0] == '\0')
437 return true;
438 std::string name =
439 absl::StrFormat("%02X %s", sprite_id, SpriteName(sprite_id));
440 std::string filter = search_filter_;
441 const auto lower = [](unsigned char c) {
442 return static_cast<char>(tolower(c));
443 };
444 std::transform(name.begin(), name.end(), name.begin(), lower);
445 std::transform(filter.begin(), filter.end(), filter.begin(), lower);
446 return name.find(filter) != std::string::npos;
447}
448
449} // namespace editor
450} // namespace yaze
const std::string & hack_name() const
bool loaded() const
Check if the manifest has been loaded.
DungeonObjectInteraction & object_interaction()
const project::YazeProject * project() const
void SetSpritePlacementMode(bool enabled, uint8_t sprite_id=0)
InteractionCoordinator & entity_coordinator()
Get the interaction coordinator for entity handling.
void SelectEntity(EntityType type, size_t index)
static bool DrawSpritePreview(ImDrawList *draw_list, const std::vector< uint8_t > &preview, const SDL_Rect &bounds, const ImVec2 &box_min, const ImVec2 &box_max, const std::array< SDL_Color, 256 > &color_table, uint8_t transparent_index=kSpritePreviewTransparentIndex)
const std::vector< SelectedEntity > & GetSelectedEntities() const
SpriteInteractionHandler & sprite_handler()
std::function< void(const zelda3::Sprite &) sprite_placed_callback_)
workbench::DungeonBrowserState browser_state_
std::string SpriteName(int sprite_id) const
void SetOpenSelectionInspectorCallback(std::function< void()> callback)
void DrawContent(bool show_room_sprites)
void SetCanvasViewer(DungeonCanvasViewer *viewer)
std::unordered_map< int, CachedSpritePreview > sprite_preview_cache_
std::string GetEditorCategory() const override
Editor category this panel belongs to.
std::string GetDisplayName() const override
Human-readable name shown in menus and title bars.
void Draw(bool *p_open) override
Draw the panel content.
std::string GetWorkflowGroup() const override
Optional workflow group for hack-centric actions.
bool MatchesFilter(int sprite_id) const
std::function< void()> open_selection_inspector_callback_
void DrawSpriteSelector(bool show_room_sprites)
zelda3::SpritePreviewResourceCache sprite_preview_resources_
void SetSpritePlacedCallback(std::function< void(const zelda3::Sprite &)> callback)
const CachedSpritePreview & GetOrCreateSpritePreview(int sprite_id, const zelda3::Room &room)
std::string GetId() const override
Unique identifier for this panel.
int GetPriority() const override
Get display priority for menu ordering.
SpriteEditorPanel(int *current_room_id, DungeonRoomStore *rooms, DungeonCanvasViewer *canvas_viewer=nullptr)
std::string GetIcon() const override
Material Design icon for this panel.
uint8_t GetSpriteId() const
Get current sprite ID for placement.
RAII scope that enables automatic widget registration.
std::string GetLabel(ResourceType type, int id) const
Get a label for a resource by type and ID.
const std::array< uint8_t, 0x10000 > & get_gfx_buffer() const
Definition room.h:1120
uint64_t graphics_revision() const
Definition room.h:1125
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)
std::span< const uint8_t > GetGraphics(const SpriteOamLayout *layout)
bool SetContext(std::string_view project_path, std::string_view assets_path, std::string_view hack_name)
A class for managing sprites in the overworld and underworld.
Definition sprite.h:39
void RenderPreviewGraphics(std::span< const uint8_t > graphics, const SpriteOamLayout *layout_override=nullptr, std::span< const uint8_t > graphics_resource={})
Definition sprite.cc:1194
auto set_id(uint8_t id)
Definition sprite.h:108
auto preview_graphics() const
Definition sprite.h:103
SDL_Rect preview_bounds() const
Definition sprite.h:106
#define ICON_MD_INFO
Definition icons.h:993
#define ICON_MD_WARNING
Definition icons.h:2123
#define ICON_MD_STAR
Definition icons.h:1848
#define ICON_MD_LIST
Definition icons.h:1094
#define ICON_MD_PERSON
Definition icons.h:1415
#define ICON_MD_KEY
Definition icons.h:1026
#define ICON_MD_OPEN_IN_NEW
Definition icons.h:1354
#define ICON_MD_CATEGORY
Definition icons.h:382
#define ICON_MD_VPN_KEY
Definition icons.h:2113
const AgentUITheme & GetTheme()
zelda3::ResourceLabelProvider LabelsForProject(const project::YazeProject *project)
bool DungeonBrowserMatches(std::string_view query, int id, std::string_view name)
void DrawDungeonBrowserControls(DungeonBrowserState &state, std::span< const char *const > categories)
bool DrawDungeonBrowserRow(int id, const std::string &name, bool selected, DrawPreview preview, std::string_view badge={})
void DrawDungeonEntityInspector(DungeonCanvasViewer &viewer, const std::function< void(int, size_t)> &jump_to_reciprocal)
void AutoRegisterLastItem(const std::string &widget_type, const std::string &explicit_label, const std::string &description)
Automatically register the last ImGui item.
std::array< SDL_Color, 256 > BuildDungeonSpriteRenderPalette(const Room &room, const GameData *game_data)
Definition room.cc:200
Modern project structure with comprehensive settings consolidation.
Definition project.h:290
core::HackManifest hack_manifest
Definition project.h:376
std::string assets_folder
Definition project.h:315
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:2675
Automatic widget registration helpers for ImGui Test Engine integration.