4#include "absl/strings/str_format.h"
7#include "imgui/imgui.h"
16 constexpr uint64_t kFnvPrime = 1099511628211ULL;
17 *fingerprint ^= value;
18 *fingerprint *= kFnvPrime;
24 : renderer_(renderer), rom_(rom) {
25 tile_editor_ = std::make_unique<zelda3::ObjectTileEditor>(rom);
39 std::string message) {
69 return absl::UnimplementedError(
70 "Standard object tile editing is supported only for audited objects "
73 if (rooms ==
nullptr) {
74 return absl::InvalidArgumentError(
75 "A dungeon room store is required to edit object tiles");
77 if (room_id < 0 || room_id >=
static_cast<int>(rooms->
size())) {
78 return absl::OutOfRangeError(
"Dungeon room id is outside the room store");
81 return absl::FailedPreconditionError(
82 "Apply, revert, or close the current Object Tile Editor session "
83 "before opening another object");
86 auto& room = (*rooms)[room_id];
88 tile_editor_->CaptureEditableObjectLayout(object_id, room, palette_group);
89 if (!layout_or.ok()) {
90 return layout_or.status();
104 return absl::OkStatus();
108 int width,
int height,
const std::string& filename, int16_t object_id,
111 return absl::FailedPreconditionError(
112 "Apply, revert, or explicitly discard the current Object Tile Editor "
113 "session before creating another object");
125 return absl::OkStatus();
192absl::StatusOr<ObjectTileEditorPanel::SourceImpactSnapshot>
195 uint64_t fingerprint = 1469598103934665603ULL;
196 MixFingerprint(&fingerprint,
206 return absl::FailedPreconditionError(
207 "No matching standard object is selected for source-impact analysis");
210 return absl::FailedPreconditionError(
211 "A loaded ROM is required for source-impact analysis");
216 if (!impact_or.ok()) {
217 return impact_or.status();
220 uint64_t fingerprint = 1469598103934665603ULL;
221 MixFingerprint(&fingerprint, impact_or->affected_objects.size());
222 for (
const auto& entry : impact_or->affected_objects) {
223 MixFingerprint(&fingerprint, 0xA11EC7EDULL);
224 MixFingerprint(&fingerprint,
static_cast<uint16_t
>(entry.object_id));
225 MixFingerprint(&fingerprint, entry.overlapping_ranges.size());
226 for (
const auto& range : entry.overlapping_ranges) {
227 MixFingerprint(&fingerprint, range.begin);
228 MixFingerprint(&fingerprint, range.end);
231 MixFingerprint(&fingerprint, impact_or->runtime_consumers.size());
232 for (
const auto& entry : impact_or->runtime_consumers) {
233 MixFingerprint(&fingerprint, 0x52754E71ULL);
234 MixFingerprint(&fingerprint,
static_cast<uint64_t
>(entry.consumer));
235 MixFingerprint(&fingerprint, entry.overlapping_ranges.size());
236 for (
const auto& range : entry.overlapping_ranges) {
237 MixFingerprint(&fingerprint, range.begin);
238 MixFingerprint(&fingerprint, range.end);
247 if (!snapshot_or.ok()) {
248 return snapshot_or.status();
250 return snapshot_or->consumer_count;
254 uint64_t fingerprint = 1469598103934665603ULL;
255 MixFingerprint(&fingerprint,
257 MixFingerprint(&fingerprint,
260 MixFingerprint(&fingerprint,
262 MixFingerprint(&fingerprint,
266 MixFingerprint(&fingerprint,
static_cast<uint32_t
>(cell.rel_x));
267 MixFingerprint(&fingerprint,
static_cast<uint32_t
>(cell.rel_y));
268 MixFingerprint(&fingerprint, cell.original_word);
269 MixFingerprint(&fingerprint, cell.source_ref.has_value() ? 1 : 0);
270 if (cell.source_ref.has_value()) {
271 MixFingerprint(&fingerprint, cell.source_ref->span_index);
272 MixFingerprint(&fingerprint, cell.source_ref->word_index);
275 MixFingerprint(&fingerprint,
279 MixFingerprint(&fingerprint,
static_cast<uint16_t
>(provenance.object_id));
280 MixFingerprint(&fingerprint, provenance.descriptor_pc_address);
281 MixFingerprint(&fingerprint, provenance.expected_descriptor_word);
282 MixFingerprint(&fingerprint, provenance.spans.size());
283 for (
const auto& span : provenance.spans) {
284 MixFingerprint(&fingerprint, span.pc_address);
285 MixFingerprint(&fingerprint, span.expected_words.size());
286 for (
const uint16_t word : span.expected_words) {
287 MixFingerprint(&fingerprint, word);
311 if (!usage_count_or.ok()) {
312 return usage_count_or.status();
314 return *usage_count_or > 1;
319 current_room_id_ < static_cast<int>(
rooms_->
size()) &&
337 if (p_open !=
nullptr) {
342 if (p_open !=
nullptr && !*p_open) {
347 ImGui::TextUnformatted(session_title.c_str());
352 if (ImGui::BeginTable(
353 "##TileEditorLayout", 2,
354 ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInnerV)) {
355 ImGui::TableSetupColumn(
"Tile Grid", ImGuiTableColumnFlags_WidthFixed,
357 ImGui::TableSetupColumn(
"Source Sheet", ImGuiTableColumnFlags_WidthStretch);
359 ImGui::TableNextRow();
360 ImGui::TableNextColumn();
363 ImGui::TableNextColumn();
378 ImGui::OpenPopup(
"Shared Tile Data (Global Edit)");
381 if (ImGui::BeginPopupModal(
"Shared Tile Data (Global Edit)",
nullptr,
382 ImGuiWindowFlags_AlwaysAutoResize)) {
383 ImGui::Text(tr(
"This tile data is shared by %d consumers."),
385 ImGui::Text(tr(
"Changes will affect all of them."));
386 ImGui::TextWrapped(tr(
387 "This global ROM tile edit is not covered by Dungeon Editor Ctrl+Z."));
389 if (ImGui::Button(tr(
"Apply Global Edit"), ImVec2(150, 0))) {
391 ImGui::CloseCurrentPopup();
394 if (ImGui::Button(tr(
"Cancel"), ImVec2(120, 0))) {
397 ImGui::CloseCurrentPopup();
450 const int cell_palette =
static_cast<int>(cell.tile_info.palette_);
465 constexpr float kScale = 4.0f;
469 ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
470 ImVec2 canvas_size(grid_width, grid_height);
477 ImGui::Dummy(canvas_size);
480 ImDrawList* draw_list = ImGui::GetWindowDrawList();
484 float line_x = canvas_pos.x + gx * 8 *
kScale;
485 draw_list->AddLine(ImVec2(line_x, canvas_pos.y),
486 ImVec2(line_x, canvas_pos.y + grid_height),
487 IM_COL32(128, 128, 128, 80));
490 float line_y = canvas_pos.y + gy * 8 *
kScale;
491 draw_list->AddLine(ImVec2(canvas_pos.x, line_y),
492 ImVec2(canvas_pos.x + grid_width, line_y),
493 IM_COL32(128, 128, 128, 80));
500 ImVec2 cell_min(canvas_pos.x + cell.rel_x * 8 *
kScale,
501 canvas_pos.y + cell.rel_y * 8 *
kScale);
502 ImVec2 cell_max(cell_min.x + 8 *
kScale, cell_min.y + 8 *
kScale);
503 draw_list->AddRect(cell_min, cell_max, IM_COL32(255, 255, 0, 255), 0, 0,
508 if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(0)) {
509 ImVec2 mouse = ImGui::GetMousePos();
511 static_cast<int>((mouse.x - canvas_pos.x) / (8 *
kScale));
513 static_cast<int>((mouse.y - canvas_pos.y) / (8 *
kScale));
528 int modified_count = 0;
546 ImGui::SetNextItemWidth(80);
551 constexpr float kAtlasScale = 2.0f;
555 ImVec2 atlas_pos = ImGui::GetCursorScreenPos();
556 ImVec2 atlas_size(display_width, display_height);
559 ImGui::BeginChild(
"##AtlasScroll", ImVec2(display_width + 16, 300),
true,
560 ImGuiWindowFlags_HorizontalScrollbar);
562 atlas_pos = ImGui::GetCursorScreenPos();
567 ImGui::Dummy(atlas_size);
570 ImDrawList* draw_list = ImGui::GetWindowDrawList();
574 float line_x = atlas_pos.x + gx * 8 * kAtlasScale;
575 draw_list->AddLine(ImVec2(line_x, atlas_pos.y),
576 ImVec2(line_x, atlas_pos.y + display_height),
577 IM_COL32(64, 64, 64, 60));
580 float line_y = atlas_pos.y + gy * 8 * kAtlasScale;
581 draw_list->AddLine(ImVec2(atlas_pos.x, line_y),
582 ImVec2(atlas_pos.x + display_width, line_y),
583 IM_COL32(64, 64, 64, 60));
592 ImVec2 sel_min(atlas_pos.x + src_col * 8 * kAtlasScale,
593 atlas_pos.y + src_row * 8 * kAtlasScale);
594 ImVec2 sel_max(sel_min.x + 8 * kAtlasScale, sel_min.y + 8 * kAtlasScale);
595 draw_list->AddRect(sel_min, sel_max, IM_COL32(0, 255, 255, 255), 0, 0,
600 if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(0)) {
601 ImVec2 mouse = ImGui::GetMousePos();
603 static_cast<int>((mouse.x - atlas_pos.x) / (8 * kAtlasScale));
605 static_cast<int>((mouse.y - atlas_pos.y) / (8 * kAtlasScale));
607 if (click_col >= 0 &&
620 cell.tile_info.id_ =
static_cast<uint16_t
>(tile_id);
622 cell.modified =
true;
639 ImGui::TextDisabled(tr(
"Select a tile cell to edit properties"));
644 ImGui::Text(tr(
"Cell (%d, %d)"), cell.rel_x, cell.rel_y);
648 int tile_id = cell.tile_info.id_;
649 ImGui::SetNextItemWidth(80);
650 if (ImGui::InputInt(tr(
"ID"), &tile_id, 1, 16)) {
651 cell.tile_info.id_ =
static_cast<uint16_t
>(tile_id & 0x3FF);
652 cell.modified =
true;
660 int pal = cell.tile_info.palette_;
661 ImGui::SetNextItemWidth(60);
662 if (ImGui::SliderInt(tr(
"Pal"), &pal, 0, 7)) {
663 cell.tile_info.palette_ =
static_cast<uint8_t
>(pal);
664 cell.modified =
true;
672 if (ImGui::Checkbox(tr(
"H"), &cell.tile_info.horizontal_mirror_)) {
673 cell.modified =
true;
678 if (ImGui::Checkbox(tr(
"V"), &cell.tile_info.vertical_mirror_)) {
679 cell.modified =
true;
684 if (ImGui::Checkbox(tr(
"Pri"), &cell.tile_info.over_)) {
685 cell.modified =
true;
698 return plan_or.status();
701 const auto& plan = *plan_or;
703 const absl::Status preflight =
705 if (!preflight.ok()) {
719 if (!impact_snapshot_or.ok()) {
726 " Apply blocked: source impact could not be resolved: "
728 impact_snapshot_or.status().message()));
733 if (confirm_shared && shared_count > 1) {
740 " Confirm global apply: %d consumers use this tile "
741 "data. This edit is not covered by Ctrl+Z.",
745 const bool missing_required_confirmation =
747 const bool confirmed_impact_changed =
750 if (!confirm_shared &&
751 (missing_required_confirmation || confirmed_impact_changed)) {
759 " Source impact changed; review and confirm the updated global "
760 "apply for %d consumers. This edit is not covered by Ctrl+Z.",
785 room.MarkObjectsDirty();
786 room.RenderRoomGraphics();
796 cell.original_word = applied_word;
799 cell.source_ref.has_value()) {
800 const auto& source_ref = *cell.source_ref;
802 if (source_ref.span_index < spans.size() &&
803 source_ref.word_index <
804 spans[source_ref.span_index].expected_words.size()) {
805 spans[source_ref.span_index].expected_words[source_ref.word_index] =
809 cell.modified =
false;
824 if (shared_count > 1) {
829 " Applied changes to shared tile data used by %d consumers.",
839 absl::StrFormat(
ICON_MD_ERROR " Apply failed: %s", status.message()));
844 int modified_count = 0;
850 bool has_mods = modified_count > 0;
853 ImGui::Text(tr(
"%d tile(s) modified"), modified_count);
861 if (!shared_count_or.ok()) {
864 if (ImGui::IsItemHovered()) {
865 ImGui::SetItemTooltip(
"%s", shared_count_or.status().message().data());
868 }
else if (*shared_count_or > 1) {
869 const int shared_count = *shared_count_or;
872 if (ImGui::IsItemHovered()) {
873 ImGui::SetItemTooltip(
874 tr(
"This object reuses tile data with %d consumers.\nApplying "
876 "will update every object or runtime consumer in that shared "
877 "data group.\nThis global ROM edit is not covered by Ctrl+Z."),
885 ImGui::BeginDisabled();
890 ImGui::EndDisabled();
896 ImGui::BeginDisabled();
904 ImGui::EndDisabled();
908 const char* close_label =
910 if (ImGui::Button(close_label)) {
911 if (p_open !=
nullptr) {
917 if (has_mods && ImGui::IsItemHovered()) {
918 ImGui::SetItemTooltip(
919 "%s", tr(
"Close the panel and discard all unapplied tile changes."));
925 " Global ROM tile edit; not covered by Ctrl+Z.");
946 ImGui::PushStyleColor(ImGuiCol_Text, status_color);
947 ImGui::PushTextWrapPos(0.0f);
949 ImGui::PopTextWrapPos();
950 ImGui::PopStyleColor();
956 if (p_open !=
nullptr) {
961 " Unapplied tile changes were kept. Reopen the panel to continue, or "
962 "use Discard & Close to throw them away.");
966 if (p_open !=
nullptr) {
974 if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows))
982 auto find_neighbor = [&](
int dx,
int dy) ->
int {
986 int target_x = cur.rel_x + dx;
987 int target_y = cur.rel_y + dy;
988 for (
int i = 0; i < cell_count; ++i) {
997 if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow,
false)) {
1001 if (ImGui::IsKeyPressed(ImGuiKey_RightArrow,
false)) {
1005 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow,
false)) {
1009 if (ImGui::IsKeyPressed(ImGuiKey_DownArrow,
false)) {
1017 for (
int key = 0; key <= 7; ++key) {
1018 if (ImGui::IsKeyPressed(
static_cast<ImGuiKey
>(ImGuiKey_0 + key),
false)) {
1019 cell.tile_info.palette_ =
static_cast<uint8_t
>(key);
1020 cell.modified =
true;
1028 if (ImGui::IsKeyPressed(ImGuiKey_H,
false)) {
1029 cell.tile_info.horizontal_mirror_ = !cell.tile_info.horizontal_mirror_;
1030 cell.modified =
true;
1036 if (ImGui::IsKeyPressed(ImGuiKey_V,
false)) {
1037 cell.tile_info.vertical_mirror_ = !cell.tile_info.vertical_mirror_;
1038 cell.modified =
true;
1044 if (ImGui::IsKeyPressed(ImGuiKey_P,
false)) {
1045 cell.tile_info.over_ = !cell.tile_info.over_;
1046 cell.modified =
true;
1053 if (ImGui::IsKeyPressed(ImGuiKey_Escape,
false)) {
1063 if (ImGui::IsKeyPressed(ImGuiKey_Tab,
false)) {
1064 if (cell_count > 0) {
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
uint64_t object_tile_revision() const
void ForEachMaterialized(Fn &&fn)
void ClearRenderedBitmaps()
StandardTilesAppliedCallback on_standard_tiles_applied_
absl::StatusOr< SourceImpactSnapshot > AnalyzeSourceImpactSnapshot() const
void ApplyChanges(bool confirm_shared=true)
void RequestSafeWindowClose(bool *p_open)
void DrawTileProperties()
void SetCurrentPaletteGroup(const gfx::PaletteGroup &group)
void SetCurrentPaletteGroupForRoom(int room_id, const gfx::PaletteGroup &group)
ObjectTileEditorPanel(gfx::IRenderer *renderer, Rom *rom)
gfx::PaletteGroup current_palette_group_
ActionStatusTone action_status_tone_
absl::Status WriteBackCurrentLayout()
std::string BuildWindowTitle() const
std::unique_ptr< zelda3::ObjectTileEditor > tile_editor_
void ResetTransientState()
bool HasRenderableRoomContext() const
std::function< void(int, const std::string &) on_object_created_)
void RenderObjectPreview()
int16_t current_object_id_
void OnClose() override
Called when panel is hidden.
absl::StatusOr< bool > HasSharedTileDataConflict() const
std::optional< SourceImpactDisplayCacheKey > source_impact_display_cache_key_
void RefreshRenderedViewsFromCurrentRoom()
StandardWritePreflightCallback standard_write_preflight_
gfx::Bitmap object_preview_bmp_
zelda3::ObjectTileLayout current_layout_
int shared_tile_data_usage_override_
absl::Status OpenForNewObject(int width, int height, const std::string &filename, int16_t object_id, int room_id, DungeonRoomStore *rooms)
absl::Status OpenForObject(int16_t object_id, int room_id, DungeonRoomStore *rooms)
void SyncSourceSelectionFromSelectedCell()
std::string action_status_message_
std::optional< absl::StatusOr< int > > source_impact_display_cache_result_
int selected_source_tile_
void Draw(bool *p_open) override
Draw the panel content.
bool show_shared_confirm_
gfx::Bitmap tile8_atlas_bmp_
absl::StatusOr< int > GetDisplayedSharedTileDataUsageCount()
uint64_t BuildSourceImpactProvenanceFingerprint() const
void SelectFirstCellIfAvailable()
void HandleKeyboardShortcuts(bool *p_open)
DungeonRoomStore * rooms_
void DrawActionBar(bool *p_open)
void SetActionStatus(ActionStatusTone tone, std::string message)
absl::StatusOr< int > GetSharedTileDataUsageCount() const
std::optional< SourceImpactSnapshot > pending_shared_confirmation_
Represents a bitmap image optimized for SNES ROM hacking.
const uint8_t * data() const
TextureHandle texture() const
void UpdateTexture()
Updates the underlying SDL_Texture when it already exists.
Defines an abstract interface for all rendering operations.
const Theme & GetCurrentTheme() const
static ThemeManager & Get()
static constexpr int kAtlasTilesPerRow
static bool IsEditableStandardObject(int16_t object_id)
static constexpr int kAtlasTileRows
static constexpr int kAtlasWidthPx
static constexpr int kAtlasHeightPx
#define ICON_MD_CHECK_CIRCLE
void MixFingerprint(uint64_t *fingerprint, uint64_t value)
uint16_t TileInfoToWord(TileInfo tile_info)
ImVec4 ConvertColorToImVec4(const Color &color)
std::string GetObjectName(int object_id)
Represents a group of palettes.
bool HasModifications() const
std::optional< ObjectTileSourceProvenance > source_provenance
static ObjectTileLayout CreateEmpty(int width, int height, int16_t object_id, const std::string &filename)
std::vector< Cell > cells
std::string custom_filename