yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
gfx_group_editor.cc
Go to the documentation of this file.
1#include "gfx_group_editor.h"
2#include "util/i18n/tr.h"
3
4#include <algorithm>
5
6#include "absl/status/status.h"
7#include "absl/strings/str_cat.h"
8#include "absl/strings/str_format.h"
9#include "absl/strings/str_join.h"
15#include "app/gui/core/color.h"
16#include "app/gui/core/icons.h"
17#include "app/gui/core/input.h"
20#include "imgui/imgui.h"
21#include "rom/rom.h"
22#include "util/macro.h"
23
24namespace yaze {
25namespace editor {
26
27using ImGui::BeginChild;
28using ImGui::BeginCombo;
29using ImGui::BeginGroup;
30using ImGui::BeginTabItem;
31using ImGui::BeginTable;
32using ImGui::EndChild;
33using ImGui::EndCombo;
34using ImGui::EndGroup;
35using ImGui::EndTabItem;
36using ImGui::EndTable;
37using ImGui::GetContentRegionAvail;
38using ImGui::GetStyle;
39using ImGui::IsItemClicked;
40using ImGui::PopID;
41using ImGui::PushID;
42using ImGui::SameLine;
43using ImGui::Selectable;
44using ImGui::Separator;
45using ImGui::SetNextItemWidth;
46using ImGui::SliderFloat;
47using ImGui::TableHeadersRow;
48using ImGui::TableNextColumn;
49using ImGui::TableNextRow;
50using ImGui::TableSetupColumn;
51using ImGui::Text;
52
53using gfx::kPaletteGroupNames;
55
56namespace {
57
58// Constants for sheet display
59constexpr int kSheetDisplayHeight = 64; // 2x scale from 32px sheets
60constexpr float kDefaultScale = 2.0f;
61constexpr int kTileSize = 16; // 8px tiles at 2x scale
62
63// Width of a table column of "Slot N" hex inputs. The column is fixed and
64// the sheet previews take the rest, so the inputs stay usable when the
65// panel is narrower than the previews (a stretch column shrank to nothing).
67 const ImGuiStyle& style = ImGui::GetStyle();
68 return ImGui::CalcTextSize("Slot 00").x +
69 gui::LayoutHelpers::GetSliderWidth() + 2 * ImGui::GetFrameHeight() +
70 4 * style.ItemInnerSpacing.x + 2 * style.CellPadding.x;
71}
72
73// Draw a single sheet with proper scaling and unique ID
74void DrawScaledSheet(gui::Canvas& canvas, gfx::Bitmap& sheet, int unique_id,
75 float scale = kDefaultScale) {
76 PushID(unique_id);
77
78 // Each sheet viewer is read-only; declare it so future canvas surfaces
79 // (cursors, hover hints, context-menu items) can default appropriately
80 // without the caller threading the policy through every frame.
82
83 // Calculate scaled dimensions
84 int display_width = static_cast<int>(gfx::kTilesheetWidth * scale);
85 int display_height = static_cast<int>(gfx::kTilesheetHeight * scale);
86
87 // Draw canvas background
88 canvas.DrawBackground(ImVec2(display_width + 1, display_height + 1));
89 canvas.DrawContextMenu();
90
91 // Draw bitmap with proper scale
92 canvas.DrawBitmap(sheet, 2, scale);
93
94 // Draw grid at scaled tile size
95 canvas.DrawGrid(static_cast<int>(8 * scale));
96 canvas.DrawOverlay();
97
98 PopID();
99}
100
101} // namespace
102
104 gfx::SheetRole role,
105 gfx::Bitmap& preview) {
107 if (!game_data_ || sheet_id < 0 ||
109 !game_data_->sheet_store.HasSheet(sheet_id)) {
110 Text("Sheet 0x%02X is not loaded", sheet_id);
111 return nullptr;
112 }
113 const auto pixels = game_data_->sheet_store.Sheet(sheet_id);
114 const bool changed =
115 !preview.surface() || preview.vector().size() != pixels.size() ||
116 !std::equal(pixels.begin(), pixels.end(), preview.vector().begin());
117 if (changed)
118 preview.Create(128, 32, 8,
119 std::vector<uint8_t>(pixels.begin(), pixels.end()));
120 const auto* palette = Ws().override_palette && current_palette_
124 if (palette && preview.palette() != *palette) {
125 preview.SetPalette(*palette);
126 if (preview.texture())
127 preview.UpdateTexture();
128 } else if (changed && preview.texture()) {
129 preview.UpdateTexture();
130 }
132 return &preview;
133}
134
136 if (!host_surface_hint_.empty()) {
137 ImGui::TextDisabled("%s", host_surface_hint_.c_str());
138 Separator();
139 }
140
142
143 // Palette controls at top for all tabs
145 Separator();
146
147 if (gui::BeginThemedTabBar("##GfxGroupEditorTabs")) {
148 if (BeginTabItem(tr("Blocksets"))) {
149 gui::InputHexByte("Selected Blockset", &Ws().selected_blockset,
150 static_cast<uint8_t>(0x24));
152 false, "blockset", std::to_string(Ws().selected_blockset),
153 absl::StrFormat("Blockset 0x%02X", Ws().selected_blockset));
155 EndTabItem();
156 }
157
158 if (BeginTabItem(tr("Roomsets"))) {
159 gui::InputHexByte("Selected Roomset", &Ws().selected_roomset,
160 static_cast<uint8_t>(81));
162 false, "roomset", std::to_string(Ws().selected_roomset),
163 absl::StrFormat("Roomset 0x%02X", Ws().selected_roomset));
165 EndTabItem();
166 }
167
168 if (BeginTabItem(tr("Spritesets"))) {
169 gui::InputHexByte("Selected Spriteset", &Ws().selected_spriteset,
170 static_cast<uint8_t>(143));
172 false, "spriteset", std::to_string(Ws().selected_spriteset),
173 absl::StrFormat("Spriteset 0x%02X", Ws().selected_spriteset));
174 DrawSpritesetUsage(Ws().selected_spriteset);
176 EndTabItem();
177 }
178
180 }
181
182 return absl::OkStatus();
183}
184
186 if (!ImGui::TreeNodeEx("Used by##SpritesetUsage",
187 ImGuiTreeNodeFlags_DefaultOpen)) {
188 return;
189 }
190 const bool refresh = ImGui::SmallButton(ICON_MD_REFRESH " Refresh");
191 HOVER_HINT("Re-read overworld areas and room headers from the ROM buffer");
192 if (rom_ != nullptr && rom_->is_loaded() &&
193 (refresh || usage_rom_ != rom_ || !usage_areas_.has_value())) {
197 }
198 if (!usage_areas_.has_value() || !usage_rooms_.has_value()) {
199 ImGui::TextDisabled(tr("Load a ROM to see usage."));
200 ImGui::TreePop();
201 return;
202 }
203
204 auto hex_list = [](const std::vector<int>& ids) {
205 return ids.empty() ? std::string("-")
206 : absl::StrJoin(ids, " ", [](std::string* out, int v) {
207 out->append(absl::StrFormat("%02X", v));
208 });
209 };
210 const auto usage =
212 static constexpr const char* kStates[] = {
213 "OW areas, state 0 (start)", "OW areas, state 1", "OW areas, state 2"};
214 for (int state = 0; state < 3; ++state) {
215 ImGui::TextWrapped("%s (%zu): %s", kStates[state],
216 usage.ow_areas_by_state[state].size(),
217 hex_list(usage.ow_areas_by_state[state]).c_str());
218 }
219 ImGui::TextWrapped("Rooms, header 0x%02X (%zu): %s",
220 std::max(0, id - zelda3::kDungeonSpritesetBase),
221 usage.rooms.size(), hex_list(usage.rooms).c_str());
223 ImGui::TextDisabled(
224 tr("Dungeon rooms use spritesets 0x40-0x8F (header + 0x40)."));
225 }
226 ImGui::TreePop();
227}
228
230 if (!game_data()) {
231 Text(tr("No game data loaded"));
232 return;
233 }
234
235 if (Ws().selected_blockset >= game_data()->main_blockset_ids.size()) {
236 Text("Main graphics group is unavailable");
237 return;
238 }
239 PushID("BlocksetViewer");
240 auto& ws = Ws();
241
242 if (BeginTable("##BlocksetTable", sheet_only ? 1 : 2,
243 ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable,
244 ImVec2(0, 0))) {
245 if (!sheet_only) {
246 TableSetupColumn("Inputs", ImGuiTableColumnFlags_WidthFixed,
247 SlotInputsColumnWidth());
248 }
249
250 TableSetupColumn("Sheets", ImGuiTableColumnFlags_WidthStretch);
251 TableHeadersRow();
252 TableNextRow();
253
254 if (!sheet_only) {
255 TableNextColumn();
256 BeginGroup();
258 EndGroup();
259 }
260
261 TableNextColumn();
262 BeginGroup();
263 for (int idx = 0; idx < 8; idx++) {
264 int sheet_id = game_data()->main_blockset_ids[ws.selected_blockset][idx];
265 auto* sheet = PrepareSheetPreview(sheet_id, gfx::RoleForBlocksetSlot(idx),
266 blockset_previews_[idx]);
267 if (!sheet)
268 continue;
269
270 // Unique ID combining blockset, slot, and sheet
271 int unique_id = (ws.selected_blockset << 16) | (idx << 8) | sheet_id;
272 DrawScaledSheet(blockset_canvases_[idx], *sheet, unique_id,
273 ws.view_scale);
274 }
275 EndGroup();
276 EndTable();
277 }
278
279 PopID();
280}
281
283 if (!game_data()) {
284 Text(tr("No game data loaded"));
285 return;
286 }
287
288 if (Ws().selected_roomset >= game_data()->room_blockset_ids.size()) {
289 ImGui::TextUnformatted("Roomset is unavailable");
290 return;
291 }
292 PushID("RoomsetViewer");
293 auto& ws = Ws();
294 Text(tr("Roomsets overwrite slots 3-6 of the main blockset"));
295
296 if (BeginTable("##RoomsTable", 3,
297 ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable,
298 ImVec2(0, 0))) {
299 TableSetupColumn("List", ImGuiTableColumnFlags_WidthFixed, 120);
300 TableSetupColumn("Inputs", ImGuiTableColumnFlags_WidthFixed,
301 SlotInputsColumnWidth());
302 TableSetupColumn("Sheets", ImGuiTableColumnFlags_WidthStretch);
303 TableHeadersRow();
304 TableNextRow();
305
306 // Roomset list column
307 TableNextColumn();
308 if (BeginChild("##RoomsetListChild", ImVec2(0, 300))) {
309 for (int idx = 0; idx < 0x52; idx++) {
310 PushID(idx);
311 std::string roomset_label = absl::StrFormat("0x%02X", idx);
312 bool is_selected = (ws.selected_roomset == static_cast<uint8_t>(idx));
313 if (Selectable(roomset_label.c_str(), is_selected)) {
314 ws.selected_roomset = static_cast<uint8_t>(idx);
315 }
316 PopID();
317 }
318 }
319 EndChild();
320
321 TableNextColumn();
323
324 // Sheets column
325 TableNextColumn();
326 BeginGroup();
327 for (int idx = 0; idx < 4; idx++) {
328 int sheet_id = game_data()->room_blockset_ids[ws.selected_roomset][idx];
329 if (sheet_id == 0) {
330 Text("Slot %d: Inherit from main group", idx + 3);
331 continue;
332 }
333 Text("Slot %d / Sheet %02X", idx + 3, sheet_id);
334 auto* sheet = PrepareSheetPreview(sheet_id, gfx::RoleForRoomsetSlot(idx),
335 roomset_previews_[idx]);
336 if (!sheet)
337 continue;
338
339 // Unique ID combining roomset, slot, and sheet
340 int unique_id =
341 (0x1000) | (ws.selected_roomset << 8) | (idx << 4) | sheet_id;
342 DrawScaledSheet(roomset_canvases_[idx], *sheet, unique_id, ws.view_scale);
343 }
344 EndGroup();
345 EndTable();
346 }
347
348 PopID();
349}
350
352 if (!game_data()) {
353 Text(tr("No game data loaded"));
354 return;
355 }
356
357 PushID("SpritesetViewer");
358 auto& ws = Ws();
359
360 if (BeginTable("##SpritesTable", sheet_only ? 1 : 2,
361 ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable,
362 ImVec2(0, 0))) {
363 if (!sheet_only) {
364 TableSetupColumn("Inputs", ImGuiTableColumnFlags_WidthFixed,
365 SlotInputsColumnWidth());
366 }
367 TableSetupColumn("Sheets", ImGuiTableColumnFlags_WidthStretch);
368 TableHeadersRow();
369 TableNextRow();
370
371 if (!sheet_only) {
372 TableNextColumn();
373 BeginGroup();
374 Text(tr("Sprite sheet IDs (base 115+):"));
375 for (int idx = 0; idx < 4; idx++) {
376 SetNextItemWidth(gui::LayoutHelpers::GetSliderWidth());
378 ("Slot " + std::to_string(idx)).c_str(),
379 &game_data()->spriteset_ids[ws.selected_spriteset][idx]);
380 }
381 EndGroup();
382 }
383
384 TableNextColumn();
385 BeginGroup();
386 for (int idx = 0; idx < 4; idx++) {
387 int sheet_offset = game_data()->spriteset_ids[ws.selected_spriteset][idx];
388 int sheet_id = 115 + sheet_offset;
389 auto* sheet = PrepareSheetPreview(
391 if (!sheet)
392 continue;
393
394 // Unique ID combining spriteset, slot, and sheet
395 int unique_id =
396 (0x2000) | (ws.selected_spriteset << 8) | (idx << 4) | sheet_offset;
397 DrawScaledSheet(spriteset_canvases_[idx], *sheet, unique_id,
398 ws.view_scale);
399 }
400 EndGroup();
401 EndTable();
402 }
403
404 PopID();
405}
406
407namespace {
409 if (palette.empty()) {
410 return;
411 }
412 for (size_t color_idx = 0; color_idx < palette.size(); color_idx++) {
413 PushID(static_cast<int>(color_idx));
414 if ((color_idx % 8) != 0) {
415 SameLine(0.0f, GetStyle().ItemSpacing.y);
416 }
417
418 // Small icon of the color in the palette
419 gui::SnesColorButton(absl::StrCat("Palette", color_idx), palette[color_idx],
420 ImGuiColorEditFlags_NoAlpha |
421 ImGuiColorEditFlags_NoPicker |
422 ImGuiColorEditFlags_NoTooltip);
423
424 PopID();
425 }
426}
427} // namespace
428
430 if (!game_data()) {
431 return;
432 }
433
434 auto& ws = Ws();
435
436 // View scale control
437 Text(ICON_MD_ZOOM_IN " View");
438 SameLine();
439 SetNextItemWidth(gui::LayoutHelpers::GetSliderWidth());
440 SliderFloat("##ViewScale", &ws.view_scale, 1.0f, 4.0f, "%.1fx");
441 SameLine();
442
443 // Palette category selector
444 Text(ICON_MD_PALETTE " Palette");
445 SameLine();
446 SetNextItemWidth(gui::LayoutHelpers::GetComboWidth());
447
448 // Use the category names array for display
449 static constexpr int kNumPaletteCategories = 14;
450 if (BeginCombo(
451 "##PaletteCategory",
452 gfx::kPaletteCategoryNames[ws.selected_palette_category].data())) {
453 for (int cat = 0; cat < kNumPaletteCategories; cat++) {
454 auto category = static_cast<PaletteCategory>(cat);
455 bool is_selected = (ws.selected_palette_category == category);
456 if (Selectable(gfx::kPaletteCategoryNames[category].data(),
457 is_selected)) {
458 ws.selected_palette_category = category;
459 ws.selected_palette_index = 0;
461 }
462 if (is_selected) {
463 ImGui::SetItemDefaultFocus();
464 }
465 }
466 EndCombo();
467 }
468
469 SameLine();
470 SetNextItemWidth(gui::LayoutHelpers::GetHexInputWidth());
471 if (gui::InputHexByte("##PaletteIndex", &ws.selected_palette_index)) {
473 }
474
475 SameLine();
476 ImGui::Checkbox(tr("Override palette"), &ws.override_palette);
477 if (ImGui::IsItemHovered()) {
478 ImGui::SetTooltip(
479 tr("When off, each sheet renders against its slot's role-default "
480 "palette (overworld main/aux/animated, sprites, hud, dungeon).\n"
481 "When on, the palette picked above is forced onto every sheet."));
482 }
483
484 // Show current palette preview
486 SameLine();
487 DrawPaletteFromPaletteGroup(*current_palette_);
488 }
489}
490
492 if (!game_data()) {
493 current_palette_ = nullptr;
494 return;
495 }
496
497 const auto& ws = Ws();
498 auto& groups = game_data()->palette_groups;
499 switch (ws.selected_palette_category) {
500 case PaletteCategory::kSword:
502 groups.swords.mutable_palette(ws.selected_palette_index);
503 break;
504 case PaletteCategory::kShield:
506 groups.shields.mutable_palette(ws.selected_palette_index);
507 break;
508 case PaletteCategory::kClothes:
510 groups.armors.mutable_palette(ws.selected_palette_index);
511 break;
512 case PaletteCategory::kWorldColors:
514 groups.overworld_main.mutable_palette(ws.selected_palette_index);
515 break;
516 case PaletteCategory::kAreaColors:
518 groups.overworld_aux.mutable_palette(ws.selected_palette_index);
519 break;
520 case PaletteCategory::kGlobalSprites:
522 groups.global_sprites.mutable_palette(ws.selected_palette_index);
523 break;
524 case PaletteCategory::kSpritesAux1:
526 groups.sprites_aux1.mutable_palette(ws.selected_palette_index);
527 break;
528 case PaletteCategory::kSpritesAux2:
530 groups.sprites_aux2.mutable_palette(ws.selected_palette_index);
531 break;
532 case PaletteCategory::kSpritesAux3:
534 groups.sprites_aux3.mutable_palette(ws.selected_palette_index);
535 break;
536 case PaletteCategory::kDungeons:
538 groups.dungeon_main.mutable_palette(ws.selected_palette_index);
539 break;
540 case PaletteCategory::kWorldMap:
541 case PaletteCategory::kDungeonMap:
543 groups.overworld_mini_map.mutable_palette(ws.selected_palette_index);
544 break;
545 case PaletteCategory::kTriforce:
546 case PaletteCategory::kCrystal:
548 groups.object_3d.mutable_palette(ws.selected_palette_index);
549 break;
550 default:
551 current_palette_ = nullptr;
552 break;
553 }
554}
555
556} // namespace editor
557} // namespace yaze
project::ResourceLabelManager * resource_label()
Definition rom.h:225
bool is_loaded() const
Definition rom.h:187
std::array< gfx::Bitmap, 4 > roomset_previews_
std::array< gui::Canvas, 8 > blockset_canvases_
zelda3::GameData * game_data() const
void DrawAssignmentControls(GroupKind kind)
std::optional< std::map< int, zelda3::OverworldAreaGfxInfo > > usage_areas_
std::array< gui::Canvas, 4 > roomset_canvases_
std::array< gfx::Bitmap, 4 > spriteset_previews_
GfxGroupWorkspaceState & Ws()
gfx::SnesPalette * current_palette_
std::optional< std::map< int, zelda3::RoomGfxInfo > > usage_rooms_
void DrawSpritesetViewer(bool sheet_only=false)
std::array< gfx::Bitmap, 8 > blockset_previews_
std::array< gui::Canvas, 4 > spriteset_canvases_
void DrawBlocksetViewer(bool sheet_only=false)
gfx::Bitmap * PrepareSheetPreview(int sheet_id, gfx::SheetRole role, gfx::Bitmap &preview)
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:70
const SnesPalette & palette() const
Definition bitmap.h:393
void Create(int width, int height, int depth, std::span< uint8_t > data)
Create a bitmap with the given dimensions and data.
Definition bitmap.cc:198
TextureHandle texture() const
Definition bitmap.h:410
const std::vector< uint8_t > & vector() const
Definition bitmap.h:411
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:452
SDL_Surface * surface() const
Definition bitmap.h:409
void UpdateTexture()
Updates the underlying SDL_Texture when it already exists.
Definition bitmap.cc:301
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
Modern, robust canvas for drawing and manipulating graphics.
Definition canvas.h:64
void DrawBitmap(Bitmap &bitmap, int border_offset, float scale)
Definition canvas.cc:1293
void DrawContextMenu()
Definition canvas.cc:823
CanvasConfig & GetConfig()
Definition canvas.h:243
void DrawBackground(ImVec2 canvas_size=ImVec2(0, 0), std::optional< ImVec2 > viewport_size=std::nullopt)
Definition canvas.cc:702
void DrawGrid(float grid_step=64.0f, int tile_id_offset=8)
Definition canvas.cc:1620
static float GetHexInputWidth()
static float GetSliderWidth()
bool HasSheet(uint16_t sheet) const
True when the buffer holds all 4096 bytes of sheet.
static constexpr uint16_t kSheetCount
absl::Span< const uint8_t > Sheet(uint16_t sheet) const
The sheet's 4096 pixels, or an empty span when it is not held.
#define ICON_MD_REFRESH
Definition icons.h:1572
#define ICON_MD_ZOOM_IN
Definition icons.h:2194
#define ICON_MD_PALETTE
Definition icons.h:1370
#define HOVER_HINT(string)
Definition macro.h:24
void DrawScaledSheet(gui::Canvas &canvas, gfx::Bitmap &sheet, int unique_id, float scale=kDefaultScale)
void EnsureSheetTextureQueued(gfx::Bitmap &sheet)
const gfx::SnesPalette * ResolveRoleDefaultPalette(gfx::SheetRole role, gfx::PaletteGroupMap &palette_groups)
PaletteCategory
Categories for organizing palette groups in the UI.
constexpr int kTilesheetHeight
Definition snes_tile.h:17
constexpr int kTilesheetWidth
Definition snes_tile.h:16
SheetRole RoleForRoomsetSlot(int slot_index)
SheetRole RoleForBlocksetSlot(int slot_index)
SheetRole RoleForSpritesetSlot(int slot_index)
IMGUI_API bool SnesColorButton(absl::string_view id, gfx::SnesColor &color, ImGuiColorEditFlags flags, const ImVec2 &size_arg)
Definition color.cc:80
bool BeginThemedTabBar(const char *id, ImGuiTabBarFlags flags)
A stylized tab bar with "Mission Control" branding.
void EndThemedTabBar()
bool InputHexByte(const char *label, uint8_t *data, float input_width, bool no_step)
Definition input.cc:510
GfxGroupUsage FindSpritesetUsage(int spriteset, const std::map< int, OverworldAreaGfxInfo > &areas, const std::map< int, RoomGfxInfo > &rooms)
std::map< int, OverworldAreaGfxInfo > CollectOverworldAreaGfx(Rom &rom, GameData *game_data)
constexpr uint16_t kDungeonSpritesetBase
std::map< int, RoomGfxInfo > CollectRoomGfx(const Rom &rom)
void SelectableLabelWithNameEdit(bool selected, const std::string &type, const std::string &key, const std::string &defaultValue)
Definition project.cc:2915
std::array< std::array< uint8_t, 4 >, kNumSpritesets > spriteset_ids
Definition game_data.h:99
std::array< std::array< uint8_t, 4 >, kNumRoomBlocksets > room_blockset_ids
Definition game_data.h:98
gfx::PaletteGroupMap palette_groups
Definition game_data.h:95
std::array< std::array< uint8_t, 8 >, kNumMainBlocksets > main_blockset_ids
Definition game_data.h:97