10#include "absl/strings/str_format.h"
19#include "imgui/imgui.h"
34 int height,
bool selected) {
37 if (!rt.
hovered || !ImGui::IsWindowHovered())
41 return ImGui::IsMouseHoveringRect(
42 min, ImVec2(min.x + width * rt.
scale, min.y + height * rt.
scale));
46 const std::vector<uint8_t>& preview,
47 const SDL_Rect& bounds,
int room_x,
int room_y,
48 const std::array<SDL_Color, 256>& color_table) {
49 if (!rt.
draw_list || bounds.w <= 0 || bounds.h <= 0) {
52 const size_t row_stride =
static_cast<size_t>(bounds.w);
53 if (
static_cast<size_t>(bounds.h) > preview.size() / row_stride) {
57 const float scale = std::max(rt.
scale, 0.01f);
62 bool drew_any_pixel =
false;
65 for (
int y = 0; y < bounds.h; ++y) {
67 while (x < bounds.w) {
68 const uint8_t palette_index =
69 preview[
static_cast<size_t>(y) * row_stride + x];
70 const SDL_Color& palette_color = color_table[palette_index];
74 if (palette_index == 0 || palette_color.a == 0) {
78 const ImU32 color = IM_COL32(palette_color.r, palette_color.g,
79 palette_color.b, palette_color.a);
81 const int start_x = x;
82 while (x < bounds.w && preview[
static_cast<size_t>(y) * row_stride + x] ==
87 const ImVec2 min(origin.x + (room_x + start_x) * scale,
88 origin.y + (room_y + y) * scale);
89 const ImVec2 max(origin.x + (room_x + x) * scale,
90 origin.y + (room_y + y + 1) * scale);
91 rt.
draw_list->AddRectFilled(min, max, color);
92 drew_any_pixel =
true;
97 return drew_any_pixel;
101 const int layer_value =
object.GetLayerValue();
103 if (layer_value == 0) {
106 return layer_value == 1 ?
"Lower BG2" :
"Invalid selector";
108 switch (layer_value) {
112 return "BG2 overlay";
114 return "BG1 overlay";
130 const int entity_size = is_touch ? 24 : 16;
131 const auto sprite_colors =
133 const std::string_view hack_name =
136 : std::string_view{};
145 const std::span<const uint8_t> room_gfx_span(room_gfx.data(),
148 const auto& selected =
151 for (
size_t index = 0; index < sprites.size(); ++index) {
152 const auto& sprite = sprites[index];
153 const int canvas_x = sprite.x() * 16;
154 const int canvas_y = sprite.y() * 16;
156 if (canvas_x >= -entity_size && canvas_y >= -entity_size &&
157 canvas_x < 512 + entity_size && canvas_y < 512 + entity_size) {
158 ImVec4 sprite_color = sprite.layer() == 0 ? theme.dungeon_sprite_layer0
159 : theme.dungeon_sprite_layer1;
161 const auto* preview_layout =
164 sprite.subtype(), sprite.IsOverlord()};
171 if (preview ==
nullptr) {
174 room_gfx_span, preview_layout,
182 uncached_preview = std::move(rendered);
183 preview = &uncached_preview;
186 const SDL_Rect preview_bounds = preview->
bounds;
187 const bool drew_preview = DrawSpritePreviewPixels(
188 rt, preview->pixels, preview_bounds, canvas_x + preview_bounds.x,
189 canvas_y + preview_bounds.y, sprite_colors);
191 const bool show_label = ShowEntityLabel(
192 rt, canvas_x, canvas_y, entity_size, entity_size,
193 std::any_of(selected.begin(), selected.end(),
194 [index](
const auto& entity) {
195 return entity.type == EntityType::Sprite &&
196 entity.index == index;
198 if (drew_preview && show_label) {
200 ImGui::GetColorU32(sprite_color));
201 }
else if (!drew_preview) {
202 gui::DrawRect(rt, canvas_x, canvas_y, entity_size, entity_size,
206 std::string full_name = preview_layout !=
nullptr
207 ? preview_layout->name
209 std::string sprite_text;
210 if (full_name.length() > 12) {
211 sprite_text = absl::StrFormat(
"%02X %s..", sprite.id(),
212 full_name.substr(0, 8).c_str());
215 absl::StrFormat(
"%02X %s", sprite.id(), full_name.c_str());
231 if (pot_items.empty()) {
235 static const char* kPotItemNames[] = {
236 "Nothing",
"Green Rupee",
"Rock",
"Bee",
"Health",
237 "Bomb",
"Heart",
"Blue Rupee",
"Key",
"Arrow",
238 "Bomb",
"Heart",
"Magic",
"Full Magic",
"Cucco",
239 "Green Soldier",
"Bush Stal",
"Blue Soldier",
"Landmine",
"Heart",
240 "Fairy",
"Heart",
"Nothing",
"Hole",
"Warp",
241 "Staircase",
"Bombable",
"Switch"};
242 constexpr size_t kPotItemNameCount =
243 sizeof(kPotItemNames) /
sizeof(kPotItemNames[0]);
245 const auto& selected =
247 for (
size_t index = 0; index < pot_items.size(); ++index) {
248 const auto& pot_item = pot_items[index];
249 const int pixel_x = pot_item.GetPixelX();
250 const int pixel_y = pot_item.GetPixelY();
251 auto [canvas_x, canvas_y] =
256 const int entity_size = is_touch ? 24 : 16;
258 if (canvas_x >= -entity_size && canvas_y >= -entity_size &&
259 canvas_x < 512 + entity_size && canvas_y < 512 + entity_size) {
261 ImVec4 pot_item_color;
262 if (pot_item.item == 0) {
263 pot_item_color = theme.status_inactive;
264 pot_item_color.w = 0.4f;
266 pot_item_color = theme.item_color;
267 pot_item_color.w = 0.75f;
270 gui::DrawRect(rt, canvas_x, canvas_y, entity_size, entity_size,
273 std::string item_name;
274 if (pot_item.item < kPotItemNameCount) {
275 item_name = kPotItemNames[pot_item.item];
277 item_name = absl::StrFormat(
"Unk%02X", pot_item.item);
280 std::string item_text =
281 absl::StrFormat(
"%02X %s", pot_item.item, item_name.c_str());
282 if (ShowEntityLabel(rt, canvas_x, canvas_y, entity_size, entity_size,
283 std::any_of(selected.begin(), selected.end(),
284 [index](
const auto& entity) {
285 return entity.type ==
287 entity.index == index;
305 if (chests.empty() ||
314 const float padding = 4.0f;
315 const float wrap_width = std::min(180.0f, rt.
canvas_sz.x - padding * 4);
317 for (
size_t i = 0; i < objects.size(); ++i) {
322 const auto&
object = objects[i];
323 const float chest_width =
object.id_ == 0xFB1 ? 32.0f : 16.0f;
325 ImVec2(
object.x_ * 8.0f,
object.y_ * 8.0f));
327 ImVec2(
object.x_ * 8.0f + chest_width,
328 object.y_ * 8.0f + (
object.id_ == 0xFB1 ? 24.0f : 16.0f)));
329 if (chest_max.x < rt.
canvas_p0.x || chest_min.x > canvas_max.x ||
330 chest_max.y < rt.
canvas_p0.y || chest_min.y > canvas_max.y) {
333 const auto item_id = chests[*chest_index].id;
334 const std::string text = absl::StrFormat(
338 const ImVec2 text_size =
339 ImGui::CalcTextSize(text.c_str(),
nullptr,
false, wrap_width);
340 const float width = text_size.x + padding * 2;
341 const float height = text_size.y + padding * 2;
343 std::clamp((chest_min.x + chest_max.x - width) * 0.5f, rt.
canvas_p0.x,
344 std::max(rt.
canvas_p0.x, canvas_max.x - width)),
345 std::max(rt.
canvas_p0.y, chest_min.y - height - 3.0f));
346 const ImVec2 max(min.x + width, min.y + height);
347 rt.
draw_list->AddRectFilled(min, max, IM_COL32(0, 0, 0, 200), 3.0f);
348 rt.
draw_list->AddRect(min, max, ImGui::GetColorU32(theme.item_color), 3.0f);
349 rt.
draw_list->AddText(ImGui::GetFont(), ImGui::GetFontSize(),
350 ImVec2(min.x + padding, min.y + padding),
351 ImGui::GetColorU32(ImGuiCol_Text), text.c_str(),
352 nullptr, wrap_width);
368 constexpr const char* kPopupId =
"##TouchEntityContextMenu";
369 const ImGuiIO& io = ImGui::GetIO();
370 const bool touch_context_click =
371 rt.
hovered && io.MouseSource == ImGuiMouseSource_TouchScreen &&
372 ImGui::IsMouseClicked(ImGuiMouseButton_Right);
374 const bool should_open_context = gesture_long_press || touch_context_click;
376 if (should_open_context) {
377 const ImVec2 gesture_pos = gesture_long_press
379 : ImGui::GetMousePos();
383 const auto [room_x, room_y] =
388 const float rel_x = room_pos.x;
389 const float rel_y = room_pos.y;
391 const int hit_size = is_touch ? 24 : 16;
394 for (
size_t idx = 0; idx < sprites.size(); ++idx) {
395 const int sprite_px = sprites[idx].x() * 16;
396 const int sprite_py = sprites[idx].y() * 16;
397 if (rel_x >= sprite_px && rel_x < sprite_px + hit_size &&
398 rel_y >= sprite_py && rel_y < sprite_py + hit_size) {
400 ImGui::OpenPopup(kPopupId);
405 if (!ImGui::IsPopupOpen(kPopupId)) {
407 for (
size_t idx = 0; idx < pot_items.size(); ++idx) {
408 const int item_px = pot_items[idx].GetPixelX();
409 const int item_py = pot_items[idx].GetPixelY();
410 if (rel_x >= item_px && rel_x < item_px + hit_size &&
411 rel_y >= item_py && rel_y < item_py + hit_size) {
413 ImGui::OpenPopup(kPopupId);
419 if (!ImGui::IsPopupOpen(kPopupId)) {
420 const auto object_index =
424 static_cast<int>(rel_y));
425 if (object_index.has_value()) {
427 ImGui::OpenPopup(kPopupId);
432 if (ImGui::BeginPopup(kPopupId)) {
435 if (sel.type == EntityType::Sprite) {
437 if (sel.index < sprites.size()) {
439 ImGui::TextDisabled(tr(
"Sprite: %02X %s"), sprites[sel.index].id(),
443 if (ImGui::MenuItem(tr(
"Delete Sprite"))) {
446 }
else if (sel.type == EntityType::Item) {
447 ImGui::TextDisabled(tr(
"Pot Item"));
449 if (ImGui::MenuItem(tr(
"Delete Item"))) {
455 if (indices.size() == 1) {
457 if (indices[0] < objects.size()) {
459 ImGui::TextDisabled(tr(
"Object: %03X %s"), objects[indices[0]].id_,
464 ImGui::TextDisabled(tr(
"%zu objects selected"),
468 if (ImGui::MenuItem(tr(
"Delete"))) {
471 if (ImGui::MenuItem(tr(
"Copy"))) {
475 if (ImGui::MenuItem(tr(
"Send to Front"))) {
478 if (ImGui::MenuItem(tr(
"Send to Back"))) {
491 for (
const auto& obj : objects) {
492 bool show_this_type =
true;
493 if (obj.id_ < 0x100) {
495 }
else if (obj.id_ >= 0x100 && obj.id_ < 0x200) {
497 }
else if (obj.id_ >= 0xF00) {
501 bool show_this_layer =
true;
502 if (obj.GetLayerValue() == 0) {
504 }
else if (obj.GetLayerValue() == 1) {
506 }
else if (obj.GetLayerValue() == 2) {
510 if (!show_this_type || !show_this_layer) {
514 auto [canvas_x, canvas_y, width, height] =
516 width = std::min(width, 512);
517 height = std::min(height, 512);
519 ImVec4 outline_color;
520 if (obj.GetLayerValue() == 0) {
521 outline_color = theme.dungeon_outline_layer0;
522 }
else if (obj.GetLayerValue() == 1) {
523 outline_color = theme.dungeon_outline_layer1;
525 outline_color = theme.dungeon_outline_layer2;
528 gui::DrawRect(rt, canvas_x, canvas_y, width, height, outline_color);
531 if (name.length() > 12) {
532 name = name.substr(0, 10) +
"..";
535 if (obj.id_ >= 0x100) {
536 label = absl::StrFormat(
"0x%03X\n%s\n%s [%dx%d]", obj.id_, name.c_str(),
537 GetStoredPlacementLabel(obj), width, height);
539 label = absl::StrFormat(
"0x%02X\n%s\n%s [%dx%d]", obj.id_, name.c_str(),
540 GetStoredPlacementLabel(obj), width, height);
565 auto map_or = working ? absl::StatusOr<zelda3::CustomCollisionMap>(
566 working->custom_collision())
573 const auto& map = map_or.value();
576 for (
int y = 0; y < 64; ++y) {
577 for (
int x = 0; x < 64; ++x) {
578 const uint8_t tile = map.tiles[
static_cast<size_t>(y * 64 + x)];
579 if (map.HasOverride(
static_cast<size_t>(y * 64 + x)) &&
585 static_cast<uint8_t
>(x),
static_cast<uint8_t
>(y), tile});
599 if (composite !=
nullptr && room !=
nullptr) {
619 entry.output = std::make_unique<RoomCompositeOutput>();
621 entry.last_used_frame =
622 ImGui::GetCurrentContext() !=
nullptr ? ImGui::GetFrameCount() : 0;
637 auto& room = *room_ptr;
639 auto& composite = output.
Prepare(room, layer_mgr);
640 if (!composite.is_active() || composite.width() <= 0) {
680 const ImVec4 mask_color(0.2f, 0.6f, 1.0f, 0.4f);
682 for (
const auto& obj : objects) {
683 if (obj.GetLayerValue() != 1) {
687 auto [canvas_x, canvas_y] =
689 auto [width, height] =
691 width = std::min(width, 512);
692 height = std::min(height, 512);
693 gui::DrawRect(rt, canvas_x, canvas_y, width, height, mask_color);
const std::string & hack_name() const
bool loaded() const
Check if the manifest has been loaded.
bool DrawRoomBackgroundLayers(int room_id)
gfx::Bitmap * PrepareConnectedRoomCompositeBitmap(int room_id)
zelda3::GameData * game_data_
ObjectOutlineToggles object_outline_toggles_
void RenderPotItems(const gui::CanvasRuntime &rt, const zelda3::Room &room)
gfx::Bitmap * PrepareRoomCompositeWithOutput(int room_id, RoomCompositeOutput &output)
RoomCompositeOutput primary_composite_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)
uint8_t current_entrance_blockset_
void DrawMaskHighlights(const gui::CanvasRuntime &rt, const zelda3::Room &room)
DungeonRoomStore * rooms_
gfx::IRenderer * renderer_
util::LruCache< SpritePreviewKey, CachedSpritePreview > sprite_preview_cache_
void RenderChestContents(const gui::CanvasRuntime &rt, const zelda3::Room &room)
gui::CanvasTouchHandler touch_handler_
EntityVisibility entity_visibility_
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)
absl::Status HandleDeleteSelected()
SelectedEntity GetSelectedEntity() const
void SendSelectedToBack()
std::vector< size_t > GetSelectedObjectIndices() const
InteractionCoordinator & entity_coordinator()
Get the interaction coordinator for entity handling.
size_t GetSelectionCount() const
absl::Status HandleCopySelected()
bool HasEntitySelection() const
void SelectEntity(EntityType type, size_t index)
void SendSelectedToFront()
static std::pair< int, int > RoomToCanvasCoordinates(int room_x, int room_y)
zelda3::Room * GetIfMaterialized(int room_id)
zelda3::Room * GetIfLoaded(int room_id)
zelda3::Room * TryEnsureRoom(int room_id)
const std::vector< SelectedEntity > & GetSelectedEntities() const
TileObjectHandler & tile_handler()
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)
Represents a bitmap image optimized for SNES ROM hacking.
TextureHandle texture() const
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)
auto global_scale() const
static bool IsTouchDevice()
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
const std::vector< chest_data > & GetChests() const
const std::vector< SDL_Color > & rendered_palette() const
const gfx::SnesPalette & rendered_dungeon_palette() const
const std::vector< zelda3::Sprite > & GetSprites() const
uint64_t graphics_revision() const
const std::vector< RoomObject > & GetTileObjects() const
const std::vector< PotItem > & GetPotItems() const
bool AreChestsLoaded() const
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.
void RenderPreviewGraphics(std::span< const uint8_t > graphics, const SpriteOamLayout *layout_override=nullptr, std::span< const uint8_t > graphics_resource={})
auto preview_graphics() const
SDL_Rect preview_bounds() const
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.
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)
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)
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)
std::vector< CollisionOverlayEntry > entries
std::array< bool, 256 > stop_tiles
std::array< bool, 256 > switch_tiles
std::array< bool, 256 > track_tiles
core::HackManifest hack_manifest
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > resource_labels
std::string assets_folder
std::string GetAbsolutePath(const std::string &relative_path) const