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);