11#include "absl/cleanup/cleanup.h"
12#include "absl/strings/str_format.h"
44using ImGui::BeginTable;
47using ImGui::Selectable;
48using ImGui::Separator;
49using ImGui::TableHeadersRow;
50using ImGui::TableNextColumn;
51using ImGui::TableNextRow;
52using ImGui::TableSetupColumn;
57void CopyStringToBuffer(
const std::string& src,
char (&dest)[N]) {
58 std::strncpy(dest, src.c_str(), N - 1);
68 long value = std::strtol(text.c_str(), &end, 0);
69 if (end == text.c_str() || errno == ERANGE) {
72 return static_cast<int>(value);
84 std::make_unique<VanillaSpriteEditorPanel>([
this]() {
86 DrawVanillaSpriteEditor();
88 ImGui::TextDisabled(tr(
"Load a ROM to view vanilla sprites"));
92 window_manager->RegisterWindowContent(
93 std::make_unique<CustomSpriteEditorPanel>(
95 window_manager->RegisterWindowContent(
117 return absl::OkStatus();
126 float current_time = ImGui::GetTime();
145 if (ImGui::IsKeyPressed(ImGuiKey_Space,
false) &&
146 !ImGui::GetIO().WantTextInput) {
151 if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket,
false)) {
157 if (ImGui::IsKeyPressed(ImGuiKey_RightBracket,
false)) {
163 if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_UpArrow,
false)) {
168 if (ImGui::GetIO().KeyCtrl &&
169 ImGui::IsKeyPressed(ImGuiKey_DownArrow,
false)) {
179 return absl::FailedPreconditionError(
180 "Autosave paused: untitled sprite assets are retained; use Sprite "
181 "Save As to choose their destinations");
192 absl::Cleanup restore = [
this, selected, frame] {
198 for (
const int index : pending) {
209 return absl::CancelledError(
"Sprite Save As was cancelled");
211 return absl::OkStatus();
213 return absl::OkStatus();
225 if (sprite_id < 0 || sprite_id >= 256)
243 if (ImGui::BeginTable(
"##SpriteCanvasTable", 3, ImGuiTableFlags_Resizable,
245 TableSetupColumn(
"Sprites List", ImGuiTableColumnFlags_WidthFixed, 256);
246 TableSetupColumn(
"Canvas", ImGuiTableColumnFlags_WidthStretch,
247 ImGui::GetContentRegionAvail().x);
248 TableSetupColumn(
"Tile Selector", ImGuiTableColumnFlags_WidthFixed, 256);
257 ImGui::TextWrapped(
"Select a sprite from the list to open its preview.");
268 if (sprite_id < 0 || sprite_id >= 256) {
274 absl::StrFormat(
"%02X %s###SpriteTab%02X", sprite_id,
277 ? ImGuiTabItemFlags_SetSelected
278 : ImGuiTabItemFlags_None;
279 const bool selected = ImGui::BeginTabItem(label.c_str(), &open, flags);
281 absl::StrFormat(
"SpriteTab%02X", sprite_id));
310 if (ImGui::BeginChild(
gui::GetID(
"##SpriteCanvas"),
311 ImGui::GetContentRegionAvail(),
true)) {
317 ImGui::SetCursorPos(ImVec2(10, 10));
321 "No static OAM layout for sprite $%02X. Open or import a ZSM in "
322 "Custom Sprites to preview authored frames.",
324 }
else if (layout->graphics_resource) {
326 "%s ($%02X) requires external graphics (%s). This editor does not "
327 "yet preview that graphics source.",
328 layout->name, layout->sprite_id, layout->graphics_resource);
335 ImGui::TextWrapped(
"Preview unavailable: %s",
336 std::string(error.message()).c_str());
340 Text(tr(
"Sprite: %s (0x%02X)"), layout->name, layout->sprite_id);
341 Text(tr(
"Tiles: %zu"), layout->tiles.size());
344 [](uint8_t pixel) { return (pixel & 0x0F) != 0; })) {
346 "The selected sheets contain no visible pixels for this layout. "
347 "Choose the sprite's spriteset or graphics sheets.");
351 "Uses the selected sheets and palette. Animation, behavior, and "
352 "runtime graphics changes are not simulated.");
355 if (ImGui::Button(layout ? tr(
"Edit preview copy")
356 : tr(
"Create empty sprite template")))
360 ?
"Creates a detached static pose. Bind any external graphics "
361 "in Custom Sprites > Graphics sources."
362 :
"This ID has no decoded static layout. The template starts "
363 "with an empty frame for authoring; game OAM is not imported.");
372 bool use_asset_binding) {
373 if (
game_data() ==
nullptr || spriteset < 0 ||
382 std::copy(slots.begin(), slots.end(),
388 std::copy(slots.begin(), slots.end(), binding.sheets.begin());
397 current_frame_ >=
static_cast<int>(sprite->editor.Frames.size())) {
398 return absl::FailedPreconditionError(
"Select a sprite frame");
401 return absl::FailedPreconditionError(
402 "Load a ROM before editing sprite pixels");
406 std::any_of(binding->graphics_sources.begin(),
407 binding->graphics_sources.end(),
408 [](
const auto& source) { return source.kind !=
"sheet"; }))
409 return absl::FailedPreconditionError(
410 "This asset uses BIN or raw ROM graphics. Edit BIN pixels in Graphics "
411 "Scratch, export the BIN, then Reload graphics sources here. Raw ROM "
412 "bindings are read-only preview ranges.");
420 std::array<uint8_t, 8> slots;
424 const std::string source =
426 ? absl::StrFormat(
"room %03X CGRAM rows 8-15; sprite slots %02X",
430 "sprite asset binding; preview spriteset %02X",
432 : absl::StrFormat(
"preview spriteset %02X",
436 sprite->sprName, source);
440 context->render_preview = [frame, slots, palettes, owner]() {
446 return absl::FailedPreconditionError(
"Editor manager unavailable");
450 context->return_to_source = [
this, manager, sprite_index, frame_index,
452 if (sprite_index >= 0 &&
466 auto* editor_set = manager->GetCurrentEditorSet();
467 if (!editor_set || !editor_set->GetGraphicsEditor()) {
468 return absl::FailedPreconditionError(
"Graphics editor unavailable");
473 status = editor_set->GetGraphicsEditor()->OpenInContext(std::move(*
context));
477 return absl::OkStatus();
481 if (!ImGui::CollapsingHeader(tr(
"Spriteset preview"),
482 ImGuiTreeNodeFlags_DefaultOpen)) {
486 ImGui::TextDisabled(tr(
"Load a ROM to preview against spritesets."));
505 "Slots 0-3 take the static sprite sheets, slots 4-7 this spriteset's "
506 "values + 0x73 (OAM tiles 0x100-0x1FF).");
512 "Values %02X %02X %02X %02X -> sheets %02X %02X %02X %02X", values[0],
513 values[1], values[2], values[3], slots[4], slots[5], slots[6], slots[7]);
517 auto hex_list = [](
const std::vector<int>& ids) {
519 return std::string(
"-");
522 for (
size_t i = 0; i < ids.size() && i < 24; ++i) {
523 out += absl::StrFormat(
"%s%02X", i == 0 ?
"" :
" ", ids[i]);
525 if (ids.size() > 24) {
526 out += absl::StrFormat(
" (+%zu)", ids.size() - 24);
530 for (
int state = 0; state < 3; ++state) {
531 ImGui::TextWrapped(
"OW areas, state %d: %s", state,
532 hex_list(usage.ow_areas_by_state[state]).c_str());
534 ImGui::TextWrapped(
"Rooms: %s", hex_list(usage.rooms).c_str());
538 ImGui::InputScalar(
"Room", ImGuiDataType_U16, &
preview_room_,
nullptr,
539 nullptr,
"%03X", ImGuiInputTextFlags_CharsHexadecimal);
555 HOVER_HINT(
"Use the room's header spriteset (+0x40) and its sprite palette");
556 if (ImGui::Checkbox(tr(
"Room palette (CGRAM rows 8-15)"),
563 "Off: palette rows from the sprite binding (Sprite Asset panel). On: "
564 "the rows the game loads for the room above.");
566 if (use_asset_binding)
574 current_frame_ >=
static_cast<int>(sprite->editor.Frames.size())) {
579 const auto before = tiles.size();
581 std::remove_if(tiles.begin(), tiles.end(),
583 const int slot = zelda3::SpriteSlotForTile(tile);
584 return slot >= 0 && slot < 8 &&
585 binding->graphics_sources[slot].kind !=
"sheet";
588 if (tiles.size() != before)
590 "%zu tiles use BIN/raw ROM sources; ROM sheet usage checks apply "
591 "only to the remaining tiles.",
592 before - tiles.size());
596 std::array<uint8_t, 8> slots;
601 for (uint8_t sheet : slots) {
602 key += absl::StrFormat(
"%02X", sheet);
604 for (
int tile : tiles) {
605 key += absl::StrFormat(
",%X", tile);
607 for (uint16_t sheet : options.reserved_sheets) {
608 key += absl::StrFormat(
"r%X", sheet);
625 " Frame %d: %zu of %zu 8x8 tiles need "
628 for (
size_t i = 0; i <
tile_issues_.size() && i < 8; ++i) {
635 :
"sheet unreadable");
636 ImGui::BulletText(
"tile 0x%03X, slot %d (sheet 0x%02X): %s", issue.tile,
637 issue.slot, issue.sheet, what);
640 ImGui::TextDisabled(
"... %zu more",
tile_issues_.size() - 8);
645 if (ImGui::BeginChild(
gui::GetID(
"sheet_label"),
646 ImVec2(ImGui::GetContentRegionAvail().x, 0),
true,
647 ImGuiWindowFlags_NoDecoration)) {
651 bool sheets_changed =
false;
655 for (
int i = 0; i < 8; i++) {
656 std::string sheet_label = absl::StrFormat(
"Sheet %d", i);
658 sheets_changed =
true;
665 if (sheets_changed) {
680 for (
int i = 0; i < 8; i++) {
681 const auto* binding =
683 if (binding && binding->graphics_sources[i].kind !=
"sheet") {
686 const std::vector<uint8_t> pixels(
689 const bool create = !sheet.is_active();
690 bool palette_changed =
false;
693 palette_changed = sheet.palette().
size() != palette.size();
694 if (!palette_changed)
695 for (
size_t color = 0; color < palette.size(); ++color)
697 sheet.palette()[color].snes() != palette[color].snes();
699 if (create || sheet.vector() != pixels || palette_changed) {
701 sheet.Create(128, 32, 8, pixels);
703 sheet.set_data(pixels);
717 if (sheet.is_active() && !sheet.texture())
718 sheet.CreateTexture();
728 if (ImGui::BeginChild(
gui::GetID(
"##SpritesList"),
729 ImVec2(ImGui::GetContentRegionAvail().x, 0),
true,
730 ImGuiWindowFlags_NoDecoration)) {
736 if (ImGui::IsItemClicked()) {
750 if (BeginTable(
"##CustomSpritesTable", 3,
751 ImGuiTableFlags_Resizable | ImGuiTableFlags_Borders,
753 TableSetupColumn(
"Sprite Data", ImGuiTableColumnFlags_WidthFixed, 300);
754 TableSetupColumn(
"Canvas", ImGuiTableColumnFlags_WidthStretch);
755 TableSetupColumn(
"Tilesheets", ImGuiTableColumnFlags_WidthFixed, 280);
762 if (ImGui::BeginChild(
"##SpriteMetadata", ImVec2(0, 0)))
786 if (!file_path.empty()) {
794 if (zsm_path.empty()) {
809 Text(tr(
"Loaded Sprites:"));
810 const float avail_y = std::max(0.0f, ImGui::GetContentRegionAvail().y);
811 const float metadata_reserve = std::min(160.0f, avail_y * 0.45f);
812 const float list_budget = std::max(0.0f, avail_y - metadata_reserve);
813 const float preferred_list = std::clamp(avail_y * 0.35f, 180.0f, 280.0f);
814 const float list_height = std::min(preferred_list, list_budget);
815 if (ImGui::BeginChild(
"SpriteList", ImVec2(0, list_height),
true)) {
820 ImGui::PushID(
static_cast<int>(i));
847 if (ImGui::BeginTabItem(tr(
"Properties"))) {
851 if (ImGui::BeginTabItem(tr(
"Animations"))) {
855 if (ImGui::BeginTabItem(tr(
"Behavior"))) {
859 if (ImGui::BeginTabItem(tr(
"Routines"))) {
866 Text(tr(
"No sprite selected"));
874 return absl::FailedPreconditionError(
875 "Select a custom sprite frame to copy");
878 if (current_frame_ < 0 || current_frame_ >=
static_cast<int>(frames.size()))
879 return absl::FailedPreconditionError(
"Select a frame to copy");
881 return absl::OkStatus();
887 if (sprite.editor.Frames.size() <= 1)
888 return absl::FailedPreconditionError(
889 "Cannot cut the last frame; copied it instead");
893 static_cast<int>(sprite.editor.Frames.size()) - 1);
897 return absl::OkStatus();
903 return absl::FailedPreconditionError(
904 "Copy a frame and select a destination sprite");
907 return absl::OutOfRangeError(
"ZSM animations address at most 256 frames");
910 current_frame_ =
static_cast<int>(sprite.editor.Frames.size()) - 1;
914 return absl::OkStatus();
921 new_sprite.
sprName =
"New Sprite";
927 new_sprite.
animations.emplace_back(0, 0, 1,
"Idle");
988 binding.zsm_path = path;
992 std::find_if(assets.begin(), assets.end(), [&](
const auto& asset) {
993 return project()->GetAbsolutePath(asset.zsm_path) == path;
995 if (found == assets.end())
996 assets.push_back(binding);
1011 if (!path.empty()) {
1025 Text(tr(
"Sprite Info"));
1028 static char name_buf[256];
1029 CopyStringToBuffer(sprite.sprName, name_buf);
1030 if (ImGui::InputText(tr(
"Name"), name_buf,
sizeof(name_buf))) {
1031 sprite.sprName = name_buf;
1032 sprite.property_sprname.Text = name_buf;
1036 static char id_buf[32];
1037 CopyStringToBuffer(sprite.property_sprid.Text, id_buf);
1038 if (ImGui::InputText(tr(
"Sprite ID"), id_buf,
sizeof(id_buf))) {
1039 sprite.property_sprid.Text = id_buf;
1056 int prize = ParseIntOrDefault(sprite.property_prize.Text);
1057 if (ImGui::InputInt(tr(
"Prize"), &prize)) {
1058 sprite.property_prize.Text = std::to_string(std::clamp(prize, 0, 255));
1062 int palette = ParseIntOrDefault(sprite.property_palette.Text);
1063 if (ImGui::InputInt(tr(
"Palette"), &palette)) {
1064 sprite.property_palette.Text = std::to_string(std::clamp(palette, 0, 7));
1068 int oamnbr = ParseIntOrDefault(sprite.property_oamnbr.Text);
1069 if (ImGui::InputInt(tr(
"OAM Count"), &oamnbr)) {
1070 sprite.property_oamnbr.Text = std::to_string(std::clamp(oamnbr, 0, 255));
1074 int hitbox = ParseIntOrDefault(sprite.property_hitbox.Text);
1075 if (ImGui::InputInt(tr(
"Hitbox"), &hitbox)) {
1076 sprite.property_hitbox.Text = std::to_string(std::clamp(hitbox, 0, 255));
1080 int health = ParseIntOrDefault(sprite.property_health.Text);
1081 if (ImGui::InputInt(tr(
"Health"), &health)) {
1082 sprite.property_health.Text = std::to_string(std::clamp(health, 0, 255));
1086 int damage = ParseIntOrDefault(sprite.property_damage.Text);
1087 if (ImGui::InputInt(tr(
"Damage"), &damage)) {
1088 sprite.property_damage.Text = std::to_string(std::clamp(damage, 0, 255));
1096 Text(tr(
"Behavior Flags"));
1099 if (ImGui::BeginTable(
"BoolProps", 2, ImGuiTableFlags_None)) {
1101 ImGui::TableNextColumn();
1102 if (ImGui::Checkbox(tr(
"Blockable"), &sprite.property_blockable.IsChecked))
1104 if (ImGui::Checkbox(tr(
"Can Fall"), &sprite.property_canfall.IsChecked))
1106 if (ImGui::Checkbox(tr(
"Collision Layer"),
1107 &sprite.property_collisionlayer.IsChecked))
1109 if (ImGui::Checkbox(tr(
"Custom Death"),
1110 &sprite.property_customdeath.IsChecked))
1112 if (ImGui::Checkbox(tr(
"Damage Sound"),
1113 &sprite.property_damagesound.IsChecked))
1115 if (ImGui::Checkbox(tr(
"Deflect Arrows"),
1116 &sprite.property_deflectarrows.IsChecked))
1118 if (ImGui::Checkbox(tr(
"Deflect Projectiles"),
1119 &sprite.property_deflectprojectiles.IsChecked))
1121 if (ImGui::Checkbox(tr(
"Fast"), &sprite.property_fast.IsChecked))
1123 if (ImGui::Checkbox(tr(
"Harmless"), &sprite.property_harmless.IsChecked))
1125 if (ImGui::Checkbox(tr(
"Impervious"),
1126 &sprite.property_impervious.IsChecked))
1130 ImGui::TableNextColumn();
1131 if (ImGui::Checkbox(tr(
"Impervious Arrow"),
1132 &sprite.property_imperviousarrow.IsChecked))
1134 if (ImGui::Checkbox(tr(
"Impervious Melee"),
1135 &sprite.property_imperviousmelee.IsChecked))
1137 if (ImGui::Checkbox(tr(
"Interaction"),
1138 &sprite.property_interaction.IsChecked))
1140 if (ImGui::Checkbox(tr(
"Is Boss"), &sprite.property_isboss.IsChecked))
1142 if (ImGui::Checkbox(tr(
"Persist"), &sprite.property_persist.IsChecked))
1144 if (ImGui::Checkbox(tr(
"Shadow"), &sprite.property_shadow.IsChecked))
1146 if (ImGui::Checkbox(tr(
"Small Shadow"),
1147 &sprite.property_smallshadow.IsChecked))
1149 if (ImGui::Checkbox(tr(
"Stasis"), &sprite.property_statis.IsChecked))
1151 if (ImGui::Checkbox(tr(
"Statue"), &sprite.property_statue.IsChecked))
1153 if (ImGui::Checkbox(tr(
"Water Sprite"),
1154 &sprite.property_watersprite.IsChecked))
1168 if (ImGui::Button(tr(
"Copy draw tables (ASM)"))) {
1172 ImGui::SetClipboardText(tables->c_str());
1175 "Copy a draw-table candidate. Does not register or patch a sprite.");
1206 (
int)sprite.editor.Frames.size() - 1);
1211 Text(tr(
"Animations"));
1212 if (ImGui::Button(
ICON_MD_ADD " Add Animation")) {
1213 int frame_count =
static_cast<int>(sprite.editor.Frames.size());
1214 sprite.animations.emplace_back(0, std::clamp(frame_count - 1, 0, 255), 1,
1220 if (ImGui::BeginChild(
"AnimList", ImVec2(0, 120),
true)) {
1221 for (
size_t i = 0; i < sprite.animations.size(); i++) {
1222 auto& anim = sprite.animations[i];
1223 std::string label = anim.frame_name.empty() ?
"Unnamed" : anim.frame_name;
1224 ImGui::PushID(
static_cast<int>(i));
1242 Text(tr(
"Animation Properties"));
1244 static char anim_name[128];
1245 CopyStringToBuffer(anim.frame_name, anim_name);
1246 if (ImGui::InputText(tr(
"Name##Anim"), anim_name,
sizeof(anim_name))) {
1247 anim.frame_name = anim_name;
1251 int start = anim.frame_start;
1252 int end = anim.frame_end;
1253 int speed = anim.frame_speed;
1255 if (ImGui::SliderInt(
1256 tr(
"Start Frame"), &start, 0,
1257 std::clamp((
int)sprite.editor.Frames.size() - 1, 0, 255))) {
1258 anim.frame_start =
static_cast<uint8_t
>(start);
1259 anim.frame_end = std::max(anim.frame_start, anim.frame_end);
1260 end = anim.frame_end;
1263 if (ImGui::SliderInt(
1264 tr(
"End Frame"), &end, 0,
1265 std::clamp((
int)sprite.editor.Frames.size() - 1, 0, 255))) {
1266 anim.frame_end =
static_cast<uint8_t
>(end);
1267 anim.frame_start = std::min(anim.frame_start, anim.frame_end);
1270 if (ImGui::SliderInt(tr(
"Ticks per frame (60 Hz)"), &speed, 1, 255)) {
1271 anim.frame_speed =
static_cast<uint8_t
>(speed);
1277 bool used_by_action =
1278 std::any_of(actions.begin(), actions.end(), [&](
const auto& action) {
1279 return action.animation == current_animation_index_;
1281 ImGui::BeginDisabled(used_by_action);
1282 if (ImGui::Button(tr(
"Delete Animation")) && sprite.animations.size() > 1) {
1283 for (
auto& action : actions)
1286 sprite.animations.erase(sprite.animations.begin() +
1292 ImGui::EndDisabled();
1294 ImGui::TextDisabled(
1295 "Assign another animation to referencing actions before deleting.");
1309 current_frame_ =
static_cast<int>(sprite.editor.Frames.size()) - 1;
1326 HOVER_HINT(
"Delete the current frame and repair animation ranges");
1327 if (ImGui::Button(tr(
"Duplicate Frame")) &&
current_frame_ >= 0 &&
1329 current_frame_ =
static_cast<int>(sprite.editor.Frames.size()) - 1;
1333 ImGui::TextDisabled(
"Up to 256 frames; duplicate appends a frame.");
1336 if (ImGui::BeginChild(
"FrameList", ImVec2(0, 80),
true,
1337 ImGuiWindowFlags_HorizontalScrollbar)) {
1338 for (
size_t i = 0; i < sprite.editor.Frames.size(); i++) {
1339 ImGui::PushID(
static_cast<int>(i));
1340 std::string label = absl::StrFormat(
"F%d", i);
1342 ImGuiSelectableFlags_None, ImVec2(40, 40))) {
1361 frame.Tiles.emplace_back();
1370 if (ImGui::BeginChild(
"TileList", ImVec2(0, 100),
true)) {
1371 for (
size_t i = 0; i < frame.Tiles.size(); i++) {
1372 auto& tile = frame.Tiles[i];
1373 std::string label = absl::StrFormat(
"Tile %d (ID: %d)", i, tile.id);
1386 int tile_id = tile.id;
1387 if (ImGui::InputInt(tr(
"Tile ID"), &tile_id)) {
1388 tile.id =
static_cast<uint16_t
>(std::clamp(tile_id, 0, 511));
1395 if (ImGui::InputInt(tr(
"X offset"), &x)) {
1396 tile.x =
static_cast<uint8_t
>(std::clamp(x, -128, 127) +
1401 if (ImGui::InputInt(tr(
"Y offset"), &y)) {
1402 tile.y =
static_cast<uint8_t
>(std::clamp(y, -112, 143) +
1408 int pal = tile.palette;
1409 if (ImGui::SliderInt(tr(
"Palette##Tile"), &pal, 0, 7)) {
1410 tile.palette =
static_cast<uint8_t
>(pal);
1415 int priority = tile.priority;
1416 if (ImGui::SliderInt(tr(
"BG priority"), &priority, 0, 3)) {
1417 tile.priority =
static_cast<uint8_t
>(priority);
1420 ImGui::TextDisabled(
1421 "BG priority is stored; preview has no background layers.");
1422 if (ImGui::Checkbox(tr(
"16x16"), &tile.size)) {
1427 if (ImGui::Checkbox(tr(
"Flip X"), &tile.mirror_x)) {
1432 if (ImGui::Checkbox(tr(
"Flip Y"), &tile.mirror_y)) {
1437 if (ImGui::Button(tr(
"Edit selected OAM pixels"))) {
1445 if (ImGui::Button(tr(
"Duplicate Tile"))) {
1446 const auto copy = tile;
1447 frame.Tiles.push_back(copy);
1451 if (ImGui::Button(tr(
"Delete Tile"))) {
1490 sprite.userRoutines.emplace_back(
"New Routine",
"; ASM code here\n");
1496 if (ImGui::BeginChild(
"RoutineList", ImVec2(0, 100),
true)) {
1497 for (
size_t i = 0; i < sprite.userRoutines.size(); i++) {
1498 auto& routine = sprite.userRoutines[i];
1513 static char routine_name[128];
1514 CopyStringToBuffer(routine.name, routine_name);
1515 if (ImGui::InputText(tr(
"Routine Name"), routine_name,
1516 sizeof(routine_name))) {
1517 routine.name = routine_name;
1521 Text(tr(
"ASM Code:"));
1524 static char code_buffer[16384];
1525 CopyStringToBuffer(routine.code, code_buffer);
1526 if (ImGui::InputTextMultiline(
"##RoutineCode", code_buffer,
1527 sizeof(code_buffer), ImVec2(-1, 200))) {
1528 routine.code = code_buffer;
1532 if (ImGui::Button(tr(
"Delete Routine"))) {
1533 sprite.userRoutines.erase(sprite.userRoutines.begin() +
1548 const auto palette_revision =
1559 for (
int slot = 0; slot < 8; ++slot) {
1560 const auto revision =
1569 for (
int slot = 0; slot < 8; ++slot) {
1592 const auto directory =
1594 ? std::filesystem::path(
project()->filepath).parent_path()
1595 : std::filesystem::path{};
1596 for (
int sheet_idx = 0; sheet_idx < 8; sheet_idx++) {
1597 const auto source = binding ? binding->graphics_sources[sheet_idx]
1604 absl::Status(pixels.status().code(),
1605 absl::StrFormat(
"Graphics sheet/source in slot %d: %s",
1606 sheet_idx, pixels.status().message()));
1609 std::copy(pixels->begin(), pixels->end(),
1631 "Load ROM graphics and palettes before previewing sprites");
1647 if (global.empty() && aux1.empty() && aux2.empty() && aux3.empty() &&
1650 "Sprite palette colors are unavailable in this ROM session");
1652 if (
const auto* binding =
1700 origin_x, origin_y);
1710 for (
size_t col_idx = 0; col_idx < 16 && col_idx < sub_pal.size();
1712 combined_palette.
AddColor(sub_pal[col_idx]);
1715 while (combined_palette.
size() < (pal_idx + 1) * 16) {
1741 if (frame_index < 0 || frame_index >= (
int)sprite.editor.Frames.size()) {
1745 auto& frame = sprite.editor.Frames[frame_index];
1776 for (
size_t col_idx = 0; col_idx < 16 && col_idx < sub_pal.size();
1778 combined_palette.
AddColor(sub_pal[col_idx]);
1781 while (combined_palette.
size() < (pal_idx + 1) * 16) {
1803 for (
size_t i = 0; i < frame.Tiles.size(); i++) {
1804 const auto& tile = frame.Tiles[i];
1805 int tile_size = tile.size ? 16 : 8;
1811 int canvas_x = 128 + signed_x;
1812 int canvas_y = 128 + signed_y;
1816 ? ImVec4(0.0f, 1.0f, 0.0f, 0.8f)
1817 : ImVec4(1.0f, 1.0f, 0.0f, 0.3f);
1821 tile_size * 2, color);
1827 if (ImGui::BeginChild(
gui::GetID(
"##ZSpriteCanvas"),
1828 ImGui::GetContentRegionAvail(),
true)) {
1837 ImGui::SetCursorPos(ImVec2(10, 34));
1841 ImGui::TextWrapped(
"Preview unavailable: %s",
1842 std::string(error.message()).c_str());
1845 ImGui::SetCursorPos(ImVec2(10, 10));
1847 "Open a ZSM or import a draw copy to preview its frames.");
1856 ImGui::SetCursorPos(ImVec2(10, 10));
1858 std::snprintf(label,
sizeof(label), tr(
"Frame: %d | Tiles: %d"),
1864 const ImVec2 position = ImGui::GetCursorScreenPos();
1865 const ImVec2 size = ImGui::CalcTextSize(label);
1867 ImGui::GetWindowDrawList()->AddRectFilled(
1868 ImVec2(position.x - 3, position.y - 2),
1869 ImVec2(position.x + size.x + 3, position.y + size.y + 2),
1870 IM_COL32(24, 24, 24, 255), 3.0f);
1871 ImGui::TextColored(ImVec4(1, 1, 1, 1),
"%s", label);
1939 std::move(restore_fn)));
1957 static const std::string kEmptyPath;
project::ResourceLabelManager * resource_label()
The EditorManager controls the main editor window and manages the various editor classes.
EditorContext context() const
UndoManager undo_manager_
zelda3::GameData * game_data() const
EditorDependencies dependencies_
void SetPalettes(const gfx::PaletteGroup *palettes)
Set the palette group for color mapping.
void ClearBitmap(gfx::Bitmap &bitmap)
Clear the bitmap with transparent color.
void DrawFrame(gfx::Bitmap &bitmap, const zsprite::Frame &frame, int origin_x, int origin_y)
Draw all tiles in a ZSM frame.
bool IsReady() const
Check if drawer is ready to render.
void SetGraphicsBuffer(const uint8_t *buffer)
Set the graphics buffer for tile lookup.
std::array< gfx::Bitmap, 8 > external_sheet_previews_
ImVector< int > active_sprites_
absl::Status OpenSpriteAsset(const std::string &path)
int current_custom_sprite_index_
void UpdateAnimationPlayback(float delta_time)
std::vector< project::SpriteAssetBinding > custom_sprite_bindings_
absl::Status ReloadProjectSpriteAssets()
std::vector< zelda3::SpriteTileIssue > tile_issues_
void LoadSpriteGraphicsBuffer(bool use_asset_binding=true)
absl::Status OpenSelectedOamGraphics()
void DrawAnimationPanel()
void EnsureCustomSpritePaths()
void DrawSpriteAssetBindings()
const zsprite::ZSprite * current_custom_sprite() const
std::set< int > dirty_sprite_indices_
void BeginUndoTransaction()
void RefreshSpritePreviewGraphics(bool use_asset_binding)
std::array< uint64_t, 8 > loaded_preview_revisions_
void RenderZSpriteFrame(int frame_index)
absl::Status Paste() override
std::array< uint8_t, 8 > loaded_preview_sheets_
bool preview_use_room_palette_
absl::StatusOr< std::string > ExportCurrentSpriteDraw()
absl::Status SaveForAutosave()
bool preview_needs_update_
void DrawZSpriteOnCanvas()
void LoadSpritePalettes(bool use_asset_binding=true)
std::optional< zsprite::Frame > frame_clipboard_
bool vanilla_preview_needs_update_
absl::Status Update() override
bool preview_uses_asset_graphics_
uint64_t preview_palette_revision_
void DrawSpriteBehaviorPanel()
void HandleEditorShortcuts()
gfx::PaletteGroup sprite_palettes_
void LoadZsmFile(const std::string &path)
bool sprite_mutated_this_frame_
void ApplyCurrentSpriteBinding()
void DrawUserRoutinesPanel()
absl::Status Cut() override
void RenderVanillaSprite(const zelda3::SpriteOamLayout &layout)
absl::Status CreateSpriteAssetFromVanilla(int sprite_id)
absl::Status palette_binding_status_
void DrawCurrentSheets(bool use_asset_binding)
void Initialize() override
void DrawFrameTileWarnings()
void DrawVanillaSpriteEditor()
int selected_routine_index_
std::string behavior_candidate_
uint8_t vanilla_sheets_[8]
std::optional< std::map< int, zelda3::RoomGfxInfo > > usage_rooms_
const std::string & GetCurrentZsmPath() const
uint8_t current_sheets_[8]
SpriteDrawer sprite_drawer_
absl::Status asset_load_status_
gui::Canvas graphics_sheet_canvas_
int room_palette_cache_room_
void RestoreFromSnapshot(const SpriteSnapshot &snapshot)
bool undo_snapshot_pending_
std::vector< zsprite::ZSprite > custom_sprites_
std::vector< uint8_t > sprite_gfx_buffer_
gfx::Bitmap sprite_preview_bitmap_
project::YazeProject * project() const
absl::Status ReloadSpriteCatalog()
int current_animation_index_
void DrawSpritesetPicker(bool use_asset_binding)
const project::SpriteAssetBinding * current_sprite_binding() const
void DrawBooleanProperties()
void DrawSpritePropertiesPanel()
void SetCurrentZsmPath(const std::string &path)
bool sprite_bindings_pending_
absl::Status Copy() override
const zelda3::GameData * preview_graphics_owner_
absl::Status Save() override
std::optional< std::map< int, zelda3::OverworldAreaGfxInfo > > usage_areas_
void DrawCustomSpritesMetadata()
SpriteSnapshot undo_before_snapshot_
void SaveZsmFile(const std::string &path)
gui::Canvas sprite_canvas_
void CommitUndoTransaction()
void ApplySpritesetToSheets(int spriteset, bool use_asset_binding)
SpriteSnapshot CaptureCurrentSpriteSnapshot() const
void DrawStatProperties()
absl::Status graphics_binding_status_
absl::Status graphics_context_status_
std::optional< gfx::PaletteGroup > room_palette_cache_
absl::Status CheckCurrentSpriteSource()
absl::Status Load() override
std::vector< std::string > custom_sprite_paths_
void SelectVanillaSprite(int sprite_id)
std::string tile_check_key_
absl::Status draw_export_status_
gfx::Bitmap vanilla_preview_bitmap_
const zelda3::SpriteOamLayout * CurrentPreviewLayout() const
void Push(std::unique_ptr< UndoAction > action)
void RegisterWindowContent(std::unique_ptr< WindowContent > window)
Register a WindowContent instance for central drawing.
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
GraphicsSheets & gfx_sheets()
const std::vector< uint8_t > & vector() const
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
uint64_t ColorRevision(const zelda3::GameData *game_data) const
static PaletteManager & Get()
Get the singleton instance.
RAII timer for automatic timing management.
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
void AddColor(const SnesColor &color)
void DrawBitmap(Bitmap &bitmap, int border_offset, float scale)
bool DrawTileSelector(int size, int size_y=0)
void DrawRect(int x, int y, int w, int h, ImVec4 color)
CanvasConfig & GetConfig()
void DrawBackground(ImVec2 canvas_size=ImVec2(0, 0), std::optional< ImVec2 > viewport_size=std::nullopt)
void DrawGrid(float grid_step=64.0f, int tile_id_offset=8)
static float GetHexInputWidth()
static std::string ShowSaveFileDialog(const std::string &default_name="", const std::string &default_extension="")
ShowSaveFileDialog opens a save file dialog and returns the selected filepath. Uses global feature fl...
static std::string ShowOpenFileDialog()
ShowOpenFileDialog opens a file dialog and returns the selected filepath. Uses global feature flag to...
uint64_t Revision(uint16_t sheet) const
static const SpriteOamLayout * GetLayout(uint8_t sprite_id)
Get the OAM layout for a sprite ID.
static const SpriteOamLayout * GetPreviewOverride(uint8_t sprite_id, std::string_view hack_name)
#define ICON_MD_MEETING_ROOM
#define ICON_MD_FOLDER_OPEN
#define ICON_MD_PLAY_ARROW
#define ICON_MD_SKIP_NEXT
#define ICON_MD_SKIP_PREVIOUS
#define HOVER_HINT(string)
zelda3::GfxSheetInventoryOptions BuildGfxSheetInventoryOptions(const project::YazeProject *project)
int ParseIntOrDefault(const std::string &text, int fallback=0)
void EnsureSpritePreviewBitmapReady(gfx::Bitmap &bmp, int width, int height, int depth, const std::vector< uint8_t > &gfx_buffer)
void PublishSpritePreviewPixels(gfx::Bitmap &bmp)
std::vector< int > FrameTiles8x8(const zsprite::Frame &frame)
gfx::PaletteGroup SpritePalettesFromCgram(const std::array< SDL_Color, 256 > &cgram)
gfx::PaletteGroup DefaultSpritePreviewPalettes(const gfx::PaletteGroup &global, const gfx::PaletteGroup &aux1, const gfx::PaletteGroup &aux2, const gfx::PaletteGroup &aux3)
int OffsetX(const zsprite::OamTile &tile)
bool Advance(const zsprite::AnimationGroup &animation, size_t frame_count, float delta, int &frame, float &remainder)
absl::StatusOr< gfx::PaletteGroup > BindPaletteRows(const project::SpriteAssetBinding &binding, const gfx::PaletteGroup &defaults, const gfx::PaletteGroup &global, const gfx::PaletteGroup &aux1, const gfx::PaletteGroup &aux2, const gfx::PaletteGroup &aux3)
int OffsetY(const zsprite::OamTile &tile)
constexpr size_t kMaxFrames
bool AppendFrame(zsprite::ZSprite &sprite, int copy_from=-1)
zsprite::Frame CopyVanillaLayout(const zelda3::SpriteOamLayout &layout)
bool DeleteFrame(zsprite::ZSprite &sprite, int index)
absl::StatusOr< GraphicsEditContext > Make(const zsprite::Frame &frame, int frame_index, int oam_index, const std::array< uint8_t, 8 > &slots, const gfx::PaletteGroup &palettes, const zelda3::GameData *owner, const std::string &source_label, const std::string &sheet_source)
absl::StatusOr< usage_preview::PreviewImage > RenderCapturedFrame(const zsprite::Frame &frame, const std::array< uint8_t, 8 > &slots, const gfx::PaletteGroup &palettes, const zelda3::GameData *owner)
absl::StatusOr< std::vector< uint8_t > > Resolve(const project::SpriteGraphicsSource &source, uint16_t sheet, const zelda3::GameData *data, const Rom *rom, const std::filesystem::path &project_directory)
constexpr ImGuiTabItemFlags kSpriteTabFlags
constexpr ImGuiTabBarFlags kSpriteTabBarFlags
ImGuiID GetID(const std::string &id)
void AutoRegisterLastItem(const std::string &widget_type, const std::string &explicit_label, const std::string &description)
Automatically register the last ImGui item.
bool BeginThemedTabBar(const char *id, ImGuiTabBarFlags flags)
A stylized tab bar with "Mission Control" branding.
bool InputHexByte(const char *label, uint8_t *data, float input_width, bool no_step)
std::string HexByte(uint8_t byte, HexStringParams params)
constexpr int kNumDungeonRooms
GfxGroupUsage FindSpritesetUsage(int spriteset, const std::map< int, OverworldAreaGfxInfo > &areas, const std::map< int, RoomGfxInfo > &rooms)
Room LoadRoomHeaderFromRom(Rom *rom, int room_id)
std::map< int, OverworldAreaGfxInfo > CollectOverworldAreaGfx(Rom &rom, GameData *game_data)
constexpr uint16_t kDungeonSpritesetBase
std::map< int, RoomGfxInfo > CollectRoomGfx(const Rom &rom)
const std::string kSpriteDefaultNames[256]
constexpr uint32_t kNumSpritesets
constexpr bool IsUnderworldSpriteset(int spriteset)
Dungeon rooms use spritesets 0x40-0x8F (room header value + 0x40).
std::array< uint8_t, 8 > SpriteSheetSlots(const std::array< uint8_t, 4 > &spriteset_values, bool underworld)
The eight sheets for a spriteset's four values.
std::array< SDL_Color, 256 > BuildDungeonSpriteRenderPalette(const Room &room, const GameData *game_data)
std::vector< SpriteTileIssue > CheckSpriteTiles(const Rom &rom, const std::vector< int > &tiles, const std::array< uint8_t, 8 > &slots, const std::set< uint16_t > &reserved_sheets)
#define RETURN_IF_ERROR(expr)
WorkspaceWindowManager * window_manager
Snapshot of a custom ZSprite's editable state for undo/redo.
project::SpriteAssetBinding binding
zsprite::ZSprite sprite_data
int current_animation_index
std::vector< Frame > Frames
void Reset()
Reset all sprite data to defaults.
absl::Status Load(const std::string &filename)
Load a ZSM file from disk.
std::vector< AnimationGroup > animations
PaletteGroup sprites_aux1
PaletteGroup sprites_aux2
PaletteGroup sprites_aux3
PaletteGroup global_sprites
auto palette(int i) const
const SnesPalette & palette_ref(int i) const
void SelectableLabelWithNameEdit(bool selected, const std::string &type, const std::string &key, const std::string &defaultValue)
std::array< uint8_t, 8 > sheets
std::vector< SpriteAssetBinding > sprite_assets
std::string GetAbsolutePath(const std::string &relative_path) const
std::array< std::array< uint8_t, 4 >, kNumSpritesets > spriteset_ids
gfx::PaletteGroupMap palette_groups
GraphicsSheetStore sheet_store
Complete OAM layout for a vanilla sprite.