Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changes for better script online compatibility #369

Merged
merged 20 commits into from
Dec 4, 2024

Conversation

estebanfer
Copy link
Contributor

For now it only has a WIP code for the rollback function hook that copies the data from one thread to another, it works but I still have to make it better, add more changes and decide how to name the heap clone functions

@estebanfer estebanfer marked this pull request as draft February 2, 2024 22:39
Comment on lines +711 to +714
// 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this has to copy more than just state (like prng)
also if you only want callback then figuring out which thread is which from ThreadStorageContainer would be enough, then you could just use detour instead of all of this asm

btw. I specially wrote not long ago patch_and_redirect function, for exactly this kind of assembly hack, so you don't have to write all the boring jumps there and back and copy game code yourself xd

Copy link
Contributor Author

@estebanfer estebanfer Feb 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah it also copies more stuff, but honestly I don't know how to handle that well in a callback, maybe make a struct for the whole storage that has prng, state, etc.

The issue with figuring out which thread is, is that I don't really know which thread is going to be the destination thread, at the start of the function you only know that to->heap2 pointer is gonna get overwritten at the with a pointer to the destination thread.
For getting to which ThreadStorage will be written to, the game code seems to do some lock and unlock stuff while looping through an array of the threads, which may affect the other threads, so I decided to not try to figure out to which one will be written to with my own code

I'll look into the patch_and_redirect functions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I don't know if I can share some of the ghidra decompiled code to show it

@estebanfer
Copy link
Contributor Author

Should I rename the PRE_CLONE_HEAP event now that there's the other events? (LOAD_STATE, SAVE_STATE)

@estebanfer estebanfer force-pushed the online-compat-stuff branch from dbe6e4a to a91419c Compare March 6, 2024 05:49
script state is now in local_state_datas
The deadlock was between a thread with g_all_backends_mutex and another with global_lua_lock
@Dregu
Copy link
Collaborator

Dregu commented Mar 8, 2024

Should I rename the PRE_CLONE_HEAP event now that there's the other events? (LOAD_STATE, SAVE_STATE)

Yeah I think the best thing moving forward would be to just call it "state" (so CLONE_STATE or COPY_STATE), grow the existing struct to start at offset 0, renaming it to "State" in the api, absorbing whatever there is before the current StateMemory and possibly after (whatever is part of this heap), making sure the globals always point to the local versions...

@estebanfer
Copy link
Contributor Author

estebanfer commented Mar 19, 2024

Would StateMemory be merged with the current existing State struct?
Or you refer to something else with the existing struct?

@Dregu
Copy link
Collaborator

Dregu commented Mar 19, 2024

I meant grow the currently existing StateMemory struct from the beginning by 0x4a0, and rename it State. I know we have a State struct already, but parts of it really have nothing to do with the state of anything and is just for initializing the script api. Rest is a random collection of api functions and stupid getter functions to read stuff from just before the StateMemory, you know, stuff that would be obsolete when the new State(Memory) actually covers that memory.

So

  • rename the current State to ScriptAPI or whatever, keeping the relevant functions, removing the soon obsolete functions
  • grow the current StateMemory by 0x4a0 and rename it State

or

  • split the current State to ScriptAPI and State
  • put old StateMemory inside this new State

and

  • fill the beginning of new State with stuff we know is in it, like the frame count and prng
  • organize the functions so new State only has functions that deal with that State, rest somewhere else

Now I don't exactly know what else is in the beginning of that new State, but there's clearly about 0x4a0 bytes of stuff that we should reverse engineer.

@Mr-Auto
Copy link
Contributor

Mr-Auto commented Mar 19, 2024

grow the current StateMemory by 0x4a0 and rename it State

put old StateMemory inside this new State

Those two points are mutually exclusive

There is also a ton of stuff after StateMemory, including a lot of the inner structs of StateMemory, level gen, liquid physics, but also save data etc.

I don't really see a reason for one giant struct, it will probably just mess with compile time or you will end up fighting the compiler alignments in it.

For the x64dbg plugin i just made one function to get heap address and then you get offset for specific thing via enum

@Dregu
Copy link
Collaborator

Dregu commented Mar 19, 2024

Those two points are mutually exclusive

Yes that's why I put "or" between them.

Anyway what we're doing currently with the stuff before StateMemory (get StateMemory* and subtract some random offset) is absolutely bonkers compared to any alternative.

@estebanfer
Copy link
Contributor Author

I was going with the option of growing state, but not I noticed we have some game functions that use StateMemory* with the current size, like spawn_companion, is_inside_active_shop_room, update_state, and some hooks...
So I guess I'm going for the second option by putting StateMemory in State, but would the lua API now use StateMemory or State?

@Dregu
Copy link
Collaborator

Dregu commented Mar 21, 2024

I was going with the option of growing state, but not I noticed we have some game functions that use StateMemory* with the current size, like spawn_companion, is_inside_active_shop_room, update_state, and some hooks... So I guess I'm going for the second option by putting StateMemory in State, but would the lua API now use StateMemory or State?

