6#include "absl/strings/str_format.h"
11#include "imgui/imgui.h"
20 constexpr uint64_t kFnvPrime = 1099511628211ULL;
21 *fingerprint ^= value;
22 *fingerprint *= kFnvPrime;
26 const std::filesystem::path& rhs) {
30 std::error_code error;
31 const bool equivalent = std::filesystem::equivalent(lhs, rhs, error);
32 return !error && equivalent;
38 : renderer_(renderer), rom_(rom) {
39 tile_editor_ = std::make_unique<zelda3::ObjectTileEditor>(rom);
59 std::string message) {
91 return absl::UnimplementedError(
92 "Standard object tile editing is supported only for audited objects "
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 global ROM tile edit is not covered by Dungeon Editor "
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) {
626 if (room ==
nullptr) {
658 const int cell_palette =
static_cast<int>(cell.tile_info.palette_);
667 uint16_t attributes = 0;
688 constexpr float kScale = 4.0f;
692 ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
693 ImVec2 canvas_size(grid_width, grid_height);
700 ImGui::Dummy(canvas_size);
703 ImDrawList* draw_list = ImGui::GetWindowDrawList();
707 float line_x = canvas_pos.x + gx * 8 *
kScale;
708 draw_list->AddLine(ImVec2(line_x, canvas_pos.y),
709 ImVec2(line_x, canvas_pos.y + grid_height),
710 IM_COL32(128, 128, 128, 80));
713 float line_y = canvas_pos.y + gy * 8 *
kScale;
714 draw_list->AddLine(ImVec2(canvas_pos.x, line_y),
715 ImVec2(canvas_pos.x + grid_width, line_y),
716 IM_COL32(128, 128, 128, 80));
723 ImVec2 cell_min(canvas_pos.x + cell.rel_x * 8 *
kScale,
724 canvas_pos.y + cell.rel_y * 8 *
kScale);
725 ImVec2 cell_max(cell_min.x + 8 *
kScale, cell_min.y + 8 *
kScale);
726 draw_list->AddRect(cell_min, cell_max, IM_COL32(255, 255, 0, 255), 0, 0,
731 if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(0)) {
732 ImVec2 mouse = ImGui::GetMousePos();
734 static_cast<int>((mouse.x - canvas_pos.x) / (8 *
kScale));
736 static_cast<int>((mouse.y - canvas_pos.y) / (8 *
kScale));
751 int modified_count = 0;
769 ImGui::SetNextItemWidth(80);
774 constexpr float kAtlasScale = 2.0f;
778 ImVec2 atlas_pos = ImGui::GetCursorScreenPos();
779 ImVec2 atlas_size(display_width, display_height);
782 ImGui::BeginChild(
"##AtlasScroll", ImVec2(display_width + 16, 300),
true,
783 ImGuiWindowFlags_HorizontalScrollbar);
785 atlas_pos = ImGui::GetCursorScreenPos();
790 ImGui::Dummy(atlas_size);
793 ImDrawList* draw_list = ImGui::GetWindowDrawList();
797 float line_x = atlas_pos.x + gx * 8 * kAtlasScale;
798 draw_list->AddLine(ImVec2(line_x, atlas_pos.y),
799 ImVec2(line_x, atlas_pos.y + display_height),
800 IM_COL32(64, 64, 64, 60));
803 float line_y = atlas_pos.y + gy * 8 * kAtlasScale;
804 draw_list->AddLine(ImVec2(atlas_pos.x, line_y),
805 ImVec2(atlas_pos.x + display_width, line_y),
806 IM_COL32(64, 64, 64, 60));
815 ImVec2 sel_min(atlas_pos.x + src_col * 8 * kAtlasScale,
816 atlas_pos.y + src_row * 8 * kAtlasScale);
817 ImVec2 sel_max(sel_min.x + 8 * kAtlasScale, sel_min.y + 8 * kAtlasScale);
818 draw_list->AddRect(sel_min, sel_max, IM_COL32(0, 255, 255, 255), 0, 0,
823 if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(0)) {
824 ImVec2 mouse = ImGui::GetMousePos();
826 static_cast<int>((mouse.x - atlas_pos.x) / (8 * kAtlasScale));
828 static_cast<int>((mouse.y - atlas_pos.y) / (8 * kAtlasScale));
830 if (click_col >= 0 &&
843 cell.tile_info.id_ =
static_cast<uint16_t
>(tile_id);
845 cell.modified =
true;
862 ImGui::TextDisabled(tr(
"Select a tile cell to edit properties"));
867 ImGui::Text(tr(
"Cell (%d, %d)"), cell.rel_x, cell.rel_y);
871 int tile_id = cell.tile_info.id_;
872 ImGui::SetNextItemWidth(80);
873 if (ImGui::InputInt(tr(
"ID"), &tile_id, 1, 16)) {
874 cell.tile_info.id_ =
static_cast<uint16_t
>(tile_id & 0x3FF);
875 cell.modified =
true;
883 int pal = cell.tile_info.palette_;
884 ImGui::SetNextItemWidth(60);
885 if (ImGui::SliderInt(tr(
"Pal"), &pal, 0, 7)) {
886 cell.tile_info.palette_ =
static_cast<uint8_t
>(pal);
887 cell.modified =
true;
895 if (ImGui::Checkbox(tr(
"H"), &cell.tile_info.horizontal_mirror_)) {
896 cell.modified =
true;
902 if (ImGui::Checkbox(tr(
"V"), &cell.tile_info.vertical_mirror_)) {
903 cell.modified =
true;
909 if (ImGui::Checkbox(tr(
"Pri"), &cell.tile_info.over_)) {
910 cell.modified =
true;
924 return plan_or.status();
927 const auto& plan = *plan_or;
929 const absl::Status preflight =
931 if (!preflight.ok()) {
945 if (!impact_snapshot_or.ok()) {
952 " Apply blocked: source impact could not be resolved: "
954 impact_snapshot_or.status().message()));
959 if (confirm_shared && shared_count > 1) {
968 " Confirm shared asset publish: %d runtime slots use this "
969 "file. This project-source edit is not covered by Ctrl+Z.",
975 " Confirm global apply: %d consumers use this tile "
976 "data. This edit is not covered by Ctrl+Z.",
981 const bool missing_required_confirmation =
983 const bool confirmed_impact_changed =
986 if (!confirm_shared &&
987 (missing_required_confirmation || confirmed_impact_changed)) {
996 " Source impact changed; review and confirm the updated "
997 "publish for %d runtime slots. This project-source edit is not "
998 "covered by Ctrl+Z.",
1005 " Source impact changed; review and confirm the updated global "
1006 "apply for %d consumers. This edit is not covered by Ctrl+Z.",
1030 room.MarkObjectsDirty();
1031 room.RenderRoomGraphics();
1039 if (cell.modified) {
1041 cell.original_word = applied_word;
1044 cell.source_ref.has_value()) {
1045 const auto& source_ref = *cell.source_ref;
1047 if (source_ref.span_index < spans.size() &&
1048 source_ref.word_index <
1049 spans[source_ref.span_index].expected_words.size()) {
1050 spans[source_ref.span_index].expected_words[source_ref.word_index] =
1054 cell.modified =
false;
1059 if (shared_count > 1) {
1065 " Published shared custom asset used by %d runtime slots.",
1072 " Applied changes to shared tile data used by %d consumers.",
1083 absl::StrFormat(
ICON_MD_ERROR " Apply failed: %s", status.message()));
1088 int modified_count = 0;
1094 bool has_mods = modified_count > 0;
1097 ImGui::Text(tr(
"%d tile(s) modified"), modified_count);
1105 if (!shared_count_or.ok()) {
1108 if (ImGui::IsItemHovered()) {
1109 ImGui::SetItemTooltip(
"%s", shared_count_or.status().message().data());
1112 }
else if (*shared_count_or > 1) {
1113 const int shared_count = *shared_count_or;
1123 if (ImGui::IsItemHovered()) {
1125 ImGui::SetItemTooltip(
1126 tr(
"%d fixed runtime slots resolve to this source file.\n"
1127 "Publishing changes updates all of them.\nThis project "
1128 "source-asset edit is not covered by Ctrl+Z."),
1131 ImGui::SetItemTooltip(
1132 tr(
"This object reuses tile data with %d consumers.\nApplying "
1133 "changes will update every object or runtime consumer in that "
1134 "shared data group.\nThis global ROM edit is not covered by "
1144 ImGui::BeginDisabled();
1148 if (ImGui::Button(primary_action_label)) {
1152 ImGui::EndDisabled();
1158 ImGui::BeginDisabled();
1163 ImGui::EndDisabled();
1167 const char* close_label =
1169 if (ImGui::Button(close_label)) {
1170 if (p_open !=
nullptr) {
1176 if (has_mods && ImGui::IsItemHovered()) {
1177 ImGui::SetItemTooltip(
1178 "%s", tr(
"Close the panel and discard all unapplied tile changes."));
1182 ImGui::TextDisabled(
"%s",
1184 " Project source-asset edit; not covered by Ctrl+Z."
1186 " Global ROM tile edit; not covered by Ctrl+Z.");
1206 ImGui::PushStyleColor(ImGuiCol_Text, status_color);
1207 ImGui::PushTextWrapPos(0.0f);
1209 ImGui::PopTextWrapPos();
1210 ImGui::PopStyleColor();
1216 if (p_open !=
nullptr) {
1221 " Unapplied tile changes were kept. Reopen the panel to continue, or "
1222 "use Discard & Close to throw them away.");
1226 if (p_open !=
nullptr) {
1234 if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows))
1242 auto find_neighbor = [&](
int dx,
int dy) ->
int {
1246 int target_x = cur.rel_x + dx;
1247 int target_y = cur.rel_y + dy;
1248 for (
int i = 0; i < cell_count; ++i) {
1257 if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow,
false)) {
1261 if (ImGui::IsKeyPressed(ImGuiKey_RightArrow,
false)) {
1265 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow,
false)) {
1269 if (ImGui::IsKeyPressed(ImGuiKey_DownArrow,
false)) {
1277 for (
int key = 0; key <= 7; ++key) {
1278 if (ImGui::IsKeyPressed(
static_cast<ImGuiKey
>(ImGuiKey_0 + key),
false)) {
1279 cell.tile_info.palette_ =
static_cast<uint8_t
>(key);
1280 cell.modified =
true;
1288 if (ImGui::IsKeyPressed(ImGuiKey_H,
false)) {
1289 cell.tile_info.horizontal_mirror_ = !cell.tile_info.horizontal_mirror_;
1290 cell.modified =
true;
1297 if (ImGui::IsKeyPressed(ImGuiKey_V,
false)) {
1298 cell.tile_info.vertical_mirror_ = !cell.tile_info.vertical_mirror_;
1299 cell.modified =
true;
1306 if (ImGui::IsKeyPressed(ImGuiKey_P,
false)) {
1307 cell.tile_info.over_ = !cell.tile_info.over_;
1308 cell.modified =
true;
1316 if (ImGui::IsKeyPressed(ImGuiKey_Escape,
false)) {
1327 if (ImGui::IsKeyPressed(ImGuiKey_Tab,
false)) {
1328 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 UpdateTexture()
Updates the underlying SDL_Texture when it already exists.
Defines an abstract interface for all rendering operations.
SNES 16-bit tile metadata container.
const Theme & GetCurrentTheme() const
static ThemeManager & Get()
static const std::array< int, 3 > & RuntimeObjectIds()
static CustomObjectManager & Get()
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::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