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

Save states I guess #370

Merged
merged 5 commits into from
Feb 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/game_data/spel2.lua

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions docs/src/includes/_globals.md
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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


Expand Down
148 changes: 37 additions & 111 deletions examples/barrymod.lua
Original file line number Diff line number Diff line change
@@ -1,130 +1,56 @@
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 = 'Creates checkpoints and restarts the current level on death like nothing happened.'
meta.author = 'Dregu'

local status = {}
local restart = false
register_option_bool('save_early', 'Multiverse of Madness Mode',
'Rerolls the level generation on death,\nbut keeps quest state and inventory.',
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('restart', 'Restart level', function()
restart_level()
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)

set_callback(function()
if state.items.player_inventory[1].health < 1 then
state.items.player_inventory[1].health = 4
if not options.save_early then
save_state(1)
end
for _, p in pairs(players) do
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)

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)
set_callback(function()
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)

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 the save state we made earlier, after updates to not mess with the running state
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 state.screen ~= SCREEN.TRANSITION then return end
local tile = get_entity(get_grid_entity_at(6, 121, LAYER.FRONT))
if tile then
get_entity(tile):destroy()
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)
end, ON.POST_LEVEL_GENERATION)
50 changes: 50 additions & 0 deletions src/game_api/rpc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1903,3 +1903,53 @@ void init_seeded(std::optional<uint32_t> 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<size_t>(arr + (from - 1) * 8);
size_t toBaseState = memory_read<size_t>(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);
};

StateMemory* get_save_state(int slot)
{
size_t arr = get_address("save_states");
size_t base = memory_read<size_t>(arr + (slot - 1) * 8);
auto state = reinterpret_cast<StateMemory*>(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;
}
}
3 changes: 3 additions & 0 deletions src/game_api/rpc.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,6 @@ void set_speedhack(std::optional<float> multiplier);
float get_speedhack();
void init_adventure();
void init_seeded(std::optional<uint32_t> seed);
void copy_state(int from, int to);
StateMemory* get_save_state(int slot);
void invalidate_save_states();
3 changes: 3 additions & 0 deletions src/game_api/script/events.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
22 changes: 22 additions & 0 deletions src/game_api/script/lua_vm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2249,6 +2249,28 @@ 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 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 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 StateMemory from a save_state 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);
Expand Down
5 changes: 5 additions & 0 deletions src/game_api/search.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2110,6 +2110,11 @@ std::unordered_map<std::string_view, AddressRule> g_address_rules{
PatternCommandBuffer{}
.from_exe_base(0x22b7ca10) // TODO
},
{
"save_states"sv,
PatternCommandBuffer{}
.from_exe_base(0x22e0d1d0) // TODO
},
};
std::unordered_map<std::string_view, size_t> g_cached_addresses;

Expand Down
Loading
Loading