6#include "absl/strings/str_format.h"
12#include "imgui/imgui.h"
21 constexpr uint64_t kFnvPrime = 1099511628211ULL;
22 *fingerprint ^= value;
23 *fingerprint *= kFnvPrime;
27 const std::filesystem::path& rhs) {
31 std::error_code error;
32 const bool equivalent = std::filesystem::equivalent(lhs, rhs, error);
33 return !error && equivalent;
39 : renderer_(renderer), rom_(rom) {
40 tile_editor_ = std::make_unique<zelda3::ObjectTileEditor>(rom);
60 std::string message) {
92 return absl::UnimplementedError(
93 "Tile editing is not yet supported for this object layout");
95 if (rooms ==
nullptr) {
96 return absl::InvalidArgumentError(
97 "A dungeon room store is required to edit object tiles");
99 if (room_id < 0 || room_id >=
static_cast<int>(rooms->
size())) {
100 return absl::OutOfRangeError(
"Dungeon room id is outside the room store");
103 return absl::FailedPreconditionError(
104 "Apply, revert, or close the current Object Tile Editor session "
105 "before opening another object");
108 auto& room = (*rooms)[room_id];
110 tile_editor_->CaptureEditableObjectLayout(object_id, room, palette_group);
111 if (!layout_or.ok()) {
112 return layout_or.status();
125 return absl::OkStatus();
137 if (rooms ==
nullptr) {
138 return absl::InvalidArgumentError(
139 "A dungeon room store is required to edit custom object tiles");
141 if (room_id < 0 || room_id >=
static_cast<int>(rooms->
size())) {
142 return absl::OutOfRangeError(
"Dungeon room id is outside the room store");
145 return absl::FailedPreconditionError(
146 "Custom Objects is disabled for the current project");
149 return absl::FailedPreconditionError(
150 "Load the current dungeon room before editing custom object graphics");
153 return absl::FailedPreconditionError(
154 "Apply, revert, or explicitly discard the current Object Tile Editor "
155 "session before opening another custom object");
158 auto layout_or =
tile_editor_->LoadCustomObjectLayout(object_id, subtype);
159 if (!layout_or.ok()) {
160 return layout_or.status();
171 return absl::OkStatus();
233 return absl::FailedPreconditionError(
234 "Only an open custom-object session can add its first tile");
237 return absl::FailedPreconditionError(
238 "The custom-object layout already contains tiles");
255 return absl::OkStatus();
259 const bool added_first_tile_to_empty_asset =
264 if (added_first_tile_to_empty_asset) {
277absl::StatusOr<ObjectTileEditorPanel::SourceImpactSnapshot>
280 uint64_t fingerprint = 1469598103934665603ULL;
281 MixFingerprint(&fingerprint,
288 return absl::FailedPreconditionError(
289 "Custom object source-impact analysis requires its resolved asset "
294 uint64_t fingerprint = 1469598103934665603ULL;
295 MixFingerprint(&fingerprint, manager.asset_generation());
296 int consumer_count = 0;
297 std::vector<int> mapped_object_ids(
300 const auto manager_state = manager.SnapshotState();
301 for (
const auto& mapping : manager_state.custom_file_map) {
302 const int object_id = mapping.first;
303 if (std::find(mapped_object_ids.begin(), mapped_object_ids.end(),
304 object_id) == mapped_object_ids.end()) {
305 mapped_object_ids.push_back(object_id);
308 std::sort(mapped_object_ids.begin(), mapped_object_ids.end());
310 for (
const int object_id : mapped_object_ids) {
311 const int subtype_count = manager.GetSubtypeCount(object_id);
312 for (
int subtype = 0; subtype < subtype_count; ++subtype) {
313 const std::string filename =
314 manager.ResolveFilename(object_id, subtype);
315 if (filename.empty()) {
319 manager.GetBasePath(), filename);
320 if (!resolved_or.ok()) {
321 return absl::FailedPreconditionError(absl::StrFormat(
322 "Could not resolve custom runtime slot 0x%02X:%02X: %s",
323 object_id, subtype, resolved_or.status().message()));
325 if (!PathsReferToSameAsset(*resolved_or,
330 MixFingerprint(&fingerprint,
static_cast<uint16_t
>(object_id));
331 MixFingerprint(&fingerprint,
static_cast<uint16_t
>(subtype));
334 if (consumer_count == 0) {
335 return absl::AbortedError(
336 "Custom object mapping or project folder changed after this asset "
337 "was opened; edits were kept");
343 return absl::FailedPreconditionError(
344 "No matching standard object is selected for source-impact analysis");
347 return absl::FailedPreconditionError(
348 "A loaded ROM is required for source-impact analysis");
353 if (!impact_or.ok()) {
354 return impact_or.status();
357 uint64_t fingerprint = 1469598103934665603ULL;
358 MixFingerprint(&fingerprint, impact_or->affected_objects.size());
359 for (
const auto& entry : impact_or->affected_objects) {
360 MixFingerprint(&fingerprint, 0xA11EC7EDULL);
361 MixFingerprint(&fingerprint,
static_cast<uint16_t
>(entry.object_id));
362 MixFingerprint(&fingerprint, entry.overlapping_ranges.size());
363 for (
const auto& range : entry.overlapping_ranges) {
364 MixFingerprint(&fingerprint, range.begin);
365 MixFingerprint(&fingerprint, range.end);
368 MixFingerprint(&fingerprint, impact_or->runtime_consumers.size());
369 for (
const auto& entry : impact_or->runtime_consumers) {
370 MixFingerprint(&fingerprint, 0x52754E71ULL);
371 MixFingerprint(&fingerprint,
static_cast<uint64_t
>(entry.consumer));
372 MixFingerprint(&fingerprint, entry.overlapping_ranges.size());
373 for (
const auto& range : entry.overlapping_ranges) {
374 MixFingerprint(&fingerprint, range.begin);
375 MixFingerprint(&fingerprint, range.end);
384 if (!snapshot_or.ok()) {
385 return snapshot_or.status();
387 return snapshot_or->consumer_count;
391 uint64_t fingerprint = 1469598103934665603ULL;
392 MixFingerprint(&fingerprint,
394 MixFingerprint(&fingerprint,
398 MixFingerprint(&fingerprint,
400 MixFingerprint(&fingerprint,
402 for (
const unsigned char character :
404 MixFingerprint(&fingerprint, character);
407 MixFingerprint(&fingerprint,
409 MixFingerprint(&fingerprint,
413 MixFingerprint(&fingerprint,
static_cast<uint32_t
>(cell.rel_x));
414 MixFingerprint(&fingerprint,
static_cast<uint32_t
>(cell.rel_y));
415 MixFingerprint(&fingerprint, cell.original_word);
416 MixFingerprint(&fingerprint, cell.source_ref.has_value() ? 1 : 0);
417 if (cell.source_ref.has_value()) {
418 MixFingerprint(&fingerprint, cell.source_ref->span_index);
419 MixFingerprint(&fingerprint, cell.source_ref->word_index);
422 MixFingerprint(&fingerprint,
426 MixFingerprint(&fingerprint,
static_cast<uint16_t
>(provenance.object_id));
427 MixFingerprint(&fingerprint, provenance.descriptor_pc_address);
428 MixFingerprint(&fingerprint, provenance.expected_descriptor_word);
429 MixFingerprint(&fingerprint, provenance.spans.size());
430 for (
const auto& span : provenance.spans) {
431 MixFingerprint(&fingerprint, span.pc_address);
432 MixFingerprint(&fingerprint, span.expected_words.size());
433 for (
const uint16_t word : span.expected_words) {
434 MixFingerprint(&fingerprint, word);
458 if (!usage_count_or.ok()) {
459 return usage_count_or.status();
461 return *usage_count_or > 1;
465 return rooms_ !=
nullptr &&
484 if (p_open !=
nullptr) {
489 if (p_open !=
nullptr && !*p_open) {
495 const uint64_t graphics_revision =
504 ImGui::TextUnformatted(session_title.c_str());
510 if (p_open !=
nullptr) {
519 "%s", tr(
"This fixed slot intentionally draws nothing. Add its first "
520 "tile to begin a graphics override."));
521 if (ImGui::Button(
ICON_MD_ADD " Add First Tile")) {
525 std::string(status.message()));
534 if (ImGui::BeginTable(
535 "##TileEditorLayout", 2,
536 ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInnerV)) {
537 ImGui::TableSetupColumn(
"Tile Grid", ImGuiTableColumnFlags_WidthFixed,
539 ImGui::TableSetupColumn(
"Source Sheet", ImGuiTableColumnFlags_WidthStretch);
541 ImGui::TableNextRow();
542 ImGui::TableNextColumn();
545 ImGui::TableNextColumn();
559 const char* shared_popup_title =
561 ?
"Shared Custom Asset###SharedTileDataGlobalEdit"
562 :
"Shared Tile Data (Global Edit)###SharedTileDataGlobalEdit";
564 ImGui::OpenPopup(shared_popup_title);
567 if (ImGui::BeginPopupModal(shared_popup_title,
nullptr,
568 ImGuiWindowFlags_AlwaysAutoResize)) {
570 ImGui::Text(tr(
"This source asset is used by %d runtime slots."),
572 ImGui::Text(tr(
"Publishing will update all of those slots."));
574 tr(
"This project source-asset edit is not covered by Dungeon Editor "
577 ImGui::Text(tr(
"This tile data is shared by %d consumers."),
579 ImGui::Text(tr(
"Changes will affect all of them."));
581 tr(
"This shared tile edit is not covered by Dungeon Editor Ctrl+Z."));
584 const char* apply_label =
586 ? tr(
"Publish Shared Asset")
587 : tr(
"Apply Global Edit");
588 if (ImGui::Button(apply_label, ImVec2(170, 0))) {
590 ImGui::CloseCurrentPopup();
593 if (ImGui::Button(tr(
"Cancel"), ImVec2(120, 0))) {
596 ImGui::CloseCurrentPopup();
605 if (room ==
nullptr) {
630 if (room ==
nullptr) {
666 const int cell_palette =
static_cast<int>(cell.tile_info.palette_);
675 uint16_t attributes = 0;
696 constexpr float kScale = 4.0f;
700 ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
701 ImVec2 canvas_size(grid_width, grid_height);
708 ImGui::Dummy(canvas_size);
711 ImDrawList* draw_list = ImGui::GetWindowDrawList();
715 float line_x = canvas_pos.x + gx * 8 *
kScale;
716 draw_list->AddLine(ImVec2(line_x, canvas_pos.y),
717 ImVec2(line_x, canvas_pos.y + grid_height),
718 IM_COL32(128, 128, 128, 80));
721 float line_y = canvas_pos.y + gy * 8 *
kScale;
722 draw_list->AddLine(ImVec2(canvas_pos.x, line_y),
723 ImVec2(canvas_pos.x + grid_width, line_y),
724 IM_COL32(128, 128, 128, 80));
731 ImVec2 cell_min(canvas_pos.x + cell.rel_x * 8 *
kScale,
732 canvas_pos.y + cell.rel_y * 8 *
kScale);
733 ImVec2 cell_max(cell_min.x + 8 *
kScale, cell_min.y + 8 *
kScale);
734 draw_list->AddRect(cell_min, cell_max, IM_COL32(255, 255, 0, 255), 0, 0,
739 if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(0)) {
740 ImVec2 mouse = ImGui::GetMousePos();
742 static_cast<int>((mouse.x - canvas_pos.x) / (8 *
kScale));
744 static_cast<int>((mouse.y - canvas_pos.y) / (8 *
kScale));
759 int modified_count = 0;
777 ImGui::SetNextItemWidth(80);
782 constexpr float kAtlasScale = 2.0f;
786 ImVec2 atlas_pos = ImGui::GetCursorScreenPos();
787 ImVec2 atlas_size(display_width, display_height);
790 ImGui::BeginChild(
"##AtlasScroll", ImVec2(display_width + 16, 300),
true,
791 ImGuiWindowFlags_HorizontalScrollbar);
793 atlas_pos = ImGui::GetCursorScreenPos();
798 ImGui::Dummy(atlas_size);
801 ImDrawList* draw_list = ImGui::GetWindowDrawList();
805 float line_x = atlas_pos.x + gx * 8 * kAtlasScale;
806 draw_list->AddLine(ImVec2(line_x, atlas_pos.y),
807 ImVec2(line_x, atlas_pos.y + display_height),
808 IM_COL32(64, 64, 64, 60));
811 float line_y = atlas_pos.y + gy * 8 * kAtlasScale;
812 draw_list->AddLine(ImVec2(atlas_pos.x, line_y),
813 ImVec2(atlas_pos.x + display_width, line_y),
814 IM_COL32(64, 64, 64, 60));
823 ImVec2 sel_min(atlas_pos.x + src_col * 8 * kAtlasScale,
824 atlas_pos.y + src_row * 8 * kAtlasScale);
825 ImVec2 sel_max(sel_min.x + 8 * kAtlasScale, sel_min.y + 8 * kAtlasScale);
826 draw_list->AddRect(sel_min, sel_max, IM_COL32(0, 255, 255, 255), 0, 0,
831 if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(0)) {
832 ImVec2 mouse = ImGui::GetMousePos();
834 static_cast<int>((mouse.x - atlas_pos.x) / (8 * kAtlasScale));
836 static_cast<int>((mouse.y - atlas_pos.y) / (8 * kAtlasScale));
838 if (click_col >= 0 &&
851 cell.tile_info.id_ =
static_cast<uint16_t
>(tile_id);
853 cell.modified =
true;
870 ImGui::TextDisabled(tr(
"Select a tile cell to edit properties"));
875 ImGui::Text(tr(
"Cell (%d, %d)"), cell.rel_x, cell.rel_y);
879 int tile_id = cell.tile_info.id_;
880 ImGui::SetNextItemWidth(80);
881 if (ImGui::InputInt(tr(
"ID"), &tile_id, 1, 16)) {
882 cell.tile_info.id_ =
static_cast<uint16_t
>(tile_id & 0x3FF);
883 cell.modified =
true;
891 int pal = cell.tile_info.palette_;
892 ImGui::SetNextItemWidth(60);
893 if (ImGui::SliderInt(tr(
"Pal"), &pal, 0, 7)) {
894 cell.tile_info.palette_ =
static_cast<uint8_t
>(pal);
895 cell.modified =
true;
903 if (ImGui::Checkbox(tr(
"H"), &cell.tile_info.horizontal_mirror_)) {
904 cell.modified =
true;
910 if (ImGui::Checkbox(tr(
"V"), &cell.tile_info.vertical_mirror_)) {
911 cell.modified =
true;
917 if (ImGui::Checkbox(tr(
"Pri"), &cell.tile_info.over_)) {
918 cell.modified =
true;
932 return plan_or.status();
935 const auto& plan = *plan_or;
937 !plan.write_ranges().empty()) {
938 const absl::Status preflight =
940 if (!preflight.ok()) {
954 if (!impact_snapshot_or.ok()) {
961 " Apply blocked: source impact could not be resolved: "
963 impact_snapshot_or.status().message()));
968 if (confirm_shared && shared_count > 1) {
977 " Confirm shared asset publish: %d runtime slots use this "
978 "file. This project-source edit is not covered by Ctrl+Z.",
984 " Confirm global apply: %d consumers use this tile "
985 "data. This edit is not covered by Ctrl+Z.",
990 const bool missing_required_confirmation =
992 const bool confirmed_impact_changed =
995 if (!confirm_shared &&
996 (missing_required_confirmation || confirmed_impact_changed)) {
1005 " Source impact changed; review and confirm the updated "
1006 "publish for %d runtime slots. This project-source edit is not "
1007 "covered by Ctrl+Z.",
1014 " Source impact changed; review and confirm the updated global "
1015 "apply for %d consumers. This edit is not covered by Ctrl+Z.",
1028 const auto refreshed =
1030 if (refreshed.ok() && *refreshed) {
1046 room.MarkObjectsDirty();
1047 room.RenderRoomGraphics();
1055 if (cell.modified) {
1057 cell.original_word = applied_word;
1060 cell.source_ref.has_value()) {
1061 const auto& source_ref = *cell.source_ref;
1063 if (source_ref.span_index < spans.size() &&
1064 source_ref.word_index <
1065 spans[source_ref.span_index].expected_words.size()) {
1066 spans[source_ref.span_index].expected_words[source_ref.word_index] =
1070 cell.modified =
false;
1075 if (shared_count > 1) {
1081 " Published shared custom asset used by %d runtime slots.",
1088 " Applied changes to shared tile data used by %d consumers.",
1099 absl::StrFormat(
ICON_MD_ERROR " Apply failed: %s", status.message()));
1104 int modified_count = 0;
1110 bool has_mods = modified_count > 0;
1113 ImGui::Text(tr(
"%d tile(s) modified"), modified_count);
1121 if (!shared_count_or.ok()) {
1124 if (ImGui::IsItemHovered()) {
1125 ImGui::SetItemTooltip(
"%s", shared_count_or.status().message().data());
1128 }
else if (*shared_count_or > 1) {
1129 const int shared_count = *shared_count_or;
1139 if (ImGui::IsItemHovered()) {
1141 ImGui::SetItemTooltip(
1142 tr(
"%d fixed runtime slots resolve to this source file.\n"
1143 "Publishing changes updates all of them.\nThis project "
1144 "source-asset edit is not covered by Ctrl+Z."),
1147 ImGui::SetItemTooltip(
1148 tr(
"This object reuses tile data with %d consumers.\nApplying "
1149 "changes will update every object or runtime consumer in that "
1150 "shared data group.\nThis shared tile edit is not covered by "
1160 ImGui::BeginDisabled();
1161 const char* primary_action_label =
1165 if (ImGui::Button(primary_action_label)) {
1169 ImGui::EndDisabled();
1175 ImGui::BeginDisabled();
1180 ImGui::EndDisabled();
1184 const char* close_label =
1186 if (ImGui::Button(close_label)) {
1187 if (p_open !=
nullptr) {
1193 if (has_mods && ImGui::IsItemHovered()) {
1194 ImGui::SetItemTooltip(
1195 "%s", tr(
"Close the panel and discard all unapplied tile changes."));
1201 ImGui::Text(
"Source: %s",
1202 std::filesystem::path(path).filename().
string().c_str());
1203 if (ImGui::IsItemHovered())
1204 ImGui::SetTooltip(
"%s", path.c_str());
1206 ImGui::TextDisabled(
1209 " Project source-asset edit; not covered by Ctrl+Z."
1210 :
ICON_MD_INFO " Global ROM tile edit; not covered by Ctrl+Z.");
1231 ImGui::PushTextWrapPos(0.0f);
1233 ImGui::PopTextWrapPos();
1239 if (p_open !=
nullptr) {
1244 " Unapplied tile changes were kept. Reopen the panel to continue, or "
1245 "use Discard & Close to throw them away.");
1249 if (p_open !=
nullptr) {
1257 if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows))
1265 auto find_neighbor = [&](
int dx,
int dy) ->
int {
1269 int target_x = cur.rel_x + dx;
1270 int target_y = cur.rel_y + dy;
1271 for (
int i = 0; i < cell_count; ++i) {
1280 if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow,
false)) {
1284 if (ImGui::IsKeyPressed(ImGuiKey_RightArrow,
false)) {
1288 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow,
false)) {
1292 if (ImGui::IsKeyPressed(ImGuiKey_DownArrow,
false)) {
1300 for (
int key = 0; key <= 7; ++key) {
1301 if (ImGui::IsKeyPressed(
static_cast<ImGuiKey
>(ImGuiKey_0 + key),
false)) {
1302 cell.tile_info.palette_ =
static_cast<uint8_t
>(key);
1303 cell.modified =
true;
1311 if (ImGui::IsKeyPressed(ImGuiKey_H,
false)) {
1312 cell.tile_info.horizontal_mirror_ = !cell.tile_info.horizontal_mirror_;
1313 cell.modified =
true;
1320 if (ImGui::IsKeyPressed(ImGuiKey_V,
false)) {
1321 cell.tile_info.vertical_mirror_ = !cell.tile_info.vertical_mirror_;
1322 cell.modified =
true;
1329 if (ImGui::IsKeyPressed(ImGuiKey_P,
false)) {
1330 cell.tile_info.over_ = !cell.tile_info.over_;
1331 cell.modified =
true;
1339 if (ImGui::IsKeyPressed(ImGuiKey_Escape,
false)) {
1350 if (ImGui::IsKeyPressed(ImGuiKey_Tab,
false)) {
1351 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
zelda3::Room * GetIfLoaded(int room_id)
void ForEachMaterialized(Fn &&fn)
void ClearRenderedBitmaps()
absl::StatusOr< SourceImpactSnapshot > AnalyzeSourceImpactSnapshot() const
void SyncSourceAttributesFromSelectedCell()
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)
absl::Status OpenForCustomObject(int16_t object_id, int subtype, int room_id, DungeonRoomStore *rooms)
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()
TilesAppliedCallback on_tiles_applied_
bool HasRenderableRoomContext() const
void RenderObjectPreview()
int16_t current_object_id_
void OnClose() override
Called when panel is hidden.
absl::StatusOr< bool > HasSharedTileDataConflict() const
absl::Status AddFirstTileToEmptyCustomLayout()
std::optional< SourceImpactDisplayCacheKey > source_impact_display_cache_key_
void RefreshRenderedViewsFromCurrentRoom()
StandardWritePreflightCallback standard_write_preflight_
void RevertCurrentLayout()
gfx::Bitmap object_preview_bmp_
zelda3::ObjectTileLayout current_layout_
int shared_tile_data_usage_override_
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_
~ObjectTileEditorPanel() override
uint64_t room_graphics_revision_
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()
uint16_t source_attributes_
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_
void RetireBitmap(Bitmap &bitmap)
Explicitly retire resources owned by a bitmap that is being erased.
Represents a bitmap image optimized for SNES ROM hacking.
const uint8_t * data() const
TextureHandle texture() const
void CreateTexture()
Creates the underlying SDL_Texture to be displayed.
void UpdateTexture()
Updates the underlying SDL_Texture when it already exists.
Defines an abstract interface for all rendering operations.
SNES 16-bit tile metadata container.
RAII guard for ImGui style colors.
const Theme & GetCurrentTheme() const
static ThemeManager & Get()
static const std::array< int, 3 > & RuntimeObjectIds()
static CustomObjectManager & Get()
absl::StatusOr< std::shared_ptr< const DungeonTilePatterns > > GetTilePatterns()
static constexpr int kAtlasTilesPerRow
static bool IsEditableStandardObject(int16_t object_id)
static constexpr int kAtlasTileRows
static constexpr int kAtlasWidthPx
static constexpr int kAtlasHeightPx
uint64_t graphics_revision() const
#define ICON_MD_VISIBILITY_OFF
#define ICON_MD_CHECK_CIRCLE
void MixFingerprint(uint64_t *fingerprint, uint64_t value)
bool PathsReferToSameAsset(const std::filesystem::path &lhs, const std::filesystem::path &rhs)
uint16_t TileInfoToWord(TileInfo tile_info)
ImVec4 ConvertColorToImVec4(const Color &color)
absl::StatusOr< fs::path > ResolveCustomObjectAssetPath(const std::string &custom_objects_folder, const std::string &filename)
std::string GetObjectName(int object_id)
Represents a group of palettes.
bool HasModifications() const
std::shared_ptr< const DungeonTilePatterns > pattern_source
std::vector< uint8_t > custom_source_bytes
std::optional< ObjectTileSourceProvenance > source_provenance
std::filesystem::path custom_resolved_path
std::vector< Cell > cells
std::string custom_filename