From 4c4d479542964e10c068b11991c97c87f3bfed22 Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Tue, 15 Oct 2024 20:08:59 +0200 Subject: [PATCH 01/25] Initial commit --- src/src/game-gui.cc | 29 ++++++++- src/src/game.hh | 4 +- src/src/gundo.cc | 86 +++++++++++++++++++++++++++ src/src/gundo.hh | 27 +++++++++ src/src/main.cc | 13 ++++ src/src/main.hh | 3 + src/src/world.cc | 142 ++++++++++++++++++++++++-------------------- src/src/world.hh | 6 ++ 8 files changed, 241 insertions(+), 69 deletions(-) create mode 100644 src/src/gundo.cc create mode 100644 src/src/gundo.hh diff --git a/src/src/game-gui.cc b/src/src/game-gui.cc index 919bcd29..ec8b423e 100644 --- a/src/src/game-gui.cc +++ b/src/src/game-gui.cc @@ -3,10 +3,13 @@ #include "game.hh" #include "game-message.hh" +#include "gundo.hh" +#include "main.hh" #include "settings.hh" #include "robot.hh" #include "object_factory.hh" #include "i1o1gate.hh" +#include "tms/core/wdg.h" #include "ui.hh" #include "adventure.hh" #include "motor.hh" @@ -691,6 +694,8 @@ game::menu_handle_event(tms::event *ev) if (found) { if (o.e) { + undo.checkpoint(); + entity *e = of::create(o.e->g_id); if (e) { menu_objects[gid_to_menu_pos[e->g_id]].highlighted = true; @@ -1000,6 +1005,10 @@ game::widget_clicked(principia_wdg *w, uint8_t button_id, int pid) } return true; + case GW_UNDO: + P.add_action(ACTION_UNDO_RESTORE, 0); + break; + case GW_MODE: { switch (G->get_mode()) { @@ -1613,18 +1622,32 @@ game::init_gui(void) this->wdg_layervis->priority = 800; this->wdg_layervis->set_tooltip("Toggle layer visibility"); + // TODO griffi-gh: add proper icon for the undo button + this->wdg_undo = this->wm->create_widget( + this->get_surface(), TMS_WDG_BUTTON, + GW_UNDO, AREA_TOP_LEFT, + gui_spritesheet::get_sprite(S_EMPTY), 0); + this->wdg_undo->priority = 700; + const char* undo_tooltip = + "Undo" +#ifdef TMS_BACKEND_PC + " (Ctrl+Z)" +#endif + ; + this->wdg_undo->set_tooltip(undo_tooltip); + this->wdg_mode = this->wm->create_widget( this->get_surface(), TMS_WDG_BUTTON, GW_MODE, AREA_TOP_LEFT, gui_spritesheet::get_sprite(S_CONFIG), 0); - this->wdg_mode->priority = 700; + this->wdg_mode->priority = 600; this->wdg_mode->set_tooltip("Change mode"); this->wdg_advanced = this->wm->create_widget( this->get_surface(), TMS_WDG_BUTTON, GW_ADVANCED, AREA_TOP_LEFT, gui_spritesheet::get_sprite(S_ADVUP), 0); - this->wdg_advanced->priority = 600; + this->wdg_advanced->priority = 500; this->wdg_advanced->set_tooltip("Toggle advanced options"); this->wdg_help = this->wm->create_widget( @@ -2371,6 +2394,8 @@ game::refresh_widgets() case 0b011: G->wdg_layervis->s[0] = gui_spritesheet::get_sprite(S_LAYERVIS_2); break; default: case 0b111: G->wdg_layervis->s[0] = gui_spritesheet::get_sprite(S_LAYERVIS_3); break; } + + this->wdg_undo->add(); // XXX griffi-gh: Is this the right place? } if ((this->state.sandbox || this->state.test_playing || W->is_paused() || W->is_puzzle()) && (W->level.type != LCAT_ADVENTURE || W->is_paused())) { diff --git a/src/src/game.hh b/src/src/game.hh index 6cc6e53a..d4c06c57 100644 --- a/src/src/game.hh +++ b/src/src/game.hh @@ -42,6 +42,7 @@ enum { GW_PLAYPAUSE, GW_ORTHOGRAPHIC, GW_LAYERVIS, + GW_UNDO, GW_MODE, GW_ADVANCED, GW_HELP, @@ -440,6 +441,7 @@ class game : public pscreen principia_wdg *wdg_playpause; principia_wdg *wdg_orthographic; principia_wdg *wdg_layervis; + principia_wdg *wdg_undo; principia_wdg *wdg_mode; principia_wdg *wdg_advanced; principia_wdg *wdg_help; @@ -712,9 +714,9 @@ class game : public pscreen float get_bmenu_y(); float get_bmenu_pad(); + void reset(void); protected: void clear_entities(void); - void reset(void); void init_panel_edit(void); void init_gearbox_edit(void); diff --git a/src/src/gundo.cc b/src/src/gundo.cc new file mode 100644 index 00000000..1d58606b --- /dev/null +++ b/src/src/gundo.cc @@ -0,0 +1,86 @@ +#include "gundo.hh" + +#include "game.hh" +#include "main.hh" +#include "tms/backend/print.h" +#include "world.hh" + +// TODO griffi-gh: compress undo items +// TODO griffi-gh: support redo + +struct undo_stack undo; + +void undo_stack::reset() { + this->items.clear(); +} + +void undo_stack::checkpoint() { + W->save(SAVE_TYPE_UNDO); + + void *data_copy = malloc(W->lb.size); + memcpy(data_copy, W->lb.buf, W->lb.size); + + struct undo_item item = {data_copy, W->lb.size}; + this->items.push_back(item); + + tms_debugf( + "undo_checkpoint: item: %lu bytes; ptr %p", + item.size, item.data + ); + + if (this->items.size() > MAX_UNDO_ITEMS) { + const void *data = this->items.front().data; + free((void *)data); + this->items.erase(this->items.begin()); + } + + tms_printf( + "undo_checkpoint: histsize: %lu/%u\n", + this->items.size(), MAX_UNDO_ITEMS + ); +} + +void undo_stack::restore() { + if (this->items.size() == 0) { + tms_fatalf("undo_load: no items to load"); + } + struct undo_item &item = this->items.back(); + this->items.pop_back(); + + // if (W->lb.buf) { + // free(W->lb.buf); + // } + // W->lb.buf = (uint8_t*)item.data; + W->lb.clear(); + W->lb.ensure(item.size); + memcpy(W->lb.buf, item.data, item.size); + W->lb.size = item.size; + + G->reset(); + W->open_internal( + item.size, + W->level_id_type, + W->level.local_id, + true, + true, + W->level.save_id, + false, + true + ); + G->apply_level_properties(); + G->add_entities(&W->all_entities, &W->groups, &W->connections, &W->cables); + W->begin(); + + G->refresh_widgets(); + + // W->cwindow->preloader.read_gentypes(&W->level, &W->lb); + // W->cwindow->preloader.read_chunks(&W->level, &W->lb); + // W->state_ptr = W->lb.rp; + // W->lb.rp += W->level.state_size; + + // free(item.data); DO NOT! + // P.add_action(ACTION_RELOAD_LEVEL, 0); + // W->reset(); + + tms_infof("Restored level from undo stack"); +} diff --git a/src/src/gundo.hh b/src/src/gundo.hh new file mode 100644 index 00000000..d3b43a5b --- /dev/null +++ b/src/src/gundo.hh @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +#define MAX_UNDO_ITEMS 100 + +struct undo_item { + void *data; + size_t size; +}; + +struct undo_stack { + private: + std::vector items; + + public: + // Do not use this function directly, use P.add_action(ACTION_UNDO_RESET, 0) instead + void reset(); + // Do not use this function directly, use P.add_action(ACTION_UNDO_CHECKPOINT, 0) instead + void checkpoint(); + // Do not use this function directly, use P.add_action(ACTION_UNDO_RESTORE, 0) instead + void restore(); +}; + +extern struct undo_stack undo; + diff --git a/src/src/main.cc b/src/src/main.cc index 85b53a36..bcb8871c 100644 --- a/src/src/main.cc +++ b/src/src/main.cc @@ -1,5 +1,6 @@ #include "game.hh" #include "main.hh" +#include "gundo.hh" #include "tms/core/err.h" #include "loading_screen.hh" #include "soundmanager.hh" @@ -1157,6 +1158,18 @@ tproject_step(void) adventure::player->damage(10000.f, 0, DAMAGE_TYPE_OTHER, DAMAGE_SOURCE_WORLD, 0); } break; + + case ACTION_UNDO_RESET: + undo.reset(); + break; + + case ACTION_UNDO_CHECKPOINT: + undo.checkpoint(); + break; + + case ACTION_UNDO_RESTORE: + undo.restore(); + break; } } P.num_actions = 0; diff --git a/src/src/main.hh b/src/src/main.hh index 7db604d8..8e442dec 100644 --- a/src/src/main.hh +++ b/src/src/main.hh @@ -103,6 +103,9 @@ enum { ACTION_OPEN_URL, ACTION_NEW_GENERATED_LEVEL, /* 70 */ ACTION_SELF_DESTRUCT, + ACTION_UNDO_RESET, + ACTION_UNDO_CHECKPOINT, + ACTION_UNDO_RESTORE, ACTION_IGNORE }; diff --git a/src/src/world.cc b/src/src/world.cc index 880c6a09..b0cc198a 100644 --- a/src/src/world.cc +++ b/src/src/world.cc @@ -2480,6 +2480,10 @@ world::save(int save_type) char filename[1024]; switch (save_type) { + // If we're saving the state, skip all of this and return immediately (we just need to generate the level buffer) + case SAVE_TYPE_UNDO: + return true; + case SAVE_TYPE_DEFAULT: if (this->level.local_id == 0) { /* this level does not have a local id, @@ -2651,14 +2655,9 @@ world::load_partial(uint32_t id, b2Vec2 position, } bool -world::open(int id_type, uint32_t id, bool paused, bool sandbox, uint32_t save_id/*=0*/) -{ - bool is_autosave = false; - - this->reset(); - this->init(paused); - +world::open(int id_type, uint32_t id, bool paused, bool sandbox, uint32_t save_id/*=0*/) { char filename[1024]; + bool is_autosave = false; if (id == 0 && id_type == LEVEL_LOCAL) { /* opening an autosave file */ @@ -2686,87 +2685,98 @@ world::open(int id_type, uint32_t id, bool paused, bool sandbox, uint32_t save_i tms_fatalf("Level file too big"); } - this->lb.reset(); - this->lb.size = 0; - this->lb.ensure((int)size); - _fread(this->lb.buf, 1, size, fp); - _fclose(fp); - this->lb.size = size; - tms_infof("read file of size: %lu", size); + this->open_internal(size, id_type, id, paused, sandbox, save_id, is_autosave, false); + } else { + tms_errorf("could not open file '%s' for reading", filename); + return false; + } +} - if (!this->level.read(&this->lb)) { - ui::message("You need to update Principia to play this level.", true); - return false; - } else { - tms_debugf("Successfully read level"); - tms_debugf("Version: %u", this->level.version); - } +bool world::open_internal( + size_t size, + int id_type, uint32_t id, bool paused, bool sandbox, uint32_t save_id/*=0*/, + bool is_autosave, + bool skip_compression/*=false*/ +) { - if (!this->read_cache(id_type, id, save_id)) { + this->reset(); + this->init(paused); - } + this->lb.reset(); + this->lb.size = 0; + this->lb.ensure((int)size); - if (!sandbox && this->level.visibility == LEVEL_LOCKED && G->state.pkg == 0) { - ui::message("This level is locked and can only be played from inside a package."); - tms_errorf("locked level"); - return false; - } + this->lb.size = size; + tms_infof("read file of size: %lu", size); - this->level_id_type = id_type; - if (is_autosave) { - this->level.local_id = this->level.autosave_id; - } else { - this->level.local_id = id; - } + if (!this->level.read(&this->lb)) { + ui::message("You need to update Principia to play this level.", true); + return false; + } else { + tms_debugf("Successfully read level"); + tms_debugf("Version: %u", this->level.version); + } - G->init_background(); + if (!this->read_cache(id_type, id, save_id)) { - this->init_level(); + } - if (this->level.version >= LEVEL_VERSION_1_5) { - this->lb.zuncompress(this->level); - } + if (!sandbox && this->level.visibility == LEVEL_LOCKED && G->state.pkg == 0) { + ui::message("This level is locked and can only be played from inside a package."); + tms_errorf("locked level"); + return false; + } - /* save the location of the state buffer so game can load it later */ - this->state_ptr = this->lb.rp; - this->lb.rp += this->level.state_size; + this->level_id_type = id_type; + if (is_autosave) { + this->level.local_id = this->level.autosave_id; + } else { + this->level.local_id = id; + } - bool result; + G->init_background(); - if (this->level.flag_active(LVL_CHUNKED_LEVEL_LOADING)) { - result = this->cwindow->preloader.preload(&this->level, &this->lb); - } else { - result = this->load_buffer(&this->level, &this->lb); - /* preloader is always used to load and write chunks, disregarding chunked level loading flag */ - this->cwindow->preloader.read_gentypes(&this->level, &this->lb); - this->cwindow->preloader.read_chunks(&this->level, &this->lb); - } + this->init_level(); - if (!result) { - ui::message("Could not load level. You may need to update Principia to the latest version.", true); - this->reset(); - return false; - } + if ((!skip_compression) && (this->level.version >= LEVEL_VERSION_1_5)) { + this->lb.zuncompress(this->level); + } + /* save the location of the state buffer so game can load it later */ + this->state_ptr = this->lb.rp; + this->lb.rp += this->level.state_size; - uint8_t extra = this->lb.r_uint8(); + bool result; - if (extra == (uint8_t)1 && sandbox) { - this->reset(); - return false; - } + if (this->level.flag_active(LVL_CHUNKED_LEVEL_LOADING)) { + result = this->cwindow->preloader.preload(&this->level, &this->lb); + } else { + result = this->load_buffer(&this->level, &this->lb); + /* preloader is always used to load and write chunks, disregarding chunked level loading flag */ + this->cwindow->preloader.read_gentypes(&this->level, &this->lb); + this->cwindow->preloader.read_chunks(&this->level, &this->lb); + } - if (!sandbox && this->level.type == LCAT_PUZZLE) this->apply_puzzle_constraints(); - if (!paused) this->optimize_connections(); + if (!result) { + ui::message("Could not load level. You may need to update Principia to the latest version.", true); + this->reset(); + return false; + } + + uint8_t extra = this->lb.r_uint8(); - return true; + if (extra == (uint8_t)1 && sandbox) { + this->reset(); + return false; } - tms_errorf("could not open file '%s' for reading", filename); - return false; + if (!sandbox && this->level.type == LCAT_PUZZLE) this->apply_puzzle_constraints(); + if (!paused) this->optimize_connections(); + + return true; } void diff --git a/src/src/world.hh b/src/src/world.hh index edb0fc6d..721e84cd 100644 --- a/src/src/world.hh +++ b/src/src/world.hh @@ -30,6 +30,7 @@ enum { SAVE_TYPE_DEFAULT = 0, SAVE_TYPE_AUTOSAVE = 1, SAVE_TYPE_STATE = 2, + SAVE_TYPE_UNDO = 3, }; #define WORLD_STEP 8000 @@ -292,6 +293,11 @@ class world : public b2QueryCallback void create(int type, uint64_t seed, bool play); void open_autosave(void); + bool open_internal( + size_t data_len, + int id_type, uint32_t id, bool paused, bool sandbox, uint32_t save_id=0, + bool is_autosave=false, bool skip_compression=false + ); bool open(int id_type, uint32_t id, bool paused, bool sandbox, uint32_t save_id=0); void begin(); bool read_cache(int level_type, uint32_t id, uint32_t save_id=0); From 84e634cd445a69dfa351cbb0d6b18b8b57e7abde Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Tue, 15 Oct 2024 20:21:09 +0200 Subject: [PATCH 02/25] Add `open_sandbox_snapshot_mem` --- src/src/game.cc | 29 +++++++++++++++++++++++++++++ src/src/game.hh | 8 +++++++- src/src/gundo.cc | 35 +---------------------------------- 3 files changed, 37 insertions(+), 35 deletions(-) diff --git a/src/src/game.cc b/src/src/game.cc index 1d702368..2c5772fe 100644 --- a/src/src/game.cc +++ b/src/src/game.cc @@ -67,6 +67,7 @@ #include "world.hh" #include "player_activator.hh" #include "gui.hh" +#include #ifdef DEBUG /* for print_screen_point_info */ #include "terrain.hh" @@ -7183,6 +7184,34 @@ game::open_sandbox(int id_type, uint32_t id) this->refresh_widgets(); } +void game::open_sandbox_snapshot_mem(const void* snapshot, size_t size) { + tms_assertf(this->state.sandbox, "level is not a sandbox"); + + W->lb.clear(); + W->lb.ensure(size); + memcpy(W->lb.buf, snapshot, size); + W->lb.size = size; + + this->reset(); + this->state.sandbox = true; + W->open_internal( + size, + W->level_id_type, + W->level.local_id, + false, + true, + W->level.save_id, + false, + true + ); + + this->apply_level_properties(); + this->add_entities(&W->all_entities, &W->groups, &W->connections, &W->cables); + W->begin(); + + this->refresh_widgets(); +} + bool game::delete_level(int id_type, uint32_t id, uint32_t save_id) { diff --git a/src/src/game.hh b/src/src/game.hh index d4c06c57..53957ebd 100644 --- a/src/src/game.hh +++ b/src/src/game.hh @@ -714,9 +714,9 @@ class game : public pscreen float get_bmenu_y(); float get_bmenu_pad(); - void reset(void); protected: void clear_entities(void); + void reset(void); void init_panel_edit(void); void init_gearbox_edit(void); @@ -1107,6 +1107,12 @@ class game : public pscreen void open_state(int id_type, uint32_t id, uint32_t save_id); void open_sandbox(int id_type, uint32_t id); void open_play(int id_type, uint32_t id, pkginfo *pkg, bool test_playing=false, int is_main_puzzle=0); + // Reload state of the *currently open* level from an in-memory snapshot. + // Shit WILL break in unpredictable ways if: + // - save data is compressed + // - save data comes from a different level + // - the level is not a sandbox level + void open_sandbox_snapshot_mem(const void* snapshot, size_t size); void begin_play(bool has_state=false); diff --git a/src/src/gundo.cc b/src/src/gundo.cc index 1d58606b..14d584c7 100644 --- a/src/src/gundo.cc +++ b/src/src/gundo.cc @@ -47,40 +47,7 @@ void undo_stack::restore() { struct undo_item &item = this->items.back(); this->items.pop_back(); - // if (W->lb.buf) { - // free(W->lb.buf); - // } - // W->lb.buf = (uint8_t*)item.data; - W->lb.clear(); - W->lb.ensure(item.size); - memcpy(W->lb.buf, item.data, item.size); - W->lb.size = item.size; - - G->reset(); - W->open_internal( - item.size, - W->level_id_type, - W->level.local_id, - true, - true, - W->level.save_id, - false, - true - ); - G->apply_level_properties(); - G->add_entities(&W->all_entities, &W->groups, &W->connections, &W->cables); - W->begin(); - - G->refresh_widgets(); - - // W->cwindow->preloader.read_gentypes(&W->level, &W->lb); - // W->cwindow->preloader.read_chunks(&W->level, &W->lb); - // W->state_ptr = W->lb.rp; - // W->lb.rp += W->level.state_size; - - // free(item.data); DO NOT! - // P.add_action(ACTION_RELOAD_LEVEL, 0); - // W->reset(); + G->open_sandbox_snapshot_mem(item.data, item.size); tms_infof("Restored level from undo stack"); } From 8ee73a8d7f5dcbede09bdbd1ede6d53e9f4746d0 Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Tue, 15 Oct 2024 20:22:36 +0200 Subject: [PATCH 03/25] fix paused value --- src/src/game.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/src/game.cc b/src/src/game.cc index 2c5772fe..b45f1b1d 100644 --- a/src/src/game.cc +++ b/src/src/game.cc @@ -7198,7 +7198,7 @@ void game::open_sandbox_snapshot_mem(const void* snapshot, size_t size) { size, W->level_id_type, W->level.local_id, - false, + W->is_paused(), true, W->level.save_id, false, From 0bb6b9649a461daecd03a7399c57b67375f72a0b Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Tue, 15 Oct 2024 20:38:13 +0200 Subject: [PATCH 04/25] Update undo api --- src/src/game-gui.cc | 2 +- src/src/gundo.cc | 18 +++++++++++++----- src/src/gundo.hh | 26 +++++++++++++++++++++----- src/src/main.cc | 16 ++++++++++++++-- 4 files changed, 49 insertions(+), 13 deletions(-) diff --git a/src/src/game-gui.cc b/src/src/game-gui.cc index ec8b423e..e036d19a 100644 --- a/src/src/game-gui.cc +++ b/src/src/game-gui.cc @@ -694,7 +694,7 @@ game::menu_handle_event(tms::event *ev) if (found) { if (o.e) { - undo.checkpoint(); + undo.checkpoint("Added object"); entity *e = of::create(o.e->g_id); if (e) { diff --git a/src/src/gundo.cc b/src/src/gundo.cc index 14d584c7..f47f9a6b 100644 --- a/src/src/gundo.cc +++ b/src/src/gundo.cc @@ -1,26 +1,31 @@ #include "gundo.hh" #include "game.hh" -#include "main.hh" #include "tms/backend/print.h" #include "world.hh" +#include -// TODO griffi-gh: compress undo items +// TODO griffi-gh: call reset on level load // TODO griffi-gh: support redo +// TODO griffi-gh: compress undo items? struct undo_stack undo; +size_t undo_stack::amount() { + return this->items.size(); +} + void undo_stack::reset() { this->items.clear(); } -void undo_stack::checkpoint() { +void undo_stack::checkpoint(const char *reason) { W->save(SAVE_TYPE_UNDO); void *data_copy = malloc(W->lb.size); memcpy(data_copy, W->lb.buf, W->lb.size); - struct undo_item item = {data_copy, W->lb.size}; + struct undo_item item = { reason, data_copy, W->lb.size }; this->items.push_back(item); tms_debugf( @@ -40,7 +45,7 @@ void undo_stack::checkpoint() { ); } -void undo_stack::restore() { +const char* undo_stack::restore() { if (this->items.size() == 0) { tms_fatalf("undo_load: no items to load"); } @@ -50,4 +55,7 @@ void undo_stack::restore() { G->open_sandbox_snapshot_mem(item.data, item.size); tms_infof("Restored level from undo stack"); + + free(item.data); + return item.reason; } diff --git a/src/src/gundo.hh b/src/src/gundo.hh index d3b43a5b..9463d999 100644 --- a/src/src/gundo.hh +++ b/src/src/gundo.hh @@ -6,6 +6,7 @@ #define MAX_UNDO_ITEMS 100 struct undo_item { + const char *reason; void *data; size_t size; }; @@ -15,12 +16,27 @@ struct undo_stack { std::vector items; public: - // Do not use this function directly, use P.add_action(ACTION_UNDO_RESET, 0) instead + // Get the number of items in the undo stack + size_t amount(); + + + // Reset the undo stack + // Call this while loading a new level + // + // Avoid using this function directly if possible, use P.add_action(ACTION_UNDO_RESET, 0) instead void reset(); - // Do not use this function directly, use P.add_action(ACTION_UNDO_CHECKPOINT, 0) instead - void checkpoint(); - // Do not use this function directly, use P.add_action(ACTION_UNDO_RESTORE, 0) instead - void restore(); + + // Save the current level state to the undo stack + // + // Avoid using this function directly if possible, use P.add_action(ACTION_UNDO_CHECKPOINT, reason) instead + // Although, if you're calling it right before the state of the level changes, you MUST call it directly + void checkpoint(const char* reason = nullptr); + + // Restore the last saved level state from the undo stack + // Returns the reason for the last checkpoint + // + // Avoid using this function directly, use P.add_action(ACTION_UNDO_RESTORE, 0) instead + const char* restore(); }; extern struct undo_stack undo; diff --git a/src/src/main.cc b/src/src/main.cc index bcb8871c..89c5a64b 100644 --- a/src/src/main.cc +++ b/src/src/main.cc @@ -35,6 +35,7 @@ #include "adventure.hh" #include "gui.hh" +#include #include #include #include @@ -1164,11 +1165,22 @@ tproject_step(void) break; case ACTION_UNDO_CHECKPOINT: - undo.checkpoint(); + undo.checkpoint((const char*)data); break; case ACTION_UNDO_RESTORE: - undo.restore(); + if (undo.amount() == 0) { + ui::message("Nothing to undo"); + } else { + const char* undo_reason = undo.restore(); + if (undo_reason) { + char tmp[1024]; + sprintf(tmp, "Undid %s", undo_reason); + ui::message(tmp); + } else { + ui::message("Undid"); + } + } break; } } From 5bd197b02837ef4ce27343d40a8ea2c3a38356c6 Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Tue, 15 Oct 2024 20:40:59 +0200 Subject: [PATCH 05/25] log size --- src/src/gundo.cc | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/src/gundo.cc b/src/src/gundo.cc index f47f9a6b..a8fd5545 100644 --- a/src/src/gundo.cc +++ b/src/src/gundo.cc @@ -39,6 +39,17 @@ void undo_stack::checkpoint(const char *reason) { this->items.erase(this->items.begin()); } +#ifdef DEBUG + size_t total_size_bytes = 0; + for (const struct undo_item &item : this->items) { + total_size_bytes += item.size; + } + tms_debugf( + "undo_checkpoint: total size: %lu bytes\n", + total_size_bytes + ); +#endif + tms_printf( "undo_checkpoint: histsize: %lu/%u\n", this->items.size(), MAX_UNDO_ITEMS From 7e95b33c2da7802c11feb7d62a9bf5b52f8f3d52 Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Tue, 15 Oct 2024 20:47:43 +0200 Subject: [PATCH 06/25] fix level loading --- src/src/world.cc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/src/world.cc b/src/src/world.cc index b0cc198a..fc37f6bc 100644 --- a/src/src/world.cc +++ b/src/src/world.cc @@ -2685,10 +2685,14 @@ world::open(int id_type, uint32_t id, bool paused, bool sandbox, uint32_t save_i tms_fatalf("Level file too big"); } + this->lb.reset(); + this->lb.size = 0; + this->lb.ensure((int)size); + this->lb.size = size; _fread(this->lb.buf, 1, size, fp); _fclose(fp); - this->open_internal(size, id_type, id, paused, sandbox, save_id, is_autosave, false); + return this->open_internal(size, id_type, id, paused, sandbox, save_id, is_autosave, false); } else { tms_errorf("could not open file '%s' for reading", filename); return false; @@ -2705,12 +2709,8 @@ bool world::open_internal( this->reset(); this->init(paused); - this->lb.reset(); - this->lb.size = 0; - this->lb.ensure((int)size); - - this->lb.size = size; tms_infof("read file of size: %lu", size); + this->lb.size = size; if (!this->level.read(&this->lb)) { ui::message("You need to update Principia to play this level.", true); From 64f422b5eb3ed9712249409b3e026e79a24d6a11 Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Tue, 15 Oct 2024 20:49:36 +0200 Subject: [PATCH 07/25] add note --- src/src/world.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/src/world.cc b/src/src/world.cc index fc37f6bc..c73aebe4 100644 --- a/src/src/world.cc +++ b/src/src/world.cc @@ -2720,6 +2720,7 @@ bool world::open_internal( tms_debugf("Version: %u", this->level.version); } + // TODO/MAYBE griffi-gh: Probably should skip this if loading from in-memory buffer???? if (!this->read_cache(id_type, id, save_id)) { } From c138b181b9c94252b6272ecf8b48e4bb6bb1cb11 Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Tue, 15 Oct 2024 21:06:29 +0200 Subject: [PATCH 08/25] add button icon --- data-shared/textures/ui/btn_undo.png | Bin 0 -> 1244 bytes data-src/ui/ui_buttons.xcf | Bin 68692 -> 70152 bytes src/src/game-gui.cc | 2 +- src/src/game.cc | 5 ++++- src/src/gui.cc | 1 + src/src/gui.hh | 1 + 6 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 data-shared/textures/ui/btn_undo.png diff --git a/data-shared/textures/ui/btn_undo.png b/data-shared/textures/ui/btn_undo.png new file mode 100644 index 0000000000000000000000000000000000000000..1a0c6b82b8873a6ecb377317d8c8de4f28b3e60a GIT binary patch literal 1244 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 = this->wm->create_widget( this->get_surface(), TMS_WDG_BUTTON, GW_UNDO, AREA_TOP_LEFT, - gui_spritesheet::get_sprite(S_EMPTY), 0); + gui_spritesheet::get_sprite(S_UNDO), 0); this->wdg_undo->priority = 700; const char* undo_tooltip = "Undo" diff --git a/src/src/game.cc b/src/src/game.cc index b45f1b1d..cda8b4fd 100644 --- a/src/src/game.cc +++ b/src/src/game.cc @@ -7185,7 +7185,10 @@ game::open_sandbox(int id_type, uint32_t id) } void game::open_sandbox_snapshot_mem(const void* snapshot, size_t size) { - tms_assertf(this->state.sandbox, "level is not a sandbox"); + tms_assertf(this->state.sandbox, + "FATAL ERROR. Level is not a sandbox. This is a bug.\n" + "open_sandbox_snapshot_mem is only supported while in sandbox mode.\n" + "You probably tried to undo while playing a level, which shouldn't be possible"); W->lb.clear(); W->lb.ensure(size); diff --git a/src/src/gui.cc b/src/src/gui.cc index 111280dd..2293fb76 100644 --- a/src/src/gui.cc +++ b/src/src/gui.cc @@ -116,6 +116,7 @@ struct sprite_load_data gui_spritesheet::sprites[NUM_SPRITES] = { { "data-shared/textures/ui/btn_ccw.png", &atlas }, { "data-shared/textures/ui/btn_lock.png", &atlas }, { "data-shared/textures/ui/btn_unlock.png", &atlas }, + { "data-shared/textures/ui/btn_undo.png", &atlas }, { "data-shared/textures/menu/menu_play.png", &atlas }, { "data-shared/textures/menu/menu_create.png", &atlas }, diff --git a/src/src/gui.hh b/src/src/gui.hh index 5d831710..58e24de7 100644 --- a/src/src/gui.hh +++ b/src/src/gui.hh @@ -92,6 +92,7 @@ enum { S_CCW, S_BTN_LOCK, S_BTN_UNLOCK, + S_UNDO, S_MENU_PLAY, S_MENU_CREATE, From 9a79d058b29149bab9ac506458220ce33a01b34b Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Tue, 15 Oct 2024 21:13:35 +0200 Subject: [PATCH 09/25] keep cam pos after undo --- src/src/gundo.cc | 13 ++++++++++++- src/src/gundo.hh | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/src/gundo.cc b/src/src/gundo.cc index a8fd5545..d5ac5dc5 100644 --- a/src/src/gundo.cc +++ b/src/src/gundo.cc @@ -56,7 +56,14 @@ void undo_stack::checkpoint(const char *reason) { ); } -const char* undo_stack::restore() { +const char* undo_stack::restore(bool keep_cam_pos /* = true */) { + float cx, cy, cz; + if (keep_cam_pos) { + cx = G->cam->_position.x; + cy = G->cam->_position.y; + cz = G->cam->_position.z; + } + if (this->items.size() == 0) { tms_fatalf("undo_load: no items to load"); } @@ -68,5 +75,9 @@ const char* undo_stack::restore() { tms_infof("Restored level from undo stack"); free(item.data); + + if (keep_cam_pos) { + G->cam->set_position(cx, cy, cz); + } return item.reason; } diff --git a/src/src/gundo.hh b/src/src/gundo.hh index 9463d999..6bf370dc 100644 --- a/src/src/gundo.hh +++ b/src/src/gundo.hh @@ -36,7 +36,7 @@ struct undo_stack { // Returns the reason for the last checkpoint // // Avoid using this function directly, use P.add_action(ACTION_UNDO_RESTORE, 0) instead - const char* restore(); + const char* restore(bool keep_cam_pos = true); }; extern struct undo_stack undo; From ab2825cfdba0a888e8ffa504924d0334e09ecab0 Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Wed, 16 Oct 2024 10:00:04 +0200 Subject: [PATCH 10/25] add more logging --- src/src/gundo.cc | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/src/gundo.cc b/src/src/gundo.cc index d5ac5dc5..acc65032 100644 --- a/src/src/gundo.cc +++ b/src/src/gundo.cc @@ -5,6 +5,10 @@ #include "world.hh" #include +#ifdef DEBUG +#include +#endif + // TODO griffi-gh: call reset on level load // TODO griffi-gh: support redo // TODO griffi-gh: compress undo items? @@ -20,6 +24,10 @@ void undo_stack::reset() { } void undo_stack::checkpoint(const char *reason) { +#ifdef DEBUG + auto started = std::chrono::high_resolution_clock::now(); +#endif + W->save(SAVE_TYPE_UNDO); void *data_copy = malloc(W->lb.size); @@ -28,11 +36,6 @@ void undo_stack::checkpoint(const char *reason) { struct undo_item item = { reason, data_copy, W->lb.size }; this->items.push_back(item); - tms_debugf( - "undo_checkpoint: item: %lu bytes; ptr %p", - item.size, item.data - ); - if (this->items.size() > MAX_UNDO_ITEMS) { const void *data = this->items.front().data; free((void *)data); @@ -40,13 +43,21 @@ void undo_stack::checkpoint(const char *reason) { } #ifdef DEBUG + auto done = std::chrono::high_resolution_clock::now(); size_t total_size_bytes = 0; for (const struct undo_item &item : this->items) { total_size_bytes += item.size; } tms_debugf( - "undo_checkpoint: total size: %lu bytes\n", - total_size_bytes + "undo_checkpoint:\n" + " - item: %lu bytes; ptr %p\n" + " - total size: %lu bytes\n" + " - histsize: %lu/%u\n" + " - time: %lu ns\n", + item.size, item.data, + total_size_bytes, + this->items.size(), MAX_UNDO_ITEMS, + std::chrono::duration_cast(done - started).count() ); #endif From 1e4bd3c7cf5c602e76d6bcaab5dbc423e93a6da0 Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Wed, 16 Oct 2024 10:25:24 +0200 Subject: [PATCH 11/25] cp on shift e --- src/src/game.cc | 9 +++++++++ src/src/gundo.cc | 20 ++++++++++++++------ src/src/gundo.hh | 5 ++++- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/src/game.cc b/src/src/game.cc index cda8b4fd..636e9cff 100644 --- a/src/src/game.cc +++ b/src/src/game.cc @@ -20,6 +20,7 @@ #include "gravityman.hh" #include "grid.hh" #include "group.hh" +#include "gundo.hh" #include "i0o1gate.hh" #include "i1o1gate.hh" #include "i2o1gate.hh" @@ -8006,6 +8007,14 @@ game::handle_input_paused(tms::event *ev, int action) case TMS_KEY_E: if (ev->data.key.mod & TMS_MOD_SHIFT && this->state.sandbox && this->selection.e) { + // Save undo state if any items are going to be modified + for (c_map::iterator it = this->pairs.begin(); it != this->pairs.end(); ++it) { + if (!it->second->typeselect) { + undo.checkpoint("Connect all"); + break; + } + } + entity *saved = this->selection.e; for (c_map::iterator it = this->pairs.begin(); it != this->pairs.end(); ++it) { connection *c = it->second; diff --git a/src/src/gundo.cc b/src/src/gundo.cc index acc65032..8a2e002b 100644 --- a/src/src/gundo.cc +++ b/src/src/gundo.cc @@ -23,17 +23,25 @@ void undo_stack::reset() { this->items.clear(); } -void undo_stack::checkpoint(const char *reason) { -#ifdef DEBUG - auto started = std::chrono::high_resolution_clock::now(); -#endif - +void* undo_stack::snapshot_state() { W->save(SAVE_TYPE_UNDO); void *data_copy = malloc(W->lb.size); memcpy(data_copy, W->lb.buf, W->lb.size); - struct undo_item item = { reason, data_copy, W->lb.size }; + return data_copy; +} + +void undo_stack::checkpoint(const char *reason, void *snapshot /* = nullptr */) { +#ifdef DEBUG + auto started = std::chrono::high_resolution_clock::now(); +#endif + + if (snapshot == nullptr) { + snapshot = this->snapshot_state(); + } + + struct undo_item item = { reason, snapshot, W->lb.size }; this->items.push_back(item); if (this->items.size() > MAX_UNDO_ITEMS) { diff --git a/src/src/gundo.hh b/src/src/gundo.hh index 6bf370dc..03741181 100644 --- a/src/src/gundo.hh +++ b/src/src/gundo.hh @@ -26,11 +26,14 @@ struct undo_stack { // Avoid using this function directly if possible, use P.add_action(ACTION_UNDO_RESET, 0) instead void reset(); + // Create a snapshot of the current level state + void* snapshot_state(); + // Save the current level state to the undo stack // // Avoid using this function directly if possible, use P.add_action(ACTION_UNDO_CHECKPOINT, reason) instead // Although, if you're calling it right before the state of the level changes, you MUST call it directly - void checkpoint(const char* reason = nullptr); + void checkpoint(const char* reason = nullptr, void* snapshot = nullptr); // Restore the last saved level state from the undo stack // Returns the reason for the last checkpoint From 48b798563b1275f2d318354a6d2ff0ef9631fd91 Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Wed, 16 Oct 2024 10:28:35 +0200 Subject: [PATCH 12/25] cp on floating connect --- src/src/game.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/src/game.cc b/src/src/game.cc index 636e9cff..deb8f1c0 100644 --- a/src/src/game.cc +++ b/src/src/game.cc @@ -9844,6 +9844,8 @@ game::check_click_conn(int x, int y) return true; } + undo.checkpoint("Connection"); + if (W->is_adventure() && W->is_playing()) { this->apply_connection(c, c->option); } else { From 9cb23034ac3b1e4126e0b0e45d3869b007b5c812 Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Wed, 16 Oct 2024 10:33:16 +0200 Subject: [PATCH 13/25] cp il con --- src/src/game.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/src/game.cc b/src/src/game.cc index deb8f1c0..14d3fefd 100644 --- a/src/src/game.cc +++ b/src/src/game.cc @@ -9635,8 +9635,10 @@ game::check_click_conntype(int x, int y) : b2Vec2(pt[this->cs_conn->layer].x, pt[this->cs_conn->layer].y); if ((p1 - point).Length() < w) { + undo.checkpoint("Connection (Fixed)"); this->apply_connection(this->cs_conn, 0); } else if ((p2 - point).Length() < w) { + undo.checkpoint("Connection (Rotating)"); this->apply_connection(this->cs_conn, 1); } From 2a049ed21fd7d7d4114c8a76f7ea4d8a6fbd24d3 Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Wed, 16 Oct 2024 10:38:12 +0200 Subject: [PATCH 14/25] cp import partial --- src/src/game.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/src/game.cc b/src/src/game.cc index 14d3fefd..04a0efc0 100644 --- a/src/src/game.cc +++ b/src/src/game.cc @@ -8378,6 +8378,7 @@ game::handle_input_paused(tms::event *ev, int action) #endif ) { tms_debugf("IMPORT (%.2f)", dist); + undo.checkpoint("Import partial"); this->import_object(this->multi.import->lvl_id); } } else { From 2a77e40d12887097f8ec811f88f7ffbca02eaa02 Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Wed, 16 Oct 2024 10:57:19 +0200 Subject: [PATCH 15/25] keep selection on restore --- src/src/gundo.cc | 29 ++++++++++++++++++++++++++--- src/src/gundo.hh | 10 +++++++++- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/src/gundo.cc b/src/src/gundo.cc index 8a2e002b..2f237d87 100644 --- a/src/src/gundo.cc +++ b/src/src/gundo.cc @@ -3,6 +3,7 @@ #include "game.hh" #include "tms/backend/print.h" #include "world.hh" +#include #include #ifdef DEBUG @@ -75,14 +76,25 @@ void undo_stack::checkpoint(const char *reason, void *snapshot /* = nullptr */) ); } -const char* undo_stack::restore(bool keep_cam_pos /* = true */) { +const char* undo_stack::restore(uint8_t flags) { + // Save the current camera position float cx, cy, cz; - if (keep_cam_pos) { + if (flags & UNDO_KEEP_CAM_POS) { cx = G->cam->_position.x; cy = G->cam->_position.y; cz = G->cam->_position.z; } + // Save the current selection + uint32_t saved_selection_id = 0; + if (flags & UNDO_KEEP_SELECTION) { + if (G->selection.e != nullptr) { + saved_selection_id = G->selection.e->id; + } else { + tms_warnf("undo_restore: no selection to keep"); + } + } + if (this->items.size() == 0) { tms_fatalf("undo_load: no items to load"); } @@ -95,8 +107,19 @@ const char* undo_stack::restore(bool keep_cam_pos /* = true */) { free(item.data); - if (keep_cam_pos) { + if (flags & UNDO_KEEP_CAM_POS) { G->cam->set_position(cx, cy, cz); } + if (flags & UNDO_KEEP_SELECTION) { + G->selection.disable(); + if (saved_selection_id) { + if (entity *e = W->get_entity_by_id(saved_selection_id)) { + G->selection.select(e); + } else { + tms_warnf("undo_restore: selection entity not found"); + } + } + } + return item.reason; } diff --git a/src/src/gundo.hh b/src/src/gundo.hh index 03741181..ebf08748 100644 --- a/src/src/gundo.hh +++ b/src/src/gundo.hh @@ -1,6 +1,7 @@ #pragma once #include +#include #include #define MAX_UNDO_ITEMS 100 @@ -11,6 +12,13 @@ struct undo_item { size_t size; }; +enum { + UNDO_KEEP_NONE = 0b00, + UNDO_KEEP_CAM_POS = 0b01, + UNDO_KEEP_SELECTION = 0b10, + UNDO_KEEP_ALL = 0b11, +}; + struct undo_stack { private: std::vector items; @@ -39,7 +47,7 @@ struct undo_stack { // Returns the reason for the last checkpoint // // Avoid using this function directly, use P.add_action(ACTION_UNDO_RESTORE, 0) instead - const char* restore(bool keep_cam_pos = true); + const char* restore(uint8_t flags = UNDO_KEEP_ALL); }; extern struct undo_stack undo; From c43d873e291264633ad1bde601a502cecfc1d584 Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Wed, 16 Oct 2024 10:59:50 +0200 Subject: [PATCH 16/25] do not checkpoint outside of paused sandbox mode --- src/src/gundo.cc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/src/gundo.cc b/src/src/gundo.cc index 2f237d87..94eff43c 100644 --- a/src/src/gundo.cc +++ b/src/src/gundo.cc @@ -34,6 +34,14 @@ void* undo_stack::snapshot_state() { } void undo_stack::checkpoint(const char *reason, void *snapshot /* = nullptr */) { + if (!W->paused) { + tms_warnf("undo_checkpoint: SKIPPING, BECAUSE not paused"); + return; + } else if (!G->state.sandbox) { + tms_warnf("undo_checkpoint: SKIPPING, BECAUSE state != sandbox"); + return; + } + #ifdef DEBUG auto started = std::chrono::high_resolution_clock::now(); #endif From 400430c176ab36e9bf0ae51a4246d38522318f82 Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Wed, 16 Oct 2024 11:02:32 +0200 Subject: [PATCH 17/25] fadeout undo button if nothing to undo --- src/src/game-gui.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/src/game-gui.cc b/src/src/game-gui.cc index 4509f6fe..2bfc829d 100644 --- a/src/src/game-gui.cc +++ b/src/src/game-gui.cc @@ -2396,6 +2396,7 @@ game::refresh_widgets() } this->wdg_undo->add(); // XXX griffi-gh: Is this the right place? + this->wdg_undo->faded = undo.amount() == 0; } if ((this->state.sandbox || this->state.test_playing || W->is_paused() || W->is_puzzle()) && (W->level.type != LCAT_ADVENTURE || W->is_paused())) { From bff01ea840a3c5c81142c1ba8eb68d0817ca037f Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Wed, 16 Oct 2024 11:09:57 +0200 Subject: [PATCH 18/25] fix return --- src/src/game-gui.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/src/game-gui.cc b/src/src/game-gui.cc index 2bfc829d..103b8d76 100644 --- a/src/src/game-gui.cc +++ b/src/src/game-gui.cc @@ -1007,7 +1007,7 @@ game::widget_clicked(principia_wdg *w, uint8_t button_id, int pid) case GW_UNDO: P.add_action(ACTION_UNDO_RESTORE, 0); - break; + return true; case GW_MODE: { From d2d6d502872aa4f227c8f7c6c74b0de13255fbb8 Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Wed, 16 Oct 2024 11:18:43 +0200 Subject: [PATCH 19/25] change condition for wdg_undo --- src/src/game-gui.cc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/src/game-gui.cc b/src/src/game-gui.cc index 103b8d76..d10d70ef 100644 --- a/src/src/game-gui.cc +++ b/src/src/game-gui.cc @@ -2394,8 +2394,12 @@ game::refresh_widgets() case 0b011: G->wdg_layervis->s[0] = gui_spritesheet::get_sprite(S_LAYERVIS_2); break; default: case 0b111: G->wdg_layervis->s[0] = gui_spritesheet::get_sprite(S_LAYERVIS_3); break; } + } - this->wdg_undo->add(); // XXX griffi-gh: Is this the right place? + // XXX griffi-gh: Is this the right condition? + // Shouldn't we always show the undo button in sandbox mode? (even without advanced mode) + if (G->state.sandbox && W->is_paused() && G->state.advanced_mode) { + this->wdg_undo->add(); this->wdg_undo->faded = undo.amount() == 0; } From 2843aa2c6165c5eb199b7a19efb07ce2e6ae8213 Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Wed, 16 Oct 2024 11:19:20 +0200 Subject: [PATCH 20/25] disable advanced mode condition --- src/src/game-gui.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/src/game-gui.cc b/src/src/game-gui.cc index d10d70ef..b668b41e 100644 --- a/src/src/game-gui.cc +++ b/src/src/game-gui.cc @@ -2398,7 +2398,7 @@ game::refresh_widgets() // XXX griffi-gh: Is this the right condition? // Shouldn't we always show the undo button in sandbox mode? (even without advanced mode) - if (G->state.sandbox && W->is_paused() && G->state.advanced_mode) { + if (G->state.sandbox && W->is_paused() /*&& G->state.advanced_mode*/) { this->wdg_undo->add(); this->wdg_undo->faded = undo.amount() == 0; } From d55a82ffd02b7e408c8c82308866b4a0f066e9d3 Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Wed, 16 Oct 2024 11:26:59 +0200 Subject: [PATCH 21/25] hopefully fix android ci --- src/src/gundo.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/src/gundo.cc b/src/src/gundo.cc index 94eff43c..096e3e20 100644 --- a/src/src/gundo.cc +++ b/src/src/gundo.cc @@ -50,7 +50,7 @@ void undo_stack::checkpoint(const char *reason, void *snapshot /* = nullptr */) snapshot = this->snapshot_state(); } - struct undo_item item = { reason, snapshot, W->lb.size }; + struct undo_item item = { reason, snapshot, static_cast(W->lb.size) }; this->items.push_back(item); if (this->items.size() > MAX_UNDO_ITEMS) { From 8d4e855089a6f2dad2100eb27e383b3e2f71b0ec Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Wed, 16 Oct 2024 16:01:54 +0200 Subject: [PATCH 22/25] implement undo stack async compression --- src/src/gundo.cc | 206 ++++++++++++++++++++++++++++++++++++++++++++--- src/src/gundo.hh | 39 ++++++++- 2 files changed, 229 insertions(+), 16 deletions(-) diff --git a/src/src/gundo.cc b/src/src/gundo.cc index 096e3e20..1081d1c6 100644 --- a/src/src/gundo.cc +++ b/src/src/gundo.cc @@ -3,24 +3,162 @@ #include "game.hh" #include "tms/backend/print.h" #include "world.hh" + +#include #include #include +#include +#include #ifdef DEBUG #include #endif +#define COMPRESSION_LEVEL 3 + +#define UNDOTHR_LOG "undo_thread_worker: " + // TODO griffi-gh: call reset on level load // TODO griffi-gh: support redo // TODO griffi-gh: compress undo items? struct undo_stack undo; +undo_stack::undo_stack() { + undo_stack::start_thread(); +} + +undo_stack::~undo_stack() { + undo_stack::reset(); + undo_stack::kill_thread(); +} + +int _thread_worker(void *data) { + undo_stack *stack = (undo_stack *)data; + + while (true) { + std::unique_lock run_compressor_lock(stack->m_run_compressor); + stack->c_run_compressor.wait(run_compressor_lock, [stack] { + return stack->run_compressor != WORKER_IDLE; + }); + if (stack->run_compressor == WORKER_KYS) { + stack->run_compressor = WORKER_IDLE; + run_compressor_lock.unlock(); + break; + } else { + stack->run_compressor = WORKER_IDLE; + run_compressor_lock.unlock(); + } + + tms_debugf(UNDOTHR_LOG "=== RUNNING COMPRESSOR ==="); + + undo_item *item_to_compress = nullptr; + + while (true) { + // Find the first uncompressed undo item + tms_debugf(UNDOTHR_LOG "look for item to compress..."); + { + std::lock_guard lock(stack->m_items); + for (struct undo_item *&item : stack->items) { + std::lock_guard lock(item->mutex); + if (!item->compressed) { + item_to_compress = item; + break; + } + } + } + + + // If no item was found, break the loop and wait for the next signal + if (item_to_compress == nullptr) { + tms_debugf(UNDOTHR_LOG "done compressing, waiting for next signal..."); + break; + } + + tms_debugf(UNDOTHR_LOG "found item: %p", item_to_compress); + + // Lock the item and compress it in-place + { + std::lock_guard lock(item_to_compress->mutex); + + size_t _original_size = item_to_compress->size; + + // 1. Allocate a temporary buffer for compression + unsigned long compressed_size = compressBound(item_to_compress->size); + unsigned char* temp_buffer = new unsigned char[compressed_size]; + + // 2. Compress the data into the temporary buffer + int result = compress2( + temp_buffer, &compressed_size, + (const Bytef*)item_to_compress->data, item_to_compress->size, + COMPRESSION_LEVEL + ); + + if (result != Z_OK) { + tms_fatalf(UNDOTHR_LOG "compression failed: %d", result); + } + + // 3. Resize the item's data buffer + item_to_compress->data = realloc(item_to_compress->data, compressed_size); + item_to_compress->size = compressed_size; + + // 4. Copy compressed data back to the original buffer + std::memcpy(item_to_compress->data, temp_buffer, compressed_size); + delete[] temp_buffer; + + // mark the item as compressed + item_to_compress->compressed = true; + + tms_debugf(UNDOTHR_LOG "undo_thread_worker: compressed %lu bytes to %lu bytes", _original_size, compressed_size); + item_to_compress = nullptr; + } + } + } + + return 0; +} + +void undo_stack::signal_to_run_compressor() { + tms_debugf("signal_to_run_compressor: enter"); + this->m_run_compressor.lock(); + this->run_compressor = true; + this->m_run_compressor.unlock(); + this->c_run_compressor.notify_all(); + tms_debugf("signal_to_run_compressor: leave"); +} + +void undo_stack::start_thread() { + if (this->compressor_thread != nullptr) { + tms_warnf("undo_thread_worker: thread already running"); + return; + } + this->compressor_thread = SDL_CreateThread(_thread_worker, "undocomp_thr", this); +} + +void undo_stack::kill_thread() { + if (this->compressor_thread == nullptr) { + tms_fatalf("thread not running"); + } + + this->m_run_compressor.lock(); + this->run_compressor = WORKER_KYS; + this->m_run_compressor.unlock(); + this->c_run_compressor.notify_all(); + + int status = 0; + SDL_WaitThread(this->compressor_thread, &status); + tms_debugf("undo_thread_worker: thread exited with status %d", status); + + this->compressor_thread = nullptr; +} + size_t undo_stack::amount() { + std::lock_guard guard(this->m_items); return this->items.size(); } void undo_stack::reset() { + std::lock_guard guard(this->m_items); this->items.clear(); } @@ -34,6 +172,8 @@ void* undo_stack::snapshot_state() { } void undo_stack::checkpoint(const char *reason, void *snapshot /* = nullptr */) { + std::lock_guard guard_items(this->m_items); + if (!W->paused) { tms_warnf("undo_checkpoint: SKIPPING, BECAUSE not paused"); return; @@ -50,20 +190,28 @@ void undo_stack::checkpoint(const char *reason, void *snapshot /* = nullptr */) snapshot = this->snapshot_state(); } - struct undo_item item = { reason, snapshot, static_cast(W->lb.size) }; + struct undo_item *item = new undo_item { + reason, + snapshot, + static_cast(W->lb.size), + static_cast(W->lb.size), + false, + }; + std::lock_guard guard_cur_item(item->mutex); this->items.push_back(item); if (this->items.size() > MAX_UNDO_ITEMS) { - const void *data = this->items.front().data; - free((void *)data); + undo_item *front_item = this->items.front(); + std::lock_guard guard(front_item->mutex); + free((void *)front_item->data); this->items.erase(this->items.begin()); } #ifdef DEBUG auto done = std::chrono::high_resolution_clock::now(); size_t total_size_bytes = 0; - for (const struct undo_item &item : this->items) { - total_size_bytes += item.size; + for (struct undo_item *&item : this->items) { + total_size_bytes += item->size; } tms_debugf( "undo_checkpoint:\n" @@ -71,7 +219,7 @@ void undo_stack::checkpoint(const char *reason, void *snapshot /* = nullptr */) " - total size: %lu bytes\n" " - histsize: %lu/%u\n" " - time: %lu ns\n", - item.size, item.data, + item->size, item->data, total_size_bytes, this->items.size(), MAX_UNDO_ITEMS, std::chrono::duration_cast(done - started).count() @@ -82,6 +230,8 @@ void undo_stack::checkpoint(const char *reason, void *snapshot /* = nullptr */) "undo_checkpoint: histsize: %lu/%u\n", this->items.size(), MAX_UNDO_ITEMS ); + + this->signal_to_run_compressor(); } const char* undo_stack::restore(uint8_t flags) { @@ -103,17 +253,45 @@ const char* undo_stack::restore(uint8_t flags) { } } - if (this->items.size() == 0) { - tms_fatalf("undo_load: no items to load"); + struct undo_item *item; + { + std::lock_guard guard(this->m_items); + if (this->items.size() == 0) { + tms_fatalf("undo_load: no items to load"); + } + item = this->items.back(); + this->items.pop_back(); + } + tms_assertf(item, "undo_restore: item is null"); + + std::lock_guard guard(item->mutex); + + void *uncompressed_data; + size_t uncompressed_size; + if (item->compressed) { + tms_debugf("data is compressed, uncompressing..."); + uncompressed_data = std::malloc(item->size_uncompressed); + uncompressed_size = item->size_uncompressed; + uncompress( + (Bytef*)uncompressed_data, (uLongf*)&uncompressed_size, + (Bytef*)item->data, item->size + ); + } else { + tms_debugf("data is not compressed, using as is..."); + uncompressed_data = item->data; + uncompressed_size = item->size; } - struct undo_item &item = this->items.back(); - this->items.pop_back(); - G->open_sandbox_snapshot_mem(item.data, item.size); + G->lock(); + G->open_sandbox_snapshot_mem(uncompressed_data, uncompressed_size); tms_infof("Restored level from undo stack"); - free(item.data); + // Free the item's data + free(item->data); + if (item->compressed) { + free(uncompressed_data); + } if (flags & UNDO_KEEP_CAM_POS) { G->cam->set_position(cx, cy, cz); @@ -129,5 +307,7 @@ const char* undo_stack::restore(uint8_t flags) { } } - return item.reason; + G->unlock(); + + return item->reason; } diff --git a/src/src/gundo.hh b/src/src/gundo.hh index ebf08748..1592016e 100644 --- a/src/src/gundo.hh +++ b/src/src/gundo.hh @@ -3,13 +3,20 @@ #include #include #include +#include +#include +#include #define MAX_UNDO_ITEMS 100 +// TODO make background compression optional struct undo_item { const char *reason; void *data; size_t size; + size_t size_uncompressed; + bool compressed; + std::mutex mutex; }; enum { @@ -19,15 +26,41 @@ enum { UNDO_KEEP_ALL = 0b11, }; +enum { + WORKER_IDLE = 0, + WORKER_RUN = 1, + WORKER_KYS = 2, +}; + +int _thread_worker(void *data); + struct undo_stack { - private: - std::vector items; + // XXX: this should be private + public: + std::mutex m_items; + std::vector items; + + SDL_Thread *compressor_thread = nullptr; + + std::mutex m_run_compressor; + uint8_t run_compressor = WORKER_IDLE; + std::condition_variable c_run_compressor; + + void signal_to_run_compressor(); public: + undo_stack(); + ~undo_stack(); + + // Start the background compression thread + void start_thread(); + + // Kill the background compression thread + void kill_thread(); + // Get the number of items in the undo stack size_t amount(); - // Reset the undo stack // Call this while loading a new level // From 59a8fdaf241dd53bad06e40fdfd444e5de28911d Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Wed, 16 Oct 2024 18:27:08 +0200 Subject: [PATCH 23/25] increase comp level --- src/src/gundo.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/src/gundo.cc b/src/src/gundo.cc index 1081d1c6..e87acc2e 100644 --- a/src/src/gundo.cc +++ b/src/src/gundo.cc @@ -14,7 +14,7 @@ #include #endif -#define COMPRESSION_LEVEL 3 +#define COMPRESSION_LEVEL Z_BEST_COMPRESSION #define UNDOTHR_LOG "undo_thread_worker: " From df74cf8e8c12d260c635c00bc7c59da3773cb023 Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Wed, 16 Oct 2024 18:54:00 +0200 Subject: [PATCH 24/25] clean up code --- src/src/gundo.cc | 81 +++++++++++++++++++++++++++++++++--------------- src/src/gundo.hh | 9 ++++-- 2 files changed, 62 insertions(+), 28 deletions(-) diff --git a/src/src/gundo.cc b/src/src/gundo.cc index e87acc2e..f83dad12 100644 --- a/src/src/gundo.cc +++ b/src/src/gundo.cc @@ -24,29 +24,34 @@ struct undo_stack undo; -undo_stack::undo_stack() { - undo_stack::start_thread(); -} +undo_stack::undo_stack() {} undo_stack::~undo_stack() { undo_stack::reset(); - undo_stack::kill_thread(); + if (this->compressor_thread != nullptr) { + tms_warnf("undo_stack: thread was not cleaned up"); + undo_stack::kill_thread(); + } } -int _thread_worker(void *data) { +static int _thread_worker_main(void *data) { undo_stack *stack = (undo_stack *)data; + stack->_thread_worker(); + return 0; +} +void undo_stack::_thread_worker() { while (true) { - std::unique_lock run_compressor_lock(stack->m_run_compressor); - stack->c_run_compressor.wait(run_compressor_lock, [stack] { - return stack->run_compressor != WORKER_IDLE; + std::unique_lock run_compressor_lock(this->m_run_compressor); + this->c_run_compressor.wait(run_compressor_lock, [this] { + return this->run_compressor != WORKER_IDLE; }); - if (stack->run_compressor == WORKER_KYS) { - stack->run_compressor = WORKER_IDLE; + if (this->run_compressor == WORKER_KYS) { + this->run_compressor = WORKER_IDLE; run_compressor_lock.unlock(); break; } else { - stack->run_compressor = WORKER_IDLE; + this->run_compressor = WORKER_IDLE; run_compressor_lock.unlock(); } @@ -58,8 +63,8 @@ int _thread_worker(void *data) { // Find the first uncompressed undo item tms_debugf(UNDOTHR_LOG "look for item to compress..."); { - std::lock_guard lock(stack->m_items); - for (struct undo_item *&item : stack->items) { + std::lock_guard lock(this->m_items); + for (struct undo_item *&item : this->items) { std::lock_guard lock(item->mutex); if (!item->compressed) { item_to_compress = item; @@ -72,6 +77,30 @@ int _thread_worker(void *data) { // If no item was found, break the loop and wait for the next signal if (item_to_compress == nullptr) { tms_debugf(UNDOTHR_LOG "done compressing, waiting for next signal..."); +#ifdef DEBUG + { + size_t size_compressed = 0; + size_t size_uncompressed = 0; + std::lock_guard lock(this->m_items); + for (struct undo_item *&item : this->items) { + std::lock_guard lock(item->mutex); + size_compressed += item->size; + size_uncompressed += item->size_uncompressed; + } + tms_debugf(UNDOTHR_LOG + "undo storage condition\n" + " - items: %lu/%d\n" + " - size compressed: %lu\n" + " - size uncompressed: %lu\n" + " - compression ratio: %f", + this->items.size(), MAX_UNDO_ITEMS, + size_compressed, + size_uncompressed, + (float)size_compressed / size_uncompressed + ); + } +#endif + // Break the loop and wait for the next signal break; } @@ -114,8 +143,6 @@ int _thread_worker(void *data) { } } } - - return 0; } void undo_stack::signal_to_run_compressor() { @@ -132,7 +159,15 @@ void undo_stack::start_thread() { tms_warnf("undo_thread_worker: thread already running"); return; } - this->compressor_thread = SDL_CreateThread(_thread_worker, "undocomp_thr", this); + tms_infof("Undo compression worker starting..."); + this->compressor_thread = SDL_CreateThread(_thread_worker_main, "undocomp_thr", this); + tms_infof("Started undo compression worker thread"); +} + +void undo_stack::_ensure_thread_running() { + if (this->compressor_thread != nullptr) return; + tms_infof("Undo compression worker not running, starting..."); + this->start_thread(); } void undo_stack::kill_thread() { @@ -149,6 +184,8 @@ void undo_stack::kill_thread() { SDL_WaitThread(this->compressor_thread, &status); tms_debugf("undo_thread_worker: thread exited with status %d", status); + tms_infof("Undo compression worker stopped"); + this->compressor_thread = nullptr; } @@ -172,6 +209,8 @@ void* undo_stack::snapshot_state() { } void undo_stack::checkpoint(const char *reason, void *snapshot /* = nullptr */) { + this->_ensure_thread_running(); + std::lock_guard guard_items(this->m_items); if (!W->paused) { @@ -209,19 +248,11 @@ void undo_stack::checkpoint(const char *reason, void *snapshot /* = nullptr */) #ifdef DEBUG auto done = std::chrono::high_resolution_clock::now(); - size_t total_size_bytes = 0; - for (struct undo_item *&item : this->items) { - total_size_bytes += item->size; - } tms_debugf( "undo_checkpoint:\n" - " - item: %lu bytes; ptr %p\n" - " - total size: %lu bytes\n" - " - histsize: %lu/%u\n" + " - item: %lu bytes (before compression); ptr %p\n" " - time: %lu ns\n", item->size, item->data, - total_size_bytes, - this->items.size(), MAX_UNDO_ITEMS, std::chrono::duration_cast(done - started).count() ); #endif diff --git a/src/src/gundo.hh b/src/src/gundo.hh index 1592016e..62ba5f1a 100644 --- a/src/src/gundo.hh +++ b/src/src/gundo.hh @@ -32,11 +32,11 @@ enum { WORKER_KYS = 2, }; -int _thread_worker(void *data); +static int _thread_worker_main(void *data); struct undo_stack { // XXX: this should be private - public: + private: std::mutex m_items; std::vector items; @@ -47,11 +47,14 @@ struct undo_stack { std::condition_variable c_run_compressor; void signal_to_run_compressor(); - + void _ensure_thread_running(); public: undo_stack(); ~undo_stack(); + // Implementation detail, do not use + void _thread_worker(); + // Start the background compression thread void start_thread(); From d068c97059e20306064a9dbc1c5d64a8b1b2493b Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Wed, 16 Oct 2024 19:17:10 +0200 Subject: [PATCH 25/25] minor changes --- src/src/gundo.cc | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/src/gundo.cc b/src/src/gundo.cc index f83dad12..36fa7224 100644 --- a/src/src/gundo.cc +++ b/src/src/gundo.cc @@ -146,9 +146,10 @@ void undo_stack::_thread_worker() { } void undo_stack::signal_to_run_compressor() { + this->_ensure_thread_running(); tms_debugf("signal_to_run_compressor: enter"); this->m_run_compressor.lock(); - this->run_compressor = true; + this->run_compressor = WORKER_RUN; this->m_run_compressor.unlock(); this->c_run_compressor.notify_all(); tms_debugf("signal_to_run_compressor: leave"); @@ -209,8 +210,6 @@ void* undo_stack::snapshot_state() { } void undo_stack::checkpoint(const char *reason, void *snapshot /* = nullptr */) { - this->_ensure_thread_running(); - std::lock_guard guard_items(this->m_items); if (!W->paused) {