From baabeb966d915d38bd296e1fdbe93d1bdb94eefc Mon Sep 17 00:00:00 2001 From: Dregu Date: Sat, 3 Feb 2024 04:35:39 +0200 Subject: [PATCH 01/17] 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 02/17] 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 03/17] 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 04/17] 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 05/17] 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) From ec886d27ab95288f9e01f5c60b1996497c411aaf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 4 Feb 2024 14:39:19 +0000 Subject: [PATCH 06/17] update slate[no ci] --- docs/index.html | 27 +++++++++++++++++++++++++++ docs/light.html | 27 +++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/docs/index.html b/docs/index.html index a4ba36b53..2b239fe95 100644 --- a/docs/index.html +++ b/docs/index.html @@ -818,6 +818,9 @@
  • get_performance_frequency
  • +
  • + get_save_state +
  • get_setting
  • @@ -869,6 +872,9 @@
  • load_screen
  • +
  • + load_state +
  • lowbias32
  • @@ -896,6 +902,9 @@
  • save_script
  • +
  • + save_state +
  • script_enabled
  • @@ -5146,6 +5155,12 @@

    get_performance_frequency

    int get_performance_frequency()

    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

    +
    +

    StateMemory get_save_state(int slot)

    +

    Get StateMemory from a save_state slot.

    get_setting

    Search script examples for get_setting

    @@ -5256,6 +5271,12 @@

    load_screen

    nil load_screen()

    Immediately load a screen based on state.screen_next and stuff

    +

    load_state

    +
    +

    Search script examples for load_state

    +
    +

    nil load_state(int slot)

    +

    Load level state from slot 1..4, if a save_state was made in this level.

    lowbias32

    Search script examples for lowbias32

    @@ -5310,6 +5331,12 @@

    save_script

    bool save_script()

    Runs the ON.SAVE callback. Fails and returns false, if you're trying to save too often (2s).

    +

    save_state

    +
    +

    Search script examples for 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

    Search script examples for script_enabled

    diff --git a/docs/light.html b/docs/light.html index 3f4c06cc6..85c66c08c 100644 --- a/docs/light.html +++ b/docs/light.html @@ -818,6 +818,9 @@
  • get_performance_frequency
  • +
  • + get_save_state +
  • get_setting
  • @@ -869,6 +872,9 @@
  • load_screen
  • +
  • + load_state +
  • lowbias32
  • @@ -896,6 +902,9 @@
  • save_script
  • +
  • + save_state +
  • script_enabled
  • @@ -5146,6 +5155,12 @@

    get_performance_frequency

    int get_performance_frequency()

    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

    +
    +

    StateMemory get_save_state(int slot)

    +

    Get StateMemory from a save_state slot.

    get_setting

    Search script examples for get_setting

    @@ -5256,6 +5271,12 @@

    load_screen

    nil load_screen()

    Immediately load a screen based on state.screen_next and stuff

    +

    load_state

    +
    +

    Search script examples for load_state

    +
    +

    nil load_state(int slot)

    +

    Load level state from slot 1..4, if a save_state was made in this level.

    lowbias32

    Search script examples for lowbias32

    @@ -5310,6 +5331,12 @@

    save_script

    bool save_script()

    Runs the ON.SAVE callback. Fails and returns false, if you're trying to save too often (2s).

    +

    save_state

    +
    +

    Search script examples for 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

    Search script examples for script_enabled

    From c8d54bab20554383023db683323d3e01eb5003b2 Mon Sep 17 00:00:00 2001 From: Dregu Date: Sun, 4 Feb 2024 17:22:57 +0200 Subject: [PATCH 07/17] fix barrymod crash --- examples/barrymod.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/barrymod.lua b/examples/barrymod.lua index 47855bd7a..cb635ebd6 100644 --- a/examples/barrymod.lua +++ b/examples/barrymod.lua @@ -13,8 +13,12 @@ register_option_callback('buttons', nil, function(ctx) if ctx:win_button('Quick Load') then load_state(1) end end) +function save_early() + return options.save_early and state.theme ~= THEME.OLMEC --typical olmec crashing stuff again +end + set_callback(function() - if not options.save_early then + if not save_early() then save_state(1) end for _, p in pairs(players) do @@ -26,7 +30,7 @@ set_callback(function() end, ON.LEVEL) set_callback(function() - if options.save_early then + if 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 From 0af1de67d25621d6122721dafe1f22fd49c0f499 Mon Sep 17 00:00:00 2001 From: Dregu Date: Sun, 4 Feb 2024 22:31:39 +0200 Subject: [PATCH 08/17] oops I broke online --- src/game_api/rpc.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/game_api/rpc.cpp b/src/game_api/rpc.cpp index 9d3cb9487..e365604a3 100644 --- a/src/game_api/rpc.cpp +++ b/src/game_api/rpc.cpp @@ -39,6 +39,7 @@ #include "math.hpp" // for AABB #include "memory.hpp" // for write_mem_prot, write_mem_recoverable #include "movable.hpp" // for Movable +#include "online.hpp" // for Online #include "particles.hpp" // for ParticleEmitterInfo #include "screen.hpp" // #include "search.hpp" // for get_address, find_inst @@ -1946,6 +1947,9 @@ StateMemory* get_save_state(int slot) void invalidate_save_states() { + auto online = get_online(); + if (online->lobby.code != 0) + return; for (int i = 1; i <= 4; ++i) { auto state = get_save_state(i); From ff47f3a88e74dba0d52a100d6c06ec24b96dfabf Mon Sep 17 00:00:00 2001 From: Dregu Date: Tue, 6 Feb 2024 22:26:32 +0200 Subject: [PATCH 09/17] hide entity tooltip when cursor is hidden --- src/injected/ui.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/injected/ui.cpp b/src/injected/ui.cpp index ba51d4dc9..d0807ca04 100644 --- a/src/injected/ui.cpp +++ b/src/injected/ui.cpp @@ -5161,7 +5161,7 @@ void render_clickhandler() { g_bucket->overlunky->hovered_uid = -1; } - if (options["draw_entity_tooltip"] && ImGui::IsWindowHovered()) + if (options["draw_entity_tooltip"] && ImGui::IsWindowHovered() && io.MouseDrawCursor) { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {4.0f, 4.0f}); tooltip(coords.c_str(), true); From 6773a6876967fce816c1497d59b7adfbf61297a0 Mon Sep 17 00:00:00 2001 From: Dregu Date: Tue, 6 Feb 2024 22:47:08 +0200 Subject: [PATCH 10/17] remember script options, add key to clear messages --- src/injected/ui.cpp | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/injected/ui.cpp b/src/injected/ui.cpp index d0807ca04..3778f5289 100644 --- a/src/injected/ui.cpp +++ b/src/injected/ui.cpp @@ -230,6 +230,7 @@ std::map default_keys{ {"speedhack_slow", VK_NEXT}, {"toggle_uncapped_fps", OL_KEY_CTRL | OL_KEY_SHIFT | 'U'}, {"respawn", OL_KEY_CTRL | 'R'}, + {"clear_messages", OL_KEY_CTRL | VK_BACK}, //{ "", 0x }, }; @@ -284,7 +285,7 @@ std::vector g_selected_ids; bool set_focus_entity = false, set_focus_world = false, set_focus_finder = false, set_focus_uid = false, scroll_to_entity = false, scroll_top = false, click_teleport = false, throw_held = false, show_app_metrics = false, lock_entity = false, lock_player = false, freeze_last = false, freeze_level = false, freeze_total = false, hide_ui = false, - enable_noclip = false, load_script_dir = true, load_packs_dir = false, enable_camp_camera = true, enable_camera_bounds = true, freeze_quest_yang = false, freeze_quest_sisters = false, freeze_quest_horsing = false, freeze_quest_sparrow = false, freeze_quest_tusk = false, freeze_quest_beg = false, run_finder = false, in_menu = false, zooming = false, g_inv = false, edit_last_id = false, edit_achievements = false, peek_layer = false, death_disable = false; + enable_noclip = false, enable_camp_camera = true, enable_camera_bounds = true, freeze_quest_yang = false, freeze_quest_sisters = false, freeze_quest_horsing = false, freeze_quest_sparrow = false, freeze_quest_tusk = false, freeze_quest_beg = false, run_finder = false, in_menu = false, zooming = false, g_inv = false, edit_last_id = false, edit_achievements = false, peek_layer = false, death_disable = false; std::optional quest_yang_state, quest_sisters_state, quest_horsing_state, quest_sparrow_state, quest_tusk_state, quest_beg_state; Entity* g_entity = 0; Entity* g_held_entity = 0; @@ -381,6 +382,8 @@ std::map options = { {"pause_last_instance", false}, {"update_check", true}, {"modifiers_clear_input", true}, + {"load_scripts", true}, + {"load_packs", false}, }; double g_engine_fps = 60.0, g_unfocused_fps = 33.0; @@ -803,7 +806,7 @@ bool SliderByte(const char* label, char* value, char min = 0, char max = 0, cons void refresh_script_files() { g_script_files.clear(); - if (load_script_dir && std::filesystem::exists(scriptpath) && std::filesystem::is_directory(scriptpath)) + if (options["load_scripts"] && std::filesystem::exists(scriptpath) && std::filesystem::is_directory(scriptpath)) { for (const auto& file : std::filesystem::directory_iterator(scriptpath)) { @@ -813,7 +816,7 @@ void refresh_script_files() } } } - else if (!load_script_dir && std::filesystem::exists(scriptpath) && std::filesystem::is_directory(scriptpath)) + else if (!options["load_scripts"] && std::filesystem::exists(scriptpath) && std::filesystem::is_directory(scriptpath)) { std::vector unload_scripts; for (const auto& script : g_scripts) @@ -831,7 +834,7 @@ void refresh_script_files() } } - if (load_packs_dir && std::filesystem::exists("Mods/Packs") && std::filesystem::is_directory("Mods/Packs")) + if (options["load_packs"] && std::filesystem::exists("Mods/Packs") && std::filesystem::is_directory("Mods/Packs")) { for (const auto& file : std::filesystem::recursive_directory_iterator("Mods/Packs")) { @@ -841,7 +844,7 @@ void refresh_script_files() } } } - else if (!load_packs_dir && std::filesystem::exists("Mods/Packs") && std::filesystem::is_directory("Mods/Packs")) + else if (!options["load_packs"] && std::filesystem::exists("Mods/Packs") && std::filesystem::is_directory("Mods/Packs")) { std::vector unload_scripts; for (const auto& script : g_scripts) @@ -2818,6 +2821,16 @@ void load_state(int slot) UI::copy_state(slot, 5); } +void clear_script_messages() +{ + for (auto& [name, script] : g_scripts) + script->consume_messages(); + for (auto& [name, script] : g_ui_scripts) + script->consume_messages(); + g_Console->consume_messages(); + g_ConsoleMessages.clear(); +} + bool process_keys(UINT nCode, WPARAM wParam, [[maybe_unused]] LPARAM lParam) { ImGuiContext& g = *GImGui; @@ -3559,6 +3572,10 @@ bool process_keys(UINT nCode, WPARAM wParam, [[maybe_unused]] LPARAM lParam) { load_state(4); } + else if (pressed("clear_messages", wParam)) + { + clear_script_messages(); + } else { return false; @@ -6512,13 +6529,17 @@ void render_scripts() ImGui::SameLine(); ImGui::Checkbox("to console##ConsoleScriptMessages", &options["console_script_messages"]); ImGui::Checkbox("Fade script messages##FadeScriptMessages", &options["fade_script_messages"]); - if (ImGui::Checkbox("Load scripts from script directory##LoadScriptsDefault", &load_script_dir)) + ImGui::SameLine(); + if (ImGui::Button("Clear##ClearMessages")) + clear_script_messages(); + tooltip("Clear all script messages from screen", "clear_messages"); + if (ImGui::Checkbox("Load scripts from script directory##LoadScriptsDefault", &options["load_scripts"])) refresh_script_files(); ImGui::SameLine(); - if (ImGui::Button("Set##SetScriptDir")) + if (ImGui::Button("Change##SetScriptDir")) set_script_dir(); tooltip(scriptpath.c_str()); - if (ImGui::Checkbox("Load scripts from Mods/Packs##LoadScriptsPacks", &load_packs_dir)) + if (ImGui::Checkbox("Load scripts from Mods/Packs##LoadScriptsPacks", &options["load_packs"])) refresh_script_files(); if (ImGui::Button("Create new quick script")) { From 921599412c5320e0a247a9cd8a86caf4e9d47b48 Mon Sep 17 00:00:00 2001 From: Dregu Date: Tue, 6 Feb 2024 22:54:15 +0200 Subject: [PATCH 11/17] add threshold to snap when selecting with m3 --- src/injected/ui.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/injected/ui.cpp b/src/injected/ui.cpp index 3778f5289..27788a6de 100644 --- a/src/injected/ui.cpp +++ b/src/injected/ui.cpp @@ -5536,7 +5536,7 @@ void render_clickhandler() g_held_entity->flags = g_held_flags; set_pos(startpos); set_vel(mouse_pos()); - if (g_held_entity && g_held_entity->is_movable()) + if (g_held_entity && g_held_entity->is_movable() && drag_delta("mouse_grab_throw") > 10.0f) UI::move_entity(g_held_id, g_x, g_y, true, g_vx, g_vy, options["snap_to_grid"]); g_x = 0; g_y = 0; @@ -5550,7 +5550,7 @@ void render_clickhandler() io.MouseDrawCursor = true; if (g_held_entity) g_held_entity->flags = g_held_flags; - if (options["snap_to_grid"] && g_held_entity->is_movable()) + if (options["snap_to_grid"] && g_held_entity->is_movable() && (drag_delta("mouse_grab") > 10.0f || drag_delta("mouse_grab_unsafe") > 10.0f)) { UI::move_entity(g_held_id, g_x, g_y, true, 0, 0, options["snap_to_grid"]); } From bb81782e6ff6bb3bb251cc72c08ad161eac56cf5 Mon Sep 17 00:00:00 2001 From: Dregu Date: Tue, 6 Feb 2024 23:22:47 +0200 Subject: [PATCH 12/17] don't destroy items on activefloor when deleting activefloor --- src/injected/ui.cpp | 13 ++++++++++--- src/injected/ui_util.cpp | 4 ++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/injected/ui.cpp b/src/injected/ui.cpp index 27788a6de..99e20e146 100644 --- a/src/injected/ui.cpp +++ b/src/injected/ui.cpp @@ -1322,9 +1322,16 @@ void smart_delete(Entity* ent, bool unsafe = false) { static auto first_door = to_id("ENT_TYPE_FLOOR_DOOR_ENTRANCE"); static auto logical_door = to_id("ENT_TYPE_LOGICAL_DOOR"); - ent->flags = set_flag(ent->flags, 1); - for (auto item : ent->items.entities()) - item->flags = set_flag(item->flags, 1); + if (!ent->is_player()) + ent->flags = set_flag(ent->flags, 1); + if ((ent->type->search_flags & 0x80) == 0) + { + for (auto item : ent->items.entities()) + { + if (!item->is_player()) + item->flags = set_flag(item->flags, 1); + } + } UI::safe_destroy(ent, unsafe); if ((ent->type->id >= first_door && ent->type->id <= first_door + 15) || ent->type->id == logical_door) { diff --git a/src/injected/ui_util.cpp b/src/injected/ui_util.cpp index 3e792d0a3..c24df6699 100644 --- a/src/injected/ui_util.cpp +++ b/src/injected/ui_util.cpp @@ -348,6 +348,8 @@ void UI::steam_achievements(bool on) } int32_t UI::destroy_entity_items(Entity* ent) { + if (ent->type->search_flags & 0x80) + return 0; auto items = entity_get_items_by(ent->uid, 0, 0); if (items.size() == 0) return -1; @@ -356,6 +358,8 @@ int32_t UI::destroy_entity_items(Entity* ent) while (it != items.rend()) { auto item = get_entity_ptr(*it); + if (item->type->search_flags & 0x81) + continue; UI::destroy_entity_items(item); UI::safe_destroy(item, false, false); it++; From 27ab16ee7fae7de621e36ca0f3a5995d13d6bfa4 Mon Sep 17 00:00:00 2001 From: Dregu Date: Tue, 6 Feb 2024 23:34:32 +0200 Subject: [PATCH 13/17] update doc --- docs/generate.py | 5 ++++- docs/src/includes/_home.md | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/generate.py b/docs/generate.py index 37f449ac2..74f6e5e07 100644 --- a/docs/generate.py +++ b/docs/generate.py @@ -153,7 +153,10 @@ def print_lf(lf): "- This doc is up to date for the Overlunky [WHIP build](https://github.com/spelunky-fyi/overlunky/releases/tag/whip) and Playlunky [nightly build](https://github.com/spelunky-fyi/Playlunky/releases/tag/nightly). If you're using a stable release from the past, you might find some things here don't work." ) print( - "- Set `OL_DEBUG=1` in the same environment where the game is running to keep the Overlunky terminal open for better debug prints. This could be `cmd` or even the system environment variables if playing on Steam. Playlunky will also print the messages to terminal (even from Overlunky) if ran with the `-console` switch." + "- Use `Overlunky.exe --console` command line switch to keep the Overlunky terminal open for better debug prints. Playlunky will also print the messages to terminal (even from Overlunky) if ran with the `-console` switch." +) +print( + "- Testing functions, exploring the types, and most inline Lua examples in here can be used in the in-game REPL console, default opened with the tilde key or regional equivalent. Full script examples can be found in the Overlunky Scripts menu if the script bundle is installed." ) print("\n## External Function Library") diff --git a/docs/src/includes/_home.md b/docs/src/includes/_home.md index 4efbe5a83..13560a5e4 100644 --- a/docs/src/includes/_home.md +++ b/docs/src/includes/_home.md @@ -8,7 +8,8 @@ - This doc and the examples are written for a person who already knows [how to program in Lua](http://lua-users.org/wiki/TutorialDirectory). - This doc doesn't cover how to actually load scripts. Check the [README](https://github.com/spelunky-fyi/overlunky/#scripts) for instructions. - This doc is up to date for the Overlunky [WHIP build](https://github.com/spelunky-fyi/overlunky/releases/tag/whip) and Playlunky [nightly build](https://github.com/spelunky-fyi/Playlunky/releases/tag/nightly). If you're using a stable release from the past, you might find some things here don't work. -- Set `OL_DEBUG=1` in the same environment where the game is running to keep the Overlunky terminal open for better debug prints. This could be `cmd` or even the system environment variables if playing on Steam. Playlunky will also print the messages to terminal (even from Overlunky) if ran with the `-console` switch. +- Use `Overlunky.exe --console` command line switch to keep the Overlunky terminal open for better debug prints. Playlunky will also print the messages to terminal (even from Overlunky) if ran with the `-console` switch. +- Testing functions, exploring the types, and most inline Lua examples in here can be used in the in-game REPL console, default opened with the tilde key or regional equivalent. Full script examples can be found in the Overlunky Scripts menu if the script bundle is installed. ## External Function Library If you use a text editor/IDE that has a Lua linter available you can download [spel2.lua](https://raw.githubusercontent.com/spelunky-fyi/overlunky/main/docs/game_data/spel2.lua), place it in a folder of your choice and specify that folder as a "external function library". For example [VSCode](https://code.visualstudio.com/) with the [Lua Extension](https://marketplace.visualstudio.com/items?itemName=sumneko.lua) offers this feature. This will allow you to get auto-completion of API functions along with linting From 191f72c472aa501eda87eb6e933f97052185ebad Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 22:13:28 +0000 Subject: [PATCH 14/17] update slate[no ci] --- docs/index.html | 3 ++- docs/light.html | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/index.html b/docs/index.html index 2b239fe95..b6713ca9b 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3902,7 +3902,8 @@

    Overlunky/Playlunky Lua API

    If you use a text editor/IDE that has a Lua linter available you can download spel2.lua, place it in a folder of your choice and specify that folder as a "external function library". For example VSCode with the Lua Extension offers this feature. This will allow you to get auto-completion of API functions along with linting

    diff --git a/docs/light.html b/docs/light.html index 85c66c08c..9b63d75de 100644 --- a/docs/light.html +++ b/docs/light.html @@ -3902,7 +3902,8 @@

    Overlunky/Playlunky Lua API

    If you use a text editor/IDE that has a Lua linter available you can download spel2.lua, place it in a folder of your choice and specify that folder as a "external function library". For example VSCode with the Lua Extension offers this feature. This will allow you to get auto-completion of API functions along with linting

    From ebf1bd1ebfc443def0785c97ad9151c0f930237d Mon Sep 17 00:00:00 2001 From: Dregu Date: Fri, 9 Feb 2024 17:53:26 +0200 Subject: [PATCH 15/17] add spawn analyzer script --- examples/spawn_analyzer.lua | 83 +++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 examples/spawn_analyzer.lua diff --git a/examples/spawn_analyzer.lua b/examples/spawn_analyzer.lua new file mode 100644 index 000000000..3ad791916 --- /dev/null +++ b/examples/spawn_analyzer.lua @@ -0,0 +1,83 @@ +meta = { + name = "Spawn Type Analyzer", + description = + "Colors all entities based on their SPAWN_TYPE, shows floor type spreading (magenta), floor filling/traps spawning in empty space (red), traps replacing floor (green), 50% tiles etc...", + version = "1.0", + author = "Dregu" +} + +for k, v in pairs(MASK) do + if v > 0 then + register_option_bool(k, k, v == MASK.FLOOR and true or false) + end +end + +a = 0.8 + +colors = { + [SPAWN_TYPE.LEVEL_GEN_TILE_CODE] = Color:new(0, 0.7, 1, a), + [SPAWN_TYPE.LEVEL_GEN_PROCEDURAL] = Color:new(1, 1, 0, a), + [SPAWN_TYPE.LEVEL_GEN_GENERAL] = Color:new(0, 1, 0, a), + [SPAWN_TYPE.LEVEL_GEN_FLOOR_SPREADING] = Color:new(1, 0, 1, a), + [SPAWN_TYPE.SYSTEMIC] = Color:new(1, 1, 1, a), + [64] = Color:new(1, 0, 0, a), +} + +uids = {} +perc = {} +map = {} + +set_callback(function() + uids = {} + perc = {} + map = {} +end, ON.PRE_LEVEL_GENERATION) + +set_post_entity_spawn(function(ent, type) + for k, v in pairs(colors) do + if test_mask(type, k) then + uids[ent.uid] = v + if test_mask(ent.type.search_flags, MASK.FLOOR) and (not map[math.floor(ent.abs_x)] or map[math.floor(ent.abs_x)][math.floor(ent.abs_y)] == "empty") then + uids[ent.uid] = colors[64] + end + end + end +end, SPAWN_TYPE.ANY, MASK.ANY) + +set_callback(function(ctx, d) + mask = 0 + for k, v in pairs(MASK) do + if options[k] then mask = set_mask(mask, v) end + end + if d == 5 then + for _, uid in pairs(get_entities_by(0, mask, state.camera_layer)) do + if uids[uid] then + ctx:draw_world_rect(get_hitbox(uid, -0.2), 20, uids[uid]) + end + end + + for _, c in pairs(perc) do + local sx, sy = screen_position(c.x, c.y) + ctx:draw_text(F "{c.c}%", sx, sy, 0.0004, 0.0004, Color:white(), VANILLA_TEXT_ALIGNMENT.CENTER, + VANILLA_FONT_STYLE.NORMAL) + end + end +end, ON.RENDER_POST_DRAW_DEPTH) + +set_callback(function(rx, ry, t, ctx) + for y = 0, 7 do + for x = 0, 9 do + local code = ctx:get_short_tile_code(x, y, 0) + local def = get_short_tile_code_definition(code) + local name = string.lower(enum_get_name(TILE_CODE, def.tile_code)) + local lx, ly = get_room_pos(rx, ry) + lx = math.floor(lx + x + 0.5) + ly = math.floor(ly - y - 0.5) + if def.chance ~= 100 then + perc[#perc + 1] = { x = lx, y = ly, c = def.chance } + end + if not map[lx] then map[lx] = {} end + map[lx][ly] = name + end + end +end, ON.PRE_HANDLE_ROOM_TILES) From 6f305f8ea82df178602c3fb3b55d9a2a9d4f62d8 Mon Sep 17 00:00:00 2001 From: Dregu Date: Fri, 9 Feb 2024 21:04:44 +0200 Subject: [PATCH 16/17] try to make floor spreading better --- examples/spawn_analyzer.lua | 65 ++++++++++++++++++++++------------ src/game_api/entity_db.cpp | 27 +------------- src/game_api/entity_db.hpp | 28 +++++++++++++++ src/game_api/script/lua_vm.cpp | 2 +- src/game_api/spawn_api.cpp | 21 ++++++++--- 5 files changed, 90 insertions(+), 53 deletions(-) diff --git a/examples/spawn_analyzer.lua b/examples/spawn_analyzer.lua index 3ad791916..717a26f7f 100644 --- a/examples/spawn_analyzer.lua +++ b/examples/spawn_analyzer.lua @@ -1,21 +1,21 @@ meta = { name = "Spawn Type Analyzer", description = - "Colors all entities based on their SPAWN_TYPE, shows floor type spreading (magenta), floor filling/traps spawning in empty space (red), traps replacing floor (green), 50% tiles etc...", - version = "1.0", + "Colors all entities based on their SPAWN_TYPE, shows floor style spreading (magenta), corner filling/traps spawning in empty space (red), traps replacing floor (green), 50% tiles and original tilecodes for all the peculiar spots.", + version = "1.1", author = "Dregu" } for k, v in pairs(MASK) do if v > 0 then - register_option_bool(k, k, v == MASK.FLOOR and true or false) + register_option_bool(k, k, test_mask(MASK.FLOOR | MASK.ACTIVEFLOOR, v)) end end a = 0.8 colors = { - [SPAWN_TYPE.LEVEL_GEN_TILE_CODE] = Color:new(0, 0.7, 1, a), + [SPAWN_TYPE.LEVEL_GEN_TILE_CODE] = Color:new(0, 0.3, 0.8, 0), [SPAWN_TYPE.LEVEL_GEN_PROCEDURAL] = Color:new(1, 1, 0, a), [SPAWN_TYPE.LEVEL_GEN_GENERAL] = Color:new(0, 1, 0, a), [SPAWN_TYPE.LEVEL_GEN_FLOOR_SPREADING] = Color:new(1, 0, 1, a), @@ -37,7 +37,7 @@ set_post_entity_spawn(function(ent, type) for k, v in pairs(colors) do if test_mask(type, k) then uids[ent.uid] = v - if test_mask(ent.type.search_flags, MASK.FLOOR) and (not map[math.floor(ent.abs_x)] or map[math.floor(ent.abs_x)][math.floor(ent.abs_y)] == "empty") then + if test_mask(ent.type.search_flags, MASK.FLOOR) and (not map[ent.layer] or not map[ent.layer][math.floor(ent.abs_x)] or map[ent.layer][math.floor(ent.abs_x)][math.floor(ent.abs_y)] == "empty") then uids[ent.uid] = colors[64] end end @@ -50,34 +50,55 @@ set_callback(function(ctx, d) if options[k] then mask = set_mask(mask, v) end end if d == 5 then + local ax, ay, bx, by = get_bounds() for _, uid in pairs(get_entities_by(0, mask, state.camera_layer)) do - if uids[uid] then - ctx:draw_world_rect(get_hitbox(uid, -0.2), 20, uids[uid]) + local x, y, l = get_position(uid) + if uids[uid] and uids[uid].a ~= 0 and x > ax and x < bx and y < ay and y > by then + ctx:draw_world_rect(get_hitbox(uid, -0.15), 20, uids[uid]) + if map[l] and map[l][x] and map[l][x][y] then + local sx, sy = screen_position(x, y - 0.2) + ctx:draw_text(F "{map[l][x][y]}", sx, sy, 0.0003, 0.0003, Color:white(), + VANILLA_TEXT_ALIGNMENT.CENTER, + VANILLA_FONT_STYLE.NORMAL) + end end end for _, c in pairs(perc) do - local sx, sy = screen_position(c.x, c.y) - ctx:draw_text(F "{c.c}%", sx, sy, 0.0004, 0.0004, Color:white(), VANILLA_TEXT_ALIGNMENT.CENTER, - VANILLA_FONT_STYLE.NORMAL) + if c.l == state.camera_layer then + local sx, sy = screen_position(c.x, c.y + 0.2) + ctx:draw_text(F "{c.c}%", sx, sy, 0.0004, 0.0004, Color:white(), VANILLA_TEXT_ALIGNMENT.CENTER, + VANILLA_FONT_STYLE.NORMAL) + if map[c.l] and map[c.l][c.x] and map[c.l][c.x][c.y] then + sx, sy = screen_position(c.x, c.y - 0.2) + ctx:draw_text(F "{map[c.l][c.x][c.y]}", sx, sy, 0.0003, 0.0003, Color:white(), + VANILLA_TEXT_ALIGNMENT.CENTER, + VANILLA_FONT_STYLE.NORMAL) + end + end end end end, ON.RENDER_POST_DRAW_DEPTH) set_callback(function(rx, ry, t, ctx) - for y = 0, 7 do - for x = 0, 9 do - local code = ctx:get_short_tile_code(x, y, 0) - local def = get_short_tile_code_definition(code) - local name = string.lower(enum_get_name(TILE_CODE, def.tile_code)) - local lx, ly = get_room_pos(rx, ry) - lx = math.floor(lx + x + 0.5) - ly = math.floor(ly - y - 0.5) - if def.chance ~= 100 then - perc[#perc + 1] = { x = lx, y = ly, c = def.chance } + for l = 0, 1 do + for y = 0, 7 do + for x = 0, 9 do + local code = ctx:get_short_tile_code(x, y, l) + if code then + local def = get_short_tile_code_definition(code) + local name = string.lower(enum_get_name(TILE_CODE, def.tile_code)) + local lx, ly = get_room_pos(rx, ry) + lx = math.floor(lx + x + 0.5) + ly = math.floor(ly - y - 0.5) + if def.chance ~= 100 then + perc[#perc + 1] = { l = l, x = lx, y = ly, c = def.chance } + end + if not map[l] then map[l] = {} end + if not map[l][lx] then map[l][lx] = {} end + map[l][lx][ly] = name + end end - if not map[lx] then map[lx] = {} end - map[lx][ly] = name end end end, ON.PRE_HANDLE_ROOM_TILES) diff --git a/src/game_api/entity_db.cpp b/src/game_api/entity_db.cpp index 83877ad8f..ff2148386 100644 --- a/src/game_api/entity_db.cpp +++ b/src/game_api/entity_db.cpp @@ -1,3 +1,4 @@ +#include "entity_db.hpp" #include "entity.hpp" #include // for IsBadWritePtr @@ -36,32 +37,6 @@ EntityDB::EntityDB(const ENT_TYPE other) } using namespace std::chrono_literals; -using EntityMap = std::unordered_map; - -struct EntityBucket -{ - void** begin; - void** current; // Note, counts down from end to begin instead of up from begin to end :shrug: - void** end; -}; -struct EntityPool -{ - std::uint32_t slot_size; - std::uint32_t initial_slots; - std::uint32_t slots_growth; - std::uint32_t current_slots; - std::uint64_t _ulong_0; - EntityBucket* _some_bucket; - EntityBucket* bucket; -}; -struct EntityFactory -{ - EntityDB types[0x395]; - bool type_set[0x395]; - std::unordered_map> entity_instance_map; - EntityMap entity_map; - void* _ptr_7; -}; EntityFactory* entity_factory() { diff --git a/src/game_api/entity_db.hpp b/src/game_api/entity_db.hpp index c9ab0ffd3..6cabb4fcd 100644 --- a/src/game_api/entity_db.hpp +++ b/src/game_api/entity_db.hpp @@ -19,6 +19,7 @@ #include "entity_structs.hpp" // for CollisionInfo #include "layer.hpp" // for EntityList #include "math.hpp" // for AABB +#include "thread_utils.hpp" // for OnHeapPointer struct RenderInfo; struct Texture; @@ -109,6 +110,8 @@ struct EntityDB EntityDB(const ENT_TYPE other); }; +using EntityMap = std::unordered_map; + struct EntityItem { std::string name; @@ -124,6 +127,31 @@ struct EntityItem } }; +struct EntityBucket +{ + void** begin; + void** current; // Note, counts down from end to begin instead of up from begin to end :shrug: + void** end; +}; +struct EntityPool +{ + std::uint32_t slot_size; + std::uint32_t initial_slots; + std::uint32_t slots_growth; + std::uint32_t current_slots; + std::uint64_t _ulong_0; + EntityBucket* _some_bucket; + EntityBucket* bucket; +}; +struct EntityFactory +{ + EntityDB types[0x395]; + bool type_set[0x395]; + std::unordered_map> entity_instance_map; + EntityMap entity_map; + void* _ptr_7; +}; + EntityDB* get_type(uint32_t id); ENT_TYPE to_id(std::string_view id); diff --git a/src/game_api/script/lua_vm.cpp b/src/game_api/script/lua_vm.cpp index f14ce496e..0ac2989d4 100644 --- a/src/game_api/script/lua_vm.cpp +++ b/src/game_api/script/lua_vm.cpp @@ -2730,7 +2730,7 @@ end // LEVEL_GEN_PROCEDURAL // Similar to LEVEL_GEN but only triggers on random level spawns, like snakes or bats. // LEVEL_GEN_FLOOR_SPREADING - // Only procs during floor spreading, both horizontal and vertical + // Includes solid floor type spreading (i.e. floorstyled bleeding to existing generic floor) but also corner filling of empty tiles. // LEVEL_GEN_GENERAL // Covers all spawns during level gen that are not covered by the other two. // SCRIPT diff --git a/src/game_api/spawn_api.cpp b/src/game_api/spawn_api.cpp index 18d1f3a7c..dae2e2297 100644 --- a/src/game_api/spawn_api.cpp +++ b/src/game_api/spawn_api.cpp @@ -18,6 +18,7 @@ #include "entities_liquids.hpp" // for Lava #include "entities_monsters.hpp" // for Shopkeeper, RoomOwner #include "entity.hpp" // for to_id, Entity, get_entity_ptr, Enti... +#include "entity_db.hpp" // for EntityFactory #include "illumination.hpp" // #include "items.hpp" // #include "layer.hpp" // for Layer, g_level_max_y, g_level_max_x @@ -613,19 +614,31 @@ void pop_spawn_type_flags(SPAWN_TYPE flags) update_spawn_type_flags(); } -struct EntityFactory; - using SpawnEntityFun = Entity*(EntityFactory*, std::uint32_t, float, float, bool, Entity*, bool); SpawnEntityFun* g_spawn_entity_trampoline{nullptr}; Entity* spawn_entity(EntityFactory* entity_factory, std::uint32_t entity_type, float x, float y, bool layer, Entity* overlay, bool some_bool) { - const auto theme_floor = State::get().ptr_local()->current_theme->get_floor_spreading_type(); - const bool is_floor_spreading = (entity_type == theme_floor) && (g_SpawnTypeFlags & SPAWN_TYPE_LEVEL_GEN) && !(g_SpawnTypeFlags & SPAWN_TYPE_LEVEL_GEN_TILE_CODE); + // TODO: This still might not work very well and corner fill isn't actually floor spreading per level config definition, and should have a different SPAWN_TYPE (corner fill still happens when floor spreading chance is set to 0) + // const auto theme_floor = State::get().ptr_local()->current_theme->get_floor_spreading_type(); + // const auto theme_floor2 = State::get().ptr_local()->current_theme->get_floor_spreading_type2(); + const bool is_decorated = (entity_factory->types[entity_type].properties_flags & 0x1) == 0x1; + const bool is_styled = (entity_factory->types[entity_type].properties_flags & 0x2) == 0x2; + const bool is_border = entity_type < 4; + const bool is_floor_spreading = (is_decorated || is_styled) && !is_border && (g_SpawnTypeFlags & SPAWN_TYPE_LEVEL_GEN) && !(g_SpawnTypeFlags & SPAWN_TYPE_LEVEL_GEN_TILE_CODE); if (is_floor_spreading) { push_spawn_type_flags(SPAWN_TYPE_LEVEL_GEN_FLOOR_SPREADING); } + /* testing + auto entity_db = entity_factory->types[entity_type]; + // solid floor that's not from a tilecode + const bool is_floor_replaced = (entity_db.search_flags & 0x100 && entity_db.default_flags & 0x4) && (g_SpawnTypeFlags & SPAWN_TYPE_LEVEL_GEN) && !(g_SpawnTypeFlags & SPAWN_TYPE_LEVEL_GEN_TILE_CODE); + if (is_floor_replaced) + { + push_spawn_type_flags(SPAWN_TYPE_LEVEL_GEN_FLOOR_REPLACED); + }*/ + Entity* spawned_ent{nullptr}; if (g_SpawnNonReplacable == 0) { From dd5ec74fab173a01fccc2fec94b01a8a3a3e4e1e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 9 Feb 2024 19:06:39 +0000 Subject: [PATCH 17/17] update slate[no ci] --- docs/index.html | 2 +- docs/light.html | 2 +- docs/src/includes/_enums.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/index.html b/docs/index.html index b6713ca9b..6475eadfe 100644 --- a/docs/index.html +++ b/docs/index.html @@ -33981,7 +33981,7 @@

    SPAWN_TYPE

    LEVEL_GEN_FLOOR_SPREADING SPAWN_TYPE_LEVEL_GEN_FLOOR_SPREADING -Only procs during floor spreading, both horizontal and vertical
    +Includes solid floor type spreading (i.e. floorstyled bleeding to existing generic floor) but also corner filling of empty tiles.
    LEVEL_GEN_GENERAL diff --git a/docs/light.html b/docs/light.html index 9b63d75de..01092cdc0 100644 --- a/docs/light.html +++ b/docs/light.html @@ -33981,7 +33981,7 @@

    SPAWN_TYPE

    LEVEL_GEN_FLOOR_SPREADING SPAWN_TYPE_LEVEL_GEN_FLOOR_SPREADING -Only procs during floor spreading, both horizontal and vertical
    +Includes solid floor type spreading (i.e. floorstyled bleeding to existing generic floor) but also corner filling of empty tiles.
    LEVEL_GEN_GENERAL diff --git a/docs/src/includes/_enums.md b/docs/src/includes/_enums.md index c95a78820..3215971b2 100644 --- a/docs/src/includes/_enums.md +++ b/docs/src/includes/_enums.md @@ -1403,7 +1403,7 @@ Name | Data | Description [LEVEL_GEN](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=SPAWN_TYPE.LEVEL_GEN) | SPAWN_TYPE_LEVEL_GEN | For any spawn happening during level generation, even if the call happened from the Lua API during a tile code callback.
    [LEVEL_GEN_TILE_CODE](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=SPAWN_TYPE.LEVEL_GEN_TILE_CODE) | SPAWN_TYPE_LEVEL_GEN_TILE_CODE | Similar to LEVEL_GEN but only triggers on tile code spawns.
    [LEVEL_GEN_PROCEDURAL](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=SPAWN_TYPE.LEVEL_GEN_PROCEDURAL) | SPAWN_TYPE_LEVEL_GEN_PROCEDURAL | Similar to LEVEL_GEN but only triggers on random level spawns, like snakes or bats.
    -[LEVEL_GEN_FLOOR_SPREADING](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=SPAWN_TYPE.LEVEL_GEN_FLOOR_SPREADING) | SPAWN_TYPE_LEVEL_GEN_FLOOR_SPREADING | Only procs during floor spreading, both horizontal and vertical
    +[LEVEL_GEN_FLOOR_SPREADING](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=SPAWN_TYPE.LEVEL_GEN_FLOOR_SPREADING) | SPAWN_TYPE_LEVEL_GEN_FLOOR_SPREADING | Includes solid floor type spreading (i.e. floorstyled bleeding to existing generic floor) but also corner filling of empty tiles.
    [LEVEL_GEN_GENERAL](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=SPAWN_TYPE.LEVEL_GEN_GENERAL) | SPAWN_TYPE_LEVEL_GEN_GENERAL | Covers all spawns during level gen that are not covered by the other two.
    [SCRIPT](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=SPAWN_TYPE.SCRIPT) | SPAWN_TYPE_SCRIPT | Runs for any spawn happening through a call from the Lua API, also during level generation.
    [SYSTEMIC](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=SPAWN_TYPE.SYSTEMIC) | SPAWN_TYPE_SYSTEMIC | Covers all other spawns, such as items from crates or the player throwing bombs.