From baabeb966d915d38bd296e1fdbe93d1bdb94eefc Mon Sep 17 00:00:00 2001 From: Dregu Date: Sat, 3 Feb 2024 04:35:39 +0200 Subject: [PATCH 1/5] Save states I guess --- src/game_api/rpc.cpp | 30 ++++++++++++++++++ src/game_api/rpc.hpp | 1 + src/game_api/script/lua_vm.cpp | 14 +++++++++ src/game_api/search.cpp | 5 +++ src/injected/ui.cpp | 56 ++++++++++++++++++++++++++++++++++ src/injected/ui_util.cpp | 5 +++ src/injected/ui_util.hpp | 1 + 7 files changed, 112 insertions(+) diff --git a/src/game_api/rpc.cpp b/src/game_api/rpc.cpp index 5c54ee53b..5a3091483 100644 --- a/src/game_api/rpc.cpp +++ b/src/game_api/rpc.cpp @@ -1903,3 +1903,33 @@ void init_seeded(std::optional seed) auto* state = State::get().ptr(); isf(state, seed.value_or(state->seed)); } + +void copy_state(int from, int to) +{ + size_t arr = get_address("save_states"); + size_t iterIdx = 1; + size_t fromBaseState = memory_read(arr + (from - 1) * 8); + size_t toBaseState = memory_read(arr + (to - 1) * 8); + do + { + size_t copyContent = *(size_t*)((fromBaseState - 8) + iterIdx * 8); + // variable used to fix pointers that point somewhere in the same Thread + size_t diff = toBaseState - fromBaseState; + if (copyContent >= fromBaseState + 0x2000000 || copyContent <= fromBaseState) + { + diff = 0; + } + *(size_t*)(toBaseState + iterIdx * 8 + -8) = diff + copyContent; + + // Almost same code as before, but on the next value, idk why + copyContent = *(size_t*)(fromBaseState + iterIdx * 8); + diff = toBaseState - fromBaseState; + if (copyContent >= fromBaseState + 0x2000000 || copyContent <= fromBaseState) + { + diff = 0; + } + *(size_t*)(toBaseState + iterIdx * 8) = diff + copyContent; + + iterIdx = iterIdx + 2; + } while (iterIdx != 0x400001); +}; diff --git a/src/game_api/rpc.hpp b/src/game_api/rpc.hpp index d901f3420..bbb817ab4 100644 --- a/src/game_api/rpc.hpp +++ b/src/game_api/rpc.hpp @@ -137,3 +137,4 @@ void set_speedhack(std::optional multiplier); float get_speedhack(); void init_adventure(); void init_seeded(std::optional seed); +void copy_state(int from, int to); diff --git a/src/game_api/script/lua_vm.cpp b/src/game_api/script/lua_vm.cpp index c79d999c2..0a45e0e44 100644 --- a/src/game_api/script/lua_vm.cpp +++ b/src/game_api/script/lua_vm.cpp @@ -2249,6 +2249,20 @@ end /// Initializes some seedeed run related values and loads the character select screen, as if starting a new seeded run after entering the seed. lua["play_seeded"] = init_seeded; + /// Save current state to slot 1..4 + lua["save_state"] = [](int slot) + { + if (slot >= 1 && slot <= 4) + copy_state(5, slot); + }; + + /// Load current state from slot 1..4 + lua["load_state"] = [](int slot) + { + if (slot >= 1 && slot <= 4) + copy_state(slot, 5); + }; + lua.create_named_table("INPUTS", "NONE", 0x0, "JUMP", 0x1, "WHIP", 0x2, "BOMB", 0x4, "ROPE", 0x8, "RUN", 0x10, "DOOR", 0x20, "MENU", 0x40, "JOURNAL", 0x80, "LEFT", 0x100, "RIGHT", 0x200, "UP", 0x400, "DOWN", 0x800); lua.create_named_table("MENU_INPUT", "NONE", 0x0, "SELECT", 0x1, "BACK", 0x2, "DELETE", 0x4, "RANDOM", 0x8, "JOURNAL", 0x10, "LEFT", 0x20, "RIGHT", 0x40, "UP", 0x80, "DOWN", 0x100); diff --git a/src/game_api/search.cpp b/src/game_api/search.cpp index 89338b408..f5d806fa8 100644 --- a/src/game_api/search.cpp +++ b/src/game_api/search.cpp @@ -2110,6 +2110,11 @@ std::unordered_map g_address_rules{ PatternCommandBuffer{} .from_exe_base(0x22b7ca10) // TODO }, + { + "save_states"sv, + PatternCommandBuffer{} + .from_exe_base(0x22e0d1d0) // TODO + }, }; std::unordered_map g_cached_addresses; diff --git a/src/injected/ui.cpp b/src/injected/ui.cpp index fd4eb6685..d6ae0a43f 100644 --- a/src/injected/ui.cpp +++ b/src/injected/ui.cpp @@ -148,6 +148,14 @@ std::map default_keys{ {"hotbar_8", '8'}, {"hotbar_9", '9'}, {"hotbar_0", '0'}, + {"load_state_1", OL_KEY_SHIFT | VK_F1}, + {"load_state_2", OL_KEY_SHIFT | VK_F2}, + {"load_state_3", OL_KEY_SHIFT | VK_F3}, + {"load_state_4", OL_KEY_SHIFT | VK_F4}, + {"save_state_1", OL_KEY_SHIFT | VK_F5}, + {"save_state_2", OL_KEY_SHIFT | VK_F6}, + {"save_state_3", OL_KEY_SHIFT | VK_F7}, + {"save_state_4", OL_KEY_SHIFT | VK_F8}, {"toggle_hotbar", OL_KEY_CTRL | OL_KEY_SHIFT | 'B'}, {"spawn_layer_door", OL_KEY_SHIFT | VK_RETURN}, {"spawn_warp_door", OL_KEY_CTRL | OL_KEY_SHIFT | VK_RETURN}, @@ -3502,6 +3510,38 @@ bool process_keys(UINT nCode, WPARAM wParam, [[maybe_unused]] LPARAM lParam) { peek_layer = true; } + else if (pressed("save_state_1", wParam)) + { + UI::copy_state(5, 1); + } + else if (pressed("save_state_2", wParam)) + { + UI::copy_state(5, 2); + } + else if (pressed("save_state_3", wParam)) + { + UI::copy_state(5, 3); + } + else if (pressed("save_state_4", wParam)) + { + UI::copy_state(5, 4); + } + else if (pressed("load_state_1", wParam)) + { + UI::copy_state(1, 5); + } + else if (pressed("load_state_2", wParam)) + { + UI::copy_state(2, 5); + } + else if (pressed("load_state_3", wParam)) + { + UI::copy_state(3, 5); + } + else if (pressed("load_state_4", wParam)) + { + UI::copy_state(4, 5); + } else { return false; @@ -8433,6 +8473,22 @@ void render_game_props() } if (submenu("State")) { + ImGui::Text("Save state"); + for (int i = 1; i <= 4; ++i) + { + ImGui::SameLine(); + if (ImGui::Button(fmt::format(" {} ##SaveState{}", i, i).c_str())) + UI::copy_state(5, i); + } + + ImGui::Text("Load state"); + for (int i = 1; i <= 4; ++i) + { + ImGui::SameLine(); + if (ImGui::Button(fmt::format(" {} ##LoadState{}", i, i).c_str())) + UI::copy_state(i, 5); + } + render_screen("Current screen", g_state->screen); render_screen("Last screen", g_state->screen_last); render_screen("Next screen", g_state->screen_next); diff --git a/src/injected/ui_util.cpp b/src/injected/ui_util.cpp index c30306edc..c599e24a5 100644 --- a/src/injected/ui_util.cpp +++ b/src/injected/ui_util.cpp @@ -824,3 +824,8 @@ void UI::set_adventure_seed(int64_t first, int64_t second) { ::set_adventure_seed(first, second); } + +void UI::copy_state(int from, int to) +{ + ::copy_state(from, to); +} diff --git a/src/injected/ui_util.hpp b/src/injected/ui_util.hpp index af9cfdfbb..b837e3973 100644 --- a/src/injected/ui_util.hpp +++ b/src/injected/ui_util.hpp @@ -97,4 +97,5 @@ class UI static void init_seeded(uint32_t seed); static std::pair get_adventure_seed(std::optional run_start); static void set_adventure_seed(int64_t first, int64_t second); + static void copy_state(int from, int to); }; From ef2a55e2cb706036482522d3a66ed0df5f282a1a Mon Sep 17 00:00:00 2001 From: Dregu Date: Sat, 3 Feb 2024 06:29:45 +0200 Subject: [PATCH 2/5] Add some kind of safeties and save state invalidation --- src/game_api/rpc.cpp | 20 +++++++++++++++++ src/game_api/rpc.hpp | 2 ++ src/game_api/script/events.cpp | 3 +++ src/game_api/script/lua_vm.cpp | 10 ++++++++- src/injected/ui.cpp | 40 ++++++++++++++++++++++++++-------- src/injected/ui_util.cpp | 5 +++++ src/injected/ui_util.hpp | 2 ++ 7 files changed, 72 insertions(+), 10 deletions(-) diff --git a/src/game_api/rpc.cpp b/src/game_api/rpc.cpp index 5a3091483..9d3cb9487 100644 --- a/src/game_api/rpc.cpp +++ b/src/game_api/rpc.cpp @@ -1933,3 +1933,23 @@ void copy_state(int from, int to) iterIdx = iterIdx + 2; } while (iterIdx != 0x400001); }; + +StateMemory* get_save_state(int slot) +{ + size_t arr = get_address("save_states"); + size_t base = memory_read(arr + (slot - 1) * 8); + auto state = reinterpret_cast(base + 0x4a0); + if (state->screen) + return state; + return nullptr; +} + +void invalidate_save_states() +{ + for (int i = 1; i <= 4; ++i) + { + auto state = get_save_state(i); + if (state) + state->screen = 0; + } +} diff --git a/src/game_api/rpc.hpp b/src/game_api/rpc.hpp index bbb817ab4..dbb8abfd7 100644 --- a/src/game_api/rpc.hpp +++ b/src/game_api/rpc.hpp @@ -138,3 +138,5 @@ float get_speedhack(); void init_adventure(); void init_seeded(std::optional seed); void copy_state(int from, int to); +StateMemory* get_save_state(int slot); +void invalidate_save_states(); diff --git a/src/game_api/script/events.cpp b/src/game_api/script/events.cpp index 631e8eb85..1533a32a7 100644 --- a/src/game_api/script/events.cpp +++ b/src/game_api/script/events.cpp @@ -78,7 +78,10 @@ bool pre_unload_level() return !block; }); if (!block) + { g_level_loaded = false; + invalidate_save_states(); + } return block; } bool pre_init_level() diff --git a/src/game_api/script/lua_vm.cpp b/src/game_api/script/lua_vm.cpp index 0a45e0e44..8b65a7aab 100644 --- a/src/game_api/script/lua_vm.cpp +++ b/src/game_api/script/lua_vm.cpp @@ -2259,10 +2259,18 @@ end /// Load current state from slot 1..4 lua["load_state"] = [](int slot) { - if (slot >= 1 && slot <= 4) + if (slot >= 1 && slot <= 4 && get_save_state(slot)) copy_state(slot, 5); }; + /// Get saved state from slot + lua["get_save_state"] = [](int slot) -> StateMemory* + { + if (slot >= 1 && slot <= 5) + return get_save_state(slot); + return nullptr; + }; + lua.create_named_table("INPUTS", "NONE", 0x0, "JUMP", 0x1, "WHIP", 0x2, "BOMB", 0x4, "ROPE", 0x8, "RUN", 0x10, "DOOR", 0x20, "MENU", 0x40, "JOURNAL", 0x80, "LEFT", 0x100, "RIGHT", 0x200, "UP", 0x400, "DOWN", 0x800); lua.create_named_table("MENU_INPUT", "NONE", 0x0, "SELECT", 0x1, "BACK", 0x2, "DELETE", 0x4, "RANDOM", 0x8, "JOURNAL", 0x10, "LEFT", 0x20, "RIGHT", 0x40, "UP", 0x80, "DOWN", 0x100); diff --git a/src/injected/ui.cpp b/src/injected/ui.cpp index d6ae0a43f..ba51d4dc9 100644 --- a/src/injected/ui.cpp +++ b/src/injected/ui.cpp @@ -2801,6 +2801,23 @@ void toggle_lights() } } +void load_state(int slot) +{ + StateMemory* target = UI::get_save_state(slot); + if (!target) + return; + if (g_state->screen == 14 && target->screen != 14) + { + g_state->screen = 12; + g_game_manager->journal_ui->fade_timer = 15; + g_game_manager->journal_ui->state = 5; + g_state->camera->focus_offset_x = 0; + g_state->camera->focus_offset_y = 0; + set_camera_bounds(true); + } + UI::copy_state(slot, 5); +} + bool process_keys(UINT nCode, WPARAM wParam, [[maybe_unused]] LPARAM lParam) { ImGuiContext& g = *GImGui; @@ -3528,19 +3545,19 @@ bool process_keys(UINT nCode, WPARAM wParam, [[maybe_unused]] LPARAM lParam) } else if (pressed("load_state_1", wParam)) { - UI::copy_state(1, 5); + load_state(1); } else if (pressed("load_state_2", wParam)) { - UI::copy_state(2, 5); + load_state(2); } else if (pressed("load_state_3", wParam)) { - UI::copy_state(3, 5); + load_state(3); } else if (pressed("load_state_4", wParam)) { - UI::copy_state(4, 5); + load_state(4); } else { @@ -8473,21 +8490,26 @@ void render_game_props() } if (submenu("State")) { - ImGui::Text("Save state"); for (int i = 1; i <= 4; ++i) { - ImGui::SameLine(); if (ImGui::Button(fmt::format(" {} ##SaveState{}", i, i).c_str())) UI::copy_state(5, i); + tooltip("Save current level state", fmt::format("save_state_{}", i).c_str()); + ImGui::SameLine(); } + ImGui::Text("Save state"); - ImGui::Text("Load state"); for (int i = 1; i <= 4; ++i) { - ImGui::SameLine(); + bool valid = UI::get_save_state(i) != nullptr; + ImGui::BeginDisabled(!valid); if (ImGui::Button(fmt::format(" {} ##LoadState{}", i, i).c_str())) - UI::copy_state(i, 5); + load_state(i); + ImGui::EndDisabled(); + tooltip("Load current level state", fmt::format("load_state_{}", i).c_str()); + ImGui::SameLine(); } + ImGui::Text("Load state"); render_screen("Current screen", g_state->screen); render_screen("Last screen", g_state->screen_last); diff --git a/src/injected/ui_util.cpp b/src/injected/ui_util.cpp index c599e24a5..3e792d0a3 100644 --- a/src/injected/ui_util.cpp +++ b/src/injected/ui_util.cpp @@ -829,3 +829,8 @@ void UI::copy_state(int from, int to) { ::copy_state(from, to); } + +StateMemory* UI::get_save_state(int slot) +{ + return ::get_save_state(slot); +} diff --git a/src/injected/ui_util.hpp b/src/injected/ui_util.hpp index b837e3973..991c97f4d 100644 --- a/src/injected/ui_util.hpp +++ b/src/injected/ui_util.hpp @@ -16,6 +16,7 @@ class SparkTrap; struct SaveData; struct Illumination; struct AABB; +struct StateMemory; constexpr uint32_t set_flag(uint32_t& flags, int bit) { @@ -98,4 +99,5 @@ class UI static std::pair get_adventure_seed(std::optional run_start); static void set_adventure_seed(int64_t first, int64_t second); static void copy_state(int from, int to); + static StateMemory* get_save_state(int slot); }; From 3662b80c99d7b2d32d219271c27ce07b94cd8dc5 Mon Sep 17 00:00:00 2001 From: Dregu Date: Sat, 3 Feb 2024 16:24:37 +0200 Subject: [PATCH 3/5] update barrymod --- examples/barrymod.lua | 132 +++++++----------------------------------- 1 file changed, 20 insertions(+), 112 deletions(-) diff --git a/examples/barrymod.lua b/examples/barrymod.lua index d6fe77524..c51cb2873 100644 --- a/examples/barrymod.lua +++ b/examples/barrymod.lua @@ -1,130 +1,38 @@ meta.name = 'Barrymod' -meta.version = 'WIP' -meta.description = 'Restarts the current level on death or manually like nothing happened. Not everything from Inventory is implemented, cause it\'s not in the api yet. Sometimes also gets the level gen wrong and atm screws up journal progress by design.' +meta.version = '1.0' +meta.description = 'Restarts the current level on death or manually like nothing happened. ' meta.author = 'Dregu' -local status = {} -local restart = false - -local vars = { - state = {'seed', 'level_count', 'time_total', 'shoppie_aggro', 'shoppie_aggro_next', 'merchant_aggro', 'kali_favor', 'kali_status', 'kali_altars_destroyed', 'level_flags', 'quest_flags', 'journal_flags', 'presence_flags', 'special_visibility_flags', 'kills_npc', 'damage_taken', 'time_last_level', 'saved_dogs', 'saved_cats', 'saved_hamsters', 'money_last_levels', 'money_shop_total', 'correct_ushabti'}, - quests = {'yang_state', 'jungle_susters_flags', 'van_horsing_state', 'sparrow_state', 'madame_tusk_state', 'beg_state'}, - inventory = {'health', 'bombs', 'ropes', 'held_item', 'held_item_metadata', 'kapala_blood_amount', 'poison_tick_timer', 'cursed', 'elixir_buff', 'mount_type', 'mount_metadata', 'kills_level', 'kills_total', 'collected_money_total'} -} - -local names = {} -for i,v in pairs(ENT_TYPE) do - names[v] = i -end - -local function save(from, arr) - for i,v in ipairs(arr) do - status[v] = from[v] - end - print("Saved state") -end - -local function load(to, arr) - for i,v in ipairs(arr) do - if status[v] ~= nil then - to[v] = status[v] - end - end - print("Loaded state") -end - -local function clear() - status = {} - status.back = -1 - status.power = {} - status.rng = {} - print("Cleared state") -end - -local function restart_level() - restart = true - state.screen_next = SCREEN.LEVEL - state.screen_last = SCREEN.TRANSITION - state.world_next = state.world - state.level_next = state.level - state.theme_next = state.theme - state.quest_flags = 1 - state.loading = 2 -end +register_option_button('load', 'Quickload', function() + load_state(1) +end) -register_option_button('restart', 'Restart level', function() - restart_level() +register_option_button('save', 'Quicksave', function() + save_state(1) end) set_callback(function() - if state.items.player_inventory[1].health < 1 then - state.items.player_inventory[1].health = 4 + save_state(1) + for _, p in pairs(players) do + set_on_player_instagib(p.uid, function(e) restart = true end) end - - if restart then - if status.rng then - for i,v in pairs(status.rng) do - prng:set_pair(i, v.a, v.b) - end - end - load(state, vars.state) - load(state.quests, vars.quests) - load(state.items.player_inventory[1], vars.inventory) - else - if not status.rng then - status.rng = {} - end - for i=0,9 do - local a,b = prng:get_pair(i) - status.rng[i] = { a=a, b=b } - end - save(state, vars.state) - save(state.quests, vars.quests) - save(state.items.player_inventory[1], vars.inventory) - end -end, ON.PRE_LEVEL_GENERATION) +end, ON.LEVEL) set_callback(function() - local ent = players[1] if restart then - if status.power then - for i,v in ipairs(status.power) do - local m = string.find(names[v], 'PACK') - if not m and not ent:has_powerup(v) then - ent:give_powerup(v) - end - end - end - if status.back and status.back ~= -1 and ent:worn_backitem() == -1 then - pick_up(ent.uid, spawn(status.back, 0, 0, LAYER.PLAYER, 0, 0)) - end - else - status.back = -1 - local backitem = worn_backitem(players[1].uid) - if backitem ~= -1 then - status.back = get_entity(backitem).type.id - end - - status.power = {} - for i,v in ipairs(players[1]:get_powerups()) do - status.power[i] = v - end + restart = nil + load_state(1) end - - set_on_kill(ent.uid, function() - restart_level() - end) - - set_on_destroy(ent.uid, function() - restart_level() - end) - - restart = false -end, ON.LEVEL) +end, ON.POST_UPDATE) set_callback(function() local tile = get_grid_entity_at(6, 121, LAYER.FRONT) if tile then - get_entity(tile):destroy() + get_entity(tile):remove() + end + + tile = get_grid_entity_at(6, 120, LAYER.FRONT) + if tile then + get_entity(tile):decorate_internal() end end, ON.TRANSITION) From f9479ea255baf6db6cb3e4ad30c9254e7d3cb2bb Mon Sep 17 00:00:00 2001 From: Dregu Date: Sat, 3 Feb 2024 18:55:00 +0200 Subject: [PATCH 4/5] update barrymod --- examples/barrymod.lua | 42 +++++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/examples/barrymod.lua b/examples/barrymod.lua index c51cb2873..5742c3ac0 100644 --- a/examples/barrymod.lua +++ b/examples/barrymod.lua @@ -3,21 +3,35 @@ meta.version = '1.0' meta.description = 'Restarts the current level on death or manually like nothing happened. ' meta.author = 'Dregu' -register_option_button('load', 'Quickload', function() - load_state(1) -end) +register_option_bool('alt', 'Multiverse of Madness Mode', + 'Rerolls the level generation on death,\nbut keeps quest state and inventory.', + false) -register_option_button('save', 'Quicksave', function() - save_state(1) +register_option_callback('buttons', nil, function(ctx) + if ctx:win_button('Quick Save') then save_state(1) end + ctx:win_inline() + if ctx:win_button('Quick Load') then load_state(1) end end) +function save_early() + return options.alt and state.theme ~= THEME.OLMEC -- typical olmec crashes with this? +end + set_callback(function() - save_state(1) + if not save_early() then + save_state(1) + end for _, p in pairs(players) do set_on_player_instagib(p.uid, function(e) restart = true end) end end, ON.LEVEL) +set_callback(function() + if save_early() then + save_state(1) + end +end, ON.PRE_LEVEL_GENERATION) + set_callback(function() if restart then restart = nil @@ -26,13 +40,15 @@ set_callback(function() end, ON.POST_UPDATE) set_callback(function() - local tile = get_grid_entity_at(6, 121, LAYER.FRONT) - if tile then - get_entity(tile):remove() - end - - tile = get_grid_entity_at(6, 120, LAYER.FRONT) + local tile = get_entity(get_grid_entity_at(6, 121, LAYER.FRONT)) if tile then - get_entity(tile):decorate_internal() + tile:remove() + tile = get_entity(get_grid_entity_at(6, 120, LAYER.FRONT)) + if tile then + tile:decorate_internal() + end + else + tile = get_entity(spawn_grid_entity(ENT_TYPE.FLOOR_GENERIC, 6, 121, LAYER.FRONT)) + tile:decorate_internal() end end, ON.TRANSITION) From 048d4228e9547da1ea65e324fc43ff8c815ba9ed Mon Sep 17 00:00:00 2001 From: Dregu Date: Sun, 4 Feb 2024 16:34:54 +0200 Subject: [PATCH 5/5] docs --- docs/game_data/spel2.lua | 12 ++++++++++++ docs/src/includes/_globals.md | 27 +++++++++++++++++++++++++++ examples/barrymod.lua | 22 ++++++++++++---------- src/game_api/script/lua_vm.cpp | 6 +++--- 4 files changed, 54 insertions(+), 13 deletions(-) diff --git a/docs/game_data/spel2.lua b/docs/game_data/spel2.lua index 0794e81a2..120d2f8d0 100644 --- a/docs/game_data/spel2.lua +++ b/docs/game_data/spel2.lua @@ -1378,6 +1378,18 @@ function play_adventure() end ---@param seed integer? ---@return nil function play_seeded(seed) end +---Save current level state to slot 1..4. These save states are invalid after you exit the level, but can be used to rollback to an earlier state in the same level. You probably definitely shouldn't use save state functions during an update, and sync them to the same event outside an update (i.e. GUIFRAME, POST_UPDATE). +---@param slot integer +---@return nil +function save_state(slot) end +---Load level state from slot 1..4, if a save_state was made in this level. +---@param slot integer +---@return nil +function load_state(slot) end +---Get StateMemory from a save_state slot. +---@param slot integer +---@return StateMemory +function get_save_state(slot) end ---@return boolean function toast_visible() end ---@return boolean diff --git a/docs/src/includes/_globals.md b/docs/src/includes/_globals.md index ff4bfb3ba..272fc1266 100644 --- a/docs/src/includes/_globals.md +++ b/docs/src/includes/_globals.md @@ -1515,6 +1515,15 @@ Retrieves the current value of the performance counter, which is a high resoluti Retrieves the frequency of the performance counter. The frequency of the performance counter is fixed at system boot and is consistent across all processors. Therefore, the frequency need only be queried upon application initialization, and the result can be cached. +### get_save_state + + +> Search script examples for [get_save_state](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=get_save_state) + +#### [StateMemory](#StateMemory) get_save_state(int slot) + +Get [StateMemory](#StateMemory) from a save_state slot. + ### get_setting @@ -1681,6 +1690,15 @@ Immediately ends the run with the death screen, also calls the [save_progress](# Immediately load a screen based on [state](#state).screen_next and stuff +### load_state + + +> Search script examples for [load_state](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=load_state) + +#### nil load_state(int slot) + +Load level state from slot 1..4, if a save_state was made in this level. + ### lowbias32 @@ -1762,6 +1780,15 @@ Saves the game to savegame.sav, unless game saves are blocked in the settings. A Runs the [ON](#ON).SAVE callback. Fails and returns false, if you're trying to save too often (2s). +### save_state + + +> Search script examples for [save_state](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=save_state) + +#### nil save_state(int slot) + +Save current level state to slot 1..4. These save states are invalid after you exit the level, but can be used to rollback to an earlier state in the same level. You probably definitely shouldn't use save state functions during an update, and sync them to the same event outside an update (i.e. GUIFRAME, POST_UPDATE). + ### script_enabled diff --git a/examples/barrymod.lua b/examples/barrymod.lua index 5742c3ac0..47855bd7a 100644 --- a/examples/barrymod.lua +++ b/examples/barrymod.lua @@ -1,9 +1,9 @@ meta.name = 'Barrymod' meta.version = '1.0' -meta.description = 'Restarts the current level on death or manually like nothing happened. ' +meta.description = 'Creates checkpoints and restarts the current level on death like nothing happened.' meta.author = 'Dregu' -register_option_bool('alt', 'Multiverse of Madness Mode', +register_option_bool('save_early', 'Multiverse of Madness Mode', 'Rerolls the level generation on death,\nbut keeps quest state and inventory.', false) @@ -13,21 +13,21 @@ register_option_callback('buttons', nil, function(ctx) if ctx:win_button('Quick Load') then load_state(1) end end) -function save_early() - return options.alt and state.theme ~= THEME.OLMEC -- typical olmec crashes with this? -end - set_callback(function() - if not save_early() then + if not options.save_early then save_state(1) end for _, p in pairs(players) do - set_on_player_instagib(p.uid, function(e) restart = true end) + set_on_player_instagib(p.uid, function(e) + -- can't load_state directly here, cause we're still in the middle of an update + restart = true + end) end end, ON.LEVEL) set_callback(function() - if save_early() then + if options.save_early then + -- for whatever prng related reason, loading a save created at this point will reroll the level rng, which is a neat I guess save_state(1) end end, ON.PRE_LEVEL_GENERATION) @@ -35,11 +35,13 @@ end, ON.PRE_LEVEL_GENERATION) set_callback(function() if restart then restart = nil + -- load the save state we made earlier, after updates to not mess with the running state load_state(1) end end, ON.POST_UPDATE) set_callback(function() + if state.screen ~= SCREEN.TRANSITION then return end local tile = get_entity(get_grid_entity_at(6, 121, LAYER.FRONT)) if tile then tile:remove() @@ -51,4 +53,4 @@ set_callback(function() tile = get_entity(spawn_grid_entity(ENT_TYPE.FLOOR_GENERIC, 6, 121, LAYER.FRONT)) tile:decorate_internal() end -end, ON.TRANSITION) +end, ON.POST_LEVEL_GENERATION) diff --git a/src/game_api/script/lua_vm.cpp b/src/game_api/script/lua_vm.cpp index 8b65a7aab..f14ce496e 100644 --- a/src/game_api/script/lua_vm.cpp +++ b/src/game_api/script/lua_vm.cpp @@ -2249,21 +2249,21 @@ end /// Initializes some seedeed run related values and loads the character select screen, as if starting a new seeded run after entering the seed. lua["play_seeded"] = init_seeded; - /// Save current state to slot 1..4 + /// Save current level state to slot 1..4. These save states are invalid after you exit the level, but can be used to rollback to an earlier state in the same level. You probably definitely shouldn't use save state functions during an update, and sync them to the same event outside an update (i.e. GUIFRAME, POST_UPDATE). lua["save_state"] = [](int slot) { if (slot >= 1 && slot <= 4) copy_state(5, slot); }; - /// Load current state from slot 1..4 + /// Load level state from slot 1..4, if a save_state was made in this level. lua["load_state"] = [](int slot) { if (slot >= 1 && slot <= 4 && get_save_state(slot)) copy_state(slot, 5); }; - /// Get saved state from slot + /// Get StateMemory from a save_state slot. lua["get_save_state"] = [](int slot) -> StateMemory* { if (slot >= 1 && slot <= 5)