9#include "absl/strings/str_format.h"
29using ImGui::BeginTable;
32using ImGui::Selectable;
33using ImGui::Separator;
34using ImGui::TableHeadersRow;
35using ImGui::TableNextColumn;
36using ImGui::TableNextRow;
37using ImGui::TableSetupColumn;
42void CopyStringToBuffer(
const std::string& src,
char (&dest)[N]) {
43 std::strncpy(dest, src.c_str(), N - 1);
53 long value = std::strtol(text.c_str(), &end, 0);
54 if (end == text.c_str() || errno == ERANGE) {
57 return static_cast<int>(value);
69 std::make_unique<VanillaSpriteEditorPanel>([
this]() {
71 DrawVanillaSpriteEditor();
73 ImGui::TextDisabled(tr(
"Load a ROM to view vanilla sprites"));
77 window_manager->RegisterWindowContent(
78 std::make_unique<CustomSpriteEditorPanel>(
84 return absl::OkStatus();
93 float current_time = ImGui::GetTime();
112 if (ImGui::IsKeyPressed(ImGuiKey_Space,
false) &&
113 !ImGui::GetIO().WantTextInput) {
118 if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket,
false)) {
124 if (ImGui::IsKeyPressed(ImGuiKey_RightBracket,
false)) {
130 if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_UpArrow,
false)) {
136 if (ImGui::GetIO().KeyCtrl &&
137 ImGui::IsKeyPressed(ImGuiKey_DownArrow,
false)) {
147 if (zsm_path.empty()) {
153 return absl::OkStatus();
165 if (ImGui::BeginTable(
"##SpriteCanvasTable", 3, ImGuiTableFlags_Resizable,
167 TableSetupColumn(
"Sprites List", ImGuiTableColumnFlags_WidthFixed, 256);
168 TableSetupColumn(
"Canvas", ImGuiTableColumnFlags_WidthStretch,
169 ImGui::GetContentRegionAvail().x);
170 TableSetupColumn(
"Tile Selector", ImGuiTableColumnFlags_WidthFixed, 256);
178 static int next_tab_id = 0;
197 if (ImGui::BeginTabItem(
199 ImGuiTabItemFlags_None)) {
222 static bool flip_x =
false;
223 static bool flip_y =
false;
224 if (ImGui::BeginChild(
gui::GetID(
"##SpriteCanvas"),
225 ImGui::GetContentRegionAvail(),
true)) {
244 ImGui::SetCursorPos(ImVec2(10, 10));
245 Text(tr(
"Sprite: %s (0x%02X)"), layout->name, layout->sprite_id);
246 Text(tr(
"Tiles: %zu"), layout->tiles.size());
253 if (ImGui::BeginTable(
"##OAMTable", 7, ImGuiTableFlags_Resizable,
255 TableSetupColumn(
"X", ImGuiTableColumnFlags_WidthStretch);
256 TableSetupColumn(
"Y", ImGuiTableColumnFlags_WidthStretch);
257 TableSetupColumn(
"Tile", ImGuiTableColumnFlags_WidthStretch);
258 TableSetupColumn(
"Palette", ImGuiTableColumnFlags_WidthStretch);
259 TableSetupColumn(
"Priority", ImGuiTableColumnFlags_WidthStretch);
260 TableSetupColumn(
"Flip X", ImGuiTableColumnFlags_WidthStretch);
261 TableSetupColumn(
"Flip Y", ImGuiTableColumnFlags_WidthStretch);
281 if (ImGui::Checkbox(
"##XFlip", &flip_x)) {
286 if (ImGui::Checkbox(
"##YFlip", &flip_y)) {
299 if (ImGui::BeginChild(
gui::GetID(
"sheet_label"),
300 ImVec2(ImGui::GetContentRegionAvail().x, 0),
true,
301 ImGuiWindowFlags_NoDecoration)) {
303 static uint8_t prev_sheets[8] = {0};
304 bool sheets_changed =
false;
306 for (
int i = 0; i < 8; i++) {
307 std::string sheet_label = absl::StrFormat(
"Sheet %d", i);
309 sheets_changed =
true;
316 if (sheets_changed || std::memcmp(prev_sheets,
current_sheets_, 8) != 0) {
326 for (
int i = 0; i < 8; i++) {
338 if (ImGui::BeginChild(
gui::GetID(
"##SpritesList"),
339 ImVec2(ImGui::GetContentRegionAvail().x, 0),
true,
340 ImGuiWindowFlags_NoDecoration)) {
346 if (ImGui::IsItemClicked()) {
362 if (ImGui::Button(tr(
"Add Frame"))) {
365 if (ImGui::Button(tr(
"Remove Frame"))) {
375 if (BeginTable(
"##CustomSpritesTable", 3,
376 ImGuiTableFlags_Resizable | ImGuiTableFlags_Borders,
378 TableSetupColumn(
"Sprite Data", ImGuiTableColumnFlags_WidthFixed, 300);
379 TableSetupColumn(
"Canvas", ImGuiTableColumnFlags_WidthStretch);
380 TableSetupColumn(
"Tilesheets", ImGuiTableColumnFlags_WidthFixed, 280);
414 if (!file_path.empty()) {
422 if (zsm_path.empty()) {
437 Text(tr(
"Loaded Sprites:"));
438 if (ImGui::BeginChild(
"SpriteList", ImVec2(0, 100),
true)) {
464 if (ImGui::BeginTabItem(tr(
"Properties"))) {
468 if (ImGui::BeginTabItem(tr(
"Animations"))) {
472 if (ImGui::BeginTabItem(tr(
"Routines"))) {
479 Text(tr(
"No sprite selected"));
486 new_sprite.
sprName =
"New Sprite";
492 new_sprite.
animations.emplace_back(0, 0, 1,
"Idle");
555 Text(tr(
"Sprite Info"));
558 static char name_buf[256];
559 CopyStringToBuffer(sprite.sprName, name_buf);
560 if (ImGui::InputText(tr(
"Name"), name_buf,
sizeof(name_buf))) {
561 sprite.sprName = name_buf;
562 sprite.property_sprname.Text = name_buf;
566 static char id_buf[32];
567 CopyStringToBuffer(sprite.property_sprid.Text, id_buf);
568 if (ImGui::InputText(tr(
"Sprite ID"), id_buf,
sizeof(id_buf))) {
569 sprite.property_sprid.Text = id_buf;
586 int prize = ParseIntOrDefault(sprite.property_prize.Text);
587 if (ImGui::InputInt(tr(
"Prize"), &prize)) {
588 sprite.property_prize.Text = std::to_string(std::clamp(prize, 0, 255));
592 int palette = ParseIntOrDefault(sprite.property_palette.Text);
593 if (ImGui::InputInt(tr(
"Palette"), &palette)) {
594 sprite.property_palette.Text = std::to_string(std::clamp(palette, 0, 7));
598 int oamnbr = ParseIntOrDefault(sprite.property_oamnbr.Text);
599 if (ImGui::InputInt(tr(
"OAM Count"), &oamnbr)) {
600 sprite.property_oamnbr.Text = std::to_string(std::clamp(oamnbr, 0, 255));
604 int hitbox = ParseIntOrDefault(sprite.property_hitbox.Text);
605 if (ImGui::InputInt(tr(
"Hitbox"), &hitbox)) {
606 sprite.property_hitbox.Text = std::to_string(std::clamp(hitbox, 0, 255));
610 int health = ParseIntOrDefault(sprite.property_health.Text);
611 if (ImGui::InputInt(tr(
"Health"), &health)) {
612 sprite.property_health.Text = std::to_string(std::clamp(health, 0, 255));
616 int damage = ParseIntOrDefault(sprite.property_damage.Text);
617 if (ImGui::InputInt(tr(
"Damage"), &damage)) {
618 sprite.property_damage.Text = std::to_string(std::clamp(damage, 0, 255));
626 Text(tr(
"Behavior Flags"));
629 if (ImGui::BeginTable(
"BoolProps", 2, ImGuiTableFlags_None)) {
631 ImGui::TableNextColumn();
632 if (ImGui::Checkbox(tr(
"Blockable"), &sprite.property_blockable.IsChecked))
634 if (ImGui::Checkbox(tr(
"Can Fall"), &sprite.property_canfall.IsChecked))
636 if (ImGui::Checkbox(tr(
"Collision Layer"),
637 &sprite.property_collisionlayer.IsChecked))
639 if (ImGui::Checkbox(tr(
"Custom Death"),
640 &sprite.property_customdeath.IsChecked))
642 if (ImGui::Checkbox(tr(
"Damage Sound"),
643 &sprite.property_damagesound.IsChecked))
645 if (ImGui::Checkbox(tr(
"Deflect Arrows"),
646 &sprite.property_deflectarrows.IsChecked))
648 if (ImGui::Checkbox(tr(
"Deflect Projectiles"),
649 &sprite.property_deflectprojectiles.IsChecked))
651 if (ImGui::Checkbox(tr(
"Fast"), &sprite.property_fast.IsChecked))
653 if (ImGui::Checkbox(tr(
"Harmless"), &sprite.property_harmless.IsChecked))
655 if (ImGui::Checkbox(tr(
"Impervious"),
656 &sprite.property_impervious.IsChecked))
660 ImGui::TableNextColumn();
661 if (ImGui::Checkbox(tr(
"Impervious Arrow"),
662 &sprite.property_imperviousarrow.IsChecked))
664 if (ImGui::Checkbox(tr(
"Impervious Melee"),
665 &sprite.property_imperviousmelee.IsChecked))
667 if (ImGui::Checkbox(tr(
"Interaction"),
668 &sprite.property_interaction.IsChecked))
670 if (ImGui::Checkbox(tr(
"Is Boss"), &sprite.property_isboss.IsChecked))
672 if (ImGui::Checkbox(tr(
"Persist"), &sprite.property_persist.IsChecked))
674 if (ImGui::Checkbox(tr(
"Shadow"), &sprite.property_shadow.IsChecked))
676 if (ImGui::Checkbox(tr(
"Small Shadow"),
677 &sprite.property_smallshadow.IsChecked))
679 if (ImGui::Checkbox(tr(
"Stasis"), &sprite.property_statis.IsChecked))
681 if (ImGui::Checkbox(tr(
"Statue"), &sprite.property_statue.IsChecked))
683 if (ImGui::Checkbox(tr(
"Water Sprite"),
684 &sprite.property_watersprite.IsChecked))
725 (
int)sprite.editor.Frames.size() - 1);
730 Text(tr(
"Animations"));
732 int frame_count =
static_cast<int>(sprite.editor.Frames.size());
733 sprite.animations.emplace_back(0, frame_count > 0 ? frame_count - 1 : 0, 1,
739 if (ImGui::BeginChild(
"AnimList", ImVec2(0, 120),
true)) {
740 for (
size_t i = 0; i < sprite.animations.size(); i++) {
741 auto& anim = sprite.animations[i];
742 std::string label = anim.frame_name.empty() ?
"Unnamed" : anim.frame_name;
758 Text(tr(
"Animation Properties"));
760 static char anim_name[128];
761 CopyStringToBuffer(anim.frame_name, anim_name);
762 if (ImGui::InputText(tr(
"Name##Anim"), anim_name,
sizeof(anim_name))) {
763 anim.frame_name = anim_name;
767 int start = anim.frame_start;
768 int end = anim.frame_end;
769 int speed = anim.frame_speed;
771 if (ImGui::SliderInt(tr(
"Start Frame"), &start, 0,
772 std::max(0, (
int)sprite.editor.Frames.size() - 1))) {
773 anim.frame_start =
static_cast<uint8_t
>(start);
776 if (ImGui::SliderInt(tr(
"End Frame"), &end, 0,
777 std::max(0, (
int)sprite.editor.Frames.size() - 1))) {
778 anim.frame_end =
static_cast<uint8_t
>(end);
781 if (ImGui::SliderInt(tr(
"Speed"), &speed, 1, 16)) {
782 anim.frame_speed =
static_cast<uint8_t
>(speed);
786 if (ImGui::Button(tr(
"Delete Animation")) && sprite.animations.size() > 1) {
787 sprite.animations.erase(sprite.animations.begin() +
805 sprite.editor.Frames.emplace_back();
812 sprite.editor.Frames.erase(sprite.editor.Frames.begin() +
current_frame_);
821 if (ImGui::BeginChild(
"FrameList", ImVec2(0, 80),
true,
822 ImGuiWindowFlags_HorizontalScrollbar)) {
823 for (
size_t i = 0; i < sprite.editor.Frames.size(); i++) {
824 ImGui::PushID(
static_cast<int>(i));
825 std::string label = absl::StrFormat(
"F%d", i);
827 ImGuiSelectableFlags_None, ImVec2(40, 40))) {
846 frame.Tiles.emplace_back();
852 if (ImGui::BeginChild(
"TileList", ImVec2(0, 100),
true)) {
853 for (
size_t i = 0; i < frame.Tiles.size(); i++) {
854 auto& tile = frame.Tiles[i];
855 std::string label = absl::StrFormat(
"Tile %d (ID: %d)", i, tile.id);
868 int tile_id = tile.id;
869 if (ImGui::InputInt(tr(
"Tile ID"), &tile_id)) {
870 tile.id =
static_cast<uint16_t
>(std::clamp(tile_id, 0, 511));
875 int x = tile.x, y = tile.y;
876 if (ImGui::InputInt(tr(
"X"), &x)) {
877 tile.x =
static_cast<uint8_t
>(std::clamp(x, 0, 251));
881 if (ImGui::InputInt(tr(
"Y"), &y)) {
882 tile.y =
static_cast<uint8_t
>(std::clamp(y, 0, 219));
887 int pal = tile.palette;
888 if (ImGui::SliderInt(tr(
"Palette##Tile"), &pal, 0, 7)) {
889 tile.palette =
static_cast<uint8_t
>(pal);
894 if (ImGui::Checkbox(tr(
"16x16"), &tile.size)) {
899 if (ImGui::Checkbox(tr(
"Flip X"), &tile.mirror_x)) {
904 if (ImGui::Checkbox(tr(
"Flip Y"), &tile.mirror_y)) {
909 if (ImGui::Button(tr(
"Delete Tile"))) {
935 float frame_duration = anim.frame_speed / 60.0f;
955 sprite.userRoutines.emplace_back(
"New Routine",
"; ASM code here\n");
961 if (ImGui::BeginChild(
"RoutineList", ImVec2(0, 100),
true)) {
962 for (
size_t i = 0; i < sprite.userRoutines.size(); i++) {
963 auto& routine = sprite.userRoutines[i];
978 static char routine_name[128];
979 CopyStringToBuffer(routine.name, routine_name);
980 if (ImGui::InputText(tr(
"Routine Name"), routine_name,
981 sizeof(routine_name))) {
982 routine.name = routine_name;
986 Text(tr(
"ASM Code:"));
989 static char code_buffer[16384];
990 CopyStringToBuffer(routine.code, code_buffer);
991 if (ImGui::InputTextMultiline(
"##RoutineCode", code_buffer,
992 sizeof(code_buffer), ImVec2(-1, 200))) {
993 routine.code = code_buffer;
997 if (ImGui::Button(tr(
"Delete Routine"))) {
998 sprite.userRoutines.erase(sprite.userRoutines.begin() +
1020 constexpr int kSheetWidth = 128;
1021 constexpr int kSheetHeight = 32;
1022 constexpr int kRowStride = 128;
1024 for (
int sheet_idx = 0; sheet_idx < 8; sheet_idx++) {
1031 if (!sheet.is_active() || sheet.size() == 0) {
1040 int dest_offset = sheet_idx * (kSheetHeight * kRowStride);
1042 const uint8_t* src_data = sheet.data();
1044 std::min(sheet.size(),
static_cast<size_t>(kSheetWidth * kSheetHeight));
1077 for (
size_t i = 0; i < global.size() && i < 8; i++) {
1094 }
else if (aux3.size() > 0) {
1108 bool changed =
false;
1109 for (
int i = 0; i < 4; i++) {
1147 for (
const auto& entry : layout.
tiles) {
1149 tile.
x =
static_cast<uint8_t
>(entry.x_offset + 128);
1150 tile.
y =
static_cast<uint8_t
>(entry.y_offset + 128);
1151 tile.
id = entry.tile_id;
1153 tile.
size = entry.size_16x16;
1169 for (
size_t col_idx = 0; col_idx < 16 && col_idx < sub_pal.size();
1171 combined_palette.
AddColor(sub_pal[col_idx]);
1174 while (combined_palette.
size() < (pal_idx + 1) * 16) {
1200 if (frame_index < 0 || frame_index >= (
int)sprite.editor.Frames.size()) {
1204 auto& frame = sprite.editor.Frames[frame_index];
1237 for (
size_t col_idx = 0; col_idx < 16 && col_idx < sub_pal.size();
1239 combined_palette.
AddColor(sub_pal[col_idx]);
1242 while (combined_palette.
size() < (pal_idx + 1) * 16) {
1264 for (
size_t i = 0; i < frame.Tiles.size(); i++) {
1265 const auto& tile = frame.Tiles[i];
1266 int tile_size = tile.size ? 16 : 8;
1269 int8_t signed_x =
static_cast<int8_t
>(tile.x);
1270 int8_t signed_y =
static_cast<int8_t
>(tile.y);
1272 int canvas_x = 128 + signed_x;
1273 int canvas_y = 128 + signed_y;
1277 ? ImVec4(0.0f, 1.0f, 0.0f, 0.8f)
1278 : ImVec4(1.0f, 1.0f, 0.0f, 0.3f);
1286 if (ImGui::BeginChild(
gui::GetID(
"##ZSpriteCanvas"),
1287 ImGui::GetContentRegionAvail(),
true)) {
1303 ImGui::SetCursorPos(ImVec2(10, 10));
1371 std::move(restore_fn)));
1386 static const std::string kEmptyPath;
project::ResourceLabelManager * resource_label()
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 DrawOamTile(gfx::Bitmap &bitmap, const zsprite::OamTile &tile, int origin_x, int origin_y)
Draw a single ZSM OAM tile to bitmap.
void SetGraphicsBuffer(const uint8_t *buffer)
Set the graphics buffer for tile lookup.
ImVector< int > active_sprites_
int current_custom_sprite_index_
void UpdateAnimationPlayback(float delta_time)
void DrawAnimationFrames()
void DrawAnimationPanel()
void EnsureCustomSpritePaths()
void BeginUndoTransaction()
void RenderZSpriteFrame(int frame_index)
void LoadSheetsForSprite(const std::array< uint8_t, 4 > &sheets)
bool preview_needs_update_
void DrawZSpriteOnCanvas()
bool vanilla_preview_needs_update_
absl::Status Update() override
void HandleEditorShortcuts()
gfx::PaletteGroup sprite_palettes_
void LoadZsmFile(const std::string &path)
bool sprite_mutated_this_frame_
void DrawUserRoutinesPanel()
void RenderVanillaSprite(const zelda3::SpriteOamLayout &layout)
void Initialize() override
void DrawVanillaSpriteEditor()
int selected_routine_index_
const std::string & GetCurrentZsmPath() const
uint8_t current_sheets_[8]
SpriteDrawer sprite_drawer_
gui::Canvas graphics_sheet_canvas_
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_
int current_animation_index_
void DrawBooleanProperties()
void DrawSpritePropertiesPanel()
void SetCurrentZsmPath(const std::string &path)
absl::Status Save() override
void DrawCustomSpritesMetadata()
SpriteSnapshot undo_before_snapshot_
void SaveZsmFile(const std::string &path)
gui::Canvas sprite_canvas_
void CommitUndoTransaction()
SpriteSnapshot CaptureCurrentSpriteSnapshot() const
void DrawStatProperties()
absl::Status Load() override
void LoadSpriteGraphicsBuffer()
std::vector< std::string > custom_sprite_paths_
void LoadSpritePalettes()
gfx::Bitmap vanilla_preview_bitmap_
void Push(std::unique_ptr< UndoAction > action)
void RegisterWindowContent(std::unique_ptr< WindowContent > window)
Register a WindowContent instance for central drawing.
std::array< gfx::Bitmap, 223 > & gfx_sheets()
Get reference to all graphics sheets.
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
void UpdateTexture()
Updates the underlying SDL_Texture when it already exists.
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))
void DrawGrid(float grid_step=64.0f, int tile_id_offset=8)
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...
static const SpriteOamLayout * GetLayout(uint8_t sprite_id)
Get the OAM layout for a sprite ID.
#define ICON_MD_FOLDER_OPEN
#define ICON_MD_PLAY_ARROW
#define ICON_MD_SKIP_NEXT
#define ICON_MD_SKIP_PREVIOUS
#define HOVER_HINT(string)
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)
constexpr ImGuiTabItemFlags kSpriteTabFlags
constexpr ImGuiTabBarFlags kSpriteTabBarFlags
bool InputHexWord(const char *label, uint16_t *data, float input_width, bool no_step)
bool BeginThemedTabBar(const char *id, ImGuiTabBarFlags flags)
A stylized tab bar with "Mission Control" branding.
ImGuiID GetID(const std::string &id)
bool InputHexByte(const char *label, uint8_t *data, float input_width, bool no_step)
std::string HexByte(uint8_t byte, HexStringParams params)
const std::string kSpriteDefaultNames[256]
WorkspaceWindowManager * window_manager
Snapshot of a custom ZSprite's editable state for undo/redo.
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
void AddPalette(SnesPalette pal)
void SelectableLabelWithNameEdit(bool selected, const std::string &type, const std::string &key, const std::string &defaultValue)
gfx::PaletteGroupMap palette_groups
Complete OAM layout for a vanilla sprite.
std::vector< SpriteOamEntry > tiles