yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_canvas_overlays.cc
Go to the documentation of this file.
1#include <algorithm>
2#include <array>
3#include <cstdint>
4#include <span>
5#include <string>
6#include <string_view>
7#include <vector>
8#include "util/i18n/tr.h"
9
10#include "absl/strings/str_format.h"
18#include "core/project.h"
20#include "imgui/imgui.h"
29
30namespace yaze::editor {
31
32namespace {
33
34bool ShowEntityLabel(const gui::CanvasRuntime& rt, int x, int y, int width,
35 int height, bool selected) {
36 if (selected)
37 return true;
38 if (!rt.hovered || !ImGui::IsWindowHovered())
39 return false;
40 const ImVec2 min(rt.canvas_p0.x + rt.scrolling.x + x * rt.scale,
41 rt.canvas_p0.y + rt.scrolling.y + y * rt.scale);
42 return ImGui::IsMouseHoveringRect(
43 min, ImVec2(min.x + width * rt.scale, min.y + height * rt.scale));
44}
45
47 const std::vector<uint8_t>& preview,
48 const SDL_Rect& bounds, int room_x, int room_y,
49 const std::array<SDL_Color, 256>& color_table) {
50 if (!rt.draw_list || bounds.w <= 0 || bounds.h <= 0) {
51 return false;
52 }
53 const size_t row_stride = static_cast<size_t>(bounds.w);
54 if (static_cast<size_t>(bounds.h) > preview.size() / row_stride) {
55 return false;
56 }
57
58 const float scale = std::max(rt.scale, 0.01f);
59 const ImVec2 origin(rt.canvas_p0.x + rt.scrolling.x,
60 rt.canvas_p0.y + rt.scrolling.y);
61 const ImVec2 canvas_max(rt.canvas_p0.x + rt.canvas_sz.x,
62 rt.canvas_p0.y + rt.canvas_sz.y);
63 bool drew_any_pixel = false;
64
65 rt.draw_list->PushClipRect(rt.canvas_p0, canvas_max, true);
66 for (int y = 0; y < bounds.h; ++y) {
67 int x = 0;
68 while (x < bounds.w) {
69 const uint8_t palette_index =
70 preview[static_cast<size_t>(y) * row_stride + x];
71 const SDL_Color& palette_color = color_table[palette_index];
72 // RenderPreviewGraphics reserves index 0 for transparency. Unlike the
73 // former 0xFF sentinel, index 0 cannot collide with a visible dungeon
74 // sprite pixel: the preview encoder emits CGRAM indices 0x71..0xFF.
75 if (palette_index == 0 || palette_color.a == 0) {
76 ++x;
77 continue;
78 }
79 const ImU32 color = IM_COL32(palette_color.r, palette_color.g,
80 palette_color.b, palette_color.a);
81
82 const int start_x = x;
83 while (x < bounds.w && preview[static_cast<size_t>(y) * row_stride + x] ==
84 palette_index) {
85 ++x;
86 }
87
88 const ImVec2 min(origin.x + (room_x + start_x) * scale,
89 origin.y + (room_y + y) * scale);
90 const ImVec2 max(origin.x + (room_x + x) * scale,
91 origin.y + (room_y + y + 1) * scale);
92 // PrimRect instead of AddRectFilled: since 1.93, AddRectFilled
93 // anti-aliases rects with fractional corners (any non-integer zoom or
94 // pan), which insets the solid color 0.5px and leaves seams between
95 // adjacent pixel runs. Sprite art needs hard edges.
96 rt.draw_list->PrimReserve(6, 4);
97 rt.draw_list->PrimRect(min, max, color);
98 drew_any_pixel = true;
99 }
100 }
101 rt.draw_list->PopClipRect();
102
103 return drew_any_pixel;
104}
105
106const char* GetStoredPlacementLabel(const zelda3::RoomObject& object) {
107 const int layer_value = object.GetLayerValue();
108 if (!zelda3::UsesRoomObjectStream(object)) {
109 if (layer_value == 0) {
110 return "Upper BG1";
111 }
112 return layer_value == 1 ? "Lower BG2" : "Invalid selector";
113 }
114 switch (layer_value) {
115 case 0:
116 return "Primary";
117 case 1:
118 return "BG2 overlay";
119 case 2:
120 return "BG1 overlay";
121 default:
122 return "Unknown";
123 }
124}
125
126} // namespace
127
129 const zelda3::Room& room) {
131 return;
132 }
133
134 const auto& theme = AgentUI::GetTheme();
135 const bool is_touch = gui::LayoutHelpers::IsTouchDevice();
136 const int entity_size = is_touch ? 24 : 16;
137 const auto sprite_colors =
139 const std::string_view hack_name =
140 project_ != nullptr && project_->hack_manifest.loaded()
141 ? std::string_view(project_->hack_manifest.hack_name())
142 : std::string_view{};
144 project_ ? project_->filepath : std::string{},
146 : std::string{},
147 hack_name)) {
148 sprite_preview_cache_.Clear();
149 }
150 const auto& room_gfx = room.get_gfx_buffer();
151 const std::span<const uint8_t> room_gfx_span(room_gfx.data(),
152 room_gfx.size());
153
154 const auto& selected =
156 const auto& sprites = room.GetSprites();
157 for (size_t index = 0; index < sprites.size(); ++index) {
158 const auto& sprite = sprites[index];
159 const int canvas_x = sprite.x() * 16;
160 const int canvas_y = sprite.y() * 16;
161
162 if (canvas_x >= -entity_size && canvas_y >= -entity_size &&
163 canvas_x < 512 + entity_size && canvas_y < 512 + entity_size) {
164 ImVec4 sprite_color = sprite.layer() == 0 ? theme.dungeon_sprite_layer0
165 : theme.dungeon_sprite_layer1;
166
167 const auto* preview_layout =
168 zelda3::SpriteOamRegistry::GetPreviewOverride(sprite.id(), hack_name);
169 const SpritePreviewKey preview_key{room.graphics_revision(), sprite.id(),
170 sprite.subtype(), sprite.IsOverlord()};
171 // Revision zero means the room has not assembled its graphics yet and
172 // is not a reusable graphics identity.
173 auto* preview = room.graphics_revision() != 0
174 ? sprite_preview_cache_.Get(preview_key)
175 : nullptr;
176 CachedSpritePreview uncached_preview;
177 if (preview == nullptr) {
178 zelda3::Sprite preview_sprite = sprite;
179 preview_sprite.RenderPreviewGraphics(
180 room_gfx_span, preview_layout,
181 sprite_preview_resources_.GetGraphics(preview_layout));
182 CachedSpritePreview rendered{*preview_sprite.preview_graphics(),
183 preview_sprite.preview_bounds()};
184 if (room.graphics_revision() != 0) {
185 preview =
186 sprite_preview_cache_.Insert(preview_key, std::move(rendered));
187 } else {
188 uncached_preview = std::move(rendered);
189 preview = &uncached_preview;
190 }
191 }
192 const SDL_Rect preview_bounds = preview->bounds;
193 const bool drew_preview = DrawSpritePreviewPixels(
194 rt, preview->pixels, preview_bounds, canvas_x + preview_bounds.x,
195 canvas_y + preview_bounds.y, sprite_colors);
196
197 const bool show_label = ShowEntityLabel(
198 rt, canvas_x, canvas_y, entity_size, entity_size,
199 std::any_of(selected.begin(), selected.end(),
200 [index](const auto& entity) {
201 return entity.type == EntityType::Sprite &&
202 entity.index == index;
203 }));
204 if (drew_preview && show_label) {
205 gui::DrawOutline(rt, canvas_x, canvas_y, entity_size, entity_size,
206 ImGui::GetColorU32(sprite_color));
207 } else if (!drew_preview) {
208 gui::DrawRect(rt, canvas_x, canvas_y, entity_size, entity_size,
209 sprite_color);
210 }
211
212 std::string full_name = preview_layout != nullptr
213 ? preview_layout->name
214 : zelda3::GetSpriteLabel(sprite.id());
215 std::string sprite_text;
216 if (full_name.length() > 12) {
217 sprite_text = absl::StrFormat("%02X %s..", sprite.id(),
218 full_name.substr(0, 8).c_str());
219 } else {
220 sprite_text =
221 absl::StrFormat("%02X %s", sprite.id(), full_name.c_str());
222 }
223
224 if (show_label)
225 gui::DrawText(rt, sprite_text, canvas_x, canvas_y);
226 }
227 }
228}
229
231 const zelda3::Room& room) {
233 return;
234 }
235
236 const auto& pot_items = room.GetPotItems();
237 if (pot_items.empty()) {
238 return;
239 }
240
241 const auto sprite_colors =
243 const auto& selected =
245 for (size_t index = 0; index < pot_items.size(); ++index) {
246 const auto& pot_item = pot_items[index];
247 const int pixel_x = pot_item.GetPixelX();
248 const int pixel_y = pot_item.GetPixelY();
249 auto [canvas_x, canvas_y] =
251 pixel_y / 8);
252
253 const bool is_touch = gui::LayoutHelpers::IsTouchDevice();
254 const int entity_size = is_touch ? 24 : 16;
255
256 if (canvas_x >= -entity_size && canvas_y >= -entity_size &&
257 canvas_x < 512 + entity_size && canvas_y < 512 + entity_size) {
258 const auto& theme = AgentUI::GetTheme();
259 ImVec4 pot_item_color;
260 if (pot_item.item == 0) {
261 pot_item_color = theme.status_inactive;
262 pot_item_color.w = 0.4f;
263 } else {
264 pot_item_color = theme.item_color;
265 pot_item_color.w = 0.75f;
266 }
267
268 const ItemPreviewKey preview_key{room.graphics_revision(), pot_item.item};
269 auto* preview = room.graphics_revision() != 0
270 ? item_preview_cache_.Get(preview_key)
271 : nullptr;
272 CachedSpritePreview uncached_preview;
273 if (preview == nullptr) {
274 CachedSpritePreview rendered;
276 pot_item.item, room, rendered.pixels, rendered.bounds)) {
277 if (room.graphics_revision() != 0) {
278 preview =
279 item_preview_cache_.Insert(preview_key, std::move(rendered));
280 } else {
281 uncached_preview = std::move(rendered);
282 preview = &uncached_preview;
283 }
284 }
285 }
286
287 bool drew_preview = false;
288 if (preview != nullptr && preview->bounds.w > 0 &&
289 preview->bounds.h > 0 && !preview->pixels.empty()) {
290 const SDL_Rect preview_bounds = preview->bounds;
291 const int offset_x = (entity_size > preview_bounds.w)
292 ? (entity_size - preview_bounds.w) / 2
293 : 0;
294 const int offset_y = (entity_size > preview_bounds.h)
295 ? (entity_size - preview_bounds.h) / 2
296 : 0;
297 drew_preview = DrawSpritePreviewPixels(
298 rt, preview->pixels, preview_bounds,
299 canvas_x + preview_bounds.x + offset_x,
300 canvas_y + preview_bounds.y + offset_y, sprite_colors);
301 }
302
303 const bool is_selected = std::any_of(
304 selected.begin(), selected.end(), [index](const auto& entity) {
305 return entity.type == EntityType::Item && entity.index == index;
306 });
307 const bool show_label = ShowEntityLabel(
308 rt, canvas_x, canvas_y, entity_size, entity_size, is_selected);
309
310 if (drew_preview && (show_label || is_selected)) {
311 gui::DrawOutline(rt, canvas_x, canvas_y, entity_size, entity_size,
312 ImGui::GetColorU32(pot_item_color));
313 } else if (!drew_preview) {
314 gui::DrawRect(rt, canvas_x, canvas_y, entity_size, entity_size,
315 pot_item_color);
316 }
317
318 std::string item_name;
319 if (pot_item.item < kDungeonPotItemCount) {
320 item_name = GetDungeonPotItemName(pot_item.item);
321 } else {
322 item_name = absl::StrFormat("Unk%02X", pot_item.item);
323 }
324
325 std::string item_text =
326 absl::StrFormat("%02X %s", pot_item.item, item_name.c_str());
327 if (show_label) {
328 gui::DrawText(rt, item_text, canvas_x, canvas_y - 10);
329 }
330 }
331 }
332}
333
335 const zelda3::Room& room) {
337 !room.AreChestsLoaded() || rt.canvas_sz.x <= 16.0f) {
338 return;
339 }
340 const auto& objects = room.GetTileObjects();
341 const auto& chests = room.GetChests();
342 // Residual records and mismatched small/big objects must not be presented as
343 // a confident reward association. The chest inspector explains the mismatch.
344 if (chests.empty() ||
345 !zelda3::ValidateChestObjectMapping(objects, chests).ok()) {
346 return;
347 }
348
349 const DungeonCanvasTransform transform(rt.canvas_p0, rt.scrolling, rt.scale);
350 const ImVec2 canvas_max(rt.canvas_p0.x + rt.canvas_sz.x,
351 rt.canvas_p0.y + rt.canvas_sz.y);
352 const auto& theme = AgentUI::GetTheme();
353 const float padding = 4.0f;
354 const float wrap_width = std::min(180.0f, rt.canvas_sz.x - padding * 4);
355 rt.draw_list->PushClipRect(rt.canvas_p0, canvas_max, true);
356 for (size_t i = 0; i < objects.size(); ++i) {
357 const auto chest_index = zelda3::ChestIndexForObject(objects, i);
358 if (!chest_index) {
359 continue;
360 }
361 const auto& object = objects[i];
362 const float chest_width = object.id_ == 0xFB1 ? 32.0f : 16.0f;
363 const ImVec2 chest_min = transform.RoomPixelsToScreen(
364 ImVec2(object.x_ * 8.0f, object.y_ * 8.0f));
365 const ImVec2 chest_max = transform.RoomPixelsToScreen(
366 ImVec2(object.x_ * 8.0f + chest_width,
367 object.y_ * 8.0f + (object.id_ == 0xFB1 ? 24.0f : 16.0f)));
368 if (chest_max.x < rt.canvas_p0.x || chest_min.x > canvas_max.x ||
369 chest_max.y < rt.canvas_p0.y || chest_min.y > canvas_max.y) {
370 continue;
371 }
372 const auto item_id = chests[*chest_index].id;
373 const std::string text = absl::StrFormat(
374 "$%02X %s", item_id,
376 item_id, project_ ? &project_->resource_labels : nullptr));
377 const ImVec2 text_size =
378 ImGui::CalcTextSize(text.c_str(), nullptr, false, wrap_width);
379 const float width = text_size.x + padding * 2;
380 const float height = text_size.y + padding * 2;
381 const ImVec2 min(
382 std::clamp((chest_min.x + chest_max.x - width) * 0.5f, rt.canvas_p0.x,
383 std::max(rt.canvas_p0.x, canvas_max.x - width)),
384 std::max(rt.canvas_p0.y, chest_min.y - height - 3.0f));
385 const ImVec2 max(min.x + width, min.y + height);
386 rt.draw_list->AddRectFilled(min, max, IM_COL32(0, 0, 0, 200), 3.0f);
387 rt.draw_list->AddRect(min, max, ImGui::GetColorU32(theme.item_color), 3.0f);
388 rt.draw_list->AddText(ImGui::GetFont(), ImGui::GetFontSize(),
389 ImVec2(min.x + padding, min.y + padding),
390 ImGui::GetColorU32(ImGuiCol_Text), text.c_str(),
391 nullptr, wrap_width);
392 // Draw-list annotations have no ImGui item and cannot take a placement,
393 // selection, drag, or pan gesture away from the room canvas.
394 }
395 rt.draw_list->PopClipRect();
396}
397
399 const zelda3::Room& room) {
400 RenderSprites(rt, room);
401 RenderPotItems(rt, room);
402 RenderChestContents(rt, room);
403}
404
406 const gui::CanvasRuntime& rt, const zelda3::Room& room) {
407 constexpr const char* kPopupId = "##TouchEntityContextMenu";
408 const ImGuiIO& io = ImGui::GetIO();
409 const bool touch_context_click =
410 rt.hovered && io.MouseSource == ImGuiMouseSource_TouchScreen &&
411 ImGui::IsMouseClicked(ImGuiMouseButton_Right);
412 const bool gesture_long_press = touch_handler_.WasLongPressed();
413 const bool should_open_context = gesture_long_press || touch_context_click;
414
415 if (should_open_context) {
416 const ImVec2 gesture_pos = gesture_long_press
418 : ImGui::GetMousePos();
419 const DungeonCanvasTransform transform(rt.canvas_p0, rt.scrolling,
420 rt.scale);
421 const ImVec2 room_pos = transform.ScreenToRoomPixels(gesture_pos);
422 const auto [room_x, room_y] =
423 transform.ScreenToRoomPixelCoordinates(gesture_pos);
424 if (!dungeon_coords::IsWithinBounds(room_x, room_y)) {
425 return;
426 }
427 const float rel_x = room_pos.x;
428 const float rel_y = room_pos.y;
429 const bool is_touch = gui::LayoutHelpers::IsTouchDevice();
430 const int hit_size = is_touch ? 24 : 16;
431
432 const auto& sprites = room.GetSprites();
433 for (size_t idx = 0; idx < sprites.size(); ++idx) {
434 const int sprite_px = sprites[idx].x() * 16;
435 const int sprite_py = sprites[idx].y() * 16;
436 if (rel_x >= sprite_px && rel_x < sprite_px + hit_size &&
437 rel_y >= sprite_py && rel_y < sprite_py + hit_size) {
438 object_interaction_.SelectEntity(EntityType::Sprite, idx);
439 ImGui::OpenPopup(kPopupId);
440 break;
441 }
442 }
443
444 if (!ImGui::IsPopupOpen(kPopupId)) {
445 const auto& pot_items = room.GetPotItems();
446 for (size_t idx = 0; idx < pot_items.size(); ++idx) {
447 const int item_px = pot_items[idx].GetPixelX();
448 const int item_py = pot_items[idx].GetPixelY();
449 if (rel_x >= item_px && rel_x < item_px + hit_size &&
450 rel_y >= item_py && rel_y < item_py + hit_size) {
451 object_interaction_.SelectEntity(EntityType::Item, idx);
452 ImGui::OpenPopup(kPopupId);
453 break;
454 }
455 }
456 }
457
458 if (!ImGui::IsPopupOpen(kPopupId)) {
459 const auto object_index =
461 .tile_handler()
462 .GetEntityAtPosition(static_cast<int>(rel_x),
463 static_cast<int>(rel_y));
464 if (object_index.has_value()) {
466 ImGui::OpenPopup(kPopupId);
467 }
468 }
469 }
470
471 if (ImGui::BeginPopup(kPopupId)) {
474 if (sel.type == EntityType::Sprite) {
475 const auto& sprites = room.GetSprites();
476 if (sel.index < sprites.size()) {
477 std::string label = zelda3::GetSpriteLabel(sprites[sel.index].id());
478 ImGui::TextDisabled(tr("Sprite: %02X %s"), sprites[sel.index].id(),
479 label.c_str());
480 ImGui::Separator();
481 }
482 if (ImGui::MenuItem(tr("Delete Sprite"))) {
484 }
485 } else if (sel.type == EntityType::Item) {
486 ImGui::TextDisabled(tr("Pot Item"));
487 ImGui::Separator();
488 if (ImGui::MenuItem(tr("Delete Item"))) {
490 }
491 }
492 } else if (object_interaction_.GetSelectionCount() > 0) {
493 const auto indices = object_interaction_.GetSelectedObjectIndices();
494 if (indices.size() == 1) {
495 const auto& objects = room.GetTileObjects();
496 if (indices[0] < objects.size()) {
497 std::string name = zelda3::GetObjectName(objects[indices[0]].id_);
498 ImGui::TextDisabled(tr("Object: %03X %s"), objects[indices[0]].id_,
499 name.c_str());
500 ImGui::Separator();
501 }
502 } else {
503 ImGui::TextDisabled(tr("%zu objects selected"),
505 ImGui::Separator();
506 }
507 if (ImGui::MenuItem(tr("Delete"))) {
509 }
510 if (ImGui::MenuItem(tr("Copy"))) {
512 }
513 ImGui::Separator();
514 if (ImGui::MenuItem(tr("Send to Front"))) {
516 }
517 if (ImGui::MenuItem(tr("Send to Back"))) {
519 }
520 }
521 ImGui::EndPopup();
522 }
523}
524
526 const gui::CanvasRuntime& rt, const zelda3::Room& room) {
527 const auto& theme = AgentUI::GetTheme();
528 const auto& objects = room.GetTileObjects();
529
530 for (const auto& obj : objects) {
531 bool show_this_type = true;
532 if (obj.id_ < 0x100) {
534 } else if (obj.id_ >= 0x100 && obj.id_ < 0x200) {
536 } else if (obj.id_ >= 0xF00) {
538 }
539
540 bool show_this_layer = true;
541 if (obj.GetLayerValue() == 0) {
543 } else if (obj.GetLayerValue() == 1) {
545 } else if (obj.GetLayerValue() == 2) {
547 }
548
549 if (!show_this_type || !show_this_layer) {
550 continue;
551 }
552
553 auto [canvas_x, canvas_y, width, height] =
555 width = std::min(width, 512);
556 height = std::min(height, 512);
557
558 ImVec4 outline_color;
559 if (obj.GetLayerValue() == 0) {
560 outline_color = theme.dungeon_outline_layer0;
561 } else if (obj.GetLayerValue() == 1) {
562 outline_color = theme.dungeon_outline_layer1;
563 } else {
564 outline_color = theme.dungeon_outline_layer2;
565 }
566
567 gui::DrawRect(rt, canvas_x, canvas_y, width, height, outline_color);
568
569 std::string name = zelda3::GetObjectName(obj.id_);
570 if (name.length() > 12) {
571 name = name.substr(0, 10) + "..";
572 }
573 std::string label;
574 if (obj.id_ >= 0x100) {
575 label = absl::StrFormat("0x%03X\n%s\n%s [%dx%d]", obj.id_, name.c_str(),
576 GetStoredPlacementLabel(obj), width, height);
577 } else {
578 label = absl::StrFormat("0x%02X\n%s\n%s [%dx%d]", obj.id_, name.c_str(),
579 GetStoredPlacementLabel(obj), width, height);
580 }
581 gui::DrawText(rt, label, canvas_x + 1, canvas_y + 1);
582 }
583}
584
587 const auto* working = rooms_ ? rooms_->GetIfLoaded(room_id) : nullptr;
588 auto it = collision_overlay_cache_.find(room_id);
589 if (!working && it != collision_overlay_cache_.end()) {
590 return it->second;
591 }
592
594 cache.entries.clear();
595
596 if (!rom_ || !rom_->is_loaded()) {
597 collision_overlay_cache_.emplace(room_id, cache);
598 return collision_overlay_cache_.at(room_id);
599 }
600
601 // A loaded room is the authoring source of truth. Rebuild its small overlay
602 // from the model so edits, Undo and save-dirty clearing cannot leave ROM-era
603 // pixels cached. Unloaded rooms still use the ROM-backed cache.
604 auto map_or = working ? absl::StatusOr<zelda3::CustomCollisionMap>(
605 working->custom_collision())
607 if (!map_or.ok()) {
608 collision_overlay_cache_.emplace(room_id, cache);
609 return collision_overlay_cache_.at(room_id);
610 }
611
612 const auto& map = map_or.value();
613 cache.has_data = map.has_data;
614 if (cache.has_data && !track_collision_config_.IsEmpty()) {
615 for (int y = 0; y < 64; ++y) {
616 for (int x = 0; x < 64; ++x) {
617 const uint8_t tile = map.tiles[static_cast<size_t>(y * 64 + x)];
618 if (map.HasOverride(static_cast<size_t>(y * 64 + x)) &&
622 cache.entries.push_back(
624 static_cast<uint8_t>(x), static_cast<uint8_t>(y), tile});
625 }
626 }
627 }
628 }
629
630 collision_overlay_cache_.insert_or_assign(room_id, std::move(cache));
631 return collision_overlay_cache_.at(room_id);
632}
633
635 gfx::Bitmap* composite =
637 zelda3::Room* room = rooms_ ? rooms_->GetIfMaterialized(room_id) : nullptr;
638 if (composite != nullptr && room != nullptr) {
640 composite, room->rendered_dungeon_palette(), room->rendered_palette());
641 }
642 return composite;
643}
644
646 int room_id) {
647 // Resolve Auto for each neighbor without changing the primary room's context.
648 // Manual mode returns the same pinned entrance for every room.
649 auto* room = EnsureRoomLoadedForConnectedView(room_id);
650 if (!room) {
651 return nullptr;
652 }
653 room->SetRenderEntranceBlockset(entrance_blockset_resolver_
656 auto& entry = connected_composite_outputs_[room_id];
657 if (!entry.output) {
658 entry.output = std::make_unique<RoomCompositeOutput>();
659 }
660 entry.last_used_frame =
661 ImGui::GetCurrentContext() != nullptr ? ImGui::GetFrameCount() : 0;
662 return PrepareRoomCompositeWithOutput(room_id, *entry.output);
663}
664
666 int room_id, RoomCompositeOutput& output) {
667 if (room_id < 0 || room_id >= zelda3::kNumberOfRooms || !rooms_ || !rom_ ||
668 !rom_->is_loaded()) {
669 return nullptr;
670 }
671
672 auto* room_ptr = rooms_->TryEnsureRoom(room_id);
673 if (!room_ptr) {
674 return nullptr;
675 }
676 auto& room = *room_ptr;
677 auto& layer_mgr = GetRoomLayerManager(room_id);
678 auto& composite = output.Prepare(room, layer_mgr);
679 if (!composite.is_active() || composite.width() <= 0) {
680 return nullptr;
681 }
682
683 // Closes the A3 first-frame race for the dungeon room composite, mirroring
684 // the title-screen path (A4). The helper queues CREATE on first sight,
685 // queues UPDATE when RoomLayerManager::CompositeToOutput marked the bitmap
686 // modified, and stamps metadata().purpose so canvas_rendering's diagnostic
687 // log can identify the bitmap.
689 return &composite;
690}
691
693 if (room_id < 0 || room_id >= zelda3::kNumberOfRooms || !rooms_) {
694 return false;
695 }
696
697 const float scale = canvas_.global_scale();
698 gfx::Bitmap* composite = PrepareRoomCompositeBitmap(room_id);
699 if (!composite) {
700 return false;
701 }
702
703 // Single-room canvas rendering prepares the composite after the normal room
704 // state upload pass. Flush here too so workbench mode can draw the freshly
705 // queued room texture in the same frame.
707
708 if (composite->texture()) {
709 canvas_.DrawBitmap(*composite, 0, 0, scale, 255);
710 return true;
711 }
712
713 return false;
714}
715
717 const zelda3::Room& room) {
718 const auto& objects = room.GetTileObjects();
719 const ImVec4 mask_color(0.2f, 0.6f, 1.0f, 0.4f);
720
721 for (const auto& obj : objects) {
722 if (obj.GetLayerValue() != 1) {
723 continue;
724 }
725
726 auto [canvas_x, canvas_y] =
728 auto [width, height] =
730 width = std::min(width, 512);
731 height = std::min(height, 512);
732 gui::DrawRect(rt, canvas_x, canvas_y, width, height, mask_color);
733 }
734}
735
736} // namespace yaze::editor
bool is_loaded() const
Definition rom.h:199
const std::string & hack_name() const
bool loaded() const
Check if the manifest has been loaded.
std::pair< int, int > ScreenToRoomPixelCoordinates(ImVec2 screen) const
ImVec2 ScreenToRoomPixels(ImVec2 screen) const
gfx::Bitmap * PrepareConnectedRoomCompositeBitmap(int room_id)
void RenderPotItems(const gui::CanvasRuntime &rt, const zelda3::Room &room)
gfx::Bitmap * PrepareRoomCompositeWithOutput(int room_id, RoomCompositeOutput &output)
zelda3::SpritePreviewResourceCache sprite_preview_resources_
const DungeonRenderingHelpers::CollisionOverlayCache & GetCollisionOverlayCache(int room_id)
std::map< int, ConnectedRoomCompositeEntry > connected_composite_outputs_
zelda3::Room * EnsureRoomLoadedForConnectedView(int room_id)
DungeonObjectInteraction object_interaction_
const project::YazeProject * project_
std::function< uint8_t(int)> entrance_blockset_resolver_
std::unordered_map< int, DungeonRenderingHelpers::CollisionOverlayCache > collision_overlay_cache_
void RenderSprites(const gui::CanvasRuntime &rt, const zelda3::Room &room)
gfx::Bitmap * PrepareRoomCompositeBitmap(int room_id)
zelda3::RoomLayerManager & GetRoomLayerManager(int room_id)
void DrawMaskHighlights(const gui::CanvasRuntime &rt, const zelda3::Room &room)
util::LruCache< SpritePreviewKey, CachedSpritePreview > sprite_preview_cache_
void RenderChestContents(const gui::CanvasRuntime &rt, const zelda3::Room &room)
std::pair< uint64_t, uint8_t > ItemPreviewKey
util::LruCache< ItemPreviewKey, CachedSpritePreview > item_preview_cache_
void RenderEntityOverlay(const gui::CanvasRuntime &rt, const zelda3::Room &room)
std::tuple< uint64_t, uint8_t, int, bool > SpritePreviewKey
DungeonRenderingHelpers::TrackCollisionConfig track_collision_config_
void DrawObjectPositionOutlines(const gui::CanvasRuntime &rt, const zelda3::Room &room)
void HandleTouchLongPressContextMenu(const gui::CanvasRuntime &rt, const zelda3::Room &room)
void SetSelectedObjects(const std::vector< size_t > &indices)
std::vector< size_t > GetSelectedObjectIndices() const
InteractionCoordinator & entity_coordinator()
Get the interaction coordinator for entity handling.
void SelectEntity(EntityType type, size_t index)
static std::pair< int, int > RoomToCanvasCoordinates(int room_x, int room_y)
static bool BuildPotItemPreview(uint8_t item_id, const zelda3::Room &room, std::vector< uint8_t > &out_pixels, SDL_Rect &out_bounds)
zelda3::Room * GetIfMaterialized(int room_id)
zelda3::Room * GetIfLoaded(int room_id)
zelda3::Room * TryEnsureRoom(int room_id)
const std::vector< SelectedEntity > & GetSelectedEntities() const
gfx::Bitmap & Prepare(zelda3::Room &room, const zelda3::RoomLayerManager &layer_manager)
std::optional< size_t > GetEntityAtPosition(int canvas_x, int canvas_y) const override
Get entity at canvas position.
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:211
static Arena & Get()
Definition arena.cc:24
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:70
TextureHandle texture() const
Definition bitmap.h:410
bool WasLongPressed() const
Check if a long-press gesture occurred this frame.
ImVec2 GetGesturePosition() const
Get the position of the last tap/gesture.
void DrawBitmap(Bitmap &bitmap, int border_offset, float scale)
Definition canvas.cc:1295
auto global_scale() const
Definition canvas.h:413
static DimensionService & Get()
std::tuple< int, int, int, int > GetSelectionBoundsPixels(const RoomObject &obj) const
std::pair< int, int > GetPixelDimensions(const RoomObject &obj) const
static PaletteDebugger & Get()
void SetCurrentPresentation(gfx::Bitmap *bitmap, const gfx::SnesPalette &palette, const std::vector< SDL_Color > &render_palette)
const std::array< uint8_t, 0x10000 > & get_gfx_buffer() const
Definition room.h:1120
const std::vector< chest_data > & GetChests() const
Definition room.h:323
const std::vector< SDL_Color > & rendered_palette() const
Definition room.h:1138
const gfx::SnesPalette & rendered_dungeon_palette() const
Definition room.h:1132
const std::vector< zelda3::Sprite > & GetSprites() const
Definition room.h:316
uint64_t graphics_revision() const
Definition room.h:1125
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:445
const std::vector< PotItem > & GetPotItems() const
Definition room.h:429
bool AreChestsLoaded() const
Definition room.h:974
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 preview_graphics() const
Definition sprite.h:103
SDL_Rect preview_bounds() const
Definition sprite.h:106
const AgentUITheme & GetTheme()
bool DrawSpritePreviewPixels(const gui::CanvasRuntime &rt, const std::vector< uint8_t > &preview, const SDL_Rect &bounds, int room_x, int room_y, const std::array< SDL_Color, 256 > &color_table)
bool ShowEntityLabel(const gui::CanvasRuntime &rt, int x, int y, int width, int height, bool selected)
bool IsWithinBounds(int canvas_x, int canvas_y, int margin=0)
Check if coordinates are within room bounds.
Editors are the view controllers for the application.
const char * GetDungeonPotItemName(uint8_t item_id)
constexpr size_t kDungeonPotItemCount
void EnsureCompositeBitmapTextureQueued(Bitmap &composite)
void DrawRect(const CanvasRuntime &rt, int x, int y, int w, int h, ImVec4 color)
void DrawOutline(const CanvasRuntime &rt, int x, int y, int w, int h, ImU32 color=IM_COL32(255, 255, 255, 200))
void DrawText(const CanvasRuntime &rt, const std::string &text, int x, int y)
absl::Status ValidateChestObjectMapping(const std::vector< RoomObject > &objects, const std::vector< chest_data > &chests)
Definition chest_edit.cc:74
std::string GetSpriteLabel(int id)
Convenience function to get a sprite label.
std::optional< size_t > ChestIndexForObject(const std::vector< RoomObject > &objects, size_t object_index)
Definition chest_edit.cc:63
std::string GetItemReceiptLabel(uint8_t id, const ResourceLabelProvider::ProjectLabels *project_labels)
absl::StatusOr< CustomCollisionMap > LoadCustomCollisionMap(Rom *rom, int room_id)
bool UsesRoomObjectStream(const RoomObject &object)
std::string GetObjectName(int object_id)
constexpr int kNumberOfRooms
std::array< SDL_Color, 256 > BuildDungeonSpriteRenderPalette(const Room &room, const GameData *game_data)
Definition room.cc:200
core::HackManifest hack_manifest
Definition project.h:376
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > resource_labels
Definition project.h:369
std::string assets_folder
Definition project.h:315
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:2675