Well yeah clearly StateMemory is its own thing cause it's referenced like that a lot, but also clearly part of a slightly larger thing, so second option makes more sense. We should really figure out what all the stuff in that 0x4a0 even is, but on the lua side things don't really have to change, as we have getters for all the known things in there anyway. Things can be added as properties of lua["state"] even if not exactly in StateMemory though, if that makes sense.

Even if we end up with just this, it's better than the magic number getters we currently have:

struct State { // offset 0
  // padding
  PRNG prng;
  uint32_t frame_count;
  // padding
  StateMemory state; // offset 0x4a0
};

Still open for suggestions for the naming of those structures, but internally it doesn't really matter that much.

@estebanfer
Copy link
Contributor Author

Things can be added as properties of lua["state"] even if not exactly in StateMemory though, if that makes sense.

Wouldn't that require doing the same as before, substracting an offset to get the stuff in State?

Still open for suggestions for the naming of those structures, but internally it doesn't really matter that much.

Having stuff like state->state doesn't sound good to me, though I don't have many ideas. Seems that we already have some name for it from before, the OnHeapPointer class in thread_utils.hpp, but idk why it was named like that

@estebanfer
Copy link
Contributor Author

estebanfer commented Mar 25, 2024

I made most or all changes, though I had some doubts, I decided just to do the changes and then get it reviewed

src/game_api/state.hpp Outdated Show resolved Hide resolved
Comment on lines +46 to +51
g_state = State::get().ptr_local();
if (g_state == nullptr)
{
g_state = State::get().ptr_main();
}
ScriptState& state = local_state_datas[g_state].state;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't backend constructed ASAP in playlunky? then this will always be main state anyway

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure about when they are constructed, are they when you create a new script in OL?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was thinking more about PL

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah but this code is for both PL and OL, also handling what happens in OL makes sense to me

@estebanfer
Copy link
Contributor Author

I have a local stash of changes to make timers and callbacks started from other callbacks to work online, by moving the callbacks unordered_map and other fields from LuaBackend to LocalStateData and creating custom structs that only get copied when they have changes, so it has many lines of code edited. Should I commit that here or make another PR, to prevent this one from growing too much?

Also, should I make PRE_COPY_STATE to be also called from the savestate functions? Makes sense to me since people using PRE_COPY_STATE probably use it for online mods and wouldn't use the other save callbacks, and also being able to test a part of the rollback without having to open an online game seems be nice

@Dregu
Copy link
Collaborator

Dregu commented Mar 31, 2024

I have a local stash of changes to make timers and callbacks started from other callbacks to work online, by moving the callbacks unordered_map and other fields from LuaBackend to LocalStateData and creating custom structs that only get copied when they have changes, so it has many lines of code edited. Should I commit that here or make another PR, to prevent this one from growing too much?

Yeah maybe we should do this in smaller parts, but I'll say I have no intention of actually testing any of this online, just to make sure some single player mods still works right.

Also, should I make PRE_COPY_STATE to be also called from the savestate functions? Makes sense to me since people using PRE_COPY_STATE probably use it for online mods and wouldn't use the other save callbacks, and also being able to test a part of the rollback without having to open an online game seems be nice

Makes sense to me too, even if just for testing.

src/game_api/state.cpp Outdated Show resolved Hide resolved
@estebanfer estebanfer marked this pull request as ready for review May 1, 2024 02:06
@estebanfer
Copy link
Contributor Author

I removed the PRE_COPY_STATE for now because I don't really know what someone could use it for, so I prefer to leave it out until having something more clear

g_SpawnTypeFlags is now SYSTEMIC by default because when some threads
run spawns for the first times, the SYSTEMIC flag may not be set,
causing desyncs online
@estebanfer
Copy link
Contributor Author

On this PR I added get_location to State, but now there is also get_state_offset in savestate.cpp which does the same. Which one should be preferred?

/* 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metatables will be transferred by reference instead of being copied

This is kind of obvious, since you just can't ever do a copy of metatable in lua

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

metatables are just tables, with fields that are accessed by lua when the table is set as a metatable of one, so those are copiable technically, but copying them doesn't make sense for what they are used most, if not all of the time (which is mostly making OOP like methods for tables). Not sure about metatables created by sol2.
https://www.lua.org/pil/13.html

@Mr-Auto
Copy link
Contributor

Mr-Auto commented Aug 3, 2024

On this PR I added get_location to State, but now there is also get_state_offset in savestate.cpp which does the same. Which one should be preferred?

I don't think that matters much. Just pick one

@estebanfer
Copy link
Contributor Author

do you mean to let anyone decide which to use, or to remove one of those?

@Mr-Auto
Copy link
Contributor

Mr-Auto commented Aug 3, 2024

do you mean to let anyone decide which to use, or to remove one of those?

Aren't they exactly the same? it doesn't really matter which one we use. So just remove one.
Also this is really not a function that's used often, so it doesn't matter.

Also also, i plan to kill this function anyway, since we can just hardocde the offset 4A0 , it's not a big deal

@estebanfer estebanfer merged commit 1851abd into spelunky-fyi:main Dec 4, 2024
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants