From 4e6697352c27ea29a757c613c7935303e4949d55 Mon Sep 17 00:00:00 2001 From: Ally Date: Sat, 25 Apr 2026 14:46:01 -0300 Subject: [PATCH] Add an undoing & redoing system to the editor (#1085) * Add undoing & redoing to the editor It's very easy to make mistakes in VVVVVV's built-in level editor, with no way to undo them. This commit adds an undo and redo system, bound to CTRL+Z for undo and CTRL+Y for redo. The undo and redo stacks don't have any limits, but could easily happen in the future. VVVVVV's data is small enough where this should never be an issue, however. No notes show up for undoing and redoing, because keeping track of what specific action you're doing may bloat the system, and would get annoying for the end-user. Notes are a bit annoying in general, even. * Add level resizing to undo/redo system * searchable gettext for undo/redo notes * A few fixes and cleanup The main fix was how holding right click to remove tiles did not commit an action. * Spikes also need to commit * Cancelling placing a terminal/script box shouldn't have undo events * Don't commit tile events if removing entities * Clear redo buffer on level load * Remove unneeded pointers from memcpy * Add strings to lang files * Fix definition in Editor.h * Explain double-uncommit * Don't unconditionally commit tiles on room change I'm not that sure what I was thinking back then, but switching rooms would commit tiles no matter what, causing a huge mess. The behavior was intended to stop the "tile placement" action and commit it, but just happened no matter what... So this has been fixed, and a few comments have been added as well. * Fix mentioned issues & make process_undo static --------- Co-authored-by: NyakoFox --- desktop_version/lang/ar/strings.xml | 2 + desktop_version/lang/ca/strings.xml | 2 + desktop_version/lang/cy/strings.xml | 2 + desktop_version/lang/de/strings.xml | 2 + desktop_version/lang/en/strings.xml | 2 + desktop_version/lang/eo/strings.xml | 2 + desktop_version/lang/es/strings.xml | 2 + desktop_version/lang/es_419/strings.xml | 2 + desktop_version/lang/es_AR/strings.xml | 2 + desktop_version/lang/fa/strings.xml | 2 + desktop_version/lang/fr/strings.xml | 2 + desktop_version/lang/ga/strings.xml | 2 + desktop_version/lang/it/strings.xml | 2 + desktop_version/lang/ja/strings.xml | 2 + desktop_version/lang/ko/strings.xml | 2 + desktop_version/lang/nl/strings.xml | 2 + desktop_version/lang/pl/strings.xml | 2 + desktop_version/lang/pt_BR/strings.xml | 2 + desktop_version/lang/pt_PT/strings.xml | 2 + desktop_version/lang/ru/strings.xml | 2 + desktop_version/lang/szl/strings.xml | 2 + desktop_version/lang/tr/strings.xml | 2 + desktop_version/lang/uk/strings.xml | 2 + desktop_version/lang/zh/strings.xml | 2 + desktop_version/lang/zh_TW/strings.xml | 2 + desktop_version/src/Editor.cpp | 403 ++++++++++++++++++++++-- desktop_version/src/Editor.h | 32 ++ 27 files changed, 452 insertions(+), 33 deletions(-) diff --git a/desktop_version/lang/ar/strings.xml b/desktop_version/lang/ar/strings.xml index e2dd70db..8fe6db86 100644 --- a/desktop_version/lang/ar/strings.xml +++ b/desktop_version/lang/ar/strings.xml @@ -646,6 +646,8 @@ + + diff --git a/desktop_version/lang/ca/strings.xml b/desktop_version/lang/ca/strings.xml index 29d89131..a0790dd8 100644 --- a/desktop_version/lang/ca/strings.xml +++ b/desktop_version/lang/ca/strings.xml @@ -638,6 +638,8 @@ + + diff --git a/desktop_version/lang/cy/strings.xml b/desktop_version/lang/cy/strings.xml index d9aa1816..9358b24f 100644 --- a/desktop_version/lang/cy/strings.xml +++ b/desktop_version/lang/cy/strings.xml @@ -638,6 +638,8 @@ + + diff --git a/desktop_version/lang/de/strings.xml b/desktop_version/lang/de/strings.xml index 979aaad6..c7551a80 100644 --- a/desktop_version/lang/de/strings.xml +++ b/desktop_version/lang/de/strings.xml @@ -638,6 +638,8 @@ + + diff --git a/desktop_version/lang/en/strings.xml b/desktop_version/lang/en/strings.xml index 45154fc9..dfe9ec99 100644 --- a/desktop_version/lang/en/strings.xml +++ b/desktop_version/lang/en/strings.xml @@ -638,6 +638,8 @@ + + diff --git a/desktop_version/lang/eo/strings.xml b/desktop_version/lang/eo/strings.xml index 735fe1e7..243bcf41 100644 --- a/desktop_version/lang/eo/strings.xml +++ b/desktop_version/lang/eo/strings.xml @@ -638,6 +638,8 @@ + + diff --git a/desktop_version/lang/es/strings.xml b/desktop_version/lang/es/strings.xml index 4ad7b06c..236aa161 100644 --- a/desktop_version/lang/es/strings.xml +++ b/desktop_version/lang/es/strings.xml @@ -638,6 +638,8 @@ + + diff --git a/desktop_version/lang/es_419/strings.xml b/desktop_version/lang/es_419/strings.xml index 5c316b44..cf7adbf1 100644 --- a/desktop_version/lang/es_419/strings.xml +++ b/desktop_version/lang/es_419/strings.xml @@ -638,6 +638,8 @@ + + diff --git a/desktop_version/lang/es_AR/strings.xml b/desktop_version/lang/es_AR/strings.xml index 66d225b2..17ba4288 100644 --- a/desktop_version/lang/es_AR/strings.xml +++ b/desktop_version/lang/es_AR/strings.xml @@ -638,6 +638,8 @@ + + diff --git a/desktop_version/lang/fa/strings.xml b/desktop_version/lang/fa/strings.xml index d81835a8..774c8c4a 100644 --- a/desktop_version/lang/fa/strings.xml +++ b/desktop_version/lang/fa/strings.xml @@ -638,6 +638,8 @@ + + diff --git a/desktop_version/lang/fr/strings.xml b/desktop_version/lang/fr/strings.xml index 8cfd849f..08c95560 100644 --- a/desktop_version/lang/fr/strings.xml +++ b/desktop_version/lang/fr/strings.xml @@ -638,6 +638,8 @@ + + diff --git a/desktop_version/lang/ga/strings.xml b/desktop_version/lang/ga/strings.xml index 625d9d6e..03111d7c 100644 --- a/desktop_version/lang/ga/strings.xml +++ b/desktop_version/lang/ga/strings.xml @@ -640,6 +640,8 @@ Déan cóip chúltaca, ar eagla na heagla." explanation="translation maintenance + + diff --git a/desktop_version/lang/it/strings.xml b/desktop_version/lang/it/strings.xml index 5075f520..c419afc9 100644 --- a/desktop_version/lang/it/strings.xml +++ b/desktop_version/lang/it/strings.xml @@ -638,6 +638,8 @@ + + diff --git a/desktop_version/lang/ja/strings.xml b/desktop_version/lang/ja/strings.xml index 166d1b58..1d1b5619 100644 --- a/desktop_version/lang/ja/strings.xml +++ b/desktop_version/lang/ja/strings.xml @@ -674,6 +674,8 @@ Steam Deckには対応していません。" explanation="" max="38*5" max_local + + diff --git a/desktop_version/lang/ko/strings.xml b/desktop_version/lang/ko/strings.xml index 9d47d4ec..ba273561 100755 --- a/desktop_version/lang/ko/strings.xml +++ b/desktop_version/lang/ko/strings.xml @@ -638,6 +638,8 @@ + + diff --git a/desktop_version/lang/nl/strings.xml b/desktop_version/lang/nl/strings.xml index 4dd6a972..abe333d4 100644 --- a/desktop_version/lang/nl/strings.xml +++ b/desktop_version/lang/nl/strings.xml @@ -638,6 +638,8 @@ + + diff --git a/desktop_version/lang/pl/strings.xml b/desktop_version/lang/pl/strings.xml index c0c44798..cebd78cd 100644 --- a/desktop_version/lang/pl/strings.xml +++ b/desktop_version/lang/pl/strings.xml @@ -638,6 +638,8 @@ + + diff --git a/desktop_version/lang/pt_BR/strings.xml b/desktop_version/lang/pt_BR/strings.xml index 1a01283f..82d2c96d 100644 --- a/desktop_version/lang/pt_BR/strings.xml +++ b/desktop_version/lang/pt_BR/strings.xml @@ -638,6 +638,8 @@ + + diff --git a/desktop_version/lang/pt_PT/strings.xml b/desktop_version/lang/pt_PT/strings.xml index 3d009543..c5ac85bb 100644 --- a/desktop_version/lang/pt_PT/strings.xml +++ b/desktop_version/lang/pt_PT/strings.xml @@ -638,6 +638,8 @@ + + diff --git a/desktop_version/lang/ru/strings.xml b/desktop_version/lang/ru/strings.xml index d412935e..da8b121c 100644 --- a/desktop_version/lang/ru/strings.xml +++ b/desktop_version/lang/ru/strings.xml @@ -663,6 +663,8 @@ + + diff --git a/desktop_version/lang/szl/strings.xml b/desktop_version/lang/szl/strings.xml index df9585b3..c67da027 100644 --- a/desktop_version/lang/szl/strings.xml +++ b/desktop_version/lang/szl/strings.xml @@ -638,6 +638,8 @@ + + diff --git a/desktop_version/lang/tr/strings.xml b/desktop_version/lang/tr/strings.xml index 5a8817f5..8a7e5142 100644 --- a/desktop_version/lang/tr/strings.xml +++ b/desktop_version/lang/tr/strings.xml @@ -638,6 +638,8 @@ + + diff --git a/desktop_version/lang/uk/strings.xml b/desktop_version/lang/uk/strings.xml index 75ec7058..046d4e8e 100644 --- a/desktop_version/lang/uk/strings.xml +++ b/desktop_version/lang/uk/strings.xml @@ -638,6 +638,8 @@ + + diff --git a/desktop_version/lang/zh/strings.xml b/desktop_version/lang/zh/strings.xml index 1487c97c..1ebf7b84 100644 --- a/desktop_version/lang/zh/strings.xml +++ b/desktop_version/lang/zh/strings.xml @@ -648,6 +648,8 @@ + + diff --git a/desktop_version/lang/zh_TW/strings.xml b/desktop_version/lang/zh_TW/strings.xml index cb176772..68a79880 100644 --- a/desktop_version/lang/zh_TW/strings.xml +++ b/desktop_version/lang/zh_TW/strings.xml @@ -648,6 +648,8 @@ + + diff --git a/desktop_version/src/Editor.cpp b/desktop_version/src/Editor.cpp index 4ee808f6..1115a71e 100644 --- a/desktop_version/src/Editor.cpp +++ b/desktop_version/src/Editor.cpp @@ -412,6 +412,11 @@ void editorclass::reset(void) state = EditorState_DRAW; substate = EditorSubState_MAIN; + + undo_buffer.clear(); + redo_buffer.clear(); + + placing_tiles = false; } void editorclass::show_note(const char* text) @@ -2262,11 +2267,29 @@ void editorclass::add_entity(int rx, int ry, int xp, int yp, int tp, int p1, int entity.p6 = p6; entity.scriptname = ""; + EditorUndoInfo info; + info.room_x = rx; + info.room_y = ry; + info.type = EditorUndoType_ENTITY_ADDED; + info.entity = entity; + info.entity_id = customentities.size(); + undo_buffer.push_back(info); + redo_buffer.clear(); + customentities.push_back(entity); } void editorclass::remove_entity(int t) { + EditorUndoInfo info; + info.room_x = levx; + info.room_y = levy; + info.type = EditorUndoType_ENTITY_REMOVED; + info.entity_id = t; + info.entity = customentities[t]; + undo_buffer.push_back(info); + redo_buffer.clear(); + customentities.erase(customentities.begin() + t); } @@ -2284,6 +2307,86 @@ int editorclass::get_entity_at(int rx, int ry, int xp, int yp) return -1; } +static void update_old_tiles() +{ + extern editorclass ed; + for (int i = 0; i < SCREEN_WIDTH_TILES * SCREEN_HEIGHT_TILES; i++) + { + ed.old_tiles[i] = ed.get_tile(i % SCREEN_WIDTH_TILES, i / SCREEN_WIDTH_TILES); + } +} + +static void commit_entity(int id) +{ + // We're gonna modify an entity, so save the old version + extern editorclass ed; + + EditorUndoInfo info; + + info.room_x = ed.levx; + info.room_y = ed.levy; + info.type = EditorUndoType_ENTITY_MODIFIED; + info.entity_id = id; + info.entity = customentities[id]; + ed.undo_buffer.push_back(info); + ed.redo_buffer.clear(); +} + +static void commit_tiles() +{ + // We either let go of the mouse button, or we switched rooms, so we need to commit the tiles to the undo buffer + extern editorclass ed; + + EditorUndoInfo info; + + info.room_x = ed.levx; + info.room_y = ed.levy; + info.type = EditorUndoType_TILES; + SDL_memcpy(info.tiles, ed.old_tiles, sizeof(ed.old_tiles)); + + ed.undo_buffer.push_back(info); + ed.redo_buffer.clear(); +} + +static void commit_roomdata_change() +{ + extern editorclass ed; + + EditorUndoInfo info; + + info.room_x = ed.levx; + info.room_y = ed.levy; + info.type = EditorUndoType_ROOMDATA; + info.room_data = *cl.getroomprop(ed.levx, ed.levy); + + ed.undo_buffer.push_back(info); + ed.redo_buffer.clear(); +} + +static void commit_roomdata_tiles_change() +{ + extern editorclass ed; + + EditorUndoInfo info; + + info.room_x = ed.levx; + info.room_y = ed.levy; + info.type = EditorUndoType_ROOMDATA_TILES; + update_old_tiles(); + SDL_memcpy(info.tiles, ed.old_tiles, sizeof(ed.old_tiles)); + info.room_data = *cl.getroomprop(ed.levx, ed.levy); + + ed.undo_buffer.push_back(info); + ed.redo_buffer.clear(); +} + +static void uncommit() +{ + extern editorclass ed; + + ed.undo_buffer.pop_back(); +} + static void set_tile_interpolated(const int x1, const int x2, const int y1, const int y2, const int tile) { extern editorclass ed; @@ -2418,9 +2521,19 @@ void editorclass::tool_remove() { case EditorTool_WALLS: case EditorTool_BACKING: + if (!placing_tiles) + { + placing_tiles = true; + update_old_tiles(); + } handle_tile_placement(0); break; case EditorTool_SPIKES: + if (!placing_tiles) + { + placing_tiles = true; + update_old_tiles(); + } set_tile_interpolated(old_tilex, tilex, old_tiley, tiley, 0); break; default: @@ -2448,11 +2561,13 @@ void editorclass::entity_clicked(const int index) { case 1: // Enemies + commit_entity(index); entity->p1 = (entity->p1 + 1) % 4; break; case 2: { // Moving Platforms and Conveyors + commit_entity(index); const bool conveyor = entity->p1 >= 5; entity->p1++; if (conveyor) @@ -2468,6 +2583,7 @@ void editorclass::entity_clicked(const int index) case 10: // Checkpoints // If it's not textured as a checkpoint, then just leave it be + commit_entity(index); if (entity->p1 == 0 || entity->p1 == 1) { entity->p1 = (entity->p1 + 1) % 2; @@ -2476,27 +2592,34 @@ void editorclass::entity_clicked(const int index) case 11: case 16: // Gravity Lines, Start Point + commit_entity(index); entity->p1 = (entity->p1 + 1) % 2; break; case 15: // Crewmates + commit_entity(index); entity->p1 = (entity->p1 + 1) % 6; break; case 17: // Roomtext + commit_entity(index); get_input_line(TEXT_ROOMTEXT, "Enter roomtext:", &entity->scriptname); text_entity = index; break; case 18: // Terminals + commit_entity(index); if (entity->p1 == 0 || entity->p1 == 1) { // Flip the terminal, but if it's not textured as a terminal leave it alone entity->p1 = (entity->p1 + 1) % 2; } - SDL_FALLTHROUGH; + get_input_line(TEXT_SCRIPT, "Enter script name:", &entity->scriptname); + text_entity = index; + break; case 19: // Script Boxes (and terminals) + commit_entity(index); get_input_line(TEXT_SCRIPT, "Enter script name:", &entity->scriptname); text_entity = index; break; @@ -2519,6 +2642,12 @@ void editorclass::tool_place() { int tile = 0; + if (!placing_tiles) + { + placing_tiles = true; + update_old_tiles(); + } + if (cl.getroomprop(levx, levy)->directmode >= 1) { tile = direct_mode_tile; @@ -2536,6 +2665,12 @@ void editorclass::tool_place() break; } case EditorTool_SPIKES: + if (!placing_tiles) + { + placing_tiles = true; + update_old_tiles(); + } + set_tile_interpolated(old_tilex, tilex, old_tiley, tiley, 8); break; case EditorTool_TRINKETS: @@ -2636,17 +2771,22 @@ void editorclass::tool_place() } break; case EditorTool_START_POINT: - //If there is another start point, destroy it + lclickdelay = 1; + //If there is another start point, move it instead for (size_t i = 0; i < customentities.size(); i++) { if (customentities[i].t == 16) { - remove_entity(i); - i--; + commit_entity(i); + customentities[i].rx = levx; + customentities[i].ry = levy; + customentities[i].x = tilex; + customentities[i].y = tiley; + customentities[i].p1 = 0; + return; } } add_entity(levx, levy, tilex, tiley, 16, 0); - lclickdelay = 1; break; default: break; @@ -2999,21 +3139,25 @@ static void handle_draw_input() { if (key.keymap[SDLK_F1]) { + commit_roomdata_tiles_change(); ed.switch_tileset(shift_down); ed.keydelay = 6; } if (key.keymap[SDLK_F2]) { + commit_roomdata_tiles_change(); ed.switch_tilecol(shift_down); ed.keydelay = 6; } if (key.keymap[SDLK_F3]) { + commit_roomdata_change(); ed.switch_enemy(shift_down); ed.keydelay = 6; } if (key.keymap[SDLK_F4]) { + commit_roomdata_change(); ed.keydelay = 6; ed.substate = EditorSubState_DRAW_BOX; ed.box_corner = BoxCorner_FIRST; @@ -3021,6 +3165,7 @@ static void handle_draw_input() } if (key.keymap[SDLK_F5]) { + commit_roomdata_change(); ed.keydelay = 6; ed.substate = EditorSubState_DRAW_BOX; ed.box_corner = BoxCorner_FIRST; @@ -3028,6 +3173,7 @@ static void handle_draw_input() } if (key.keymap[SDLK_F10]) { + commit_roomdata_tiles_change(); if (cl.getroomprop(ed.levx, ed.levy)->directmode == 1) { cl.setroomdirectmode(ed.levx, ed.levy, 0); @@ -3057,11 +3203,13 @@ static void handle_draw_input() if (key.keymap[SDLK_w]) { + commit_roomdata_change(); ed.switch_warpdir(shift_down); ed.keydelay = 6; } if (key.keymap[SDLK_e]) { + commit_roomdata_change(); ed.keydelay = 6; ed.get_input_line(TEXT_ROOMNAME, "Enter new room name:", const_cast(&(cl.getroomprop(ed.levx, ed.levy)->roomname))); game.mapheld = true; @@ -3104,6 +3252,7 @@ static void handle_draw_input() const bool shift = key.keymap[SDLK_LSHIFT] || key.keymap[SDLK_RSHIFT]; if (key.keymap[SDLK_COMMA]) { + commit_roomdata_change(); if (ctrl) { if (shift) @@ -3123,6 +3272,7 @@ static void handle_draw_input() } else if (key.keymap[SDLK_PERIOD]) { + commit_roomdata_change(); if (ctrl) { if (shift) @@ -3194,6 +3344,127 @@ void editorclass::get_input_line(const enum TextMode mode, const std::string& pr old_entity_text = key.keybuffer; } +static void handle_undo(const bool undo) +{ + extern editorclass ed; + + std::vector* buffer = undo ? &ed.undo_buffer : &ed.redo_buffer; + + if (buffer->size() == 0) + { + ed.show_note(undo ? loc::gettext("ERROR: Nothing to undo") : loc::gettext("ERROR: Nothing to redo")); + return; + } + + EditorUndoInfo info = buffer->back(); + buffer->pop_back(); + + ed.levx = info.room_x; + ed.levy = info.room_y; + + ed.updatetiles = true; + ed.changeroom = true; + graphics.backgrounddrawn = false; + graphics.foregrounddrawn = false; + + EditorUndoInfo new_info; + + new_info.room_x = info.room_x; + new_info.room_y = info.room_y; + new_info.type = info.type; + + switch (info.type) + { + case EditorUndoType_TILES: + for (size_t i = 0; i < SCREEN_WIDTH_TILES * SCREEN_HEIGHT_TILES; i++) + { + const int x = i % SCREEN_WIDTH_TILES; + const int y = i / SCREEN_WIDTH_TILES; + ed.old_tiles[i] = ed.get_tile(x, y); + cl.settile(ed.levx, ed.levy, x, y, info.tiles[i]); + } + + SDL_memcpy(new_info.tiles, ed.old_tiles, sizeof(ed.old_tiles)); + break; + case EditorUndoType_ENTITY_ADDED: + // Remove the entity + + if (!INBOUNDS_VEC(info.entity_id, customentities)) + { + // Not sure how this would happen, but we should just consume it... + return; + } + + new_info.type = EditorUndoType_ENTITY_REMOVED; + new_info.entity = customentities[info.entity_id]; + new_info.entity_id = info.entity_id; + customentities.erase(customentities.begin() + info.entity_id); + break; + case EditorUndoType_ENTITY_REMOVED: + // Add the entity back + + customentities.insert(customentities.begin() + info.entity_id, info.entity); + new_info.type = EditorUndoType_ENTITY_ADDED; + new_info.entity_id = info.entity_id; + new_info.entity = info.entity; + break; + case EditorUndoType_ENTITY_MODIFIED: + // Restore the entity + + if (!INBOUNDS_VEC(info.entity_id, customentities)) + { + return; + } + + new_info.entity = customentities[info.entity_id]; + new_info.entity_id = info.entity_id; + customentities[info.entity_id] = info.entity; + break; + case EditorUndoType_ROOMDATA: + new_info.room_data = cl.roomproperties[info.room_x + info.room_y * cl.maxwidth]; + cl.roomproperties[info.room_x + info.room_y * cl.maxwidth] = info.room_data; + graphics.backgrounddrawn = false; + break; + case EditorUndoType_ROOMDATA_TILES: + // Restore the room data + + for (size_t i = 0; i < SCREEN_WIDTH_TILES * SCREEN_HEIGHT_TILES; i++) + { + const int x = i % SCREEN_WIDTH_TILES; + const int y = i / SCREEN_WIDTH_TILES; + ed.old_tiles[i] = ed.get_tile(x, y); + cl.settile(ed.levx, ed.levy, x, y, info.tiles[i]); + } + + SDL_memcpy(new_info.tiles, ed.old_tiles, sizeof(ed.old_tiles)); + + new_info.room_data = cl.roomproperties[info.room_x + info.room_y * cl.maxwidth]; + + cl.roomproperties[info.room_x + info.room_y * cl.maxwidth] = info.room_data; + graphics.backgrounddrawn = false; + graphics.foregrounddrawn = false; + ed.updatetiles = true; + break; + case EditorUndoType_LEVEL_SIZE: + // Restore the level size + new_info.level_width = cl.mapwidth; + new_info.level_height = cl.mapheight; + + cl.mapwidth = info.level_width; + cl.mapheight = info.level_height; + break; + } + + if (undo) + { + ed.redo_buffer.push_back(new_info); + } + else + { + ed.undo_buffer.push_back(new_info); + } +} + void editorinput(void) { extern editorclass ed; @@ -3203,6 +3474,12 @@ void editorinput(void) return; } + bool undo_pressed = false; + bool redo_pressed = false; + + bool shift_down = key.keymap[SDLK_LSHIFT] || key.keymap[SDLK_RSHIFT]; + bool ctrl_down = key.keymap[SDLK_LCTRL] || key.keymap[SDLK_RCTRL]; + ed.old_tilex = ed.tilex; ed.old_tiley = ed.tiley; @@ -3228,11 +3505,23 @@ void editorinput(void) { game.press_right = true; } - if (key.isDown(KEYBOARD_z) || key.isDown(KEYBOARD_SPACE) || key.isDown(KEYBOARD_v) || key.isDown(game.controllerButton_flip)) + if ((key.isDown(KEYBOARD_z) && !ctrl_down) || key.isDown(KEYBOARD_SPACE) || key.isDown(KEYBOARD_v) || key.isDown(game.controllerButton_flip)) { game.press_action = true; }; + if (key.isDown(KEYBOARD_z) && ctrl_down && (ed.keydelay == 0)) + { + ed.keydelay = 6; + undo_pressed = true; + } + + if (key.isDown(SDLK_y) && ctrl_down && (ed.keydelay == 0)) + { + ed.keydelay = 6; + redo_pressed = true; + } + if (key.keymap[SDLK_F9] && (ed.keydelay == 0)) { ed.keydelay = 30; ed.show_note(loc::gettext("Reloaded resources")); @@ -3263,9 +3552,6 @@ void editorinput(void) game.mapheld = false; } - bool shift_down = key.keymap[SDLK_LSHIFT] || key.keymap[SDLK_RSHIFT]; - bool ctrl_down = key.keymap[SDLK_LCTRL] || key.keymap[SDLK_RCTRL]; - // Do different things depending on the current state (and substate) switch (ed.state) { @@ -3274,6 +3560,16 @@ void editorinput(void) switch (ed.substate) { case EditorSubState_MAIN: + + if (undo_pressed) + { + handle_undo(true); + } + if (redo_pressed) + { + handle_undo(false); + } + if (escape_pressed) { // We're just in draw mode, so go to the settings menu @@ -3322,6 +3618,8 @@ void editorinput(void) } else if (shift_down) { + int old_width = cl.mapwidth; + int old_height = cl.mapheight; if (up_pressed) cl.mapheight--; if (down_pressed) cl.mapheight++; @@ -3331,26 +3629,48 @@ void editorinput(void) cl.mapwidth = SDL_clamp(cl.mapwidth, 1, cl.maxwidth); cl.mapheight = SDL_clamp(cl.mapheight, 1, cl.maxheight); - ed.updatetiles = true; - ed.changeroom = true; - graphics.backgrounddrawn = false; - graphics.foregrounddrawn = false; + if (old_width != cl.mapwidth || old_height != cl.mapheight) + { + ed.updatetiles = true; + ed.changeroom = true; + graphics.backgrounddrawn = false; + graphics.foregrounddrawn = false; - ed.levx = POS_MOD(ed.levx, cl.mapwidth); - ed.levy = POS_MOD(ed.levy, cl.mapheight); + ed.levx = POS_MOD(ed.levx, cl.mapwidth); + ed.levy = POS_MOD(ed.levy, cl.mapheight); - char buffer[3 * SCREEN_WIDTH_CHARS + 1]; - vformat_buf( - buffer, sizeof(buffer), - loc::gettext("Mapsize is now [{width},{height}]"), - "width:int, height:int", - cl.mapwidth, cl.mapheight - ); + char buffer[3 * SCREEN_WIDTH_CHARS + 1]; + vformat_buf( + buffer, sizeof(buffer), + loc::gettext("Mapsize is now [{width},{height}]"), + "width:int, height:int", + cl.mapwidth, cl.mapheight + ); + + ed.show_note(buffer); - ed.show_note(buffer); + EditorUndoInfo info; + info.type = EditorUndoType_LEVEL_SIZE; + info.level_width = old_width; + info.level_height = old_height; + info.room_x = ed.levx; + info.room_y = ed.levy; + + ed.undo_buffer.push_back(info); + ed.redo_buffer.clear(); + } } else { + if (ed.placing_tiles) + { + // We were in the middle of placing tiles. Commit it, since we're done with the previous room. + commit_tiles(); + + // Must be done after every tile commit, as it's responsible for the "old tiles" cache + ed.placing_tiles = false; + } + ed.updatetiles = true; ed.changeroom = true; graphics.backgrounddrawn = false; @@ -3372,19 +3692,26 @@ void editorinput(void) } // Mouse input - if (key.leftbutton && ed.lclickdelay == 0) - { - ed.tool_place(); - } - else if (!key.leftbutton) - { - ed.lclickdelay = 0; - } - if (key.rightbutton) { ed.tool_remove(); } + else + { + if (key.leftbutton && ed.lclickdelay == 0) + { + ed.tool_place(); + } + else if (!key.leftbutton) + { + ed.lclickdelay = 0; + if (ed.placing_tiles) + { + commit_tiles(); + ed.placing_tiles = false; + } + } + } if (key.middlebutton) { @@ -3536,14 +3863,24 @@ void editorinput(void) if (escape_pressed) { - // Cancel it, and remove the enemy it's tied to if necessary + // Escape was pressed, cancel text entry key.disabletextentry(); + if (ed.current_text_mode >= FIRST_ENTTEXT && ed.current_text_mode <= LAST_ENTTEXT) { *ed.current_text_ptr = ed.old_entity_text; + + // Looks like we're giving an entity text for the first time, so cancelling should remove the entity if (ed.old_entity_text == "") { + // Remove it. ed.remove_entity(ed.text_entity); + + // We have to uncommit twice here; once to prevent saving the "remove entity" action... + uncommit(); + + // ...and once more to undo the "add entity" action we're cancelling + uncommit(); } } diff --git a/desktop_version/src/Editor.h b/desktop_version/src/Editor.h index f9fff570..770ead39 100644 --- a/desktop_version/src/Editor.h +++ b/desktop_version/src/Editor.h @@ -133,6 +133,33 @@ struct GhostInfo int frame; // .drawframe }; + +enum EditorUndoTypes +{ + EditorUndoType_TILES, // Tiles modified + EditorUndoType_ROOMDATA, // Room data modified + EditorUndoType_ROOMDATA_TILES, // Room data modified (and stores tiles) + EditorUndoType_ENTITY_ADDED, // Entity added + EditorUndoType_ENTITY_REMOVED, // Entity removed + EditorUndoType_ENTITY_MODIFIED, // Entity properties modified + EditorUndoType_LEVEL_SIZE // Level size modified +}; + +struct EditorUndoInfo +{ + EditorUndoTypes type; + int tiles[SCREEN_WIDTH_TILES * SCREEN_HEIGHT_TILES]; + int room_x; + int room_y; + EditorTilesets tileset; + int tilecol; + int entity_id; + CustomEntity entity; + RoomProperty room_data; + int level_width; + int level_height; +}; + class editorclass { public: @@ -282,6 +309,11 @@ public: std::vector ghosts; int current_ghosts; + + std::vector undo_buffer; + std::vector redo_buffer; + bool placing_tiles; + int old_tiles[SCREEN_WIDTH_TILES * SCREEN_HEIGHT_TILES]; }; void editorrender(void);