diff --git a/docs/game_data/spel2.lua b/docs/game_data/spel2.lua index 915759e34..03f7f2a20 100644 --- a/docs/game_data/spel2.lua +++ b/docs/game_data/spel2.lua @@ -1216,10 +1216,10 @@ function set_level_string(str) end ---@return nil function set_ending_unlock(type) end ---Get the thread-local version of state ----@return nil +---@return StateMemory function get_local_state() end ---Get the thread-local version of players ----@return nil +---@return Player[] function get_local_players() end ---List files in directory relative to the script root. Returns table of file/directory names or nil if not found. ---@param dir string? @@ -1436,7 +1436,7 @@ function get_raw_input() end ---@return nil function seed_prng(seed) end ---Get the thread-local version of prng ----@return nil +---@return PRNG function get_local_prng() end ---Same as `Player.get_name` ---@param type_id ENT_TYPE @@ -2222,6 +2222,7 @@ do ---@field liquid LiquidPhysics ---@field next_entity_uid integer @Next entity spawned will have this uid ---@field room_owners RoomOwnersInfo @Holds info about owned rooms and items (shops, challenge rooms, vault etc.) + ---@field user_data any ---@class LightParams ---@field red number diff --git a/docs/src/includes/_globals.md b/docs/src/includes/_globals.md index eefdda17e..e4626f617 100644 --- a/docs/src/includes/_globals.md +++ b/docs/src/includes/_globals.md @@ -744,7 +744,7 @@ Gets a grid entity, such as floor or spikes, at the given position and layer. > Search script examples for [get_local_players](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=get_local_players) -#### nil get_local_players() +#### vector<[Player](#Player)> get_local_players() Get the thread-local version of players @@ -1542,7 +1542,7 @@ Get the current layer that the liquid is spawn in. Related function [set_liquid_ > Search script examples for [get_local_prng](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=get_local_prng) -#### nil get_local_prng() +#### [PRNG](#PRNG) get_local_prng() Get the thread-local version of prng @@ -1551,7 +1551,7 @@ Get the thread-local version of prng > Search script examples for [get_local_state](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=get_local_state) -#### nil get_local_state() +#### [StateMemory](#StateMemory) get_local_state() Get the thread-local version of state diff --git a/docs/src/includes/_types.md b/docs/src/includes/_types.md index 30a4ae45e..36c92f083 100644 --- a/docs/src/includes/_types.md +++ b/docs/src/includes/_types.md @@ -3115,6 +3115,7 @@ array<[THEME](#THEME), 9> | [journal_progress_theme_slots](https://github. [LiquidPhysics](#LiquidPhysics) | [liquid](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=liquid) | int | [next_entity_uid](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=next_entity_uid) | Next entity spawned will have this uid [RoomOwnersInfo](#RoomOwnersInfo) | [room_owners](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=room_owners) | Holds info about owned rooms and items (shops, challenge rooms, vault etc.) +any | [user_data](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=user_data) | You can store a table (or lua primitive) here and it will store data correctly in online multiplayer, by having a different copy on each state and being copied over when the game does.
Doesn't support recursive tables / cyclic references. Metatables will be transferred by reference instead of being copied
## Texture types diff --git a/src/game_api/containers/custom_allocator.cpp b/src/game_api/containers/custom_allocator.cpp index 0c39c79e1..beb631870 100644 --- a/src/game_api/containers/custom_allocator.cpp +++ b/src/game_api/containers/custom_allocator.cpp @@ -12,12 +12,14 @@ using CustomFreeFun = void*(void*, void*); void* custom_malloc(std::size_t size) { static CustomMallocFun* _malloc = (CustomMallocFun*)get_address("custom_malloc"sv); - static void* _alloc_base = OnHeapPointer(*(size_t*)get_address("malloc_base"sv)).decode(); // probably should be decode_local + static size_t _heap_ptr_malloc_base = *reinterpret_cast(get_address("malloc_base"sv)); + void* _alloc_base = OnHeapPointer(_heap_ptr_malloc_base).decode_local(); return _malloc(_alloc_base, size); } void custom_free(void* mem) { static CustomFreeFun* _free = (CustomFreeFun*)get_address("custom_free"sv); - static void* _alloc_base = OnHeapPointer(*(size_t*)get_address("malloc_base"sv)).decode(); // probably should be decode_local + static size_t _heap_ptr_malloc_base = *reinterpret_cast(get_address("malloc_base"sv)); + void* _alloc_base = OnHeapPointer(_heap_ptr_malloc_base).decode_local(); _free(_alloc_base, mem); } diff --git a/src/game_api/savestate.cpp b/src/game_api/savestate.cpp index 758806111..2774d784c 100644 --- a/src/game_api/savestate.cpp +++ b/src/game_api/savestate.cpp @@ -5,12 +5,12 @@ #include "script/events.hpp" // for pre_load_state #include "state.hpp" // for State, get_state_ptr, enum_to_layer -size_t get_state_offset() +StateMemory* get_save_state_raw(int slot) { - auto addr = get_address("state_location"); - if (addr) - return memory_read(addr); - return 0x4a0; + size_t arr = get_address("save_states"); + size_t base = memory_read(arr + (slot - 1) * 8); + auto state = reinterpret_cast(base + State::get().get_offset()); + return state; } void copy_save_slot(int from, int to) @@ -18,6 +18,7 @@ void copy_save_slot(int from, int to) if ((from == 5 && pre_save_state(to, get_save_state(to))) || (to == 5 && pre_load_state(from, get_save_state(from)))) return; + pre_copy_state_event(get_save_state_raw(from), get_save_state_raw(to)); size_t arr = get_address("save_states"); size_t fromBaseState = memory_read(arr + (from - 1) * 8); size_t toBaseState = memory_read(arr + (to - 1) * 8); @@ -57,9 +58,7 @@ void copy_state(size_t fromBaseState, size_t toBaseState) 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 + get_state_offset()); + auto state = get_save_state_raw(slot); if (state->screen) return state; return nullptr; @@ -88,15 +87,17 @@ StateMemory* SaveState::get_state() const { if (!addr) return nullptr; - return reinterpret_cast(addr + get_state_offset()); + return reinterpret_cast(addr + State::get().get_offset()); } void SaveState::load() { if (!addr) return; - size_t to = (size_t)(State::get().ptr_main()) - get_state_offset(); - auto state = reinterpret_cast(addr + get_state_offset()); + State& state_g = State::get(); + size_t offset = state_g.get_offset(); + size_t to = (size_t)(state_g.ptr_main()) - offset; + auto state = reinterpret_cast(addr + offset); if (pre_load_state(-1, state)) return; copy_state(addr, to); @@ -107,8 +108,10 @@ void SaveState::save() { if (!addr) return; - size_t from = (size_t)(State::get().ptr_main()) - get_state_offset(); - auto state = reinterpret_cast(addr + get_state_offset()); + State& state_g = State::get(); + size_t offset = state_g.get_offset(); + size_t from = (size_t)(state_g.ptr_main()) - offset; + auto state = reinterpret_cast(addr + offset); if (pre_save_state(-1, state)) return; copy_state(from, addr); diff --git a/src/game_api/script/events.cpp b/src/game_api/script/events.cpp index 7b0301757..a39e0b241 100644 --- a/src/game_api/script/events.cpp +++ b/src/game_api/script/events.cpp @@ -517,3 +517,13 @@ void post_event(ON event) return true; }); } + +void pre_copy_state_event(StateMemory* from, StateMemory* to) +{ + LuaBackend::for_each_backend( + [&](LuaBackend::LockedBackend backend) + { + backend->pre_copy_state(from, to); + return true; + }); +} diff --git a/src/game_api/script/events.hpp b/src/game_api/script/events.hpp index bbed4af55..e1896f797 100644 --- a/src/game_api/script/events.hpp +++ b/src/game_api/script/events.hpp @@ -26,6 +26,7 @@ bool pre_unload_level(); bool pre_unload_layer(LAYER layer); bool pre_save_state(int slot, StateMemory* saved); bool pre_load_state(int slot, StateMemory* loaded); +void pre_copy_state_event(StateMemory* from, StateMemory* to); void post_load_screen(); void post_init_layer(LAYER layer); diff --git a/src/game_api/script/lua_backend.cpp b/src/game_api/script/lua_backend.cpp index 403b9cee1..3bf95bd4e 100644 --- a/src/game_api/script/lua_backend.cpp +++ b/src/game_api/script/lua_backend.cpp @@ -6,9 +6,10 @@ #include // for format_error #include // for _List_iterator, _List_co... #include // for table_proxy, optional -#include // for stack -#include // for get -#include // for vector +#include +#include // for stack +#include // for get +#include // for vector #include "aliases.hpp" // for IMAGE, JournalPageType #include "bucket.hpp" // for Bucket @@ -36,7 +37,6 @@ #include "usertypes/vanilla_render_lua.hpp" // for VanillaRenderContext #include "window_api.hpp" // for get_window -std::recursive_mutex g_all_backends_mutex; std::vector> g_all_backends; std::unordered_map g_hotkeys; int g_hotkey_count = 0; @@ -44,11 +44,16 @@ int g_hotkey_count = 0; LuaBackend::LuaBackend(SoundManager* sound_mgr, LuaConsole* con) : lua{get_lua_vm(sound_mgr), sol::create}, vm{acquire_lua_vm(sound_mgr)}, sound_manager{sound_mgr}, console{con} { - g_state = State::get().ptr_main(); + g_state = State::get().ptr_local(); + if (g_state == nullptr) + { + g_state = State::get().ptr_main(); + } + ScriptState& state = local_state_datas[g_state].state; state.screen = g_state->screen; state.time_level = g_state->time_level; state.time_total = g_state->time_total; - state.time_global = get_frame_count_main(); + state.time_global = State::get_frame_count(g_state); state.frame = state.frame; state.loading = g_state->loading; state.reset = (g_state->quest_flags & 1); @@ -56,7 +61,7 @@ LuaBackend::LuaBackend(SoundManager* sound_mgr, LuaConsole* con) populate_lua_env(lua); - std::lock_guard lock{g_all_backends_mutex}; + std::lock_guard lock{global_lua_lock}; g_all_backends.emplace_back(new ProtectedBackend{this}); self = g_all_backends.back().get(); } @@ -76,12 +81,17 @@ LuaBackend::~LuaBackend() } { - std::lock_guard lock{g_all_backends_mutex}; + std::lock_guard lock{global_lua_lock}; std::erase_if(g_all_backends, [this](const std::unique_ptr& protected_backend) { return protected_backend.get() == self; }); } } +LocalStateData& LuaBackend::get_locals() +{ + return local_state_datas[State::get().ptr()]; +} + void LuaBackend::clear() { clear_all_callbacks(); @@ -268,29 +278,31 @@ bool LuaBackend::update() } /*moved to pre_load_screen - if (g_state->loading == 1 && g_state->loading != state.loading && g_state->screen_next != (int)ON::OPTIONS && g_state->screen != (int)ON::OPTIONS && g_state->screen_last != (int)ON::OPTIONS) + if (state->loading == 1 && state->loading != script_state.loading && state->screen_next != (int)ON::OPTIONS && state->screen != (int)ON::OPTIONS && state->screen_last != (int)ON::OPTIONS) { level_timers.clear(); script_input.clear(); clear_custom_shopitem_names(); }*/ - if (g_state->screen != state.screen) + ScriptState& script_state = get_locals().state; + StateMemory* state = State::get().ptr(); + if (state->screen != script_state.screen) { if (on_screen) on_screen.value()(); } - if (on_frame && g_state->time_level != state.time_level && g_state->screen == (int)ON::LEVEL) + if (on_frame && state->time_level != script_state.time_level && state->screen == (int)ON::LEVEL) { on_frame.value()(); } - if (g_state->screen == (int)ON::CAMP && g_state->screen_last != (int)ON::OPTIONS && g_state->loading != state.loading && g_state->loading == 3 && g_state->time_level == 1) + if (state->screen == (int)ON::CAMP && state->screen_last != (int)ON::OPTIONS && state->loading != script_state.loading && state->loading == 3 && state->time_level == 1) { if (on_camp) on_camp.value()(); } - if (g_state->screen == (int)ON::LEVEL && g_state->screen_last != (int)ON::OPTIONS && g_state->loading != state.loading && g_state->loading == 3 && g_state->time_level == 1) + if (state->screen == (int)ON::LEVEL && state->screen_last != (int)ON::OPTIONS && state->loading != script_state.loading && state->loading == 3 && state->time_level == 1) { - if (g_state->level_count == 0) + if (state->level_count == 0) { if (on_start) on_start.value()(); @@ -298,17 +310,17 @@ bool LuaBackend::update() if (on_level) on_level.value()(); } - if (g_state->screen == (int)ON::TRANSITION && state.screen != (int)ON::TRANSITION) + if (state->screen == (int)ON::TRANSITION && script_state.screen != (int)ON::TRANSITION) { if (on_transition) on_transition.value()(); } - if (g_state->screen == (int)ON::DEATH && state.screen != (int)ON::DEATH) + if (state->screen == (int)ON::DEATH && script_state.screen != (int)ON::DEATH) { if (on_death) on_death.value()(); } - if ((g_state->screen == (int)ON::WIN && state.screen != (int)ON::WIN) || (g_state->screen == (int)ON::CONSTELLATION && state.screen != (int)ON::CONSTELLATION)) + if ((state->screen == (int)ON::WIN && script_state.screen != (int)ON::WIN) || (state->screen == (int)ON::CONSTELLATION && script_state.screen != (int)ON::CONSTELLATION)) { if (on_win) on_win.value()(); @@ -366,7 +378,7 @@ bool LuaBackend::update() for (auto it = global_timers.begin(); it != global_timers.end();) { - int now = get_frame_count(); + int now = State::get_frame_count(state); if (auto cb = std::get_if(&it->second)) { if (now >= cb->lastRan + cb->interval && !is_callback_cleared(it->first)) @@ -403,7 +415,7 @@ bool LuaBackend::update() } } - auto now = get_frame_count(); + auto now = State::get_frame_count(state); for (auto& [id, callback] : load_callbacks) { if (callback.lastRan < 0) @@ -421,17 +433,17 @@ bool LuaBackend::update() continue; set_current_callback(-1, id, CallbackType::Normal); - if ((ON)g_state->screen == callback.screen && g_state->screen != state.screen && g_state->screen_last != (int)ON::OPTIONS) // game screens + if ((ON)state->screen == callback.screen && state->screen != script_state.screen && state->screen_last != (int)ON::OPTIONS) // game screens { handle_function(this, callback.func); callback.lastRan = now; } - else if (callback.screen == ON::LEVEL && g_state->screen == (int)ON::LEVEL && g_state->screen_last != (int)ON::OPTIONS && state.loading != g_state->loading && g_state->loading == 3 && g_state->time_level <= 1) + else if (callback.screen == ON::LEVEL && state->screen == (int)ON::LEVEL && state->screen_last != (int)ON::OPTIONS && script_state.loading != state->loading && state->loading == 3 && state->time_level <= 1) { handle_function(this, callback.func); callback.lastRan = now; } - else if (callback.screen == ON::CAMP && g_state->screen == (int)ON::CAMP && g_state->screen_last != (int)ON::OPTIONS && state.loading != g_state->loading && g_state->loading == 3 && g_state->time_level == 1) + else if (callback.screen == ON::CAMP && state->screen == (int)ON::CAMP && state->screen_last != (int)ON::OPTIONS && script_state.loading != state->loading && state->loading == 3 && state->time_level == 1) { handle_function(this, callback.func); callback.lastRan = now; @@ -442,7 +454,7 @@ bool LuaBackend::update() { case ON::FRAME: { - if (g_state->time_level != state.time_level && g_state->screen == (int)ON::LEVEL) + if (state->time_level != script_state.time_level && state->screen == (int)ON::LEVEL) { handle_function(this, callback.func); callback.lastRan = now; @@ -451,8 +463,8 @@ bool LuaBackend::update() } case ON::GAMEFRAME: { - if (!g_state->pause && get_frame_count() != state.time_global && - ((g_state->screen >= (int)ON::CAMP && g_state->screen <= (int)ON::DEATH) || g_state->screen == (int)ON::ARENA_MATCH)) + if (!state->pause && State::get_frame_count(state) != script_state.time_global && + ((state->screen >= (int)ON::CAMP && state->screen <= (int)ON::DEATH) || state->screen == (int)ON::ARENA_MATCH)) { handle_function(this, callback.func); callback.lastRan = now; @@ -461,7 +473,7 @@ bool LuaBackend::update() } case ON::SCREEN: { - if (g_state->screen != state.screen) + if (state->screen != script_state.screen) { handle_function(this, callback.func); callback.lastRan = now; @@ -470,7 +482,7 @@ bool LuaBackend::update() } case ON::START: { - if (g_state->screen == (int)ON::LEVEL && g_state->screen_last != (int)ON::OPTIONS && g_state->level_count == 0 && g_state->loading != state.loading && g_state->loading == 3 && g_state->time_level <= 1) + if (state->screen == (int)ON::LEVEL && state->screen_last != (int)ON::OPTIONS && state->level_count == 0 && state->loading != script_state.loading && state->loading == 3 && state->time_level <= 1) { handle_function(this, callback.func); callback.lastRan = now; @@ -479,7 +491,7 @@ bool LuaBackend::update() } case ON::LOADING: { - if (g_state->loading > 0 && g_state->loading != state.loading) + if (state->loading > 0 && state->loading != script_state.loading) { handle_function(this, callback.func); callback.lastRan = now; @@ -488,7 +500,7 @@ bool LuaBackend::update() } case ON::RESET: { - if ((g_state->quest_flags & 1) > 0 && (g_state->quest_flags & 1) != state.reset) + if ((state->quest_flags & 1) > 0 && (state->quest_flags & 1) != script_state.reset) { handle_function(this, callback.func); callback.lastRan = now; @@ -501,7 +513,7 @@ bool LuaBackend::update() } clear_current_callback(); } - const int now_l = g_state->time_level; + const int now_l = state->time_level; for (auto it = level_timers.begin(); it != level_timers.end();) { if (auto cb = std::get_if(&it->second)) @@ -545,7 +557,7 @@ bool LuaBackend::update() for (auto& [id, callback] : save_callbacks) { set_current_callback(-1, id, CallbackType::Normal); - if ((g_state->loading != state.loading && g_state->loading == 1) || manual_save) + if ((state->loading != script_state.loading && state->loading == 1) || manual_save) { handle_function(this, callback.func, SaveContext{get_root(), get_name()}); callback.lastRan = now; @@ -553,14 +565,14 @@ bool LuaBackend::update() clear_current_callback(); } - state.screen = g_state->screen; - state.time_level = g_state->time_level; - state.time_total = g_state->time_total; - state.time_global = get_frame_count(); - state.frame = get_frame_count(); - state.loading = g_state->loading; - state.reset = (g_state->quest_flags & 1); - state.quest_flags = g_state->quest_flags; + script_state.screen = state->screen; + script_state.time_level = state->time_level; + script_state.time_total = state->time_total; + script_state.time_global = get_frame_count(); + script_state.frame = get_frame_count(); + script_state.loading = state->loading; + script_state.reset = (state->quest_flags & 1); + script_state.quest_flags = state->quest_flags; if (manual_save) { @@ -1694,7 +1706,7 @@ void LuaBackend::set_error(std::string err) */ void LuaBackend::for_each_backend(std::function fun, bool stop_propagation) { - std::lock_guard lock{g_all_backends_mutex}; + std::lock_guard lock{global_lua_lock}; for (std::unique_ptr& backend : g_all_backends) { if (!fun(backend->Lock()) && stop_propagation) @@ -1709,7 +1721,7 @@ LuaBackend::LockedBackend LuaBackend::get_backend(std::string_view id) } std::optional LuaBackend::get_backend_safe(std::string_view id) { - std::lock_guard lock{g_all_backends_mutex}; + std::lock_guard lock{global_lua_lock}; for (std::unique_ptr& backend : g_all_backends) { LockedBackend locked = backend->Lock(); @@ -1726,7 +1738,7 @@ LuaBackend::LockedBackend LuaBackend::get_backend_by_id(std::string_view id, std } std::optional LuaBackend::get_backend_by_id_safe(std::string_view id, std::string_view ver) { - std::lock_guard lock{g_all_backends_mutex}; + std::lock_guard lock{global_lua_lock}; for (std::unique_ptr& backend : g_all_backends) { LockedBackend locked = backend->Lock(); @@ -1860,6 +1872,78 @@ void LuaBackend::on_post(ON event) } } +sol::table deepcopy_lua_table(sol::state& sol_state, sol::table& from_r) +{ + sol::table new_table(sol_state, sol::create); + for (auto& [k, v] : from_r.as()) + { + if (v.is()) + { + sol::table v_table = v.as(); + new_table.raw_set(k, deepcopy_lua_table(sol_state, v_table)); + } + else + { + new_table.raw_set(k, v); + } + } + auto maybe_metatable = from_r.raw_get>(sol::metatable_key); + if (maybe_metatable) + { + new_table.raw_set(sol::metatable_key, maybe_metatable.value()); + } + return new_table; +} + +inline sol::object deepcopy_lua(sol::state& sol_state, sol::object& from) +{ + if (from.is()) + { + auto from_t = from.as(); + return deepcopy_lua_table(sol_state, from_t); + } + else + { + return from; + } +} + +void LuaBackend::copy_locals(StateMemory* from, StateMemory* to) +{ + if (!local_state_datas.contains(from)) + return; + + auto& to_data = local_state_datas[to]; + auto& from_data = local_state_datas[from]; + to_data.state = from_data.state; + sol::object from_user_data = from_data.user_data; + if (from_user_data != sol::lua_nil) + { + to_data.user_data = deepcopy_lua(*vm, from_user_data); + } +} + +void LuaBackend::pre_copy_state(StateMemory* from, StateMemory* to) +{ + if (!get_enabled()) + return; + + copy_locals(from, to); + // auto now = get_frame_count(); + // for (auto& [id, callback] : callbacks) + // { + // if (is_callback_cleared(id)) + // continue; + + // if (callback.screen == ON::PRE_COPY_STATE) + // { + // set_current_callback(-1, id, CallbackType::Normal); + // handle_function(this, callback.func, from, to); + // clear_current_callback(); + // callback.lastRan = now; + // } + // } +} bool LuaBackend::pre_save_state(int slot, StateMemory* saved) { if (!get_enabled()) diff --git a/src/game_api/script/lua_backend.hpp b/src/game_api/script/lua_backend.hpp index d0bef1f7a..09d12a72d 100644 --- a/src/game_api/script/lua_backend.hpp +++ b/src/game_api/script/lua_backend.hpp @@ -139,6 +139,7 @@ enum class ON BLOCKED_UPDATE, BLOCKED_GAME_LOOP, BLOCKED_PROCESS_INPUT, + // PRE_COPY_STATE, }; struct IntOption @@ -284,6 +285,12 @@ class SoundManager; class LuaConsole; struct RenderInfo; +struct LocalStateData +{ + sol::object user_data; + ScriptState state = {0, 0, 0, 0, 0, 0, 0, 0}; +}; + class LuaBackend : public HookHandler, public HookHandler, @@ -300,7 +307,6 @@ class LuaBackend std::unordered_set loaded_modules; std::string result; - ScriptState state = {0, 0, 0, 0, 0, 0, 0, 0}; int cbcount = 0; CurrentCallback current_cb = {0, 0, CallbackType::None}; @@ -331,6 +337,7 @@ class LuaBackend std::unordered_map script_input; std::unordered_set windows; std::unordered_set console_commands; + std::unordered_map local_state_datas; bool manual_save{false}; uint32_t last_save{0}; @@ -349,6 +356,8 @@ class LuaBackend LuaBackend(SoundManager* sound_manager, LuaConsole* console); virtual ~LuaBackend(); + LocalStateData& get_locals(); + void copy_locals(StateMemory* from, StateMemory* to); void clear(); void clear_all_callbacks(); bool update(); @@ -462,6 +471,7 @@ class LuaBackend void load_user_data(); bool on_pre(ON event); void on_post(ON event); + void pre_copy_state(StateMemory* from, StateMemory* to); void hotkey_callback(int cb); int register_hotkey(HotKeyCallback cb, HOTKEY_TYPE flags); diff --git a/src/game_api/script/lua_vm.cpp b/src/game_api/script/lua_vm.cpp index 61397577f..16e0b381c 100644 --- a/src/game_api/script/lua_vm.cpp +++ b/src/game_api/script/lua_vm.cpp @@ -1990,13 +1990,13 @@ end lua["set_ending_unlock"] = set_ending_unlock; /// Get the thread-local version of state - lua["get_local_state"] = []() + lua["get_local_state"] = []() -> StateMemory* { return State::get().ptr_local(); }; /// Get the thread-local version of players - lua["get_local_players"] = []() + lua["get_local_players"] = []() -> std::vector { return get_players(State::get().ptr_local()); }; diff --git a/src/game_api/script/usertypes/prng_lua.cpp b/src/game_api/script/usertypes/prng_lua.cpp index 246212a54..d84905a08 100644 --- a/src/game_api/script/usertypes/prng_lua.cpp +++ b/src/game_api/script/usertypes/prng_lua.cpp @@ -51,7 +51,7 @@ void register_usertypes(sol::state& lua) lua["prng"] = &PRNG::get_main(); /// Get the thread-local version of prng - lua["get_local_prng"] = []() + lua["get_local_prng"] = []() -> PRNG* { return &PRNG::get_local(); }; diff --git a/src/game_api/script/usertypes/state_lua.cpp b/src/game_api/script/usertypes/state_lua.cpp index d99677bff..4280d63b7 100644 --- a/src/game_api/script/usertypes/state_lua.cpp +++ b/src/game_api/script/usertypes/state_lua.cpp @@ -12,18 +12,19 @@ #include // for move, declval, decay_t, reference_... #include // for min, max -#include "entities_chars.hpp" // IWYU pragma: keep -#include "entity.hpp" // IWYU pragma: keep -#include "illumination.hpp" // IWYU pragma: keep -#include "items.hpp" // for Items, SelectPlayerSlot, Items::is... -#include "level_api.hpp" // IWYU pragma: keep -#include "online.hpp" // for OnlinePlayer, OnlineLobby, Online -#include "savestate.hpp" // for SaveState -#include "screen.hpp" // IWYU pragma: keep -#include "screen_arena.hpp" // IWYU pragma: keep -#include "script/events.hpp" // for pre_load_state -#include "state.hpp" // for StateMemory, State, StateMemory::a... -#include "state_structs.hpp" // for ArenaConfigArenas, ArenaConfigItems +#include "entities_chars.hpp" // IWYU pragma: keep +#include "entity.hpp" // IWYU pragma: keep +#include "illumination.hpp" // IWYU pragma: keep +#include "items.hpp" // for Items, SelectPlayerSlot, Items::is... +#include "level_api.hpp" // IWYU pragma: keep +#include "online.hpp" // for OnlinePlayer, OnlineLobby, Online +#include "savestate.hpp" // for SaveState +#include "screen.hpp" // IWYU pragma: keep +#include "screen_arena.hpp" // IWYU pragma: keep +#include "script/events.hpp" // for pre_load_state +#include "script/lua_backend.hpp" // for LuaBackend +#include "state.hpp" // for StateMemory, State, StateMemory::a... +#include "state_structs.hpp" // for ArenaConfigArenas, ArenaConfigItems namespace NState { @@ -414,6 +415,31 @@ void register_usertypes(sol::state& lua) statememory_type["next_entity_uid"] = &StateMemory::next_entity_uid; statememory_type["room_owners"] = &StateMemory::room_owners; + auto state_get_user_data = [](StateMemory& state) -> sol::object + { + auto backend = LuaBackend::get_calling_backend(); + auto local_datas = backend->local_state_datas; + if (local_datas.contains(&state)) + { + return local_datas[&state].user_data; + } + return sol::nil; + }; + + auto state_set_user_data = [](StateMemory& state, sol::object user_data) -> void + { + auto backend = LuaBackend::get_calling_backend(); + backend->local_state_datas[&state].user_data = user_data; + }; + auto user_data = sol::property(state_get_user_data, state_set_user_data); + + statememory_type["user_data"] = std::move(user_data); + /* StateMemory + // user_data + // You can store a table (or lua primitive) here and it will store data correctly in online multiplayer, by having a different copy on each state and being copied over when the game does. + // Doesn't support recursive tables / cyclic references. Metatables will be transferred by reference instead of being copied + */ + lua.create_named_table("FADE", "NONE", 0, "OUT", 1, "LOAD", 2, "IN", 3); lua.create_named_table("QUEST_FLAG", "RESET", 1, "DARK_LEVEL_SPAWNED", 2, "VAULT_SPAWNED", 3, "SPAWN_OUTPOST", 4, "SHOP_SPAWNED", 5, "SHORTCUT_USED", 6, "SEEDED", 7, "DAILY", 8, "CAVEMAN_SHOPPIE_AGGROED", 9, "WADDLER_AGGROED", 10, "SHOP_BOUGHT_OUT", 11, "EGGPLANT_CROWN_PICKED_UP", 12, "UDJAT_EYE_SPAWNED", 17, "BLACK_MARKET_SPAWNED", 18, "DRILL_SPAWNED", 19, "MOON_CHALLENGE_SPAWNED", 25, "STAR_CHALLENGE_SPAWNED", 26, "SUN_CHALLENGE_SPAWNED", 27); diff --git a/src/game_api/search.cpp b/src/game_api/search.cpp index c0fc44f13..3a78aebbe 100644 --- a/src/game_api/search.cpp +++ b/src/game_api/search.cpp @@ -1839,6 +1839,15 @@ std::unordered_map g_address_rules{ .at_exe() .function_start(), }, + { + // Put write bp on state.win_state and enter a multiplayer game + "heap_clone"sv, + PatternCommandBuffer{} + .find_inst("4c 8d 05 f4 ca 27 00"_gh) + .find_next_inst("eb 27"_gh) + .offset(-0xC) + .at_exe(), + }, { // ^ writes to state.pause on state.loading == 3 "unpause_level"sv, diff --git a/src/game_api/spawn_api.cpp b/src/game_api/spawn_api.cpp index 556f351b8..11966c5d7 100644 --- a/src/game_api/spawn_api.cpp +++ b/src/game_api/spawn_api.cpp @@ -35,10 +35,10 @@ struct Items; -std::uint32_t g_SpawnNonReplacable; -SpawnType g_SpawnTypeFlags; -std::array g_SpawnTypes{}; -std::function g_temp_entity_spawn_hook; +thread_local std::uint32_t g_SpawnNonReplacable; +thread_local SpawnType g_SpawnTypeFlags{SPAWN_TYPE_SYSTEMIC}; +thread_local std::array g_SpawnTypes{}; +thread_local std::function g_temp_entity_spawn_hook; void spawn_liquid(ENT_TYPE entity_type, float x, float y) { diff --git a/src/game_api/state.cpp b/src/game_api/state.cpp index 31839f977..003141c9a 100644 --- a/src/game_api/state.cpp +++ b/src/game_api/state.cpp @@ -299,6 +299,7 @@ State& State::get() init_state_update_hook(); init_process_input_hook(); init_game_loop_hook(); + init_state_clone_hook(); auto bucket = Bucket::get(); bucket->count++; @@ -625,6 +626,10 @@ uint32_t State::get_frame_count() const { return memory_read((size_t)ptr() - 0xd0); } +uint32_t State::get_frame_count(StateMemory* state) +{ + return memory_read((size_t)state - 0xd0); +} int64_t get_global_frame_count() { return global_frame_count; @@ -692,6 +697,42 @@ void init_state_update_hook() } } +void HeapClone(uint64_t heap_to, uint64_t heap_container_from) +{ + uint64_t location = State::get().get_offset(); + StateMemory* state_from = reinterpret_cast(memory_read(heap_container_from + 0x88) + location); + StateMemory* state_to = reinterpret_cast(heap_to + location); + pre_copy_state_event(state_from, state_to); +} + +// Original function params: clone_heap(ThreadStorageContainer to, ThreadStorageContainer from) +// HeapContainer has heap1 and heap2 variables, and some sort of timer, that just increases constantly, I guess to handle the rollback and multi-threaded stuff +// The rest of what HeapContainer has is unknown for now +// After writing to a chosen storage from the content of `from->heap1`, sets `to->heap2` to the newly copied thread storage +void init_state_clone_hook() +{ + auto heap_clone = get_address("heap_clone"); + // Hook the function after it has chosen a thread storage to write to, and pass it to the hook + size_t heap_clone_redirect_from_addr = heap_clone + 0x65; + const std::string redirect_code = fmt::format( + "\x51" // PUSH RCX + "\x52" // PUSH RDX + "\x41\x50" // PUSH R8 + "\x41\x51" // PUSH R9 + "\x48\x83\xEC\x28" // SUB RSP, 28 // Shadow space + Stack alignment + "\x4C\x89\xC9" // MOV RCX, R9 == heap_to + "\x48\xb8{}" // MOV RAX, &HeapClone + "\xff\xd0" // CALL RAX + "\x48\x83\xC4\x28" // ADD RSP, 28 + "\x41\x59" // POP R9 + "\x41\x58" // POP R8 + "\x5A" // POP RDX + "\x59"sv, // POP RCX + to_le_bytes(&HeapClone)); + + patch_and_redirect(heap_clone_redirect_from_addr, 7, redirect_code, false, 0, false); +} + using OnProcessInput = void(void*); OnProcessInput* g_process_input_trampoline{nullptr}; void ProcessInput(void* s) diff --git a/src/game_api/state.hpp b/src/game_api/state.hpp index 4ff458190..93f8c155d 100644 --- a/src/game_api/state.hpp +++ b/src/game_api/state.hpp @@ -8,6 +8,7 @@ #include "aliases.hpp" // for ENT_TYPE, LAYER #include "containers/custom_vector.hpp" // +#include "memory.hpp" // for memory_read #include "state_structs.hpp" // for JournalProgressStickerSlot, ... class Entity; @@ -310,6 +311,10 @@ struct StateMemory uint8_t unknown43; uint32_t unknown44; // probably padding + /* for the autodoc + any user_data; + */ + /// This function should only be used in a very specific circumstance (forcing the exiting theme when manually transitioning). Will crash the game if used inappropriately! void force_current_theme(THEME t); @@ -381,6 +386,7 @@ struct State uint32_t get_frame_count_main() const; uint32_t get_frame_count() const; + static uint32_t get_frame_count(StateMemory* state); std::vector read_prng() const; @@ -392,6 +398,11 @@ struct State void set_seed(uint32_t seed); SaveData* savedata(); LiquidPhysicsEngine* get_correct_liquid_engine(ENT_TYPE liquid_type) const; + // Get the 0x4A0 offset + size_t get_offset() const + { + return memory_read(location); + } private: State(size_t addr) @@ -404,6 +415,7 @@ struct State void init_state_update_hook(); void init_process_input_hook(); void init_game_loop_hook(); +void init_state_clone_hook(); uint8_t enum_to_layer(const LAYER layer, Vec2& player_position); uint8_t enum_to_layer(const LAYER layer);