yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
object_tile_editor_panel.cc
Go to the documentation of this file.
2#include "util/i18n/tr.h"
3
4#include "absl/strings/str_format.h"
7#include "imgui/imgui.h"
9
10namespace yaze {
11namespace editor {
12
14 : renderer_(renderer), rom_(rom) {
15 tile_editor_ = std::make_unique<zelda3::ObjectTileEditor>(rom);
16}
17
22
27
29 std::string message) {
31 action_status_message_ = std::move(message);
32}
33
45
46void ObjectTileEditorPanel::OpenForObject(int16_t object_id, int room_id,
47 DungeonRoomStore* rooms) {
48 current_object_id_ = object_id;
49 current_room_id_ = room_id;
50 rooms_ = rooms;
51 is_new_object_ = false;
52 current_layout_ = {};
54 is_open_ = true;
55
56 if (!rooms_ || current_room_id_ < 0 ||
57 current_room_id_ >= static_cast<int>(rooms_->size())) {
58 return;
59 }
60
61 auto& room = (*rooms_)[current_room_id_];
62 auto layout_or = tile_editor_->CaptureObjectLayout(object_id, room,
64 if (layout_or.ok()) {
65 current_layout_ = std::move(layout_or.value());
67 }
68}
69
70void ObjectTileEditorPanel::OpenForNewObject(int width, int height,
71 const std::string& filename,
72 int16_t object_id, int room_id,
73 DungeonRoomStore* rooms) {
74 current_object_id_ = object_id;
75 current_room_id_ = room_id;
76 rooms_ = rooms;
78 is_open_ = true;
79 is_new_object_ = true;
80
82 zelda3::ObjectTileLayout::CreateEmpty(width, height, object_id, filename);
84}
85
87 is_open_ = false;
88 is_new_object_ = false;
89 current_layout_ = {};
92 rooms_ = nullptr;
94}
95
102
104 if (is_new_object_) {
105 return absl::StrFormat(
106 ICON_MD_ADD_BOX " New Object (%dx%d) - %s###ObjTileEditor",
109 }
110
112 return absl::StrFormat(
113 ICON_MD_GRID_ON " Custom Object 0x%03X - %s###ObjTileEditor",
115 }
116
117 return absl::StrFormat(ICON_MD_GRID_ON " Object 0x%03X - %s###ObjTileEditor",
120}
121
132
136 }
137
139 current_object_id_ < 0 || rom_ == nullptr || !rom_->is_loaded()) {
140 return 0;
141 }
142
143 return tile_editor_->CountObjectsSharingTileData(current_object_id_);
144}
145
149
151 return rooms_ != nullptr && current_room_id_ >= 0 &&
152 current_room_id_ < static_cast<int>(rooms_->size()) &&
153 !current_layout_.cells.empty();
154}
155
157 preview_dirty_ = true;
158 atlas_dirty_ = true;
159
161 return;
162 }
163
166}
167
168void ObjectTileEditorPanel::Draw(bool* p_open) {
169 if (!is_open_ || current_layout_.cells.empty())
170 return;
171
172 const std::string title = BuildWindowTitle();
173
174 ImGui::SetNextWindowSize(ImVec2(550, 500), ImGuiCond_FirstUseEver);
175 if (!ImGui::Begin(title.c_str(), &is_open_)) {
176 ImGui::End();
177 return;
178 }
179
180 if (!is_open_) {
181 Close();
182 ImGui::End();
183 return;
184 }
185
186 // Two-column layout: tile grid + source sheet
187 if (ImGui::BeginTable(
188 "##TileEditorLayout", 2,
189 ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInnerV)) {
190 ImGui::TableSetupColumn("Tile Grid", ImGuiTableColumnFlags_WidthFixed,
191 280.0f);
192 ImGui::TableSetupColumn("Source Sheet", ImGuiTableColumnFlags_WidthStretch);
193
194 ImGui::TableNextRow();
195 ImGui::TableNextColumn();
196 DrawTileGrid();
197
198 ImGui::TableNextColumn();
200
201 ImGui::EndTable();
202 }
203
204 ImGui::Separator();
206 ImGui::Separator();
208
210
211 // Shared tile data confirmation modal
213 ImGui::OpenPopup("Shared Tile Data");
214 show_shared_confirm_ = false;
215 }
216 if (ImGui::BeginPopupModal("Shared Tile Data", nullptr,
217 ImGuiWindowFlags_AlwaysAutoResize)) {
218 ImGui::Text(tr("This tile data is shared by %d objects."),
220 ImGui::Text(tr("Changes will affect all of them."));
221 ImGui::Spacing();
222 if (ImGui::Button(tr("Apply Anyway"), ImVec2(120, 0))) {
223 ApplyChanges(/*confirm_shared=*/false);
224 ImGui::CloseCurrentPopup();
225 }
226 ImGui::SameLine();
227 if (ImGui::Button(tr("Cancel"), ImVec2(120, 0))) {
228 ImGui::CloseCurrentPopup();
229 }
230 ImGui::EndPopup();
231 }
232
233 ImGui::End();
234}
235
237 if (!rooms_ || current_room_id_ < 0 ||
238 current_room_id_ >= static_cast<int>(rooms_->size())) {
240 return;
241 }
242 auto& room = (*rooms_)[current_room_id_];
243
244 auto status = tile_editor_->RenderLayoutToBitmap(
245 current_layout_, object_preview_bmp_, room.get_gfx_buffer().data(),
247 if (status.ok()) {
249 preview_dirty_ = false;
250 } else {
252 }
253}
254
256 if (!rooms_ || current_room_id_ < 0 ||
257 current_room_id_ >= static_cast<int>(rooms_->size())) {
259 return;
260 }
261 auto& room = (*rooms_)[current_room_id_];
262
263 auto status = tile_editor_->BuildTile8Atlas(
264 tile8_atlas_bmp_, room.get_gfx_buffer().data(), current_palette_group_,
266 if (status.ok()) {
268 atlas_dirty_ = false;
269 } else {
271 }
272}
273
275 if (selected_cell_index_ < 0 ||
276 selected_cell_index_ >= static_cast<int>(current_layout_.cells.size())) {
277 return;
278 }
279
280 const auto& cell = current_layout_.cells[selected_cell_index_];
281 selected_source_tile_ = static_cast<int>(cell.tile_info.id_);
282
283 const int cell_palette = static_cast<int>(cell.tile_info.palette_);
284 if (source_palette_ != cell_palette) {
285 source_palette_ = cell_palette;
286 atlas_dirty_ = true;
287 }
288}
289
291 if (preview_dirty_) {
293 }
294
295 ImGui::Text(tr("Object Tiles (%dx%d)"), current_layout_.bounds_width,
297
298 constexpr float kScale = 4.0f;
299 float grid_width = current_layout_.bounds_width * 8 * kScale;
300 float grid_height = current_layout_.bounds_height * 8 * kScale;
301
302 ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
303 ImVec2 canvas_size(grid_width, grid_height);
304
305 // Draw the preview bitmap as background
307 ImGui::Image((ImTextureID)(intptr_t)object_preview_bmp_.texture(),
308 canvas_size);
309 } else {
310 ImGui::Dummy(canvas_size);
311 }
312
313 ImDrawList* draw_list = ImGui::GetWindowDrawList();
314
315 // Draw 8px grid overlay
316 for (int gx = 0; gx <= current_layout_.bounds_width; ++gx) {
317 float line_x = canvas_pos.x + gx * 8 * kScale;
318 draw_list->AddLine(ImVec2(line_x, canvas_pos.y),
319 ImVec2(line_x, canvas_pos.y + grid_height),
320 IM_COL32(128, 128, 128, 80));
321 }
322 for (int gy = 0; gy <= current_layout_.bounds_height; ++gy) {
323 float line_y = canvas_pos.y + gy * 8 * kScale;
324 draw_list->AddLine(ImVec2(canvas_pos.x, line_y),
325 ImVec2(canvas_pos.x + grid_width, line_y),
326 IM_COL32(128, 128, 128, 80));
327 }
328
329 // Highlight selected cell
330 if (selected_cell_index_ >= 0 &&
331 selected_cell_index_ < static_cast<int>(current_layout_.cells.size())) {
332 const auto& cell = current_layout_.cells[selected_cell_index_];
333 ImVec2 cell_min(canvas_pos.x + cell.rel_x * 8 * kScale,
334 canvas_pos.y + cell.rel_y * 8 * kScale);
335 ImVec2 cell_max(cell_min.x + 8 * kScale, cell_min.y + 8 * kScale);
336 draw_list->AddRect(cell_min, cell_max, IM_COL32(255, 255, 0, 255), 0, 0,
337 2.0f);
338 }
339
340 // Handle clicks on the grid
341 if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(0)) {
342 ImVec2 mouse = ImGui::GetMousePos();
343 int click_tile_x =
344 static_cast<int>((mouse.x - canvas_pos.x) / (8 * kScale));
345 int click_tile_y =
346 static_cast<int>((mouse.y - canvas_pos.y) / (8 * kScale));
347
348 // Find the cell at this position
349 for (int idx = 0; idx < static_cast<int>(current_layout_.cells.size());
350 ++idx) {
351 if (current_layout_.cells[idx].rel_x == click_tile_x &&
352 current_layout_.cells[idx].rel_y == click_tile_y) {
355 break;
356 }
357 }
358 }
359
360 // Show cell count
361 int modified_count = 0;
362 for (const auto& cell : current_layout_.cells) {
363 if (cell.modified)
364 ++modified_count;
365 }
366 ImGui::Text(tr("%zu tiles, %d modified"), current_layout_.cells.size(),
367 modified_count);
368}
369
371 if (atlas_dirty_) {
373 }
374
375 ImGui::Text(tr("Source Tiles (Palette %d)"), source_palette_);
376
377 // Palette selector
378 ImGui::SameLine();
379 ImGui::SetNextItemWidth(80);
380 if (ImGui::SliderInt("##SrcPal", &source_palette_, 0, 7)) {
381 atlas_dirty_ = true;
382 }
383
384 constexpr float kAtlasScale = 2.0f;
385 float display_width = zelda3::ObjectTileEditor::kAtlasWidthPx * kAtlasScale;
386 float display_height = zelda3::ObjectTileEditor::kAtlasHeightPx * kAtlasScale;
387
388 ImVec2 atlas_pos = ImGui::GetCursorScreenPos();
389 ImVec2 atlas_size(display_width, display_height);
390
391 // Scrollable child for the atlas
392 ImGui::BeginChild("##AtlasScroll", ImVec2(display_width + 16, 300), true,
393 ImGuiWindowFlags_HorizontalScrollbar);
394
395 atlas_pos = ImGui::GetCursorScreenPos();
396
398 ImGui::Image((ImTextureID)(intptr_t)tile8_atlas_bmp_.texture(), atlas_size);
399 } else {
400 ImGui::Dummy(atlas_size);
401 }
402
403 ImDrawList* draw_list = ImGui::GetWindowDrawList();
404
405 // Draw 8px grid
406 for (int gx = 0; gx <= zelda3::ObjectTileEditor::kAtlasTilesPerRow; ++gx) {
407 float line_x = atlas_pos.x + gx * 8 * kAtlasScale;
408 draw_list->AddLine(ImVec2(line_x, atlas_pos.y),
409 ImVec2(line_x, atlas_pos.y + display_height),
410 IM_COL32(64, 64, 64, 60));
411 }
412 for (int gy = 0; gy <= zelda3::ObjectTileEditor::kAtlasTileRows; ++gy) {
413 float line_y = atlas_pos.y + gy * 8 * kAtlasScale;
414 draw_list->AddLine(ImVec2(atlas_pos.x, line_y),
415 ImVec2(atlas_pos.x + display_width, line_y),
416 IM_COL32(64, 64, 64, 60));
417 }
418
419 // Highlight selected source tile
420 if (selected_source_tile_ >= 0) {
421 int src_col =
423 int src_row =
425 ImVec2 sel_min(atlas_pos.x + src_col * 8 * kAtlasScale,
426 atlas_pos.y + src_row * 8 * kAtlasScale);
427 ImVec2 sel_max(sel_min.x + 8 * kAtlasScale, sel_min.y + 8 * kAtlasScale);
428 draw_list->AddRect(sel_min, sel_max, IM_COL32(0, 255, 255, 255), 0, 0,
429 2.0f);
430 }
431
432 // Handle clicks on the atlas
433 if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(0)) {
434 ImVec2 mouse = ImGui::GetMousePos();
435 int click_col =
436 static_cast<int>((mouse.x - atlas_pos.x) / (8 * kAtlasScale));
437 int click_row =
438 static_cast<int>((mouse.y - atlas_pos.y) / (8 * kAtlasScale));
439
440 if (click_col >= 0 &&
442 click_row >= 0 &&
444 int tile_id =
445 click_row * zelda3::ObjectTileEditor::kAtlasTilesPerRow + click_col;
446 selected_source_tile_ = tile_id;
447
448 // If a cell is selected, replace its tile
449 if (selected_cell_index_ >= 0 &&
451 static_cast<int>(current_layout_.cells.size())) {
453 cell.tile_info.id_ = static_cast<uint16_t>(tile_id);
454 cell.tile_info.palette_ = static_cast<uint8_t>(source_palette_);
455 cell.modified = true;
456 preview_dirty_ = true;
458 }
459 }
460 }
461
462 ImGui::EndChild();
463
464 if (selected_source_tile_ >= 0) {
465 ImGui::Text(tr("Tile: 0x%03X"), selected_source_tile_);
466 }
467}
468
470 if (selected_cell_index_ < 0 ||
471 selected_cell_index_ >= static_cast<int>(current_layout_.cells.size())) {
472 ImGui::TextDisabled(tr("Select a tile cell to edit properties"));
473 return;
474 }
475
477 ImGui::Text(tr("Cell (%d, %d)"), cell.rel_x, cell.rel_y);
478 ImGui::SameLine();
479
480 // Tile ID
481 int tile_id = cell.tile_info.id_;
482 ImGui::SetNextItemWidth(80);
483 if (ImGui::InputInt(tr("ID"), &tile_id, 1, 16)) {
484 cell.tile_info.id_ = static_cast<uint16_t>(tile_id & 0x3FF);
485 cell.modified = true;
486 preview_dirty_ = true;
489 }
490 ImGui::SameLine();
491
492 // Palette
493 int pal = cell.tile_info.palette_;
494 ImGui::SetNextItemWidth(60);
495 if (ImGui::SliderInt(tr("Pal"), &pal, 0, 7)) {
496 cell.tile_info.palette_ = static_cast<uint8_t>(pal);
497 cell.modified = true;
498 preview_dirty_ = true;
501 }
502 ImGui::SameLine();
503
504 // Flip flags
505 if (ImGui::Checkbox(tr("H"), &cell.tile_info.horizontal_mirror_)) {
506 cell.modified = true;
507 preview_dirty_ = true;
509 }
510 ImGui::SameLine();
511 if (ImGui::Checkbox(tr("V"), &cell.tile_info.vertical_mirror_)) {
512 cell.modified = true;
513 preview_dirty_ = true;
515 }
516 ImGui::SameLine();
517 if (ImGui::Checkbox(tr("Pri"), &cell.tile_info.over_)) {
518 cell.modified = true;
519 preview_dirty_ = true;
521 }
522}
523
524void ObjectTileEditorPanel::ApplyChanges(bool confirm_shared) {
525 // Check for shared tile data and ask for confirmation
526 const int shared_count = GetSharedTileDataUsageCount();
527 if (confirm_shared && shared_count > 1) {
528 shared_object_count_ = shared_count;
532 absl::StrFormat(ICON_MD_WARNING
533 " Confirm shared apply: %d objects use this tile data.",
534 shared_count));
535 return;
536 }
537
538 show_shared_confirm_ = false;
540
541 auto status = tile_editor_->WriteBack(current_layout_);
542 if (status.ok()) {
543 // Re-render room after applying changes
545 auto& room = (*rooms_)[current_room_id_];
546 room.MarkObjectsDirty();
547 room.RenderRoomGraphics();
548 }
549
550 // Update original words to match current state
551 for (auto& cell : current_layout_.cells) {
552 if (cell.modified) {
553 cell.original_word = gfx::TileInfoToWord(cell.tile_info);
554 cell.modified = false;
555 }
556 }
557
558 // Successful first save should always exit new-object mode. If the caller
559 // wired a callback, notify it exactly once as part of that transition.
560 if (is_new_object_) {
561 if (on_object_created_) {
564 }
565 is_new_object_ = false;
566 }
567
569 if (shared_count > 1) {
572 absl::StrFormat(
574 " Applied changes to shared tile data used by %d objects.",
575 shared_count));
576 } else {
578 }
579 return;
580 }
581
584 absl::StrFormat(ICON_MD_ERROR " Apply failed: %s", status.message()));
585}
586
588 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
589 int modified_count = 0;
590 for (const auto& cell : current_layout_.cells) {
591 if (cell.modified)
592 ++modified_count;
593 }
594
595 bool has_mods = modified_count > 0;
596
597 if (has_mods) {
598 ImGui::Text(tr("%d tile(s) modified"), modified_count);
599 ImGui::SameLine();
600 }
601
602 // Shared tile data warning
603 const int shared_count = GetSharedTileDataUsageCount();
604 if (shared_count > 1) {
605 ImGui::TextColored(gui::ConvertColorToImVec4(theme.warning),
606 ICON_MD_WARNING " Shared by %d objects", shared_count);
607 if (ImGui::IsItemHovered()) {
608 ImGui::SetItemTooltip(
609 tr("This object reuses tile data with %d objects.\nApplying changes "
610 "will update every object in that shared data group."),
611 shared_count);
612 }
613 ImGui::SameLine();
614 }
615
616 // Apply button
617 if (!has_mods)
618 ImGui::BeginDisabled();
619 if (ImGui::Button(ICON_MD_SAVE " Apply")) {
620 ApplyChanges();
621 }
622 if (!has_mods)
623 ImGui::EndDisabled();
624
625 ImGui::SameLine();
626
627 // Revert button
628 if (!has_mods)
629 ImGui::BeginDisabled();
630 if (ImGui::Button(ICON_MD_UNDO " Revert")) {
632 preview_dirty_ = true;
635 }
636 if (!has_mods)
637 ImGui::EndDisabled();
638
639 ImGui::SameLine();
640
641 if (ImGui::Button(ICON_MD_CLOSE " Close")) {
642 Close();
643 }
644
646 !action_status_message_.empty()) {
647 ImVec4 status_color = gui::ConvertColorToImVec4(theme.text_secondary);
648 switch (action_status_tone_) {
650 status_color = gui::ConvertColorToImVec4(theme.warning);
651 break;
653 status_color = gui::ConvertColorToImVec4(theme.success);
654 break;
656 status_color = gui::ConvertColorToImVec4(theme.error);
657 break;
659 break;
660 }
661
662 ImGui::Spacing();
663 ImGui::PushStyleColor(ImGuiCol_Text, status_color);
664 ImGui::PushTextWrapPos(0.0f);
665 ImGui::TextUnformatted(action_status_message_.c_str());
666 ImGui::PopTextWrapPos();
667 ImGui::PopStyleColor();
668 }
669}
670
672 // Only handle shortcuts when this window is focused
673 if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows))
674 return;
675 if (current_layout_.cells.empty())
676 return;
677
678 int cell_count = static_cast<int>(current_layout_.cells.size());
679
680 // Arrow keys: navigate selected cell by spatial position
681 auto find_neighbor = [&](int dx, int dy) -> int {
682 if (selected_cell_index_ < 0)
683 return 0;
684 const auto& cur = current_layout_.cells[selected_cell_index_];
685 int target_x = cur.rel_x + dx;
686 int target_y = cur.rel_y + dy;
687 for (int i = 0; i < cell_count; ++i) {
688 if (current_layout_.cells[i].rel_x == target_x &&
689 current_layout_.cells[i].rel_y == target_y) {
690 return i;
691 }
692 }
694 };
695
696 if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow, false)) {
697 selected_cell_index_ = find_neighbor(-1, 0);
699 }
700 if (ImGui::IsKeyPressed(ImGuiKey_RightArrow, false)) {
701 selected_cell_index_ = find_neighbor(1, 0);
703 }
704 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow, false)) {
705 selected_cell_index_ = find_neighbor(0, -1);
707 }
708 if (ImGui::IsKeyPressed(ImGuiKey_DownArrow, false)) {
709 selected_cell_index_ = find_neighbor(0, 1);
711 }
712
713 // Number keys 0-7: set palette on selected cell
714 if (selected_cell_index_ >= 0 && selected_cell_index_ < cell_count) {
716 for (int key = 0; key <= 7; ++key) {
717 if (ImGui::IsKeyPressed(static_cast<ImGuiKey>(ImGuiKey_0 + key), false)) {
718 cell.tile_info.palette_ = static_cast<uint8_t>(key);
719 cell.modified = true;
720 preview_dirty_ = true;
723 }
724 }
725
726 // H: toggle horizontal flip
727 if (ImGui::IsKeyPressed(ImGuiKey_H, false)) {
728 cell.tile_info.horizontal_mirror_ = !cell.tile_info.horizontal_mirror_;
729 cell.modified = true;
730 preview_dirty_ = true;
732 }
733
734 // V: toggle vertical flip
735 if (ImGui::IsKeyPressed(ImGuiKey_V, false)) {
736 cell.tile_info.vertical_mirror_ = !cell.tile_info.vertical_mirror_;
737 cell.modified = true;
738 preview_dirty_ = true;
740 }
741
742 // P: toggle priority
743 if (ImGui::IsKeyPressed(ImGuiKey_P, false)) {
744 cell.tile_info.over_ = !cell.tile_info.over_;
745 cell.modified = true;
746 preview_dirty_ = true;
748 }
749 }
750
751 // Escape: deselect or close
752 if (ImGui::IsKeyPressed(ImGuiKey_Escape, false)) {
753 if (selected_cell_index_ >= 0) {
755 } else {
756 Close();
757 }
758 }
759
760 // Tab: cycle to next cell
761 if (ImGui::IsKeyPressed(ImGuiKey_Tab, false)) {
762 if (cell_count > 0) {
763 selected_cell_index_ = (selected_cell_index_ + 1) % cell_count;
765 }
766 }
767}
768
769} // namespace editor
770} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
void ApplyChanges(bool confirm_shared=true)
void SetCurrentPaletteGroup(const gfx::PaletteGroup &group)
ObjectTileEditorPanel(gfx::IRenderer *renderer, Rom *rom)
void OpenForObject(int16_t object_id, int room_id, DungeonRoomStore *rooms)
std::unique_ptr< zelda3::ObjectTileEditor > tile_editor_
std::function< void(int, const std::string &) on_object_created_)
void Draw(bool *p_open) override
Draw the panel content.
void OpenForNewObject(int width, int height, const std::string &filename, int16_t object_id, int room_id, DungeonRoomStore *rooms)
void SetActionStatus(ActionStatusTone tone, std::string message)
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:67
const uint8_t * data() const
Definition bitmap.h:398
TextureHandle texture() const
Definition bitmap.h:401
bool is_active() const
Definition bitmap.h:405
void UpdateTexture()
Updates the underlying SDL_Texture when it already exists.
Definition bitmap.cc:297
Defines an abstract interface for all rendering operations.
Definition irenderer.h:60
const Theme & GetCurrentTheme() const
static ThemeManager & Get()
static constexpr int kAtlasTilesPerRow
static constexpr int kAtlasTileRows
static constexpr int kAtlasHeightPx
#define ICON_MD_WARNING
Definition icons.h:2123
#define ICON_MD_ERROR
Definition icons.h:686
#define ICON_MD_GRID_ON
Definition icons.h:896
#define ICON_MD_ADD_BOX
Definition icons.h:90
#define ICON_MD_CHECK_CIRCLE
Definition icons.h:400
#define ICON_MD_SAVE
Definition icons.h:1644
#define ICON_MD_CLOSE
Definition icons.h:418
#define ICON_MD_UNDO
Definition icons.h:2039
uint16_t TileInfoToWord(TileInfo tile_info)
Definition snes_tile.cc:361
ImVec4 ConvertColorToImVec4(const Color &color)
Definition color.h:134
std::string GetObjectName(int object_id)
Represents a group of palettes.
static ObjectTileLayout CreateEmpty(int width, int height, int16_t object_id, const std::string &filename)